appium-uiautomator2-driver 1.73.0 → 2.0.1

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,8 +1,6 @@
1
1
  import { CssSelectorParser } from 'css-selector-parser';
2
2
  import { escapeRegExp } from 'lodash';
3
- import { errors } from 'appium-base-driver';
4
-
5
- const CssConverter = {};
3
+ import { errors } from '@appium/base-driver';
6
4
 
7
5
  const parser = new CssSelectorParser();
8
6
  parser.registerSelectorPseudos('has');
@@ -113,16 +111,6 @@ function getWordMatcherRegex (word) {
113
111
  return `\\b(\\w*${escapeRegExp(word)}\\w*)\\b`;
114
112
  }
115
113
 
116
- /**
117
- * Add android:id/ to beginning of string if it's not there already
118
- *
119
- * @param {string} locator The initial locator
120
- * @returns {string} String with `android:id/` prepended (if it wasn't already)
121
- */
122
- function formatIdLocator (locator) {
123
- return ID_LOCATOR_PATTERN.test(locator) ? locator : `android:id/${locator}`;
124
- }
125
-
126
114
  /**
127
115
  * @typedef {Object} CssAttr
128
116
  * @property {?string} valueType Type of attribute (must be string or empty)
@@ -131,62 +119,20 @@ function formatIdLocator (locator) {
131
119
  */
132
120
 
133
121
  /**
134
- * Convert a CSS attribute into a UiSelector method call
135
- *
136
- * @param {CssAttr} cssAttr CSS attribute object
137
- * @returns {string} CSS attribute parsed as UiSelector
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
138
130
  */
139
- function parseAttr (cssAttr) {
140
- if (cssAttr.valueType && cssAttr.valueType !== 'string') {
141
- throw new Error(`'${cssAttr.name}=${cssAttr.value}' is an invalid attribute. ` +
142
- `Only 'string' and empty attribute types are supported. Found '${cssAttr.valueType}'`);
143
- }
144
- const attrName = assertGetAttrName(cssAttr);
145
- const methodName = toSnakeCase(attrName);
146
-
147
- // Validate that it's a supported attribute
148
- if (!STR_ATTRS.includes(attrName) && !BOOLEAN_ATTRS.includes(attrName)) {
149
- throw new Error(`'${attrName}' is not supported. Supported attributes are ` +
150
- `'${[...STR_ATTRS, ...BOOLEAN_ATTRS].join(', ')}'`);
151
- }
152
-
153
- // Parse boolean, if it's a boolean attribute
154
- if (BOOLEAN_ATTRS.includes(attrName)) {
155
- return `.${methodName}(${assertGetBool(cssAttr)})`;
156
- }
157
-
158
- // Otherwise parse as string
159
- let value = cssAttr.value || '';
160
- if (attrName === RESOURCE_ID) {
161
- value = formatIdLocator(value);
162
- }
163
- if (value === '') {
164
- return `.${methodName}Matches("")`;
165
- }
166
131
 
167
- switch (cssAttr.operator) {
168
- case '=':
169
- return `.${methodName}("${value}")`;
170
- case '*=':
171
- if (['description', 'text'].includes(attrName)) {
172
- return `.${methodName}Contains("${value}")`;
173
- }
174
- return `.${methodName}Matches("${escapeRegExp(value)}")`;
175
- case '^=':
176
- if (['description', 'text'].includes(attrName)) {
177
- return `.${methodName}StartsWith("${value}")`;
178
- }
179
- return `.${methodName}Matches("^${escapeRegExp(value)}")`;
180
- case '$=':
181
- return `.${methodName}Matches("${escapeRegExp(value)}$")`;
182
- case '~=':
183
- return `.${methodName}Matches("${getWordMatcherRegex(value)}")`;
184
- default:
185
- // Unreachable, but adding error in case a new CSS attribute is added.
186
- throw new Error(`Unsupported CSS attribute operator '${cssAttr.operator}'. ` +
187
- ` '=', '*=', '^=', '$=' and '~=' are supported.`);
188
- }
189
- }
132
+ /**
133
+ * @typedef {Object} CssObject
134
+ * @property {?string} type Type of CSS object. 'rule', 'ruleset' or 'selectors'
135
+ */
190
136
 
