@testim/testim-cli 3.213.0 → 3.217.0

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.
@@ -3,7 +3,7 @@
3
3
  const path = require('path');
4
4
  const findRoot = require('find-root');
5
5
  const dataUriToBuffer = require('data-uri-to-buffer');
6
- const {spawn: threadSpawn, config} = require('threads');
6
+ const { spawn: threadSpawn, config } = require('threads');
7
7
  const Promise = require('bluebird');
8
8
  const fs = require('fs-extra');
9
9
  const utils = require('../../../utils');
@@ -11,7 +11,7 @@ const logger = require('../../../commons/logger').getLogger("cli-service");
11
11
  const { getS3Artifact } = require('../../../commons/testimServicesApi');
12
12
  const npmWrapper = require('../../../commons/npmWrapper');
13
13
  const featureFlags = require('../../../commons/featureFlags');
14
- const {NpmPackageError} = require('../../../errors');
14
+ const { NpmPackageError } = require('../../../errors');
15
15
 
16
16
  config.set({
17
17
  basepath: {
@@ -210,10 +210,10 @@ function runCode(transactionId, incomingParams, context, code, packageLocalLocat
210
210
  const thread = threadSpawn(constructWithArguments(Function, ['input', 'done', 'progress', runFn]));
211
211
  return new Promise((resolve) => {
212
212
  thread
213
- .send({incomingParams, context, code})
213
+ .send({ incomingParams, context, code })
214
214
  .on('message', message => {
215
- const messageWithLogs = Object.assign({}, message, {tstConsoleLogs: testimConsoleLogDataAggregates})
216
- logger.debug('Run code worker response', {messageWithLogs, transactionId});
215
+ const messageWithLogs = Object.assign({}, message, { tstConsoleLogs: testimConsoleLogDataAggregates })
216
+ logger.debug('Run code worker response', { messageWithLogs, transactionId });
217
217
  resolve(messageWithLogs);
218
218
  })
219
219
  .on('progress', (logMessage) => {
@@ -221,11 +221,11 @@ function runCode(transactionId, incomingParams, context, code, packageLocalLocat
221
221
  })
222
222
  .on('error', (err) => {
223
223
  if (err.message === "malformed data: URI") {
224
- logger.error('Run code worker error', {err, transactionId, fileDataUrl});
224
+ logger.error('Run code worker error', { err, transactionId, fileDataUrl });
225
225
  } else {
226
- logger.error('Run code worker error', {err, transactionId});
226
+ logger.error('Run code worker error', { err, transactionId });
227
227
  }
228
-
228
+
229
229
  resolve({
230
230
  tstConsoleLogs: testimConsoleLogDataAggregates,
231
231
  status: 'failed',
@@ -239,11 +239,11 @@ function runCode(transactionId, incomingParams, context, code, packageLocalLocat
239
239
  });
240
240
  })
241
241
  .on('exit', () => {
242
- logger.debug('Run code worker has been terminated', {transactionId});
242
+ logger.debug('Run code worker has been terminated', { transactionId });
243
243
  });
244
244
  }).timeout(timeout)
245
245
  .catch(Promise.TimeoutError, err => {
246
- logger.warn("timeout to run code", {transactionId, err});
246
+ logger.warn("timeout to run code", { transactionId, err });
247
247
  return Promise.resolve({
248
248
  tstConsoleLogs: testimConsoleLogDataAggregates,
249
249
  status: 'failed',
@@ -254,7 +254,7 @@ function runCode(transactionId, incomingParams, context, code, packageLocalLocat
254
254
  exportsGlobal: {}
255
255
  },
256
256
  success: false
257
- });
257
+ });
258
258
  })
259
259
  .finally(() => thread && thread.kill());
260
260
  }
@@ -264,7 +264,7 @@ function removeFolder(installFolder) {
264
264
  return fs.remove(installFolder)
265
265
  .then(resolve)
266
266
  .catch(err => {
267
- logger.warn(`failed to remove install npm packages folder`, {err});
267
+ logger.warn(`failed to remove install npm packages folder`, { err });
268
268
  return resolve();
269
269
  });
270
270
  });
@@ -272,7 +272,7 @@ function removeFolder(installFolder) {
272
272
 
273
273
  function cleanPackages(transactionId) {
274
274
  function cleanInstallFolder() {
275
- const {installFolder} = transactions[transactionId];
275
+ const { installFolder } = transactions[transactionId];
276
276
  if (!installFolder) {
277
277
  return Promise.resolve();
278
278
  }
@@ -310,16 +310,10 @@ function mapNpmInstallDataToInstallData(npmInstallData, packageData) {
310
310
  function installPackage(stepId, testResultId, retryIndex, packageData, stepResultId, timeout) {
311
311
  const transactionId = getTransactionId(stepResultId, testResultId, stepId, retryIndex);
312
312
  return runNpmInstall(transactionId, packageData, timeout)
313
- .then(({data, installFolder}) => {
313
+ .then(({ data, installFolder }) => {
314
314
  transactions[transactionId] = transactions[transactionId] || {};
315
315
  transactions[transactionId].installFolder = installFolder;
316
316
  return Promise.resolve(data);
317
- }).then(npmInstallData => {
318
- if (!featureFlags.flags.enableNpmPackageInstallUsingNpmCli.isEnabled()) {
319
- return mapNpmInstallDataToInstallData(npmInstallData, packageData)
320
- } else {
321
- return npmInstallData;
322
- }
323
317
  });
324
318
 
325
319
  }
@@ -339,80 +333,47 @@ function runCodeWithPackages(code, stepId, incomingParams, context, testResultId
339
333
  if (s3fileDataUrl) {
340
334
  fileDataUrl = s3fileDataUrl;
341
335
  }
342
- })
343
- .then(() => runCode(transactionId, incomingParams, context, code, packageLocalLocations, timeout, fileDataUrl))
344
- .then(res => Object.assign({}, res, {nodeVersion: process.version}))
345
- .finally(() => cleanPackages(transactionId));
336
+ }).then(() => runCode(transactionId, incomingParams, context, code, packageLocalLocations, timeout, fileDataUrl))
337
+ .then(res => Object.assign({}, res, { nodeVersion: process.version }))
338
+ .finally(() => cleanPackages(transactionId));
346
339
  }
347
340
 
348
341
  function runNpmInstall(transactionId, packageData, timeout) {
349
-
350
- if (featureFlags.flags.enableNpmPackageInstallUsingNpmCli.isEnabled()) {
351
- const packages = packageData.map(data => `${data.packageName}@${data.packageVersion}`);
352
-
353
- const localPackageInstallFolder = getLocalPackageInstallFolder();
354
- const installFolder = path.join(localPackageInstallFolder, `/${transactionId}`);
355
- const proxyUri = global.proxyUri;
356
- return new Promise(async (resolve, reject) => {
357
- let output = '';
358
- try {
359
- output = await npmWrapper.installPackages(installFolder, packages, proxyUri, timeout);
360
- logger.info('npm package install finished', {transactionId ,output, timeout});
361
-
362
- const packageLines = output.match(/\+ (\w|-)+@(\d|.)+/g);
363
- const fullnames = packageLines.map(l => l.substr(2));
364
-
365
- const packageDataWithVersions = packageData.map(pData => {
366
- const packageFullName = fullnames.find(name => {
367
- const strudelIndex = name.lastIndexOf('@');
368
- const npmInstalledPackageName = name.substr(0, strudelIndex);
369
- return npmInstalledPackageName === pData.packageName;
370
- });
371
- const packageLocalLocation = path.resolve(installFolder, 'node_modules', pData.packageName);
372
- const packageWithVersion = Object.assign(pData, { packageFullName, packageLocalLocation});
373
- return packageWithVersion;
374
- });
375
-
376
- resolve({data:packageDataWithVersions, installFolder});
377
- } catch (err) {
378
- logger.warn('npm package install failed', {transactionId, err});
379
- reject(err);
342
+ const packages = packageData.map(data => `${data.packageName}@${data.packageVersion}`);
343
+ const localPackageInstallFolder = getLocalPackageInstallFolder();
344
+ const installFolder = path.join(localPackageInstallFolder, `/${transactionId}`);
345
+ const proxyUri = global.proxyUri;
346
+ return new Promise(async (resolve, reject) => {
347
+ let output = '';
348
+ try {
349
+ output = await npmWrapper.installPackages(installFolder, packages, proxyUri, timeout);
350
+ logger.info('npm package install finished', { transactionId, output, timeout });
351
+ if (Number(output.trim().split(' ')[1]) < packages.length) {
352
+ reject('npm package install failed, couldn\'t install all packages');
353
+ return;
380
354
  }
355
+ const packageDataWithVersions = packageData.map(pData => {
356
+ const version = npmWrapper.getLocallyInstalledPackageVersion(installFolder, pData.packageName);
357
+ const packageFullName = `${pData.packageName}@${version}`;
358
+ const packageLocalLocation = path.resolve(installFolder, 'node_modules', pData.packageName);
359
+ return Object.assign({}, pData, {
360
+ packageFullName,
361
+ packageLocalLocation
362
+ })
363
+ });
381
364
 
382
- })
365
+ resolve({ data: packageDataWithVersions, installFolder });
366
+ } catch (err) {
367
+ logger.warn('npm package install failed', { transactionId, err });
368
+ reject(err);
369
+ }
370
+
371
+ })
383
372
  .timeout(timeout)
384
373
  .catch(Promise.TimeoutError, err => {
385
- logger.warn("timeout to install package", {packages, transactionId, err, timeout});
374
+ logger.warn("timeout to install package", { packages, transactionId, err, timeout });
386
375
  throw err;
387
376
  });
388
- } else {
389
- const thread = threadSpawn('runNpmWorker.js');
390
- const localPackageInstallFolder = getLocalPackageInstallFolder();
391
- const packages = packageData.map(data => `${data.packageName}@${data.packageVersion}`);
392
- return new Promise((resolve, reject) => {
393
- thread
394
- .send({transactionId, packages, command: "install", localPackageInstallFolder, proxyUri: global.proxyUri})
395
- .on('message', message => {
396
- resolve(message);
397
- logger.debug("Npm worker response", {message, transactionId});
398
- })
399
- .on('error', err => {
400
- if (err.stack.includes("NpmPackageError")) {
401
- return reject(new NpmPackageError(err.message));
402
- }
403
- return reject(err);
404
- });
405
- })
406
- .timeout(timeout)
407
- .catch(Promise.TimeoutError, err => {
408
- logger.warn("timeout to install package", {packageData, transactionId, err});
409
- throw err;
410
- })
411
- .finally(() => thread && thread.kill());
412
-
413
- }
414
-
415
-
416
377
  }
417
378
 
418
379
  function getLocalPackageInstallFolder() {
@@ -107,7 +107,6 @@ async function saveTest({
107
107
  await fs.writeFileAsync(filename, body);
108
108
  await fs.mkdirAsync(path.join(folder, 'locators')).catch(() => {});
109
109
  for (const { id, body } of locators) {
110
- // eslint-disable-next-line no-await-in-loop
111
110
  await fs.writeFileAsync(path.join(folder, 'locators', `locator.${id}.json`), JSON.stringify(body));
112
111
  }
113
112
  const locatorMap = fromPairs(locators.map(({ name, id }) => [name, id]));
@@ -129,7 +128,6 @@ async function saveLocators(locators, { mergeIntoExisting } = { mergeIntoExistin
129
128
  await fs.mkdirAsync(path.join(folder, 'locators')).catch(() => {});
130
129
 
131
130
  for (const { name, id, elementLocator } of locators) {
132
- // eslint-disable-next-line no-await-in-loop
133
131
  await fs.writeFileAsync(path.join(folder, 'locators', `locator.${id}.json`), JSON.stringify({ name, id, elementLocator }));
134
132
  }
135
133
  const locatorMap = fromPairs(locators.map(({ name, id }) => [name, id]));
package/cli.js CHANGED
@@ -34,15 +34,6 @@ async function checkNodeVersion() {
34
34
  if (!semver.satisfies(process.version, version)) {
35
35
  throw new ArgError(`Required node version ${version} not satisfied with current version ${process.version}`);
36
36
  }
37
-
38
- if (process.version.startsWith('v10.')) {
39
- // give one day grace period.
40
- const limitDate = new Date('2021-12-13T00:00:00.000z');
41
- if (limitDate < new Date()) {
42
- throw new ArgError('Node.js v10 is no longer supported, please upgrade to Node.js 12, 14 or 16.');
43
- }
44
- console.warn('Node.js v10 is no longer supported by the Testim CLI. Please upgrade to Node.js 12, 14 or 16.');
45
- }
46
37
  }
47
38
 
48
39
  function main() {
package/cliAgentMode.js CHANGED
@@ -81,7 +81,6 @@ async function runAgentMode(options) {
81
81
  ];
82
82
 
83
83
  for (const packageToInstall of packages) {
84
- // eslint-disable-next-line no-await-in-loop
85
84
  await lazyRequire(packageToInstall, { silent: true }).catch(() => {});
86
85
  }
87
86
  }, LOAD_PLAYER_DELAY);
@@ -5,9 +5,9 @@
5
5
  "requires": true,
6
6
  "dependencies": {
7
7
  "typescript": {
8
- "version": "3.6.4",
9
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz",
10
- "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==",
8
+ "version": "4.5.5",
9
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz",
10
+ "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==",
11
11
  "dev": true
12
12
  }
13
13
  }
@@ -9,6 +9,6 @@
9
9
  "author": "Testim Developers",
10
10
  "license": "Proprietary",
11
11
  "devDependencies": {
12
- "typescript": "3.6.4"
12
+ "typescript": "4.5.5"
13
13
  }
14
14
  }
@@ -5,9 +5,9 @@
5
5
  "dependencies": {
6
6
  "testim": "latest",
7
7
  "@testim/testim-cli": "latest",
8
- "@types/chai": "4.2.2",
9
- "chai": "4.2.0",
10
- "cross-env": "5.2.1"
8
+ "@types/chai": "4.3.0",
9
+ "chai": "4.3.4",
10
+ "cross-env": "7.0.3"
11
11
  },
12
12
  "scripts": {
13
13
  "start": "cross-env NODE_OPTIONS=--max-old-space-size=8196 testim run \"./tests/**/*.test.js\" --require-credentials",
@@ -15,7 +15,7 @@
15
15
  "dev-test": "cross-env NODE_OPTIONS=--max-old-space-size=8196 testim run \"./tests/**/*.test.js\" --require-credentials --reporters=chrome,console",
16
16
  "debug": "cross-env NODE_OPTIONS=--max-old-space-size=8196 testim --inspect 9229 run \"./tests/**/*.test.js\" --require-credentials",
17
17
  "debug-file": "cross-env NODE_OPTIONS=--max-old-space-size=8196 testim --inspect 9229 --require-credentials run",
18
- "debug-examples": "cross-env NODE_OPTIONS=--max-old-space-size=8196 testim --inspect 9229 run \"./tests/examples/*.test.js\" --require-credentials",
18
+ "debug-examples": "cross-env NODE_OPTIONS=--max-old-space-size=8196 testim --inspect 9229 run \"./tests/examples/*.test.js\" --require-credentials",
19
19
  "headless": "cross-env NODE_OPTIONS=--max-old-space-size=8196 testim --inspect 9229 run \"./tests/**/*.test.js\" --require-credentials --headless",
20
20
  "ci": "cross-env NODE_OPTIONS=--max-old-space-size=8196 testim run \"./tests/**/*.test.js\" --grid testim-grid"
21
21
  }
@@ -6,11 +6,11 @@
6
6
  "testim": "latest",
7
7
  "@testim/testim-cli": "latest",
8
8
  "@types/node": "12.7.1",
9
- "@types/chai": "4.2.2",
10
- "chai": "4.2.0",
9
+ "@types/chai": "4.3.0",
10
+ "chai": "4.3.4",
11
11
  "ts-loader": "6.0.4",
12
- "typescript": "3.5.3",
13
- "cross-env": "5.2.1"
12
+ "typescript": "4.5.5",
13
+ "cross-env": "7.0.3"
14
14
  },
15
15
  "scripts": {
16
16
  "start": "cross-env NODE_OPTIONS=--max-old-space-size=8196 testim --webpackConfig='./webpack.config.js' run \"./tests/**/*.test.ts\" --require-credentials",
@@ -38,7 +38,6 @@ class FeatureFlagsService {
38
38
  useClickimVisibilityChecks: new Rox.Flag(),
39
39
  useIEWebdriverVisibilityChecks: new Rox.Flag(),
40
40
  runGetElementCodeInAut: new Rox.Flag(),
41
- enableNpmPackageInstallUsingNpmCli: new Rox.Flag(),
42
41
  enableFrameSwitchOptimization: new Rox.Flag(),
43
42
  maximumJsResultSize: new Rox.Configuration(2000 * 1024),
44
43
  skipFileInputClicks: new Rox.Flag(),
@@ -56,6 +55,7 @@ class FeatureFlagsService {
56
55
  usePortedHtml5DragDrop: new Rox.Flag(),
57
56
  applitoolsNewIntegration: new Rox.Flag(),
58
57
  testNamesToBeforeSuiteHook: new Rox.Flag(),
58
+ addCustomCapabilities: new Rox.Variant('{}'),
59
59
  };
60
60
  Rox.register('default', this.flags);
61
61
  }
@@ -232,7 +232,10 @@ function download(url) {
232
232
  }
233
233
 
234
234
  return Promise.fromCallback((callback) => request.end(callback))
235
- .tap(() => logger.info('finished to download', { url }))
235
+ .then(response => {
236
+ logger.info('finished to download', { url });
237
+ return response;
238
+ })
236
239
  .catch(logErrorAndRethrow('failed to download', { url }));
237
240
  }
238
241
 
@@ -60,6 +60,11 @@ async function installPackageLocally(rootPath, packageName, execOptions) {
60
60
  return regexResult[1];
61
61
  }
62
62
 
63
+ // https://github.com/npm/arborist/pull/362
64
+ function npmSevenAndEightMissingPermissions(error) {
65
+ return /The "to" argument must be of type string./.exec(error.stderr);
66
+ }
67
+
63
68
  // this is here because our shrinkwrap blocks our lazy deps for some reason
64
69
  const oldShrinkwrap = path.join(rootPath, 'npm-shrinkwrap.json');
65
70
  const newShrinkwrap = path.join(rootPath, 'npm-shrinkwrap-dummy.json');
@@ -73,23 +78,24 @@ async function installPackageLocally(rootPath, packageName, execOptions) {
73
78
  } catch (err) {
74
79
  // ignore error
75
80
  }
76
- return await exec(`npm i ${packageName} --no-save --no-package-lock --no-prune --prefer-offline --no-audit --progress=false`, { ...execOptions, cwd: rootPath }).catch(err => {
81
+ return await exec(`npm i ${packageName} --no-save --no-package-lock --no-prune --prefer-offline --no-audit --legacy-peer-deps --progress=false`, { ...execOptions, cwd: rootPath }).catch(err => {
77
82
  const pathWithMissingPermissions = getPathWithMissingPermissions(err);
78
- if (pathWithMissingPermissions) {
83
+ const npmEightMissingPermissions = npmSevenAndEightMissingPermissions(err);
84
+ if (pathWithMissingPermissions || npmEightMissingPermissions) {
79
85
  logger.info('Failed to install package due to insufficient write access', {
80
86
  ...additionalLogDetails(),
81
87
  package: packageName,
82
- path: pathWithMissingPermissions,
88
+ path: pathWithMissingPermissions || rootPath,
83
89
  });
84
90
  // eslint-disable-next-line no-console
85
91
  console.error(`
86
92
 
87
93
  Testim failed installing the package ${packageName} due to insufficient permissions.
88
94
  This is probably due to an installation of @testim/testim-cli with sudo, and running it without sudo.
89
- Testim had missing write access to ${pathWithMissingPermissions}
95
+ Testim had missing write access to ${pathWithMissingPermissions || rootPath}
90
96
 
91
97
  `);
92
- throw new NpmPermissionsError(pathWithMissingPermissions);
98
+ throw new NpmPermissionsError(pathWithMissingPermissions || rootPath);
93
99
  }
94
100
  throw err;
95
101
  });
@@ -104,7 +110,7 @@ Testim had missing write access to ${pathWithMissingPermissions}
104
110
  }
105
111
  }
106
112
 
107
- const localNpmLocation = path.resolve(require.resolve('npm'), '../../bin/npm-cli.js');
113
+ const localNpmLocation = path.resolve(require.resolve('npm').replace('index.js',''), 'bin','npm-cli.js');
108
114
 
109
115
  function installPackages(prefix, packageNames, proxyUri, timeoutMs) {
110
116
  return new Promise((resolve, reject) => {
@@ -115,7 +121,8 @@ function installPackages(prefix, packageNames, proxyUri, timeoutMs) {
115
121
  let stdout = '';
116
122
  let stderr = '';
117
123
 
118
- const npmInstall = spawn('node', [localNpmLocation, 'i', '--prefix', prefix, '--prefer-offline', '--no-audit', ...packageNames, ...proxyFlag], envVars);
124
+ const ops = '--no-save --no-package-lock --no-prune --prefer-offline --no-audit --legacy-peer-deps --progress=false'.split(' ');
125
+ const npmInstall = spawn('node', [localNpmLocation, 'i', '--prefix', prefix, ...ops, ...packageNames, ...proxyFlag], envVars);
119
126
  npmInstall.stderr.pipe(process.stderr);
120
127
  npmInstall.stdout.pipe(process.stdout);
121
128
 
@@ -54,7 +54,7 @@ describe('npmWrapper', () => {
54
54
  fakeChildProcess.exec.yields(undefined, []); //resolve without errors
55
55
  const cwd = '/some/dir';
56
56
  const pkg = 'some-package';
57
- const expectedCmd = 'npm i some-package --no-save --no-package-lock --no-prune --prefer-offline --no-audit --progress=false';
57
+ const expectedCmd = 'npm i some-package --no-save --no-package-lock --no-prune --prefer-offline --no-audit --legacy-peer-deps --progress=false';
58
58
  const expectedExecParams = { cwd };
59
59
 
60
60
  await npmWrapper.installPackageLocally(cwd, pkg);
@@ -567,6 +567,27 @@ function buildSeleniumOptions(browserOptions, testName, testRunConfig, gridInfo,
567
567
 
568
568
  _.merge(opts.desiredCapabilities, browserOptions.seleniumCapsFileContent);
569
569
 
570
+ try {
571
+ /**
572
+ * Targeted custom capabilities can be added to the desired capabilities object via the addCustomCapabilities FF.
573
+ * No targeting: { selenium_version: '3.141.59' }
574
+ * One level targeting (either grid provider, host, browser name or browser version): { "devicefarm": { selenium_version: '3.141.59' } }
575
+ * Two level targeting: { "internet explorer": { "11": { selenium_version: '3.141.59' } } }
576
+ */
577
+ const hostToProvider = { 'hub.lambdatest.com': 'lambdatest', 'public-grid.testim.io': 'testim', 'testgrid-devicefarm.us-west-2.amazonaws.com': 'devicefarm' };
578
+ const byGrid = (capabilities) => capabilities[gridInfo.provider] || capabilities[opts.host] || capabilities[hostToProvider[opts.host]];
579
+ const getTargetingGroup = (capabilities) => byGrid(capabilities) || capabilities[opts.desiredCapabilities.browserName] || capabilities[opts.desiredCapabilities.version] || capabilities || {};
580
+ const capabilities = JSON.parse(featureFlags.flags.addCustomCapabilities.getValue() || '{}');
581
+ const customCapabilities = getTargetingGroup(getTargetingGroup(capabilities));
582
+
583
+ if (Object.keys(customCapabilities).length) {
584
+ logger.info(`Adding custom capabilities: ${JSON.stringify(customCapabilities)}`);
585
+ Object.assign(opts.desiredCapabilities, customCapabilities);
586
+ }
587
+ } catch (e) {
588
+ logger.error(`Failed to load custom capabilities: ${e.message}`, { customCapabilities: featureFlags.flags.addCustomCapabilities.getValue() });
589
+ }
590
+
570
591
  if (isDFGrid(gridInfo) && opts.desiredCapabilities && !opts.capabilities) {
571
592
  convertToNewCapabilitiesFormat(opts.desiredCapabilities);
572
593
  opts.capabilities = { alwaysMatch: opts.desiredCapabilities, firstMatch: [{}] };