selenium-webdriver 4.48.0 → 4.50.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.
Files changed (52) hide show
  1. package/CHANGES.md +26 -0
  2. package/bidi/generated/bluetooth.d.ts +199 -37
  3. package/bidi/generated/bluetooth.js +212 -106
  4. package/bidi/generated/browser.d.ts +81 -28
  5. package/bidi/generated/browser.js +97 -69
  6. package/bidi/generated/browsing_context.d.ts +274 -87
  7. package/bidi/generated/browsing_context.js +328 -241
  8. package/bidi/generated/common.d.ts +105 -21
  9. package/bidi/generated/common.js +64 -15
  10. package/bidi/generated/digital_credentials.d.ts +41 -0
  11. package/bidi/generated/digital_credentials.js +54 -0
  12. package/bidi/generated/emulation.d.ts +354 -58
  13. package/bidi/generated/emulation.js +262 -101
  14. package/bidi/generated/input.d.ts +63 -35
  15. package/bidi/generated/input.js +77 -51
  16. package/bidi/generated/log.d.ts +39 -18
  17. package/bidi/generated/log.js +23 -43
  18. package/bidi/generated/network.d.ts +248 -76
  19. package/bidi/generated/network.js +247 -181
  20. package/bidi/generated/permissions.d.ts +22 -4
  21. package/bidi/generated/permissions.js +31 -17
  22. package/bidi/generated/script.d.ts +166 -70
  23. package/bidi/generated/script.js +203 -116
  24. package/bidi/generated/session.d.ts +76 -36
  25. package/bidi/generated/session.js +81 -48
  26. package/bidi/generated/speculation.d.ts +15 -5
  27. package/bidi/generated/speculation.js +20 -16
  28. package/bidi/generated/storage.d.ts +41 -17
  29. package/bidi/generated/storage.js +58 -40
  30. package/bidi/generated/user_agent_client_hints.d.ts +25 -13
  31. package/bidi/generated/user_agent_client_hints.js +29 -18
  32. package/bidi/generated/webextension.d.ts +24 -10
  33. package/bidi/generated/webextension.js +39 -25
  34. package/bidi/serialization/enum.js +6 -3
  35. package/bidi/serialization/record.js +34 -29
  36. package/bin/linux-arm64/selenium-manager +0 -0
  37. package/bin/{linux → linux-x86_64}/selenium-manager +0 -0
  38. package/bin/macos/selenium-manager +0 -0
  39. package/bin/selenium-manager-THIRD-PARTY-NOTICES.txt +327 -51
  40. package/bin/selenium-manager.cdx.json +491 -491
  41. package/bin/windows/selenium-manager.exe +0 -0
  42. package/chromium.js +1 -1
  43. package/common/seleniumManager.js +2 -2
  44. package/eslint.config.js +2 -1
  45. package/example/chrome_mobile_emulation.js +1 -1
  46. package/firefox.js +7 -104
  47. package/index.js +8 -0
  48. package/lib/atoms/find-elements.js +14 -2
  49. package/package.json +10 -10
  50. package/remote/index.js +12 -3
  51. package/remote/util.js +5 -5
  52. package/testing/index.js +84 -1
Binary file
package/chromium.js CHANGED
@@ -506,7 +506,7 @@ class Options extends Capabilities {
506
506
  * __Example 1: Using a Pre-configured Device__
507
507
  *
508
508
  * let options = new chrome.Options().setMobileEmulation(
509
- * {deviceName: 'Google Nexus 5'});
509
+ * {deviceName: 'Pixel 10'});
510
510
  *
511
511
  * let driver = chrome.Driver.createSession(options);
512
512
  *
@@ -21,7 +21,7 @@
21
21
  * Wrapper for getting information from the Selenium Manager binaries
22
22
  */
23
23
 
24
- const { platform } = require('node:process')
24
+ const { platform, arch } = require('node:process')
25
25
  const path = require('node:path')
26
26
  const fs = require('node:fs')
27
27
  const spawnSync = require('node:child_process').spawnSync
@@ -39,7 +39,7 @@ function getBinary() {
39
39
  darwin: 'macos',
40
40
  win32: 'windows',
41
41
  cygwin: 'windows',
42
- linux: 'linux',
42
+ linux: arch === 'arm64' ? 'linux-arm64' : 'linux-x86_64',
43
43
  }[platform]
