UNPKG

15.2 kBJavaScriptView Raw
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
35const { isatty } = require('tty')
36const chrome = require('../chrome')
37const edge = require('../edge')
38const firefox = require('../firefox')
39const ie = require('../ie')
40const remote = require('../remote')
41const safari = require('../safari')
42const opera = require('../opera')
43const { Browser, Capabilities } = require('../lib/capabilities')
44const { Builder } = require('../index')
45
46/**
47 * Describes a browser targeted by a {@linkplain suite test suite}.
48 * @record
49 */
50function TargetBrowser() { }
51
52/**
53 * The {@linkplain Browser name} of the targeted browser.
54 * @type {string}
55 */
56TargetBrowser.prototype.name
57
58/**
59 * The specific version of the targeted browser, if any.
60 * @type {(string|undefined)}
61 */
62TargetBrowser.prototype.version
63
64/**
65 * The specific {@linkplain ../lib/capabilities.Platform platform} for the
66 * targeted browser, if any.
67 * @type {(string|undefined)}.
68 */
69TargetBrowser.prototype.platform
70
71/** @suppress {checkTypes} */
72function color(c, s) {
73 return isatty(process.stdout) ? `\u001b[${c}m${s}\u001b[0m` : s
74}
75function green(s) {
76 return color(32, s)
77}
78function cyan(s) {
79 return color(36, s)
80}
81function info(msg) {
82 console.info(`${green('[INFO]')} ${msg}`)
83}
84function 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 */
94function 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 */
117function 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
147let wasInit
148let targetBrowsers
149let seleniumJar
150let seleniumUrl
151let 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 */
188function 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
238const TARGET_MAP = /** !WeakMap<!Environment, !TargetBrowser> */ new WeakMap()
239const 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 */
246class 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 */
315function SuiteOptions() { }
316
317/**
318 * The browsers to run the test suite against.
319 * @type {!Array<!(Browser|TargetBrowser)>}
320 */
321SuiteOptions.prototype.browsers
322
323let 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 */
368function 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 */
444function 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 */
484function 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
499module.exports = {
500 Environment,
501 SuiteOptions,
502 init,
503 ignore,
504 suite,
505}