Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/javascript/selenium-webdriver/testing/index.js
2884 views
1
// Licensed to the Software Freedom Conservancy (SFC) under one
2
// or more contributor license agreements. See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership. The SFC licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License. You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied. See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
/**
19
* @fileoverview Provides extensions for
20
* [Jasmine](https://jasmine.github.io) and [Mocha](https://mochajs.org).
21
*
22
* You may conditionally suppress a test function using the exported
23
* "ignore" function. If the provided predicate returns true, the attached
24
* test case will be skipped:
25
*
26
* test.ignore(maybe()).it('is flaky', function() {
27
* if (Math.random() < 0.5) throw Error();
28
* });
29
*
30
* function maybe() { return Math.random() < 0.5; }
31
*/
32
33
'use strict'
34
35
const fs = require('node:fs')
36
const path = require('node:path')
37
const { isatty } = require('node:tty')
38
const chrome = require('../chrome')
39
const edge = require('../edge')
40
const firefox = require('../firefox')
41
const ie = require('../ie')
42
const remote = require('../remote')
43
const safari = require('../safari')
44
const { Browser } = require('../lib/capabilities')
45
const { Builder } = require('../index')
46
const { getBinaryPaths } = require('../common/driverFinder')
47
48
let runfiles
49
try {
50
// Attempt to require @bazel/runfiles
51
runfiles = require('@bazel/runfiles').runfiles
52
} catch {
53
// Fall through
54
}
55
56
/**
57
* Describes a browser targeted by a {@linkplain suite test suite}.
58
* @record
59
*/
60
function TargetBrowser() {}
61
62
/**
63
* The {@linkplain Browser name} of the targeted browser.
64
* @type {string}
65
*/
66
TargetBrowser.prototype.name
67
68
/**
69
* The specific version of the targeted browser, if any.
70
* @type {(string|undefined)}
71
*/
72
TargetBrowser.prototype.version
73
74
/**
75
* The specific {@linkplain ../lib/capabilities.Platform platform} for the
76
* targeted browser, if any.
77
* @type {(string|undefined)}.
78
*/
79
TargetBrowser.prototype.platform
80
81
/** @suppress {checkTypes} */
82
function color(c, s) {
83
return isatty(process.stdout) ? `\u001b[${c}m${s}\u001b[0m` : s
84
}
85
86
function green(s) {
87
return color(32, s)
88
}
89
90
function cyan(s) {
91
return color(36, s)
92
}
93
94
function info(msg) {
95
console.info(`${green('[INFO]')} ${msg}`)
96
}
97
98
function warn(msg) {
99
console.warn(`${cyan('[WARNING]')} ${msg}`)
100
}
101
102
/**
103
* Extracts the browsers for a test suite to target from the `SELENIUM_BROWSER`
104
* environment variable.
105
*
106
* @return {{name: string, version: string, platform: string}}[] the browsers to target.
107
*/
108
function getBrowsersToTestFromEnv() {
109
let browsers = process.env['SELENIUM_BROWSER']
110
if (!browsers) {
111
return []
112
}
113
return browsers.split(',').map((spec) => {
114
const parts = spec.split(/:/, 3)
115
let name = parts[0]
116
if (name === 'ie') {
117
name = Browser.INTERNET_EXPLORER
118
} else if (name === 'edge') {
119
name = Browser.EDGE
120
}
121
let version = parts[1]
122
let platform = parts[2]
123
return { name, version, platform }
124
})
125
}
126
127
/**
128
* @return {!Array<!TargetBrowser>} the browsers available for testing on this
129
* system.
130
*/
131
function getAvailableBrowsers() {
132
info(`Searching for WebDriver executables installed on the current system...`)
133
134
let targets = [
135
[getBinaryPaths(new chrome.Options()), Browser.CHROME],
136
[getBinaryPaths(new edge.Options()), Browser.EDGE],
137
[getBinaryPaths(new firefox.Options()), Browser.FIREFOX],
138
]
139
if (process.platform === 'win32') {
140
targets.push([getBinaryPaths(new ie.Options()), Browser.INTERNET_EXPLORER])
141
}
142
if (process.platform === 'darwin') {
143
targets.push([getBinaryPaths(new safari.Options()), Browser.SAFARI])
144
}
145
146
let availableBrowsers = []
147
for (let pair of targets) {
148
const driverPath = pair[0].driverPath
149
const browserPath = pair[0].browserPath
150
const name = pair[1]
151
const capabilities = pair[2]
152
if (driverPath.length > 0 && browserPath && browserPath.length > 0) {
153
info(`... located ${name}`)
154
availableBrowsers.push({ name, capabilities })
155
}
156
}
157
158
if (availableBrowsers.length === 0) {
159
warn(`Unable to locate any WebDriver executables for testing`)
160
}
161
162
return availableBrowsers
163
}
164
165
let wasInit
166
let targetBrowsers
167
let seleniumJar
168
let seleniumUrl
169
let seleniumServer
170
171
/**
172
* Initializes this module by determining which browsers a
173
* {@linkplain ./index.suite test suite} should run against. The default
174
* behavior is to run tests against every browser with a WebDriver executables
175
* (chromedriver, firefoxdriver, etc.) are installed on the system by `PATH`.
176
*
177
* Specific browsers can be selected at runtime by setting the
178
* `SELENIUM_BROWSER` environment variable. This environment variable has the
179
* same semantics as with the WebDriver {@link ../index.Builder Builder},
180
* except you may use a comma-delimited list to run against multiple browsers:
181
*
182
* SELENIUM_BROWSER=chrome,firefox mocha --recursive tests/
183
*
184
* The `SELENIUM_REMOTE_URL` environment variable may be set to configure tests
185
* to run against an externally managed (usually remote) Selenium server. When
186
* set, the WebDriver builder provided by each
187
* {@linkplain TestEnvironment#builder TestEnvironment} will automatically be
188
* configured to use this server instead of starting a browser driver locally.
189
*
190
* The `SELENIUM_SERVER_JAR` environment variable may be set to the path of a
191
* standalone Selenium server on the local machine that should be used for
192
* WebDriver sessions. When set, the WebDriver builder provided by each
193
* {@linkplain TestEnvironment} will automatically be configured to use the
194
* started server instead of using a browser driver directly. It should only be
195
* necessary to set the `SELENIUM_SERVER_JAR` when testing locally against
196
* browsers not natively supported by the WebDriver
197
* {@link ../index.Builder Builder}.
198
*
199
* When either of the `SELENIUM_REMOTE_URL` or `SELENIUM_SERVER_JAR` environment
200
* variables are set, the `SELENIUM_BROWSER` variable must also be set.
201
*
202
* @param {boolean=} force whether to force this module to re-initialize and
203
* scan `process.env` again to determine which browsers to run tests
204
* against.
205
*/
206
function init(force = false) {
207
if (wasInit && !force) {
208
return
209
}
210
wasInit = true
211
212
// If force re-init, kill the current server if there is one.
213
if (seleniumServer) {
214
seleniumServer.kill()
215
seleniumServer = null
216
}
217
218
seleniumJar = process.env['SELENIUM_SERVER_JAR']
219
seleniumUrl = process.env['SELENIUM_REMOTE_URL']
220
if (seleniumJar) {
221
info(`Using Selenium server jar: ${seleniumJar}`)
222
}
223
224
if (seleniumUrl) {
225
info(`Using Selenium remote end: ${seleniumUrl}`)
226
}
227
228
if (seleniumJar && seleniumUrl) {
229
throw Error(
230
'Ambiguous test configuration: both SELENIUM_REMOTE_URL' +
231
' && SELENIUM_SERVER_JAR environment variables are set',
232
)
233
}
234
235
const envBrowsers = getBrowsersToTestFromEnv()
236
if ((seleniumJar || seleniumUrl) && envBrowsers.length === 0) {
237
throw Error(
238
'Ambiguous test configuration: when either the SELENIUM_REMOTE_URL or' +
239
' SELENIUM_SERVER_JAR environment variable is set, the' +
240
' SELENIUM_BROWSER variable must also be set.',
241
)
242
}
243
244
targetBrowsers = envBrowsers.length > 0 ? envBrowsers : getAvailableBrowsers()
245
info(`Running tests against [${targetBrowsers.map((b) => b.name).join(', ')}]`)
246
247
after(function () {
248
if (seleniumServer) {
249
return seleniumServer.kill()
250
}
251
})
252
}
253
254
const TARGET_MAP = /** !WeakMap<!Environment, !TargetBrowser> */ new WeakMap()
255
const URL_MAP = /** !WeakMap<!Environment, ?(string|remote.SeleniumServer)> */ new WeakMap()
256
257
/**
258
* Defines the environment a {@linkplain suite test suite} is running against.
259
* @final
260
*/
261
class Environment {
262
/**
263
* @param {!TargetBrowser} browser the browser targeted in this environment.
264
* @param {?(string|remote.SeleniumServer)=} url remote URL of an existing
265
* Selenium server to test against.
266
*/
267
constructor(browser, url = undefined) {
268
browser = /** @type {!TargetBrowser} */ (Object.seal(Object.assign({}, browser)))
269
270
TARGET_MAP.set(this, browser)
271
URL_MAP.set(this, url || null)
272
}
273
274
/** @return {!TargetBrowser} the target browser for this test environment. */
275
get browser() {
276
return TARGET_MAP.get(this)
277
}
278
279
/**
280
* Returns a predicate function that will suppress tests in this environment
281
* if the {@linkplain #browser current browser} is in the list of
282
* `browsersToIgnore`.
283
*
284
* @param {...(string|!Browser)} browsersToIgnore the browsers that should
285
* be ignored.
286
* @return {function(): boolean} a new predicate function.
287
*/
288
browsers(...browsersToIgnore) {
289
return () => browsersToIgnore.indexOf(this.browser.name) !== -1
290
}
291
292
/**
293
* @return {!Builder} a new WebDriver builder configured to target this
294
* environment's {@linkplain #browser browser}.
295
*/
296
builder() {
297
const browser = this.browser
298
const urlOrServer = URL_MAP.get(this)
299
300
const builder = new Builder()
301
302
// Sniff the environment variables for paths to use for the common browsers
303
// Chrome
304
if ('SE_CHROMEDRIVER' in process.env) {
305
const found = locate(process.env.SE_CHROMEDRIVER)
306
const service = new chrome.ServiceBuilder(found)
307
builder.setChromeService(service)
308
}
309
if ('SE_CHROME' in process.env) {
310
const binary = locate(process.env.SE_CHROME)
311
const options = new chrome.Options()
312
options.setChromeBinaryPath(binary)
313
options.setAcceptInsecureCerts(true)
314
options.addArguments('disable-infobars', 'disable-breakpad', 'disable-dev-shm-usage', 'no-sandbox')
315
builder.setChromeOptions(options)
316
}
317
// Edge
318
// Firefox
319
if ('SE_GECKODRIVER' in process.env) {
320
const found = locate(process.env.SE_GECKODRIVER)
321
const service = new firefox.ServiceBuilder(found)
322
builder.setFirefoxService(service)
323
}
324
if ('SE_FIREFOX' in process.env) {
325
const binary = locate(process.env.SE_FIREFOX)
326
const options = new firefox.Options()
327
options.enableBidi()
328
options.setBinary(binary)
329
builder.setFirefoxOptions(options)
330
}
331
332
builder.disableEnvironmentOverrides()
333
334
const realBuild = builder.build
335
builder.build = function () {
336
builder.forBrowser(browser.name, browser.version, browser.platform)
337
338
if (browser.capabilities) {
339
builder.getCapabilities().merge(browser.capabilities)
340
}
341
342
if (browser.name === 'firefox') {
343
builder.setCapability('moz:debuggerAddress', true)
344
}
345
346
// Enable BiDi for supporting browsers.
347
if (browser.name === Browser.FIREFOX || browser.name === Browser.CHROME || browser.name === Browser.EDGE) {
348
builder.setCapability('webSocketUrl', true)
349
builder.setCapability('unhandledPromptBehavior', 'ignore')
350
}
351
352
if (typeof urlOrServer === 'string') {
353
builder.usingServer(urlOrServer)
354
} else if (urlOrServer) {
355
builder.usingServer(urlOrServer.address())
356
}
357
return realBuild.call(builder)
358
}
359
360
return builder
361
}
362
}
363
364
/**
365
* Configuration options for a {@linkplain ./index.suite test suite}.
366
* @record
367
*/
368
function SuiteOptions() {}
369
370
/**
371
* The browsers to run the test suite against.
372
* @type {!Array<!(Browser|TargetBrowser)>}
373
*/
374
SuiteOptions.prototype.browsers
375
376
let inSuite = false
377
378
/**
379
* Defines a test suite by calling the provided function once for each of the
380
* target browsers. If a suite is not limited to a specific set of browsers in
381
* the provided {@linkplain ./index.SuiteOptions suite options}, the suite will
382
* be configured to run against each of the {@linkplain ./index.init runtime
383
* target browsers}.
384
*
385
* Sample usage:
386
*
387
* const {By, Key, until} = require('selenium-webdriver');
388
* const {suite} = require('selenium-webdriver/testing');
389
*
390
* suite(function(env) {
391
* describe('Google Search', function() {
392
* let driver;
393
*
394
* before(async function() {
395
* driver = await env.builder().build();
396
* });
397
*
398
* after(() => driver.quit());
399
*
400
* it('demo', async function() {
401
* await driver.get('http://www.google.com/ncr');
402
*
403
* let q = await driver.findElement(By.name('q'));
404
* await q.sendKeys('webdriver', Key.RETURN);
405
* await driver.wait(
406
* until.titleIs('webdriver - Google Search'), 1000);
407
* });
408
* });
409
* });
410
*
411
* By default, this example suite will run against every WebDriver-enabled
412
* browser on the current system. Alternatively, the `SELENIUM_BROWSER`
413
* environment variable may be used to run against a specific browser:
414
*
415
* SELENIUM_BROWSER=firefox mocha -t 120000 example_test.js
416
*
417
* @param {function(!Environment)} fn the function to call to build the test
418
* suite.
419
* @param {SuiteOptions=} options configuration options.
420
*/
421
function suite(fn, options = undefined) {
422
if (inSuite) {
423
throw Error('Calls to suite() may not be nested')
424
}
425
try {
426
init()
427
inSuite = true
428
429
const suiteBrowsers = new Map()
430
if (options && options.browsers) {
431
for (let browser of options.browsers) {
432
if (typeof browser === 'string') {
433
suiteBrowsers.set(browser, { name: browser })
434
} else {
435
suiteBrowsers.set(browser.name, browser)
436
}
437
}
438
}
439
440
for (let browser of targetBrowsers) {
441
if (suiteBrowsers.size > 0 && !suiteBrowsers.has(browser.name)) {
442
continue
443
}
444
445
describe(`[${browser.name}]`, function () {
446
if (!seleniumUrl && seleniumJar && !seleniumServer) {
447
seleniumServer = new remote.SeleniumServer(seleniumJar)
448
449
const startTimeout = 65 * 1000
450
451
function startSelenium() {
452
if (typeof this.timeout === 'function') {
453
this.timeout(startTimeout) // For mocha.
454
}
455
456
info(`Starting selenium server ${seleniumJar}`)
457
return seleniumServer.start(60 * 1000)
458
}
459
460
const /** !Function */ beforeHook = global.beforeAll || global.before
461
beforeHook(startSelenium, startTimeout)
462
}
463
464
fn(new Environment(browser, seleniumUrl || seleniumServer))
465
})
466
}
467
} finally {
468
inSuite = false
469
}
470
}
471
472
/**
473
* Returns an object with wrappers for the standard mocha/jasmine test
474
* functions: `describe` and `it`, which will redirect to `xdescribe` and `xit`,
475
* respectively, if provided predicate function returns false.
476
*
477
* Sample usage:
478
*
479
* const {Browser} = require('selenium-webdriver');
480
* const {suite, ignore} = require('selenium-webdriver/testing');
481
*
482
* suite(function(env) {
483
*
484
* // Skip tests the current environment targets Chrome.
485
* ignore(env.browsers(Browser.CHROME)).
486
* describe('something', async function() {
487
* let driver = await env.builder().build();
488
* // etc.
489
* });
490
* });
491
*
492
* @param {function(): boolean} predicateFn A predicate to call to determine
493
* if the test should be suppressed. This function MUST be synchronous.
494
* @return {{describe: !Function, it: !Function}} an object with wrapped
495
* versions of the `describe` and `it` test functions.
496
*/
497
function ignore(predicateFn) {
498
const isJasmine = global.jasmine && typeof global.jasmine === 'object'
499
500
const hooks = {
501
describe: getTestHook('describe'),
502
xdescribe: getTestHook('xdescribe'),
503
it: getTestHook('it'),
504
xit: getTestHook('xit'),
505
}
506
hooks.fdescribe = isJasmine ? getTestHook('fdescribe') : hooks.describe.only
507
hooks.fit = isJasmine ? getTestHook('fit') : hooks.it.only
508
509
let describe = wrap(hooks.xdescribe, hooks.describe)
510
let fdescribe = wrap(hooks.xdescribe, hooks.fdescribe)
511
//eslint-disable-next-line no-only-tests/no-only-tests
512
describe.only = fdescribe
513
514
let it = wrap(hooks.xit, hooks.it)
515
let fit = wrap(hooks.xit, hooks.fit)
516
//eslint-disable-next-line no-only-tests/no-only-tests
517
it.only = fit
518
519
return { describe, it }
520
521
function wrap(onSkip, onRun) {
522
return function (...args) {
523
if (predicateFn()) {
524
onSkip(...args)
525
} else {
526
onRun(...args)
527
}
528
}
529
}
530
}
531
532
/**
533
* @param {string} name
534
* @return {!Function}
535
* @throws {TypeError}
536
*/
537
function getTestHook(name) {
538
let fn = global[name]
539
let type = typeof fn
540
if (type !== 'function') {
541
throw TypeError(
542
`Expected global.${name} to be a function, but is ${type}.` +
543
' This can happen if you try using this module when running with' +
544
' node directly instead of using jasmine or mocha',
545
)
546
}
547
return fn
548
}
549
550
function locate(fileLike) {
551
if (fs.existsSync(fileLike)) {
552
return fileLike
553
}
554
555
if (!runfiles) {
556
throw new Error('Unable to find ' + fileLike)
557
}
558
559
try {
560
return runfiles.resolve(fileLike)
561
} catch {
562
// Fall through
563
}
564
565
// Is the item in the workspace?
566
try {
567
return runfiles.resolveWorkspaceRelative(fileLike)
568
} catch {
569
// Fall through
570
}
571
572
// Find the repo mapping file
573
let repoMappingFile
574
try {
575
repoMappingFile = runfiles.resolve('_repo_mapping')
576
} catch {
577
throw new Error('Unable to locate (no repo mapping file): ' + fileLike)
578
}
579
const lines = fs.readFileSync(repoMappingFile, { encoding: 'utf8' }).split('\n')
580
581
// Build a map of "repo we declared we need" to "path"
582
const mapping = {}
583
for (const line of lines) {
584
if (line.startsWith(',')) {
585
const parts = line.split(',', 3)
586
mapping[parts[1]] = parts[2]
587
}
588
}
589
590
// Get the first segment of the path
591
const pathSegments = fileLike.split('/')
592
if (!pathSegments.length) {
593
throw new Error('Unable to locate ' + fileLike)
594
}
595
596
pathSegments[0] = mapping[pathSegments[0]] ? mapping[pathSegments[0]] : '_main'
597
598
try {
599
return runfiles.resolve(path.join(...pathSegments))
600
} catch {
601
// Fall through
602
}
603
604
throw new Error('Unable to find ' + fileLike)
605
}
606
607
// PUBLIC API
608
609
module.exports = {
610
Environment,
611
SuiteOptions,
612
init,
613
ignore,
614
suite,
615
}
616
617