44
44
 
45
45
  const file = directory === 'windows' ? 'selenium-manager.exe' : 'selenium-manager'
package/eslint.config.js CHANGED
@@ -20,7 +20,8 @@ const noOnlyTests = require('eslint-plugin-no-only-tests')
20
20
  const js = require('@eslint/js')
21
21
  const eslintPluginPrettierRecommended = require('eslint-plugin-prettier/recommended')
22
22
  const mochaPlugin = require('eslint-plugin-mocha').default
23
- const nodePlugin = require('eslint-plugin-n').default
23
+ // eslint-plugin-n 18 is an ES module; require() returns its 'module.exports' export, which has no .default.
24
+ const nodePlugin = require('eslint-plugin-n')
24
25
 
25
26
  module.exports = [
26
27
  js.configs.recommended,
@@ -30,7 +30,7 @@ const { Options } = require('../chrome')
30
30
  try {
31
31
  driver = await new Builder()
32
32
  .forBrowser('chrome')
33
- .setChromeOptions(new Options().setMobileEmulation({ deviceName: 'Nexus 5X' }))
33
+ .setChromeOptions(new Options().setMobileEmulation({ deviceName: 'Pixel 10' }))
34
34
  .build()
35
35
  await driver.get('http://www.google.com/ncr')
36
36
  await driver.findElement(By.name('q')).sendKeys('webdriver', Key.RETURN)
package/firefox.js CHANGED
@@ -27,15 +27,13 @@
27
27
  * __Customizing the Firefox Profile__
28
28
  *
29
29
  * The profile used for each WebDriver session may be configured using the
30
- * {@linkplain Options} class. For example, you may install an extension, like
31
- * Firebug:
30
+ * {@linkplain Options} class. For example, you may set a preference:
32
31
  *
33
32
  * const {Builder} = require('selenium-webdriver');
34
33
  * const firefox = require('selenium-webdriver/firefox');
35
34
  *
36
35
  * let options = new firefox.Options()
37
- * .addExtensions('/path/to/firebug.xpi')
38
- * .setPreference('extensions.firebug.showChromeErrors', true);
36
+ * .setPreference('browser.download.folderList', 2);
39
37
  *
40
38
  * let driver = new Builder()
41
39
  * .forBrowser('firefox')
@@ -117,81 +115,16 @@ const http = require('./http')
117
115
  const io = require('./io')
118
116
  const remote = require('./remote')
119
117
  const webdriver = require('./lib/webdriver')
120
- const zip = require('./io/zip')
121
118
  const { Browser, Capabilities, Capability } = require('./lib/capabilities')
122
119
  const { Zip } = require('./io/zip')
123
120
  const { getBinaryPaths } = require('./common/driverFinder')
124
121
  const { findFreePort } = require('./net/portprober')
125
122
  const FIREFOX_CAPABILITY_KEY = 'moz:firefoxOptions'
126
123
 
127
- /**
128
- * Thrown when there an add-on is malformed.
129
- * @final
130
- */
131
- class AddonFormatError extends Error {
132
- /** @param {string} msg The error message. */
133
- constructor(msg) {
134
- super(msg)
135
- /** @override */
136
- this.name = this.constructor.name
137
- }
138
- }
139
-
140
- /**
141
- * Installs an extension to the given directory.
142
- * @param {string} extension Path to the xpi extension file to install.
143
- * @param {string} dir Path to the directory to install the extension in.
144
- * @return {!Promise<string>} A promise for the add-on ID once
145
- * installed.
146
- */
147
- async function installExtension(extension, dir) {
148
- const ext = extension.slice(-4)
149
- if (ext !== '.xpi' && ext !== '.zip') {
150
- throw Error('File name does not end in ".zip" or ".xpi": ' + ext)
151
- }
152
-
153
- let archive = await zip.load(extension)
154
- if (!archive.has('manifest.json')) {
155
- throw new AddonFormatError(`Couldn't find manifest.json in ${extension}`)
156
- }
157
-
158
- let buf = await archive.getFile('manifest.json')
159
- let parsedJSON = JSON.parse(buf.toString('utf8'))
160
-
161
- let { browser_specific_settings } =
162
- /** @type {{browser_specific_settings:{gecko:{id:string}}}} */
163
- parsedJSON
164
-
165
- if (browser_specific_settings && browser_specific_settings.gecko) {
166
- /* browser_specific_settings is an alternative to applications
167
- * It is meant to facilitate cross-browser plugins since Firefox48
168
- * see https://bugzilla.mozilla.org/show_bug.cgi?id=1262005
169
- */
170
- parsedJSON.applications = browser_specific_settings
171
- }
172
-
173
- let { applications } =
174
- /** @type {{applications:{gecko:{id:string}}}} */
175
- parsedJSON
176
- if (!(applications && applications.gecko && applications.gecko.id)) {
177
- throw new AddonFormatError(`Could not find add-on ID for ${extension}`)
178
- }
179
-
180
- await io.copy(extension, `${path.join(dir, applications.gecko.id)}.xpi`)
181
- return applications.gecko.id
182
- }
183
-
184
124
  class Profile {
185
125
  constructor() {
186
126
  /** @private {?string} */
187
127
  this.template_ = null
188
-
189
- /** @private {!Array<string>} */
190
- this.extensions_ = []
191
- }
192
-
193
- addExtensions(/** !Array<string> */ paths) {
194
- this.extensions_ = this.extensions_.concat(...paths)
195
128
  }
196
129
 
197
130
  /**
@@ -199,39 +132,21 @@ class Profile {
199
132
  * profile, or undefined if there's no data to include.
200
133
  */
201
134
  [Symbols.serialize]() {
202
- if (this.template_ || this.extensions_.length) {
203
- return buildProfile(this.template_, this.extensions_)
135
+ if (this.template_) {
136
+ return buildProfile(this.template_)
204
137
  }
205
138
  return undefined
206
139
  }
207
140
  }
208
141
 
209
142
  /**
210
- * @param {?string} template path to an existing profile to use as a template.
211
- * @param {!Array<string>} extensions paths to extensions to install in the new
212
- * profile.
143
+ * @param {string} template path to an existing profile to use as a template.
213
144
  * @return {!Promise<string>} a promise for the base64 encoded profile.
214
145
  */
215
- async function buildProfile(template, extensions) {
216
- let dir = template
217
-
218
- if (extensions.length) {
219
- dir = await io.tmpDir()
220
- if (template) {
221
- await io.copyDir(/** @type {string} */ (template), dir, /(parent\.lock|lock|\.parentlock)/)
222
- }
223
-
224
- const extensionsDir = path.join(dir, 'extensions')
225
- await io.mkdir(extensionsDir)
226
-
227
- for (let i = 0; i < extensions.length; i++) {
228
- await installExtension(extensions[i], extensionsDir)
229
- }
230
- }
231
-
146
+ async function buildProfile(template) {
232
147
  let zip = new Zip()
233
148
  return zip
234
- .addDir(dir)
149
+ .addDir(template)
235
150
  .then(() => zip.toBuffer())
236
151
  .then((buf) => buf.toString('base64'))
237
152
  }
@@ -312,18 +227,6 @@ class Options extends Capabilities {
312
227
  return this.addArguments(`--width=${width}`, `--height=${height}`)
313
228
  }
314
229
 
315
- /**
316
- * Add extensions that should be installed when starting Firefox.
317
- *
318
- * @param {...string} paths The paths to the extension XPI files to install.
319
- * @return {!Options} A self reference.
320
- * @deprecated Use {@link Driver#installAddon} instead.
321
- */
322
- addExtensions(...paths) {
323
- this.profile_().addExtensions(paths)
324
- return this
325
- }
326
-
327
230
  /**
328
231
  * @param {string} key the preference key.
329
232
  * @param {(string|number|boolean)} value the preference value.
package/index.js CHANGED
@@ -540,6 +540,14 @@ class Builder {
540
540
  return this
541
541
  }
542
542
 
543
+ /**
544
+ * @return {edge.Options} the Edge specific options currently configured
545
+ * for this builder.
546
+ */
547
+ getEdgeOptions() {
548
+ return this.edgeOptions_
549
+ }
550
+
543
551
  /**
544
552
  * Sets the {@link edge.ServiceBuilder} to use to manage the
545
553
  * MicrosoftEdgeDriver child process when creating sessions locally.
@@ -83,7 +83,16 @@ module.exports = (function findElements(target, root) {
83
83
  if (target === '') {
84
84
  throw botError(INVALID_SELECTOR, 'Unable to locate an element with the tagName ""');
85
85
  }
86
- return Array.from(root.getElementsByTagName(target));
86
+ if ('getElementsByTagName' in root) {
87
+ return Array.from(root.getElementsByTagName(target));
88
+ }
89
+ const elements = Array.from(root.querySelectorAll('*'));
90
+ if (target === '*') {
91
+ return elements;
92
+ }
93
+ const html = 'http://www.w3.org/1999/xhtml';
94
+ const folded = target.toLowerCase();
95
+ return elements.filter(el => el.namespaceURI === html ? el.localName === folded : el.localName === target);
87
96
  }
88
97
  const DEFAULT_NS_RESOLVER = (function () {
89
98
  const ns = { svg: 'http://www.w3.org/2000/svg' };
@@ -166,7 +175,10 @@ module.exports = (function findElements(target, root) {
166
175
  return resolveAnchor(selector());
167
176
  }
168
177
  if (selector && typeof selector === 'object') {
169
- const found = findElements(selector);
178
+ let found = root ? findElements(selector, root) : [];
179
+ if (!found.length) {
180
+ found = findElements(selector);
181
+ }
170
182
  if (!found.length) {
171
183
  throw botError(NO_SUCH_ELEMENT, 'No element has been found by ' + JSON.stringify(selector));
172
184
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "selenium-webdriver",
3
- "version": "4.48.0",
3
+ "version": "4.50.0",
4
4
  "description": "The official WebDriver JavaScript bindings from the Selenium project",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -24,29 +24,29 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@bazel/runfiles": "^6.5.0",
27
- "jszip": "^3.10.1",
27
+ "jszip": "^3.10.2",
28
28
  "tmp": "^0.2.7",
29
- "ws": "^8.21.3"
29
+ "ws": "^8.22.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@eslint/js": "^10.0.1",
33
- "cddl": "^0.21.1",
34
- "cddl2ts": "^0.10.1",
33
+ "cddl": "^0.21.2",
34
+ "cddl2ts": "^0.10.2",
35
35
  "clean-jsdoc-theme": "^4.3.3",
36
- "eslint": "^10.8.1",
36
+ "eslint": "^10.11.0",
37
37
  "eslint-config-prettier": "^10.1.8",
38
38
  "eslint-plugin-mocha": "^11.3.0",
39
- "eslint-plugin-n": "^18.3.0",
39
+ "eslint-plugin-n": "^18.4.0",
40
40
  "eslint-plugin-no-only-tests": "^3.4.0",
41
41
  "eslint-plugin-prettier": "^5.5.6",
42
42
  "express": "^5.2.1",
43
- "globals": "^17.9.0",
43
+ "globals": "^17.12.0",
44
44
  "has-flag": "^5.0.1",
45
45
  "jsdoc": "^4.0.5",
46
46
  "mocha": "^11.8.0",
47
47
  "mocha-junit-reporter": "^2.2.1",
48
- "multer": "^2.2.0",
49
- "prettier": "^3.9.6",
48
+ "multer": "^2.4.0",
49
+ "prettier": "^3.9.9",
50
50
  "serve-index": "^1.9.2",
51
51
  "sinon": "^22.1.0",
52
52
  "supports-color": "^10.2.2"
package/remote/index.js CHANGED
@@ -471,6 +471,8 @@ class SeleniumServer extends DriverService {
471
471
  throw Error('Port must be >= 0: ' + options.port)
472
472
  }
473
473
 
474
+ const java = options.java || getJavaPath()
475
+
474
476
  let port = options.port || portprober.findFreePort()
475
477
  let args = Promise.all([port, options.jvmArgs || [], options.args || []]).then((resolved) => {
476
478
  let port = resolved[0]
@@ -479,11 +481,9 @@ class SeleniumServer extends DriverService {
479
481
 
480
482
  const fullArgsList = jvmArgs.concat('-jar', jar, '-port', port).concat(args)
481
483
 
482
- return formatSpawnArgs(jar, fullArgsList)
484
+ return formatSpawnArgs(jar, fullArgsList, java)
483
485
  })
484
486
 
485
- const java = getJavaPath()
486
-
487
487
  super(java, {
488
488
  loopback: options.loopback,
489
489
  port: port,
@@ -536,6 +536,15 @@ SeleniumServer.Options = class {
536
536
  */
537
537
  this.jvmArgs
538
538
 
539
+ /**
540
+ * Path to the java executable that runs the server. Defaults to
541
+ * `$JAVA_HOME/bin/java` when `JAVA_HOME` is set, otherwise `java` on the
542
+ * `PATH`.
543
+ *
544
+ * @type {(string|undefined)}
545
+ */
546
+ this.java
547
+
539
548
  /**
540
549
  * The environment variables that should be visible to the server
541
550
  * process. Defaults to inheriting the current process's environment.
package/remote/util.js CHANGED
@@ -31,11 +31,10 @@ function getJavaPath() {
31
31
 
32
32
  /**
33
33
  * @param {string} seleniumStandalonePath path to standalone server
34
+ * @param {string=} javaPath the java executable; defaults to {@link getJavaPath}
34
35
  * @returns {boolean}
35
36
  */
36
- function isSelenium3x(seleniumStandalonePath) {
37
- const javaPath = getJavaPath()
38
-
37
+ function isSelenium3x(seleniumStandalonePath, javaPath = getJavaPath()) {
39
38
  const execRes = cp.execFileSync(javaPath, ['-jar', seleniumStandalonePath, '--version'])
40
39
 
41
40
  return execRes.toString().trim().startsWith('Selenium server version: 3')
@@ -44,11 +43,12 @@ function isSelenium3x(seleniumStandalonePath) {
44
43
  /**
45
44
  * @param {string} seleniumStandalonePath path to standalone server
46
45
  * @param {Array.<string>} args spawn arguments array
46
+ * @param {string=} javaPath the java executable; defaults to {@link getJavaPath}
47
47
  * returns formatted args based on selenium standalone server version
48
48
  * @returns {Array.<string>}
49
49
  */
50
- function formatSpawnArgs(seleniumStandalonePath, args) {
51
- if (isSelenium3x(seleniumStandalonePath)) {
50
+ function formatSpawnArgs(seleniumStandalonePath, args, javaPath = getJavaPath()) {
51
+ if (isSelenium3x(seleniumStandalonePath, javaPath)) {
52
52
  logging
53
53
  .getLogger(logging.Type.SERVER)
54
54
  .warning('Deprecation: Support for Standalone Server 3.x will be removed soon. Please update to version 4.x')
package/testing/index.js CHANGED
@@ -218,6 +218,7 @@ function init(force = false) {
218
218
  seleniumJar = process.env['SELENIUM_SERVER_JAR']
219
219
  seleniumUrl = process.env['SELENIUM_REMOTE_URL']
220
220
  if (seleniumJar) {
221
+ seleniumJar = locate(seleniumJar)
221
222
  info(`Using Selenium server jar: ${seleniumJar}`)
222
223
  }
223
224
 
@@ -289,6 +290,19 @@ class Environment {
289
290
  return () => browsersToIgnore.indexOf(this.browser.name) !== -1
290
291
  }
291
292
 
293
+ /**
294
+ * Returns a predicate function that will suppress tests in this environment
295
+ * when it runs them through a Selenium server (such as a Grid) rather than a
296
+ * browser driver started locally.
297
+ *
298
+ * ignore(env.remote()).it('reads a browser preference', ...)
299
+ *
300
+ * @return {function(): boolean} a new predicate function.
301
+ */
302
+ remote() {
303
+ return () => URL_MAP.get(this) !== null
304
+ }
305
+
292
306
  /**
293
307
  * @return {!Builder} a new WebDriver builder configured to target this
294
308
  * environment's {@linkplain #browser browser}.
@@ -315,6 +329,19 @@ class Environment {
315
329
  builder.setChromeOptions(options)
316
330
  }
317
331
  // Edge
332
+ if ('SE_EDGEDRIVER' in process.env) {
333
+ const found = locate(process.env.SE_EDGEDRIVER)
334
+ const service = new edge.ServiceBuilder(found)
335
+ builder.setEdgeService(service)
336
+ }
337
+ if ('SE_EDGE' in process.env) {
338
+ const binary = locate(process.env.SE_EDGE)
339
+ const options = new edge.Options()
340
+ options.setBinaryPath(binary)
341
+ options.setAcceptInsecureCerts(true)
342
+ options.addArguments('disable-infobars', 'disable-breakpad', 'disable-dev-shm-usage', 'no-sandbox')
343
+ builder.setEdgeOptions(options)
344
+ }
318
345
  // Firefox
319
346
  if ('SE_GECKODRIVER' in process.env) {
320
347
  const found = locate(process.env.SE_GECKODRIVER)
@@ -444,7 +471,11 @@ function suite(fn, options = undefined) {
444
471
 
445
472
  describe(`[${browser.name}]`, function () {
446
473
  if (!seleniumUrl && seleniumJar && !seleniumServer) {
447
- seleniumServer = new remote.SeleniumServer(seleniumJar)
474
+ seleniumServer = new remote.SeleniumServer(seleniumJar, {
475
+ java: bazelJava(),
476
+ loopback: true,
477
+ args: ['--host', '127.0.0.1', ...pinnedGridArgs(targetBrowsers.map((b) => b.name))],
478
+ })
448
479
 
449
480
  const startTimeout = 65 * 1000
450
481
 
@@ -547,6 +578,58 @@ function getTestHook(name) {
547
578
  return fn
548
579
  }
549
580
 
581
+ /**
582
+ * Bazel's hermetic java, from `SE_BAZEL_JAVA_LOCATION` (a file holding the `$(JAVA)` path).
583
+ * @return {(string|undefined)} the java executable, or undefined to use the server's default.
584
+ */
585
+ function bazelJava() {
586
+ const javaLocation = process.env['SE_BAZEL_JAVA_LOCATION']
587
+ if (!javaLocation) {
588
+ return undefined
589
+ }
590
+ // $(JAVA) is an exec path (external/<repo>/...); without the prefix it is a runfiles path.
591
+ const execPath = fs.readFileSync(locate(javaLocation), { encoding: 'utf8' }).trim()
592
+ const java = locate(execPath.replace(/^external\//, ''))
593
+ // Resolve the JDK symlink to its real path to dodge a Windows JVM bug mapping lib\modules.
594
+ return process.platform === 'win32' ? fs.realpathSync(java) : java
595
+ }
596
+
597
+ /** Browser name -> [driver env var, browser env var, vendor options key], for pinned browsers. */
598
+ const PINNED_BROWSERS = {
599
+ [Browser.CHROME]: ['SE_CHROMEDRIVER', 'SE_CHROME', 'goog:chromeOptions'],
600
+ [Browser.FIREFOX]: ['SE_GECKODRIVER', 'SE_FIREFOX', 'moz:firefoxOptions'],
601
+ [Browser.EDGE]: ['SE_EDGEDRIVER', 'SE_EDGE', 'ms:edgeOptions'],
602
+ }
603
+
604
+ /**
605
+ * Pins the Grid node to the Bazel-provided driver and browser; on CI, Selenium Manager would find
606
+ * no installed browser.
607
+ * @param {!Array<string>} browserNames the browsers sharing the server.
608
+ * @return {!Array<string>} empty to keep driver detection.
609
+ */
610
+ function pinnedGridArgs(browserNames) {
611
+ // The command line merges repeated --driver-configuration flags into one, so only a single
612
+ // browser can be pinned; with several, detection must stay on to give each one a slot.
613
+ if (browserNames.length !== 1) {
614
+ return []
615
+ }
616
+ const [browserName] = browserNames
617
+ const [driverVar, browserVar, vendorKey] = PINNED_BROWSERS[browserName] ?? []
618
+ if (!process.env[driverVar] || !process.env[browserVar]) {
619
+ return []
620
+ }
621
+ const stereotype = JSON.stringify({ browserName, [vendorKey]: { binary: locate(process.env[browserVar]) } })
622
+ return [
623
+ '--detect-drivers',
624
+ 'false',
625
+ '--driver-configuration',
626
+ `display-name=${browserName}`,
627
+ 'max-sessions=1',
628
+ `webdriver-executable=${locate(process.env[driverVar])}`,
629
+ `stereotype=${stereotype}`,
630
+ ]
631
+ }
632
+
550
633
  function locate(fileLike) {
551
634
  if (fs.existsSync(fileLike)) {
552
635
  return fileLike