191
137
  /**
192
138
  * @typedef {Object} CssPseudo
@@ -195,126 +141,188 @@ function parseAttr (cssAttr) {
195
141
  * @property {?string} value The value of the pseudo selector
196
142
  */
197
143
 
198
- /**
199
- * Convert a CSS pseudo class to a UiSelector
200
- *
201
- * @param {CssPseudo} cssPseudo CSS Pseudo class
202
- * @returns {string} Pseudo selector parsed as UiSelector
203
- */
204
- function parsePseudo (cssPseudo) {
205
- if (cssPseudo.valueType && cssPseudo.valueType !== 'string') {
206
- throw new Error(`'${cssPseudo.name}=${cssPseudo.value}'. ` +
207
- `Unsupported css pseudo class value type: '${cssPseudo.valueType}'. Only 'string' type or empty is supported.`);
208
- }
209
-
210
- const pseudoName = assertGetAttrName(cssPseudo);
144
+ class CssConverter {
211
145
 
212
- if (BOOLEAN_ATTRS.includes(pseudoName)) {
213
- return `.${toSnakeCase(pseudoName)}(${assertGetBool(cssPseudo)})`;
146
+ constructor (selector, pkg) {
147
+ this.selector = selector;
148
+ this.pkg = pkg;
214
149
  }
215
150
 
216
- if (NUMERIC_ATTRS.includes(pseudoName)) {
217
- return `.${pseudoName}(${cssPseudo.value})`;
151
+ /**
152
+ * Add `<pkgName>:id/` prefix to beginning of string if it's not there already
153
+ *
154
+ * @param {string} locator The initial locator
155
+ * @returns {string} String with `<pkgName>:id/` prepended (if it wasn't already)
156
+ */
157
+ formatIdLocator (locator) {
158
+ return ID_LOCATOR_PATTERN.test(locator)
159
+ ? locator
160
+ : `${this.pkg || 'android'}:id/${locator}`;
218
161
  }
219
- }
220
162
 
221
- /**
222
- * @typedef {Object} CssRule
223
- * @property {?string} nestingOperator The nesting operator (aka: combinator https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors)
224
- * @property {?string} tagName The tag name (aka: type selector https://developer.mozilla.org/en-US/docs/Web/CSS/Type_selectors)
225
- * @property {?string[]} classNames An array of CSS class names
226
- * @property {?CssAttr[]} attrs An array of CSS attributes
227
- * @property {?CssPseudo[]} attrs An array of CSS pseudos
228
- * @property {?string} id CSS identifier
229
- * @property {?CssRule} rule A descendant of this CSS rule
230
- */
163
+ /**
164
+ * Convert a CSS attribute into a UiSelector method call
165
+ *
166
+ * @param {CssAttr} cssAttr CSS attribute object
167
+ * @returns {string} CSS attribute parsed as UiSelector
168
+ */
169
+ 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}'`);
173
+ }
174
+ const attrName = assertGetAttrName(cssAttr);
175
+ const methodName = toSnakeCase(attrName);
231
176
 
232
- /**
233
- * Convert a CSS rule to a UiSelector
234
- * @param {CssRule} cssRule CSS rule definition
235
- */
236
- function parseCssRule (cssRule) {
237
- const { nestingOperator } = cssRule;
238
- if (nestingOperator && nestingOperator !== ' ') {
239
- throw new Error(`'${nestingOperator}' is not a supported combinator. ` +
240
- `Only child combinator (>) and descendant combinator are supported.`);
241
- }
177
+ // Validate that it's a supported attribute
178
+ if (!STR_ATTRS.includes(attrName) && !BOOLEAN_ATTRS.includes(attrName)) {
179
+ throw new Error(`'${attrName}' is not supported. Supported attributes are ` +
180
+ `'${[...STR_ATTRS, ...BOOLEAN_ATTRS].join(', ')}'`);
181
+ }
242
182
 
243
- let uiAutomatorSelector = 'new UiSelector()';
244
- if (cssRule.tagName && cssRule.tagName !== '*') {
245
- let androidClass = [cssRule.tagName];
246
- if (cssRule.classNames) {
247
- for (const cssClassNames of cssRule.classNames) {
248
- androidClass.push(cssClassNames);
249
- }
250
- uiAutomatorSelector += `.className("${androidClass.join('.')}")`;
251
- } else {
252
- uiAutomatorSelector += `.classNameMatches("${cssRule.tagName}")`;
183
+ // Parse boolean, if it's a boolean attribute
184
+ if (BOOLEAN_ATTRS.includes(attrName)) {
185
+ return `.${methodName}(${assertGetBool(cssAttr)})`;
253
186
  }
254
- } else if (cssRule.classNames) {
255
- uiAutomatorSelector += `.classNameMatches("${cssRule.classNames.join('\\.')}")`;
256
- }
257
- if (cssRule.id) {
258
- uiAutomatorSelector += `.resourceId("${formatIdLocator(cssRule.id)}")`;
259
- }
260
- if (cssRule.attrs) {
261
- for (const attr of cssRule.attrs) {
262
- uiAutomatorSelector += parseAttr(attr);
187
+
188
+ // Otherwise parse as string
189
+ let value = cssAttr.value || '';
190
+ if (attrName === RESOURCE_ID) {
191
+ value = this.formatIdLocator(value);
263
192
  }
264
- }
265
- if (cssRule.pseudos) {
266
- for (const pseudo of cssRule.pseudos) {
267
- uiAutomatorSelector += parsePseudo(pseudo);
193
+ if (value === '') {
194
+ return `.${methodName}Matches("")`;
195
+ }
196
+
197
+ switch (cssAttr.operator) {
198
+ case '=':
199
+ return `.${methodName}("${value}")`;
200
+ case '*=':
201
+ if (['description', 'text'].includes(attrName)) {
202
+ return `.${methodName}Contains("${value}")`;
203
+ }
204
+ return `.${methodName}Matches("${escapeRegExp(value)}")`;
205
+ case '^=':
206
+ if (['description', 'text'].includes(attrName)) {
207
+ return `.${methodName}StartsWith("${value}")`;
208
+ }
209
+ return `.${methodName}Matches("^${escapeRegExp(value)}")`;
210
+ case '$=':
211
+ return `.${methodName}Matches("${escapeRegExp(value)}$")`;
212
+ case '~=':
213
+ return `.${methodName}Matches("${getWordMatcherRegex(value)}")`;
214
+ default:
215
+ // Unreachable, but adding error in case a new CSS attribute is added.
216
+ throw new Error(`Unsupported CSS attribute operator '${cssAttr.operator}'. ` +
217
+ ` '=', '*=', '^=', '$=' and '~=' are supported.`);
268
218
  }
269
219
  }
270
- if (cssRule.rule) {
271
- uiAutomatorSelector += `.childSelector(${parseCssRule(cssRule.rule)})`;
220
+
221
+ /**
222
+ * Convert a CSS pseudo class to a UiSelector
223
+ *
224
+ * @param {CssPseudo} cssPseudo CSS Pseudo class
225
+ * @returns {string} Pseudo selector parsed as UiSelector
226
+ */
227
+ 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.`);
231
+ }
232
+
233
+ const pseudoName = assertGetAttrName(cssPseudo);
234
+
235
+ if (BOOLEAN_ATTRS.includes(pseudoName)) {
236
+ return `.${toSnakeCase(pseudoName)}(${assertGetBool(cssPseudo)})`;
237
+ }
238
+
239
+ if (NUMERIC_ATTRS.includes(pseudoName)) {
240
+ return `.${pseudoName}(${cssPseudo.value})`;
241
+ }
272
242
  }
273
- return uiAutomatorSelector;
274
- }
275
243
 
276
- /**
277
- * @typedef {Object} CssObject
278
- * @property {?string} type Type of CSS object. 'rule', 'ruleset' or 'selectors'
279
- */
244
+ /**
245
+ * Convert a CSS rule to a UiSelector
246
+ * @param {CssRule} cssRule CSS rule definition
247
+ */
248
+ parseCssRule (cssRule) {
249
+ const { nestingOperator } = cssRule;
250
+ if (nestingOperator && nestingOperator !== ' ') {
251
+ throw new Error(`'${nestingOperator}' is not a supported combinator. ` +
252
+ `Only child combinator (>) and descendant combinator are supported.`);
253
+ }
280
254
 
281
- /**
282
- * Convert CSS object to UiAutomator2 selector
283
- * @param {CssObject} css CSS object
284
- * @returns {string} The CSS object parsed as a UiSelector
285
- */
286
- function parseCssObject (css) {
287
- switch (css.type) {
288
- case 'rule':
289
- return parseCssRule(css);
290
- case 'ruleSet':
291
- return parseCssObject(css.rule);
292
- case 'selectors':
293
- return css.selectors.map((selector) => parseCssObject(selector)).join('; ');
294
-
295
- default:
296
- // This is never reachable, but if it ever is do this.
297
- throw new Error(`UiAutomator does not support '${css.type}' css. Only supports 'rule', 'ruleSet', 'selectors' `);
255
+ let uiAutomatorSelector = 'new UiSelector()';
256
+ if (cssRule.tagName && cssRule.tagName !== '*') {
257
+ let androidClass = [cssRule.tagName];
258
+ if (cssRule.classNames) {
259
+ for (const cssClassNames of cssRule.classNames) {
260
+ androidClass.push(cssClassNames);
261
+ }
262
+ uiAutomatorSelector += `.className("${androidClass.join('.')}")`;
263
+ } else {
264
+ uiAutomatorSelector += `.classNameMatches("${cssRule.tagName}")`;
265
+ }
266
+ } else if (cssRule.classNames) {
267
+ uiAutomatorSelector += `.classNameMatches("${cssRule.classNames.join('\\.')}")`;
268
+ }
269
+ if (cssRule.id) {
270
+ uiAutomatorSelector += `.resourceId("${this.formatIdLocator(cssRule.id)}")`;
271
+ }
272
+ if (cssRule.attrs) {
273
+ for (const attr of cssRule.attrs) {
274
+ uiAutomatorSelector += this.parseAttr(attr);
275
+ }
276
+ }
277
+ if (cssRule.pseudos) {
278
+ for (const pseudo of cssRule.pseudos) {
279
+ uiAutomatorSelector += this.parsePseudo(pseudo);
280
+ }
281
+ }
282
+ if (cssRule.rule) {
283
+ uiAutomatorSelector += `.childSelector(${this.parseCssRule(cssRule.rule)})`;
284
+ }
285
+ return uiAutomatorSelector;
298
286
  }
299
- }
300
287
 
301
- /**
302
- * Convert a CSS selector to a UiAutomator2 selector
303
- * @param {string} cssSelector CSS Selector
304
- * @returns {string} The CSS selector converted to a UiSelector
305
- */
306
- CssConverter.toUiAutomatorSelector = function toUiAutomatorSelector (cssSelector) {
307
- let cssObj;
308
- try {
309
- cssObj = parser.parse(cssSelector);
310
- } catch (e) {
311
- throw new errors.InvalidSelectorError(`Invalid CSS selector '${cssSelector}'. Reason: '${e}'`);
288
+ /**
289
+ * Convert CSS object to UiAutomator2 selector
290
+ * @param {CssObject} css CSS object
291
+ * @returns {string} The CSS object parsed as a UiSelector
292
+ */
293
+ 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' `);
305
+ }
312
306
  }
313
- try {
314
- return parseCssObject(cssObj);
315
- } catch (e) {
316
- throw new errors.InvalidSelectorError(`Unsupported CSS selector '${cssSelector}'. Reason: '${e}'`);
307
+
308
+ /**
309
+ * Convert a CSS selector to a UiAutomator2 selector
310
+ *
311
+ * @returns {string} The CSS selector converted to a UiSelector
312
+ */
313
+ toUiAutomatorSelector () {
314
+ let cssObj;
315
+ try {
316
+ cssObj = parser.parse(this.selector);
317
+ } catch (e) {
318
+ throw new errors.InvalidSelectorError(`Invalid CSS selector '${this.selector}'. Reason: '${e}'`);
319
+ }
320
+ try {
321
+ return this.parseCssObject(cssObj);
322
+ } catch (e) {
323
+ throw new errors.InvalidSelectorError(`Unsupported CSS selector '${this.selector}'. Reason: '${e}'`);
324
+ }
317
325
  }
318
- };
326
+ }
319
327
 
320
328
  export default CssConverter;
package/lib/driver.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import _ from 'lodash';
2
- import { BaseDriver, DeviceSettings } from 'appium-base-driver';
2
+ import { BaseDriver, DeviceSettings } from '@appium/base-driver';
3
3
  import {
4
4
  UiAutomator2Server, SERVER_PACKAGE_ID, SERVER_TEST_PACKAGE_ID
5
5
  } from './uiautomator2';
6
- import { fs, util, mjpeg } from 'appium-support';
6
+ import { fs, util, mjpeg } from '@appium/support';
7
7
  import { retryInterval } from 'asyncbox';
8
8
  import B from 'bluebird';
9
9
  import logger from './logger';
package/lib/helpers.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import path from 'path';
2
- import { fs, system } from 'appium-support';
2
+ import { fs, system } from '@appium/support';
3
3
 
4
4
 
5
5
  let helpers = {};
package/lib/logger.js CHANGED
@@ -1,4 +1,4 @@
1
- import { logger } from 'appium-support';
1
+ import { logger } from '@appium/support';
2
2
 
3
3
 
4
4
  const log = logger.getLogger('UiAutomator2');
package/lib/server.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import log from './logger';
2
- import { server as baseServer, routeConfiguringFunction as makeRouter } from 'appium-base-driver';
2
+ import { server as baseServer, routeConfiguringFunction as makeRouter } from '@appium/base-driver';
3
3
  import AndroidUiautomator2Driver from './driver';
4
4
 
5
5
 
@@ -1,5 +1,5 @@
1
1
  import _ from 'lodash';
2
- import { JWProxy, errors } from 'appium-base-driver';
2
+ import { JWProxy, errors } from '@appium/base-driver';
3
3
  import { waitForCondition } from 'asyncbox';
4
4
  import log from './logger';
5
5
  import {
@@ -9,7 +9,7 @@ import {
9
9
  } from 'appium-uiautomator2-server';
10
10
  import {
11
11
  util, logger, tempDir, fs, timing
12
- } from 'appium-support';
12
+ } from '@appium/support';
13
13
  import B from 'bluebird';
14
14
  import helpers from './helpers';
15
15
  import axios from 'axios';
@@ -306,7 +306,7 @@ class UiAutomator2Server {
306
306
 
307
307
  try {
308
308
  const {value} = (await axios({
309
- url: `http://${this.host}:${this.systemPort}/wd/hub/sessions`,
309
+ url: `http://${this.host}:${this.systemPort}/sessions`,
310
310
  timeout: 500,
311
311
  })).data;
312
312
  const activeSessionIds = value.map(({id}) => id).filter(Boolean);
@@ -314,7 +314,7 @@ class UiAutomator2Server {
314
314
  log.debug(`The following obsolete sessions are still running: ${JSON.stringify(activeSessionIds)}`);
315
315
  log.debug(`Cleaning up ${util.pluralize('obsolete session', activeSessionIds.length, true)}`);
316
316
  await B.all(activeSessionIds
317
- .map((id) => axios.delete(`http://${this.host}:${this.systemPort}/wd/hub/session/${id}`))
317
+ .map((id) => axios.delete(`http://${this.host}:${this.systemPort}/session/${id}`))
318
318
  );
319
319
  // Let all sessions to be properly terminated before continuing
320
320
  await B.delay(1000);
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "automated testing",
8
8
  "android"
9
9
  ],
10
- "version": "1.73.0",
10
+ "version": "2.0.1",
11
11
  "author": "appium",
12
12
  "license": "Apache-2.0",
13
13
  "repository": {
@@ -40,20 +40,20 @@
40
40
  "build/lib"
41
41
  ],
42
42
  "dependencies": {
43
+ "@appium/base-driver": "^8.0.0",
44
+ "@appium/support": "^2.55.3",
43
45
  "@babel/runtime": "^7.0.0",
44
- "appium-adb": "^8.10.0",
45
- "appium-android-driver": "^4.51.0",
46
- "appium-base-driver": "^7.0.0",
47
- "appium-chromedriver": "^4.23.1",
48
- "appium-support": "^2.49.0",
49
- "appium-uiautomator2-server": "^4.28.0",
46
+ "appium-adb": "^9.0.0",
47
+ "appium-android-driver": "^5.0.0",
48
+ "appium-chromedriver": "^5.0.1",
49
+ "appium-uiautomator2-server": "^5.1.0",
50
50
  "asyncbox": "^2.3.1",
51
51
  "axios": "^0.x",
52
52
  "bluebird": "^3.5.1",
53
53
  "css-selector-parser": "^1.4.1",
54
54
  "lodash": "^4.17.4",
55
- "portscanner": "2.2.0",
56
- "source-map-support": "^0.5.5",
55
+ "portscanner": "^2.2.0",
56
+ "source-map-support": "^0.x",
57
57
  "teen_process": "^1.3.1"
58
58
  },
59
59
  "scripts": {
@@ -81,13 +81,19 @@
81
81
  "test"
82
82
  ],
83
83
  "devDependencies": {
84
+ "@appium/gulp-plugins": "^6.0.0",
85
+ "@appium/eslint-config-appium": "^5.0.0",
86
+ "@appium/test-support": "^1.0.0",
87
+ "@babel/core": "^7.16.0",
88
+ "@babel/eslint-parser": "^7.16.3",
84
89
  "@xmldom/xmldom": "^0.x",
85
- "android-apidemos": "^3.0.0",
86
- "appium-gulp-plugins": "^5.4.0",
87
- "appium-test-support": "^1.0.0",
90
+ "android-apidemos": "^4.0.0",
88
91
  "chai": "^4.1.0",
89
92
  "chai-as-promised": "^7.1.1",
90
- "eslint-config-appium": "^4.0.1",
93
+ "eslint": "^7.32.0",
94
+ "eslint-plugin-import": "^2.25.3",
95
+ "eslint-plugin-mocha": "^9.0.0",
96
+ "eslint-plugin-promise": "^6.0.0",
91
97
  "gps-demo-app": "^2.1.1",
92
98
  "gulp": "^4.0.0",
93
99
  "mocha": "^9.0.0",
@@ -97,8 +103,8 @@
97
103
  "pre-commit": "^1.2.2",
98
104
  "rimraf": "^3.0.0",
99
105
  "sinon": "^12.0.0",
100
- "unzipper": "^0.10.0",
101
- "wd": "^1.10.3",
106
+ "unzipper": "^0.x",
107
+ "webdriverio": "^7.0.0",
102
108
  "xpath": "^0.x"
103
109
  }
104
110
  }