appium-uiautomator2-driver 2.26.1 → 2.26.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,28 @@
1
- import { CssSelectorParser } from 'css-selector-parser';
2
- import { escapeRegExp } from 'lodash';
1
+ import { createParser } from 'css-selector-parser';
2
+ import _ from 'lodash';
3
3
  import { errors } from 'appium/driver';
4
-
5
- const parser = new CssSelectorParser();
6
- parser.registerSelectorPseudos('has');
7
- parser.registerNestingOperators('>', '+', '~');
8
- parser.registerAttrEqualityMods('^', '$', '*', '~');
9
- parser.enableSubstitutes();
4
+ import log from './logger';
5
+
6
+ const parseCssSelector = createParser({
7
+ syntax: {
8
+ pseudoClasses: {
9
+ unknown: 'accept',
10
+ definitions: {
11
+ Selector: ['has'],
12
+ }
13
+ },
14
+ combinators: ['>', '+', '~'],
15
+ attributes: {
16
+ operators: ['^=', '$=', '*=', '~=', '=']
17
+ },
18
+ ids: true,
19
+ classNames: true,
20
+ tag: {
21
+ wildcard: true
22
+ },
23
+ },
24
+ substitutes: true
25
+ });
10
26
 
11
27
  const RESOURCE_ID = 'resource-id';
12
28
  const ID_LOCATOR_PATTERN = /^[a-zA-Z_][a-zA-Z0-9._]*:id\/[\S]+$/;
