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 |
|
34 |
|
35 | const { isatty } = require('tty')
|
36 | const chrome = require('../chrome')
|
37 | const edge = require('../edge')
|
38 | const firefox = require('../firefox')
|
39 | const ie = require('../ie')
|
40 | const remote = require('../remote')
|
41 | const safari = require('../safari')
|
42 | const opera = require('../opera')
|
43 | const { Browser, Capabilities } = require('../lib/capabilities')
|
44 | const { Builder } = require('../index')
|
45 |
|
46 | /**
|
47 | * Describes a browser targeted by a {@linkplain suite test suite}.
|
48 | * @record
|
49 | */
|
50 | function TargetBrowser() { }
|
51 |
|
52 | /**
|
53 | * The {@linkplain Browser name} of the targeted browser.
|
54 | * @type {string}
|
55 | */
|
56 | TargetBrowser.prototype.name
|
57 |
|
58 | /**
|
59 | * The specific version of the targeted browser, if any.
|
60 | * @type {(string|undefined)}
|
61 | */
|
62 | TargetBrowser.prototype.version
|
63 |
|
64 | /**
|
65 | * The specific {@linkplain ../lib/capabilities.Platform platform} for the
|
66 | * targeted browser, if any.
|
67 | * @type {(string|undefined)}.
|
68 | */
|
69 | TargetBrowser.prototype.platform
|
70 |
|
71 | /** @suppress {checkTypes} */
|
72 | function color(c, s) {
|
73 | return isatty(process.stdout) ? `\u001b[${c}m${s}\u001b[0m` : s
|
74 | }
|
75 | function green(s) {
|
76 | return color(32, s)
|
77 | }
|
78 | function cyan(s) {
|
79 | return color(36, s)
|
80 | }
|
81 | function info(msg) {
|
82 | console.info(`${green('[INFO]')} ${msg}`)
|
83 | }
|
84 | function warn(msg) {
|
85 | console.warn(`${cyan('[WARNING]')} ${msg}`)
|
86 | }
|
87 |
|
88 | /**
|
89 | * Extracts the browsers for a test suite to target from the `SELENIUM_BROWSER`
|
90 | * environment variable.
|
91 | *
|
92 | * @return {!Array<!TargetBrowser>} the browsers to target.
|
93 | */
|
94 | function getBrowsersToTestFromEnv() {
|
95 | let browsers = process.env['SELENIUM_BROWSER']
|
96 | if (!browsers) {
|
97 | return []
|
98 | }
|
99 | return browsers.split(',').map((spec) => {
|
100 | const parts = spec.split(/:/, 3)
|
101 | let name = parts[0]
|
102 | if (name === 'ie') {
|
103 | name = Browser.INTERNET_EXPLORER
|
104 | } else if (name === 'edge') {
|
105 | name = Browser.EDGE
|
106 | }
|
107 | let version = parts[1]
|
108 | let platform = parts[2]
|
109 | return { name, version, platform }
|
110 | })
|
111 | }
|
112 |
|
113 | /**
|
114 | * @return {!Array<!TargetBrowser>} the browsers available for testing on this
|
115 | * system.
|
116 | */
|
117 | function getAvailableBrowsers() {
|
118 | info(`Searching for WebDriver executables installed on the current system...`)
|
119 |
|
120 | let targets = [
|
121 | [chrome.locateSynchronously, Browser.CHROME],
|
122 | [edge.locateSynchronously, Browser.EDGE],
|
123 | [firefox.locateSynchronously, Browser.FIREFOX],
|
124 | [ie.locateSynchronously, Browser.INTERNET_EXPLORER],
|
125 | [safari.locateSynchronously, Browser.SAFARI],
|
126 | [opera.locateSynchronously, Browser.OPERA],
|
127 | ]
|
128 |
|
129 | let availableBrowsers = []
|
130 | for (let pair of targets) {
|
131 | const fn = pair[0]
|
132 | const name = pair[1]
|
133 | const capabilities = pair[2]
|
134 | if (fn()) {
|
135 | info(`... located ${name}`)
|
136 | availableBrowsers.push({ name, capabilities })
|
137 | }
|
138 | }
|
139 |
|
140 | if (availableBrowsers.length === 0) {
|
141 | warn(`Unable to locate any WebDriver executables for testing`)
|
142 | }
|
143 |
|
144 | return availableBrowsers
|
145 | }
|
146 |
|
147 | let wasInit
|
148 | let targetBrowsers
|
149 | let seleniumJar
|
150 | let seleniumUrl
|
151 | let seleniumServer
|
152 |
|
153 | /**
|
154 | * Initializes this module by determining which browsers a
|
155 | * {@linkplain ./index.suite test suite} should run against. The default
|
156 | * behavior is to run tests against every browser with a WebDriver executables
|
157 | * (chromedriver, firefoxdriver, etc.) are installed on the system by `PATH`.
|
158 | *
|
159 | * Specific browsers can be selected at runtime by setting the
|
160 | * `SELENIUM_BROWSER` environment variable. This environment variable has the
|
161 | * same semantics as with the WebDriver {@link ../index.Builder Builder},
|
162 | * except you may use a comma-delimited list to run against multiple browsers:
|
163 | *
|
164 | * SELENIUM_BROWSER=chrome,firefox mocha --recursive tests/
|
165 | *
|
166 | * The `SELENIUM_REMOTE_URL` environment variable may be set to configure tests
|
167 | * to run against an externally managed (usually remote) Selenium server. When
|
168 | * set, the WebDriver builder provided by each
|
169 | * {@linkplain TestEnvironment#builder TestEnvironment} will automatically be
|
170 | * configured to use this server instead of starting a browser driver locally.
|
171 | *
|
172 | * The `SELENIUM_SERVER_JAR` environment variable may be set to the path of a
|
173 | * standalone Selenium server on the local machine that should be used for
|
174 | * WebDriver sessions. When set, the WebDriver builder provided by each
|
175 | * {@linkplain TestEnvironment} will automatically be configured to use the
|
176 | * started server instead of using a browser driver directly. It should only be
|
177 | * necessary to set the `SELENIUM_SERVER_JAR` when testing locally against
|
178 | * browsers not natively supported by the WebDriver
|
179 | * {@link ../index.Builder Builder}.
|
180 | *
|
181 | * When either of the `SELENIUM_REMOTE_URL` or `SELENIUM_SERVER_JAR` environment
|
182 | * variables are set, the `SELENIUM_BROWSER` variable must also be set.
|
183 | *
|
184 | * @param {boolean=} force whether to force this module to re-initialize and
|
185 | * scan `process.env` again to determine which browsers to run tests
|
186 | * against.
|
187 | */
|
188 | function init(force = false) {
|
189 | if (wasInit && !force) {
|
190 | return
|
191 | }
|
192 | wasInit = true
|
193 |
|
194 | // If force re-init, kill the current server if there is one.
|
195 | if (seleniumServer) {
|
196 | seleniumServer.kill()
|
197 | seleniumServer = null
|
198 | }
|
199 |
|
200 | seleniumJar = process.env['SELENIUM_SERVER_JAR']
|
201 | seleniumUrl = process.env['SELENIUM_REMOTE_URL']
|
202 | if (seleniumJar) {
|
203 | info(`Using Selenium server jar: ${seleniumJar}`)
|
204 | }
|
205 |
|
206 | if (seleniumUrl) {
|
207 | info(`Using Selenium remote end: ${seleniumUrl}`)
|
208 | }
|
209 |
|
210 | if (seleniumJar && seleniumUrl) {
|
211 | throw Error(
|
212 | 'Ambiguous test configuration: both SELENIUM_REMOTE_URL' +
|
213 | ' && SELENIUM_SERVER_JAR environment variables are set'
|
214 | )
|
215 | }
|
216 |
|
217 | const envBrowsers = getBrowsersToTestFromEnv()
|
218 | if ((seleniumJar || seleniumUrl) && envBrowsers.length === 0) {
|
219 | throw Error(
|
220 | 'Ambiguous test configuration: when either the SELENIUM_REMOTE_URL or' +
|
221 | ' SELENIUM_SERVER_JAR environment variable is set, the' +
|
222 | ' SELENIUM_BROWSER variable must also be set.'
|
223 | )
|
224 | }
|
225 |
|
226 | targetBrowsers = envBrowsers.length > 0 ? envBrowsers : getAvailableBrowsers()
|
227 | info(
|
228 | `Running tests against [${targetBrowsers.map((b) => b.name).join(', ')}]`
|
229 | )
|
230 |
|
231 | after(function () {
|
232 | if (seleniumServer) {
|
233 | return seleniumServer.kill()
|
234 | }
|
235 | })
|
236 | }
|
237 |
|
238 | const TARGET_MAP = /** !WeakMap<!Environment, !TargetBrowser> */ new WeakMap()
|
239 | const URL_MAP =
|
240 | /** !WeakMap<!Environment, ?(string|remote.SeleniumServer)> */ new WeakMap()
|
241 |
|
242 | /**
|
243 | * Defines the environment a {@linkplain suite test suite} is running against.
|
244 | * @final
|
245 | */
|
246 | class Environment {
|
247 | /**
|
248 | * @param {!TargetBrowser} browser the browser targetted in this environment.
|
249 | * @param {?(string|remote.SeleniumServer)=} url remote URL of an existing
|
250 | * Selenium server to test against.
|
251 | */
|
252 | constructor(browser, url = undefined) {
|
253 | browser = /** @type {!TargetBrowser} */ (
|
254 | Object.seal(Object.assign({}, browser))
|
255 | )
|
256 |
|
257 | TARGET_MAP.set(this, browser)
|
258 | URL_MAP.set(this, url || null)
|
259 | }
|
260 |
|
261 | /** @return {!TargetBrowser} the target browser for this test environment. */
|
262 | get browser() {
|
263 | return TARGET_MAP.get(this)
|
264 | }
|
265 |
|
266 | /**
|
267 | * Returns a predicate function that will suppress tests in this environment
|
268 | * if the {@linkplain #browser current browser} is in the list of
|
269 | * `browsersToIgnore`.
|
270 | *
|
271 | * @param {...(string|!Browser)} browsersToIgnore the browsers that should
|
272 | * be ignored.
|
273 | * @return {function(): boolean} a new predicate function.
|
274 | */
|
275 | browsers(...browsersToIgnore) {
|
276 | return () => browsersToIgnore.indexOf(this.browser.name) != -1
|
277 | }
|
278 |
|
279 | /**
|
280 | * @return {!Builder} a new WebDriver builder configured to target this
|
281 | * environment's {@linkplain #browser browser}.
|
282 | */
|
283 | builder() {
|
284 | const browser = this.browser
|
285 | const urlOrServer = URL_MAP.get(this)
|
286 |
|
287 | const builder = new Builder()
|
288 | builder.disableEnvironmentOverrides()
|
289 |
|
290 | const realBuild = builder.build
|
291 | builder.build = function () {
|
292 |
|
293 | builder.withCapabilities(Capabilities[browser.name].apply(Capabilities))
|
294 |
|
295 | if (browser.capabilities) {
|
296 | builder.getCapabilities().merge(browser.capabilities)
|
297 | }
|
298 |
|
299 | if (typeof urlOrServer === 'string') {
|
300 | builder.usingServer(urlOrServer)
|
301 | } else if (urlOrServer) {
|
302 | builder.usingServer(urlOrServer.address())
|
303 | }
|
304 | return realBuild.call(builder)
|
305 | }
|
306 |
|
307 | return builder
|
308 | }
|
309 | }
|
310 |
|
311 | /**
|
312 | * Configuration options for a {@linkplain ./index.suite test suite}.
|
313 | * @record
|
314 | */
|
315 | function SuiteOptions() { }
|
316 |
|
317 | /**
|
318 | * The browsers to run the test suite against.
|
319 | * @type {!Array<!(Browser|TargetBrowser)>}
|
320 | */
|
321 | SuiteOptions.prototype.browsers
|
322 |
|
323 | let inSuite = false
|
324 |
|
325 | /**
|
326 | * Defines a test suite by calling the provided function once for each of the
|
327 | * target browsers. If a suite is not limited to a specific set of browsers in
|
328 | * the provided {@linkplain ./index.SuiteOptions suite options}, the suite will
|
329 | * be configured to run against each of the {@linkplain ./index.init runtime
|
330 | * target browsers}.
|
331 | *
|
332 | * Sample usage:
|
333 | *
|
334 | * const {By, Key, until} = require('selenium-webdriver');
|
335 | * const {suite} = require('selenium-webdriver/testing');
|
336 | *
|
337 | * suite(function(env) {
|
338 | * describe('Google Search', function() {
|
339 | * let driver;
|
340 | *
|
341 | * before(async function() {
|
342 | * driver = await env.builder().build();
|
343 | * });
|
344 | *
|
345 | * after(() => driver.quit());
|
346 | *
|
347 | * it('demo', async function() {
|
348 | * await driver.get('http://www.google.com/ncr');
|
349 | *
|
350 | * let q = await driver.findElement(By.name('q'));
|
351 | * await q.sendKeys('webdriver', Key.RETURN);
|
352 | * await driver.wait(
|
353 | * until.titleIs('webdriver - Google Search'), 1000);
|
354 | * });
|
355 | * });
|
356 | * });
|
357 | *
|
358 | * By default, this example suite will run against every WebDriver-enabled
|
359 | * browser on the current system. Alternatively, the `SELENIUM_BROWSER`
|
360 | * environment variable may be used to run against a specific browser:
|
361 | *
|
362 | * SELENIUM_BROWSER=firefox mocha -t 120000 example_test.js
|
363 | *
|
364 | * @param {function(!Environment)} fn the function to call to build the test
|
365 | * suite.
|
366 | * @param {SuiteOptions=} options configuration options.
|
367 | */
|
368 | function suite(fn, options = undefined) {
|
369 | if (inSuite) {
|
370 | throw Error('Calls to suite() may not be nested')
|
371 | }
|
372 | try {
|
373 | init()
|
374 | inSuite = true
|
375 |
|
376 | const suiteBrowsers = new Map()
|
377 | if (options && options.browsers) {
|
378 | for (let browser of options.browsers) {
|
379 | if (typeof browser === 'string') {
|
380 | suiteBrowsers.set(browser, { name: browser })
|
381 | } else {
|
382 | suiteBrowsers.set(browser.name, browser)
|
383 | }
|
384 | }
|
385 | }
|
386 |
|
387 | for (let browser of targetBrowsers) {
|
388 | if (suiteBrowsers.size > 0 && !suiteBrowsers.has(browser.name)) {
|
389 | continue
|
390 | }
|
391 |
|
392 | describe(`[${browser.name}]`, function () {
|
393 | if (!seleniumUrl && seleniumJar && !seleniumServer) {
|
394 | seleniumServer = new remote.SeleniumServer(seleniumJar)
|
395 |
|
396 | const startTimeout = 65 * 1000
|
397 | // eslint-disable-next-line no-inner-declarations
|
398 | function startSelenium() {
|
399 | if (typeof this.timeout === 'function') {
|
400 | this.timeout(startTimeout) // For mocha.
|
401 | }
|
402 |
|
403 | info(`Starting selenium server ${seleniumJar}`)
|
404 | return seleniumServer.start(60 * 1000)
|
405 | }
|
406 |
|
407 | const /** !Function */ beforeHook = global.beforeAll || global.before
|
408 | beforeHook(startSelenium, startTimeout)
|
409 | }
|
410 |
|
411 | fn(new Environment(browser, seleniumUrl || seleniumServer))
|
412 | })
|
413 | }
|
414 | } finally {
|
415 | inSuite = false
|
416 | }
|
417 | }
|
418 |
|
419 | /**
|
420 | * Returns an object with wrappers for the standard mocha/jasmine test
|
421 | * functions: `describe` and `it`, which will redirect to `xdescribe` and `xit`,
|
422 | * respectively, if provided predicate function returns false.
|
423 | *
|
424 | * Sample usage:
|
425 | *
|
426 | * const {Browser} = require('selenium-webdriver');
|
427 | * const {suite, ignore} = require('selenium-webdriver/testing');
|
428 | *
|
429 | * suite(function(env) {
|
430 | *
|
431 | * // Skip tests the current environment targets Chrome.
|
432 | * ignore(env.browsers(Browser.CHROME)).
|
433 | * describe('something', async function() {
|
434 | * let driver = await env.builder().build();
|
435 | * // etc.
|
436 | * });
|
437 | * });
|
438 | *
|
439 | * @param {function(): boolean} predicateFn A predicate to call to determine
|
440 | * if the test should be suppressed. This function MUST be synchronous.
|
441 | * @return {{describe: !Function, it: !Function}} an object with wrapped
|
442 | * versions of the `describe` and `it` wtest functions.
|
443 | */
|
444 | function ignore(predicateFn) {
|
445 | const isJasmine = global.jasmine && typeof global.jasmine === 'object'
|
446 |
|
447 | const hooks = {
|
448 | describe: getTestHook('describe'),
|
449 | xdescribe: getTestHook('xdescribe'),
|
450 | it: getTestHook('it'),
|
451 | xit: getTestHook('xit'),
|
452 | }
|
453 | hooks.fdescribe = isJasmine ? getTestHook('fdescribe') : hooks.describe.only
|
454 | hooks.fit = isJasmine ? getTestHook('fit') : hooks.it.only
|
455 |
|
456 | let describe = wrap(hooks.xdescribe, hooks.describe)
|
457 | let fdescribe = wrap(hooks.xdescribe, hooks.fdescribe)
|
458 | //eslint-disable-next-line no-only-tests/no-only-tests
|
459 | describe.only = fdescribe
|
460 |
|
461 | let it = wrap(hooks.xit, hooks.it)
|
462 | let fit = wrap(hooks.xit, hooks.fit)
|
463 | //eslint-disable-next-line no-only-tests/no-only-tests
|
464 | it.only = fit
|
465 |
|
466 | return { describe, it }
|
467 |
|
468 | function wrap(onSkip, onRun) {
|
469 | return function (...args) {
|
470 | if (predicateFn()) {
|
471 | onSkip(...args)
|
472 | } else {
|
473 | onRun(...args)
|
474 | }
|
475 | }
|
476 | }
|
477 | }
|
478 |
|
479 | /**
|
480 | * @param {string} name
|
481 | * @return {!Function}
|
482 | * @throws {TypeError}
|
483 | */
|
484 | function getTestHook(name) {
|
485 | let fn = global[name]
|
486 | let type = typeof fn
|
487 | if (type !== 'function') {
|
488 | throw TypeError(
|
489 | `Expected global.${name} to be a function, but is ${type}.` +
|
490 | ' This can happen if you try using this module when running with' +
|
491 | ' node directly instead of using jasmine or mocha'
|
492 | )
|
493 | }
|
494 | return fn
|
495 | }
|
496 |
|
497 | // PUBLIC API
|
498 |
|
499 | module.exports = {
|
500 | Environment,
|
501 | SuiteOptions,
|
502 | init,
|
503 | ignore,
|
504 | suite,
|
505 | }
|