appium-uiautomator2-driver 6.7.3 → 6.7.5
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 +12 -0
- package/build/lib/commands/find.js +2 -5
- package/build/lib/commands/find.js.map +1 -1
- package/build/lib/css-converter.d.ts +9 -42
- package/build/lib/css-converter.d.ts.map +1 -1
- package/build/lib/css-converter.js +73 -148
- package/build/lib/css-converter.js.map +1 -1
- package/build/lib/extensions.d.ts +2 -2
- package/build/lib/extensions.d.ts.map +1 -1
- package/build/lib/extensions.js +2 -4
- package/build/lib/extensions.js.map +1 -1
- package/build/lib/helpers.d.ts +3 -12
- package/build/lib/helpers.d.ts.map +1 -1
- package/build/lib/helpers.js +1 -11
- package/build/lib/helpers.js.map +1 -1
- package/build/lib/logger.d.ts +1 -2
- package/build/lib/logger.d.ts.map +1 -1
- package/build/lib/logger.js +2 -2
- package/build/lib/logger.js.map +1 -1
- package/build/lib/uiautomator2.d.ts +1 -1
- package/build/lib/uiautomator2.d.ts.map +1 -1
- package/build/lib/uiautomator2.js +0 -1
- package/build/lib/uiautomator2.js.map +1 -1
- package/build/test/unit/css-converter-specs.js +3 -6
- package/build/test/unit/css-converter-specs.js.map +1 -1
- package/build/test/unit/uiautomator2-specs.js +4 -4
- package/build/test/unit/uiautomator2-specs.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/lib/commands/find.js +1 -1
- package/lib/css-converter.ts +283 -0
- package/lib/extensions.ts +3 -0
- package/lib/helpers.ts +31 -0
- package/lib/logger.ts +4 -0
- package/lib/uiautomator2.ts +0 -1
- package/npm-shrinkwrap.json +47 -59
- package/package.json +2 -2
- package/lib/css-converter.js +0 -329
- package/lib/extensions.js +0 -4
- package/lib/helpers.js +0 -37
- package/lib/logger.js +0 -6
package/lib/css-converter.js
DELETED
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
import { createParser } from 'css-selector-parser';
|
|
2
|
-
import _ from 'lodash';
|
|
3
|
-
import { errors } from 'appium/driver';
|
|
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
|
-
});
|
|
26
|
-
|
|
27
|
-
const RESOURCE_ID = 'resource-id';
|
|
28
|
-
const ID_LOCATOR_PATTERN = /^[a-zA-Z_][a-zA-Z0-9._]*:id\/[\S]+$/;
|
|
29
|
-
|
|
30
|
-
const BOOLEAN_ATTRS = [
|
|
31
|
-
'checkable', 'checked', 'clickable', 'enabled', 'focusable',
|
|
32
|
-
'focused', 'long-clickable', 'scrollable', 'selected',
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
const NUMERIC_ATTRS = [
|
|
36
|
-
'index', 'instance',
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
const STR_ATTRS = [
|
|
40
|
-
'description', RESOURCE_ID, 'text', 'class-name', 'package-name'
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
const ALL_ATTRS = [
|
|
44
|
-
...BOOLEAN_ATTRS,
|
|
45
|
-
...NUMERIC_ATTRS,
|
|
46
|
-
...STR_ATTRS,
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
/** @type {[string, string[]][]} */
|
|
50
|
-
const ATTRIBUTE_ALIASES = [
|
|
51
|
-
[RESOURCE_ID, ['id']],
|
|
52
|
-
['description', [
|
|
53
|
-
'content-description', 'content-desc',
|
|
54
|
-
'desc', 'accessibility-id',
|
|
55
|
-
]],
|
|
56
|
-
['index', ['nth-child']],
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Convert hyphen separated word to snake case
|
|
61
|
-
*
|
|
62
|
-
* @param {string?} str
|
|
63
|
-
* @returns {string} The hyphen separated word translated to snake case
|
|
64
|
-
*/
|
|
65
|
-
function toSnakeCase (str) {
|
|
66
|
-
if (!str) {
|
|
67
|
-
return '';
|
|
68
|
-
}
|
|
69
|
-
const tokens = str.split('-').map((str) => str.charAt(0).toUpperCase() + str.slice(1).toLowerCase());
|
|
70
|
-
const out = tokens.join('');
|
|
71
|
-
return out.charAt(0).toLowerCase() + out.slice(1);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Get the boolean from a CSS object. If empty, return true. If not true/false/empty, throw exception
|
|
76
|
-
*
|
|
77
|
-
* @param {import('css-selector-parser').AstAttribute|import('css-selector-parser').AstPseudoClass} css A
|
|
78
|
-
* CSS object that has 'name' and 'value'
|
|
79
|
-
* @returns {string} Either 'true' or 'false'. If value is empty, return 'true'
|
|
80
|
-
*/
|
|
81
|
-
function requireBoolean (css) {
|
|
82
|
-
// @ts-ignore Attributes should exist
|
|
83
|
-
const val = _.toLower((css.value ?? css.argument)?.value) || 'true'; // an omitted boolean attribute means 'true' (e.g.: input[checked] means checked is true)
|
|
84
|
-
if (['true', 'false'].includes(val)) {
|
|
85
|
-
return val;
|
|
86
|
-
}
|
|
87
|
-
// @ts-ignore The attribute should exist
|
|
88
|
-
throw new Error(`'${css.name}' must be true, false or empty. Found '${css.value}'`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Get the canonical form of a CSS attribute name
|
|
93
|
-
*
|
|
94
|
-
* Converts to lowercase and if an attribute name is an alias for something else, return
|
|
95
|
-
* what it is an alias for
|
|
96
|
-
*
|
|
97
|
-
* @param {import('css-selector-parser').AstAttribute|import('css-selector-parser').AstPseudoClass} cssEntity CSS object
|
|
98
|
-
* @returns {string} The canonical attribute name
|
|
99
|
-
*/
|
|
100
|
-
function requireEntityName (cssEntity) {
|
|
101
|
-
const attrName = cssEntity.name.toLowerCase();
|
|
102
|
-
|
|
103
|
-
// Check if it's supported and if it is, return it
|
|
104
|
-
if (ALL_ATTRS.includes(attrName)) {
|
|
105
|
-
return attrName.toLowerCase();
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// If attrName is an alias for something else, return that
|
|
109
|
-
for (const [officialAttr, aliasAttrs] of ATTRIBUTE_ALIASES) {
|
|
110
|
-
if (aliasAttrs.includes(attrName)) {
|
|
111
|
-
return officialAttr;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
throw new Error(`'${attrName}' is not a valid attribute. ` +
|
|
115
|
-
`Supported attributes are '${ALL_ATTRS.join(', ')}'`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Get a regex that matches a whole word. For the ~= CSS attribute selector.
|
|
120
|
-
*
|
|
121
|
-
* @param {string} word
|
|
122
|
-
* @returns {string} A regex "word" matcher
|
|
123
|
-
*/
|
|
124
|
-
function getWordMatcherRegex (word) {
|
|
125
|
-
return `\\b(\\w*${_.escapeRegExp(word)}\\w*)\\b`;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
class CssConverter {
|
|
130
|
-
|
|
131
|
-
constructor (selector, pkg) {
|
|
132
|
-
this.selector = selector;
|
|
133
|
-
this.pkg = pkg;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Add `<pkgName>:id/` prefix to beginning of string if it's not there already
|
|
138
|
-
*
|
|
139
|
-
* @param {string} locator The initial locator
|
|
140
|
-
* @returns {string} String with `<pkgName>:id/` prepended (if it wasn't already)
|
|
141
|
-
*/
|
|
142
|
-
formatIdLocator (locator) {
|
|
143
|
-
return ID_LOCATOR_PATTERN.test(locator)
|
|
144
|
-
? locator
|
|
145
|
-
: `${this.pkg || 'android'}:id/${locator}`;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Convert a CSS attribute into a UiSelector method call
|
|
150
|
-
*
|
|
151
|
-
* @param {import('css-selector-parser').AstAttribute} cssAttr CSS attribute object
|
|
152
|
-
* @returns {string} CSS attribute parsed as UiSelector
|
|
153
|
-
*/
|
|
154
|
-
parseAttr (cssAttr) {
|
|
155
|
-
// @ts-ignore Value should be present
|
|
156
|
-
const attrValue = cssAttr.value?.value;
|
|
157
|
-
if (!_.isString(attrValue) && !_.isEmpty(attrValue)) {
|
|
158
|
-
throw new Error(`'${cssAttr.name}=${attrValue}' is an invalid attribute. ` +
|
|
159
|
-
`Only 'string' and empty attribute types are supported. Found '${attrValue}'`);
|
|
160
|
-
}
|
|
161
|
-
const attrName = requireEntityName(cssAttr);
|
|
162
|
-
const methodName = toSnakeCase(attrName);
|
|
163
|
-
|
|
164
|
-
// Validate that it's a supported attribute
|
|
165
|
-
if (!STR_ATTRS.includes(attrName) && !BOOLEAN_ATTRS.includes(attrName)) {
|
|
166
|
-
throw new Error(`'${attrName}' is not supported. Supported attributes are ` +
|
|
167
|
-
`'${[...STR_ATTRS, ...BOOLEAN_ATTRS].join(', ')}'`);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Parse boolean, if it's a boolean attribute
|
|
171
|
-
if (BOOLEAN_ATTRS.includes(attrName)) {
|
|
172
|
-
return `.${methodName}(${requireBoolean(cssAttr)})`;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Otherwise parse as string
|
|
176
|
-
let value = attrValue || '';
|
|
177
|
-
if (attrName === RESOURCE_ID) {
|
|
178
|
-
value = this.formatIdLocator(value);
|
|
179
|
-
}
|
|
180
|
-
if (value === '') {
|
|
181
|
-
return `.${methodName}Matches("")`;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
switch (cssAttr.operator) {
|
|
185
|
-
case '=':
|
|
186
|
-
return `.${methodName}("${value}")`;
|
|
187
|
-
case '*=':
|
|
188
|
-
if (['description', 'text'].includes(attrName)) {
|
|
189
|
-
return `.${methodName}Contains("${value}")`;
|
|
190
|
-
}
|
|
191
|
-
return `.${methodName}Matches("${_.escapeRegExp(value)}")`;
|
|
192
|
-
case '^=':
|
|
193
|
-
if (['description', 'text'].includes(attrName)) {
|
|
194
|
-
return `.${methodName}StartsWith("${value}")`;
|
|
195
|
-
}
|
|
196
|
-
return `.${methodName}Matches("^${_.escapeRegExp(value)}")`;
|
|
197
|
-
case '$=':
|
|
198
|
-
return `.${methodName}Matches("${_.escapeRegExp(value)}$")`;
|
|
199
|
-
case '~=':
|
|
200
|
-
return `.${methodName}Matches("${getWordMatcherRegex(value)}")`;
|
|
201
|
-
default:
|
|
202
|
-
// Unreachable, but adding error in case a new CSS attribute is added.
|
|
203
|
-
throw new Error(`Unsupported CSS attribute operator '${cssAttr.operator}'. ` +
|
|
204
|
-
` '=', '*=', '^=', '$=' and '~=' are supported.`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Convert a CSS pseudo class to a UiSelector
|
|
210
|
-
*
|
|
211
|
-
* @param {import('css-selector-parser').AstPseudoClass} cssPseudo CSS Pseudo class
|
|
212
|
-
* @returns {string|null|undefined} Pseudo selector parsed as UiSelector
|
|
213
|
-
*/
|
|
214
|
-
parsePseudo (cssPseudo) {
|
|
215
|
-
// @ts-ignore The attribute should exist
|
|
216
|
-
const argValue = cssPseudo.argument?.value;
|
|
217
|
-
if (!_.isString(argValue) && !_.isEmpty(argValue)) {
|
|
218
|
-
throw new Error(`'${cssPseudo.name}=${argValue}'. ` +
|
|
219
|
-
`Unsupported css pseudo class value: '${argValue}'. Only 'string' type or empty is supported.`);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const pseudoName = requireEntityName(cssPseudo);
|
|
223
|
-
|
|
224
|
-
if (BOOLEAN_ATTRS.includes(pseudoName)) {
|
|
225
|
-
return `.${toSnakeCase(pseudoName)}(${requireBoolean(cssPseudo)})`;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (NUMERIC_ATTRS.includes(pseudoName)) {
|
|
229
|
-
return `.${pseudoName}(${argValue})`;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Convert a CSS rule to a UiSelector
|
|
235
|
-
* @param {import('css-selector-parser').AstRule} cssRule CSS rule definition
|
|
236
|
-
*/
|
|
237
|
-
parseCssRule (cssRule) {
|
|
238
|
-
if (cssRule.combinator && ![' ', '>'].includes(cssRule.combinator)) {
|
|
239
|
-
throw new Error(`'${cssRule.combinator}' is not a supported combinator. ` +
|
|
240
|
-
`Only child combinator (>) and descendant combinator are supported.`);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/** @type {string[]} */
|
|
244
|
-
const uiAutomatorSelector = ['new UiSelector()'];
|
|
245
|
-
/** @type {import('css-selector-parser').AstClassName[]} */
|
|
246
|
-
// @ts-ignore This should work
|
|
247
|
-
const astClassNames = cssRule.items.filter(({type}) => type === 'ClassName');
|
|
248
|
-
const classNames = astClassNames.map(({name}) => name);
|
|
249
|
-
/** @type {import('css-selector-parser').AstTagName|undefined} */
|
|
250
|
-
// @ts-ignore This should work
|
|
251
|
-
const astTag = cssRule.items.find(({type}) => type === 'TagName');
|
|
252
|
-
const tagName = astTag?.name;
|
|
253
|
-
if (tagName && tagName !== '*') {
|
|
254
|
-
const androidClass = [tagName];
|
|
255
|
-
if (classNames.length) {
|
|
256
|
-
for (const cssClassNames of classNames) {
|
|
257
|
-
androidClass.push(cssClassNames);
|
|
258
|
-
}
|
|
259
|
-
uiAutomatorSelector.push(`.className("${androidClass.join('.')}")`);
|
|
260
|
-
} else {
|
|
261
|
-
uiAutomatorSelector.push(`.classNameMatches("${tagName}")`);
|
|
262
|
-
}
|
|
263
|
-
} else if (classNames.length) {
|
|
264
|
-
uiAutomatorSelector.push(`.classNameMatches("${classNames.join('\\.')}")`);
|
|
265
|
-
}
|
|
266
|
-
/** @type {import('css-selector-parser').AstId[]} */
|
|
267
|
-
// @ts-ignore This should work
|
|
268
|
-
const astIds = cssRule.items.filter(({type}) => type === 'Id');
|
|
269
|
-
const ids = astIds.map(({name}) => name);
|
|
270
|
-
if (ids.length) {
|
|
271
|
-
uiAutomatorSelector.push(`.resourceId("${this.formatIdLocator(ids[0])}")`);
|
|
272
|
-
}
|
|
273
|
-
/** @type {import('css-selector-parser').AstAttribute[]} */
|
|
274
|
-
// @ts-ignore This should work
|
|
275
|
-
const attributes = cssRule.items.filter(({type}) => type === 'Attribute');
|
|
276
|
-
for (const attr of attributes) {
|
|
277
|
-
uiAutomatorSelector.push(this.parseAttr(attr));
|
|
278
|
-
}
|
|
279
|
-
/** @type {import('css-selector-parser').AstPseudoClass[]} */
|
|
280
|
-
// @ts-ignore This should work
|
|
281
|
-
const pseudoClasses = cssRule.items.filter(({type}) => type === 'PseudoClass');
|
|
282
|
-
for (const pseudo of pseudoClasses) {
|
|
283
|
-
const sel = this.parsePseudo(pseudo);
|
|
284
|
-
if (sel) {
|
|
285
|
-
uiAutomatorSelector.push(sel);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
if (cssRule.nestedRule) {
|
|
289
|
-
uiAutomatorSelector.push(`.childSelector(${this.parseCssRule(cssRule.nestedRule)})`);
|
|
290
|
-
}
|
|
291
|
-
return uiAutomatorSelector.join('');
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Convert CSS object to UiAutomator2 selector
|
|
296
|
-
* @param {import('css-selector-parser').AstSelector} css CSS object
|
|
297
|
-
* @returns {string} The CSS object parsed as a UiSelector
|
|
298
|
-
*/
|
|
299
|
-
parseCssObject (css) {
|
|
300
|
-
if (!_.isEmpty(css.rules)) {
|
|
301
|
-
return this.parseCssRule(css.rules[0]);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
throw new Error('No rules could be parsed out of the current selector');
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Convert a CSS selector to a UiAutomator2 selector
|
|
309
|
-
*
|
|
310
|
-
* @returns {string} The CSS selector converted to a UiSelector
|
|
311
|
-
*/
|
|
312
|
-
toUiAutomatorSelector () {
|
|
313
|
-
let cssObj;
|
|
314
|
-
try {
|
|
315
|
-
cssObj = parseCssSelector(this.selector);
|
|
316
|
-
} catch (e) {
|
|
317
|
-
log.debug(e.stack);
|
|
318
|
-
throw new errors.InvalidSelectorError(`Invalid CSS selector '${this.selector}'. Reason: '${e.message}'`);
|
|
319
|
-
}
|
|
320
|
-
try {
|
|
321
|
-
return this.parseCssObject(cssObj);
|
|
322
|
-
} catch (e) {
|
|
323
|
-
log.debug(e.stack);
|
|
324
|
-
throw new errors.InvalidSelectorError(`Unsupported CSS selector '${this.selector}'. Reason: '${e.message}'`);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
export default CssConverter;
|
package/lib/extensions.js
DELETED
package/lib/helpers.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import { fs, system } from 'appium/support';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @param {string} filePath
|
|
6
|
-
* @returns {Promise<boolean>}
|
|
7
|
-
*/
|
|
8
|
-
export async function isWriteable(filePath) {
|
|
9
|
-
try {
|
|
10
|
-
await fs.access(filePath, fs.constants.W_OK);
|
|
11
|
-
if (system.isWindows()) {
|
|
12
|
-
// On operating systems, where access-control policies may
|
|
13
|
-
// limit access to the file system, `fs.access` does not work
|
|
14
|
-
// as expected. See https://groups.google.com/forum/#!topic/nodejs/qmZtIwDRSYo
|
|
15
|
-
// for more details
|
|
16
|
-
await fs.close(await fs.open(filePath, 'r+'));
|
|
17
|
-
}
|
|
18
|
-
return true;
|
|
19
|
-
} catch {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
26
|
-
* @param {import('appium-adb').ADB} adb
|
|
27
|
-
* @param {string} appPath
|
|
28
|
-
* @returns {Promise<void>}
|
|
29
|
-
*/
|
|
30
|
-
export async function signApp(adb, appPath) {
|
|
31
|
-
if (!await isWriteable(appPath)) {
|
|
32
|
-
throw new Error(`The application at '${appPath}' is not writeable. ` +
|
|
33
|
-
`Please grant write permissions to this file or to its parent folder '${path.dirname(appPath)}' ` +
|
|
34
|
-
`for the Appium process, so it could sign the application`);
|
|
35
|
-
}
|
|
36
|
-
await adb.sign(appPath);
|
|
37
|
-
}
|