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.
- package/CHANGELOG.md +14 -0
- package/README.md +3 -3
- package/build/lib/commands/general.js +2 -2
- package/build/lib/commands/general.js.map +1 -1
- package/build/lib/css-converter.js +75 -56
- package/build/lib/css-converter.js.map +1 -1
- package/lib/commands/general.js +1 -1
- package/lib/css-converter.js +84 -102
- package/npm-shrinkwrap.json +82 -48
- package/package.json +3 -3
package/lib/css-converter.js
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { createParser } from 'css-selector-parser';
|
|
2
|
+
import _ from 'lodash';
|
|
3
3
|
import { errors } from 'appium/driver';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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 {
|
|
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
|
|
70
|
-
const val = css.value?.
|
|
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 {
|
|
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
|
|
87
|
-
const attrName =
|
|
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 {
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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 =
|
|
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}(${
|
|
168
|
+
return `.${methodName}(${requireBoolean(cssAttr)})`;
|
|
186
169
|
}
|
|
187
170
|
|
|
188
171
|
// Otherwise parse as string
|
|
189
|
-
let 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 {
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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 =
|
|
217
|
+
const pseudoName = requireEntityName(cssPseudo);
|
|
234
218
|
|
|
235
219
|
if (BOOLEAN_ATTRS.includes(pseudoName)) {
|
|
236
|
-
return `.${toSnakeCase(pseudoName)}(${
|
|
220
|
+
return `.${toSnakeCase(pseudoName)}(${requireBoolean(cssPseudo)})`;
|
|
237
221
|
}
|
|
238
222
|
|
|
239
223
|
if (NUMERIC_ATTRS.includes(pseudoName)) {
|
|
240
|
-
return `.${pseudoName}(${
|
|
224
|
+
return `.${pseudoName}(${argValue})`;
|
|
241
225
|
}
|
|
242
226
|
}
|
|
243
227
|
|
|
244
228
|
/**
|
|
245
229
|
* Convert a CSS rule to a UiSelector
|
|
246
|
-
* @param {
|
|
230
|
+
* @param {import('css-selector-parser').AstRule} cssRule CSS rule definition
|
|
247
231
|
*/
|
|
248
232
|
parseCssRule (cssRule) {
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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("${
|
|
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.
|
|
270
|
-
uiAutomatorSelector += `.resourceId("${this.formatIdLocator(cssRule.
|
|
253
|
+
if (!_.isEmpty(cssRule.ids)) {
|
|
254
|
+
uiAutomatorSelector += `.resourceId("${this.formatIdLocator(cssRule.ids[0])}")`;
|
|
271
255
|
}
|
|
272
|
-
if (cssRule.
|
|
273
|
-
for (const attr of cssRule.
|
|
256
|
+
if (cssRule.attributes) {
|
|
257
|
+
for (const attr of cssRule.attributes) {
|
|
274
258
|
uiAutomatorSelector += this.parseAttr(attr);
|
|
275
259
|
}
|
|
276
260
|
}
|
|
277
|
-
if (cssRule.
|
|
278
|
-
for (const pseudo of cssRule.
|
|
279
|
-
|
|
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.
|
|
283
|
-
uiAutomatorSelector += `.childSelector(${this.parseCssRule(cssRule.
|
|
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 {
|
|
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
|
-
|
|
295
|
-
|
|
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 =
|
|
296
|
+
cssObj = parseCssSelector(this.selector);
|
|
317
297
|
} catch (e) {
|
|
318
|
-
|
|
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
|
-
|
|
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;
|