@@ -42,7 +58,7 @@ const ATTRIBUTE_ALIASES = [
42
58
  /**
43
59
  * Convert hyphen separated word to snake case
44
60
  *
45
- * @param {string} str
61
+ * @param {string?} str
46
62
  * @returns {string} The hyphen separated word translated to snake case
47
63
  */
48
64
  function toSnakeCase (str) {
@@ -54,20 +70,15 @@ function toSnakeCase (str) {
54
70
  return out.charAt(0).toLowerCase() + out.slice(1);
55
71
  }
56
72
 
57
- /**
58
- * @typedef {Object} CssNameValueObject
59
- * @property {?name} name The name of the CSS object
60
- * @property {?string} value The value of the CSS object
61
- */
62
-
63
73
  /**
64
74
  * Get the boolean from a CSS object. If empty, return true. If not true/false/empty, throw exception
65
75
  *
66
- * @param {CssNameValueObject} css A CSS object that has 'name' and 'value'
76
+ * @param {import('css-selector-parser').AstAttribute|import('css-selector-parser').AstPseudoClass} css A
77
+ * CSS object that has 'name' and 'value'
67
78
  * @returns {string} Either 'true' or 'false'. If value is empty, return 'true'
68
79
  */
69
- function assertGetBool (css) {
70
- const val = css.value?.toLowerCase() || 'true'; // an omitted boolean attribute means 'true' (e.g.: input[checked] means checked is true)
80
+ function requireBoolean (css) {
81
+ const val = _.toLower((css.value ?? css.argument)?.value) || 'true'; // an omitted boolean attribute means 'true' (e.g.: input[checked] means checked is true)
71
82
  if (['true', 'false'].includes(val)) {
72
83
  return val;
73
84
  }
@@ -80,11 +91,11 @@ function assertGetBool (css) {
80
91
  * Converts to lowercase and if an attribute name is an alias for something else, return
81
92
  * what it is an alias for
82
93
  *
83
- * @param {Object} css CSS object
94
+ * @param {import('css-selector-parser').AstAttribute|import('css-selector-parser').AstPseudoClass} cssEntity CSS object
84
95
  * @returns {string} The canonical attribute name
85
96
  */
86
- function assertGetAttrName (css) {
87
- const attrName = css.name.toLowerCase();
97
+ function requireEntityName (cssEntity) {
98
+ const attrName = cssEntity.name.toLowerCase();
88
99
 
89
100
  // Check if it's supported and if it is, return it
90
101
  if (ALL_ATTRS.includes(attrName)) {
@@ -108,38 +119,9 @@ function assertGetAttrName (css) {
108
119
  * @returns {string} A regex "word" matcher
109
120
  */
110
121
  function getWordMatcherRegex (word) {
111
- return `\\b(\\w*${escapeRegExp(word)}\\w*)\\b`;
122
+ return `\\b(\\w*${_.escapeRegExp(word)}\\w*)\\b`;
112
123
  }
113
124
 
114
- /**
115
- * @typedef {Object} CssAttr
116
- * @property {?string} valueType Type of attribute (must be string or empty)
117
- * @property {?string} value Value of the attribute
118
- * @property {?string} operator The operator between value and value type (=, *=, , ^=, $=)
119
- */
120
-
121
- /**
122
- * @typedef {Object} CssRule
123
- * @property {?string} nestingOperator The nesting operator (aka: combinator https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors)
124
- * @property {?string} tagName The tag name (aka: type selector https://developer.mozilla.org/en-US/docs/Web/CSS/Type_selectors)
125
- * @property {?string[]} classNames An array of CSS class names
126
- * @property {?CssAttr[]} attrs An array of CSS attributes
127
- * @property {?CssPseudo[]} attrs An array of CSS pseudos
128
- * @property {?string} id CSS identifier
129
- * @property {?CssRule} rule A descendant of this CSS rule
130
- */
131
-
132
- /**
133
- * @typedef {Object} CssObject
134
- * @property {?string} type Type of CSS object. 'rule', 'ruleset' or 'selectors'
135
- */
136
-
137
- /**
138
- * @typedef {Object} CssPseudo
139
- * @property {?string} valueType The type of CSS pseudo selector (https://www.npmjs.com/package/css-selector-parser for reference)
140
- * @property {?string} name The name of the pseudo selector
141
- * @property {?string} value The value of the pseudo selector
142
- */
143
125
 
144
126
  class CssConverter {
145
127
 
@@ -163,15 +145,16 @@ class CssConverter {
163
145
  /**
164
146
  * Convert a CSS attribute into a UiSelector method call
165
147
  *
166
- * @param {CssAttr} cssAttr CSS attribute object
148
+ * @param {import('css-selector-parser').AstAttribute} cssAttr CSS attribute object
167
149
  * @returns {string} CSS attribute parsed as UiSelector
168
150
  */
169
151
  parseAttr (cssAttr) {
170
- if (cssAttr.valueType && cssAttr.valueType !== 'string') {
171
- throw new Error(`'${cssAttr.name}=${cssAttr.value}' is an invalid attribute. ` +
172
- `Only 'string' and empty attribute types are supported. Found '${cssAttr.valueType}'`);
152
+ const attrValue = cssAttr.value?.value;
153
+ if (!_.isString(attrValue) && !_.isEmpty(attrValue)) {
154
+ throw new Error(`'${cssAttr.name}=${attrValue}' is an invalid attribute. ` +
155
+ `Only 'string' and empty attribute types are supported. Found '${attrValue}'`);
173
156
  }
174
- const attrName = assertGetAttrName(cssAttr);
157
+ const attrName = requireEntityName(cssAttr);
175
158
  const methodName = toSnakeCase(attrName);
176
159
 
177
160
  // Validate that it's a supported attribute
@@ -182,11 +165,11 @@ class CssConverter {
182
165
 
183
166
  // Parse boolean, if it's a boolean attribute
184
167
  if (BOOLEAN_ATTRS.includes(attrName)) {
185
- return `.${methodName}(${assertGetBool(cssAttr)})`;
168
+ return `.${methodName}(${requireBoolean(cssAttr)})`;
186
169
  }
187
170
 
188
171
  // Otherwise parse as string
189
- let value = cssAttr.value || '';
172
+ let value = attrValue || '';
190
173
  if (attrName === RESOURCE_ID) {
191
174
  value = this.formatIdLocator(value);
192
175
  }
@@ -201,14 +184,14 @@ class CssConverter {
201
184
  if (['description', 'text'].includes(attrName)) {
202
185
  return `.${methodName}Contains("${value}")`;
203
186
  }
204
- return `.${methodName}Matches("${escapeRegExp(value)}")`;
187
+ return `.${methodName}Matches("${_.escapeRegExp(value)}")`;
205
188
  case '^=':
206
189
  if (['description', 'text'].includes(attrName)) {
207
190
  return `.${methodName}StartsWith("${value}")`;
208
191
  }
209
- return `.${methodName}Matches("^${escapeRegExp(value)}")`;
192
+ return `.${methodName}Matches("^${_.escapeRegExp(value)}")`;
210
193
  case '$=':
211
- return `.${methodName}Matches("${escapeRegExp(value)}$")`;
194
+ return `.${methodName}Matches("${_.escapeRegExp(value)}$")`;
212
195
  case '~=':
213
196
  return `.${methodName}Matches("${getWordMatcherRegex(value)}")`;
214
197
  default:
@@ -221,88 +204,85 @@ class CssConverter {
221
204
  /**
222
205
  * Convert a CSS pseudo class to a UiSelector
223
206
  *
224
- * @param {CssPseudo} cssPseudo CSS Pseudo class
225
- * @returns {string} Pseudo selector parsed as UiSelector
207
+ * @param {import('css-selector-parser').AstPseudoClass} cssPseudo CSS Pseudo class
208
+ * @returns {string?} Pseudo selector parsed as UiSelector
226
209
  */
227
210
  parsePseudo (cssPseudo) {
228
- if (cssPseudo.valueType && cssPseudo.valueType !== 'string') {
229
- throw new Error(`'${cssPseudo.name}=${cssPseudo.value}'. ` +
230
- `Unsupported css pseudo class value type: '${cssPseudo.valueType}'. Only 'string' type or empty is supported.`);
211
+ const argValue = cssPseudo.argument?.value;
212
+ if (!_.isString(argValue) && !_.isEmpty(argValue)) {
213
+ throw new Error(`'${cssPseudo.name}=${argValue}'. ` +
214
+ `Unsupported css pseudo class value: '${argValue}'. Only 'string' type or empty is supported.`);
231
215
  }
232
216
 
233
- const pseudoName = assertGetAttrName(cssPseudo);
217
+ const pseudoName = requireEntityName(cssPseudo);
234
218
 
235
219
  if (BOOLEAN_ATTRS.includes(pseudoName)) {
236
- return `.${toSnakeCase(pseudoName)}(${assertGetBool(cssPseudo)})`;
220
+ return `.${toSnakeCase(pseudoName)}(${requireBoolean(cssPseudo)})`;
237
221
  }
238
222
 
239
223
  if (NUMERIC_ATTRS.includes(pseudoName)) {
240
- return `.${pseudoName}(${cssPseudo.value})`;
224
+ return `.${pseudoName}(${argValue})`;
241
225
  }
242
226
  }
243
227
 
244
228
  /**
245
229
  * Convert a CSS rule to a UiSelector
246
- * @param {CssRule} cssRule CSS rule definition
230
+ * @param {import('css-selector-parser').AstRule} cssRule CSS rule definition
247
231
  */
248
232
  parseCssRule (cssRule) {
249
- const { nestingOperator } = cssRule;
250
- if (nestingOperator && nestingOperator !== ' ') {
251
- throw new Error(`'${nestingOperator}' is not a supported combinator. ` +
233
+ if (cssRule.combinator && ![' ', '>'].includes(cssRule.combinator)) {
234
+ throw new Error(`'${cssRule.combinator}' is not a supported combinator. ` +
252
235
  `Only child combinator (>) and descendant combinator are supported.`);
253
236
  }
254
237
 
255
238
  let uiAutomatorSelector = 'new UiSelector()';
256
- if (cssRule.tagName && cssRule.tagName !== '*') {
257
- let androidClass = [cssRule.tagName];
239
+ const tagName = cssRule.tag?.name;
240
+ if (tagName && tagName !== '*') {
241
+ let androidClass = [tagName];
258
242
  if (cssRule.classNames) {
259
243
  for (const cssClassNames of cssRule.classNames) {
260
244
  androidClass.push(cssClassNames);
261
245
  }
262
246
  uiAutomatorSelector += `.className("${androidClass.join('.')}")`;
263
247
  } else {
264
- uiAutomatorSelector += `.classNameMatches("${cssRule.tagName}")`;
248
+ uiAutomatorSelector += `.classNameMatches("${tagName}")`;
265
249
  }
266
- } else if (cssRule.classNames) {
250
+ } else if (!_.isEmpty(cssRule.classNames)) {
267
251
  uiAutomatorSelector += `.classNameMatches("${cssRule.classNames.join('\\.')}")`;
268
252
  }
269
- if (cssRule.id) {
270
- uiAutomatorSelector += `.resourceId("${this.formatIdLocator(cssRule.id)}")`;
253
+ if (!_.isEmpty(cssRule.ids)) {
254
+ uiAutomatorSelector += `.resourceId("${this.formatIdLocator(cssRule.ids[0])}")`;
271
255
  }
272
- if (cssRule.attrs) {
273
- for (const attr of cssRule.attrs) {
256
+ if (cssRule.attributes) {
257
+ for (const attr of cssRule.attributes) {
274
258
  uiAutomatorSelector += this.parseAttr(attr);
275
259
  }
276
260
  }
277
- if (cssRule.pseudos) {
278
- for (const pseudo of cssRule.pseudos) {
279
- uiAutomatorSelector += this.parsePseudo(pseudo);
261
+ if (cssRule.pseudoClasses) {
262
+ for (const pseudo of cssRule.pseudoClasses) {
263
+ const sel = this.parsePseudo(pseudo);
264
+ if (sel) {
265
+ uiAutomatorSelector += sel;
266
+ }
280
267
  }
281
268
  }
282
- if (cssRule.rule) {
283
- uiAutomatorSelector += `.childSelector(${this.parseCssRule(cssRule.rule)})`;
269
+ if (cssRule.nestedRule) {
270
+ uiAutomatorSelector += `.childSelector(${this.parseCssRule(cssRule.nestedRule)})`;
284
271
  }
285
272
  return uiAutomatorSelector;
286
273
  }
287
274
 
288
275
  /**
289
276
  * Convert CSS object to UiAutomator2 selector
290
- * @param {CssObject} css CSS object
277
+ * @param {import('css-selector-parser').AstSelector} css CSS object
291
278
  * @returns {string} The CSS object parsed as a UiSelector
292
279
  */
293
280
  parseCssObject (css) {
294
- switch (css.type) {
295
- case 'rule':
296
- return this.parseCssRule(css);
297
- case 'ruleSet':
298
- return this.parseCssObject(css.rule);
299
- case 'selectors':
300
- return css.selectors.map((selector) => this.parseCssObject(selector)).join('; ');
301
-
302
- default:
303
- // This is never reachable, but if it ever is do this.
304
- throw new Error(`UiAutomator does not support '${css.type}' css. Only supports 'rule', 'ruleSet', 'selectors' `);
281
+ if (!_.isEmpty(css.rules)) {
282
+ return this.parseCssRule(css.rules[0]);
305
283
  }
284
+
285
+ throw new Error('No rules could be parsed out of the current selector');
306
286
  }
307
287
 
308
288
  /**
@@ -313,16 +293,18 @@ class CssConverter {
313
293
  toUiAutomatorSelector () {
314
294
  let cssObj;
315
295
  try {
316
- cssObj = parser.parse(this.selector);
296
+ cssObj = parseCssSelector(this.selector);
317
297
  } catch (e) {
318
- throw new errors.InvalidSelectorError(`Invalid CSS selector '${this.selector}'. Reason: '${e}'`);
298
+ log.debug(e.stack);
299
+ throw new errors.InvalidSelectorError(`Invalid CSS selector '${this.selector}'. Reason: '${e.message}'`);
319
300
  }
320
301
  try {
321
302
  return this.parseCssObject(cssObj);
322
303
  } catch (e) {
323
- throw new errors.InvalidSelectorError(`Unsupported CSS selector '${this.selector}'. Reason: '${e}'`);
304
+ log.debug(e.stack);
305
+ throw new errors.InvalidSelectorError(`Unsupported CSS selector '${this.selector}'. Reason: '${e.message}'`);
324
306
  }
325
307
  }
326
308
  }
327
309
 
328
- export default CssConverter;
310
+ export default CssConverter;