| // Licensed to the Software Freedom Conservancy (SFC) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The SFC licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| /** |
| * @fileoverview Provides extensions for |
| * [Jasmine](https://jasmine.github.io) and [Mocha](https://mochajs.org). |
| * |
| * You may conditionally suppress a test function using the exported |
| * "ignore" function. If the provided predicate returns true, the attached |
| * test case will be skipped: |
| * |
| * test.ignore(maybe()).it('is flaky', function() { |
| * if (Math.random() < 0.5) throw Error(); |
| * }); |
| * |
| * function maybe() { return Math.random() < 0.5; } |
| */ |
| |
| 'use strict' |
| |
| const { isatty } = require('tty') |
| const chrome = require('../chrome') |
| const edge = require('../edge') |
| const firefox = require('../firefox') |
| const ie = require('../ie') |
| const remote = require('../remote') |
| const safari = require('../safari') |
| const { Browser } = require('../lib/capabilities') |
| const { Builder } = require('../index') |
| const { getPath } = require('../common/driverFinder') |
| |
| /** |
| * Describes a browser targeted by a {@linkplain suite test suite}. |
| * @record |
| */ |
| function TargetBrowser() {} |
| |
| /** |
| * The {@linkplain Browser name} of the targeted browser. |
| * @type {string} |
| */ |
| TargetBrowser.prototype.name |
| |
| /** |
| * The specific version of the targeted browser, if any. |
| * @type {(string|undefined)} |
| */ |
| TargetBrowser.prototype.version |
| |
| /** |
| * The specific {@linkplain ../lib/capabilities.Platform platform} for the |
| * targeted browser, if any. |
| * @type {(string|undefined)}. |
| */ |
| TargetBrowser.prototype.platform |
| |
| /** @suppress {checkTypes} */ |
| function color(c, s) { |
| return isatty(process.stdout) ? `\u001b[${c}m${s}\u001b[0m` : s |
| } |
| function green(s) { |
| return color(32, s) |
| } |
| function cyan(s) { |
| return color(36, s) |
| } |
| function info(msg) { |
| console.info(`${green('[INFO]')} ${msg}`) |
| } |
| function warn(msg) { |
| console.warn(`${cyan('[WARNING]')} ${msg}`) |
| } |
| |
| /** |
| * Extracts the browsers for a test suite to target from the `SELENIUM_BROWSER` |
| * environment variable. |
| * |
| * @return {{name: string, version: string, platform: string}}[] the browsers to target. |
| */ |
| function getBrowsersToTestFromEnv() { |
| let browsers = process.env['SELENIUM_BROWSER'] |
| if (!browsers) { |
| return [] |
| } |
| return browsers.split(',').map((spec) => { |
| const parts = spec.split(/:/, 3) |
| let name = parts[0] |
| if (name === 'ie') { |
| name = Browser.INTERNET_EXPLORER |
| } else if (name === 'edge') { |
| name = Browser.EDGE |
| } |
| let version = parts[1] |
| let platform = parts[2] |
| return { name, version, platform } |
| }) |
| } |
| |
| /** |
| * @return {!Array<!TargetBrowser>} the browsers available for testing on this |
| * system. |
| */ |
| function getAvailableBrowsers() { |
| info(`Searching for WebDriver executables installed on the current system...`) |
| |
| let targets = [ |
| [getPath(new chrome.Options()), Browser.CHROME], |
| [getPath(new edge.Options()), Browser.EDGE], |
| [getPath(new firefox.Options()), Browser.FIREFOX], |
| ] |
| if (process.platform === 'win32') { |
| targets.push([getPath(new ie.Options()), Browser.INTERNET_EXPLORER]) |
| } |
| if (process.platform === 'darwin') { |
| targets.push([getPath(new safari.Options()), Browser.SAFARI]) |
| } |
| |
| let availableBrowsers = [] |
| for (let pair of targets) { |
| const driverPath = pair[0].driverPath |
| const browserPath = pair[0].browserPath |
| const name = pair[1] |
| const capabilities = pair[2] |
| if (driverPath.length > 0 && browserPath && browserPath.length > 0) { |
| info(`... located ${name}`) |
| availableBrowsers.push({ name, capabilities }) |
| } |
| } |
| |
| if (availableBrowsers.length === 0) { |
| warn(`Unable to locate any WebDriver executables for testing`) |
| } |
| |
| return availableBrowsers |
| } |
| |
| let wasInit |
| let targetBrowsers |
| let seleniumJar |
| let seleniumUrl |
| let seleniumServer |
| |
| /** |
| * Initializes this module by determining which browsers a |
| * {@linkplain ./index.suite test suite} should run against. The default |
| * behavior is to run tests against every browser with a WebDriver executables |
| * (chromedriver, firefoxdriver, etc.) are installed on the system by `PATH`. |
| * |
| * Specific browsers can be selected at runtime by setting the |
| * `SELENIUM_BROWSER` environment variable. This environment variable has the |
| * same semantics as with the WebDriver {@link ../index.Builder Builder}, |
| * except you may use a comma-delimited list to run against multiple browsers: |
| * |
| * SELENIUM_BROWSER=chrome,firefox mocha --recursive tests/ |
| * |
| * The `SELENIUM_REMOTE_URL` environment variable may be set to configure tests |
| * to run against an externally managed (usually remote) Selenium server. When |
| * set, the WebDriver builder provided by each |
| * {@linkplain TestEnvironment#builder TestEnvironment} will automatically be |
| * configured to use this server instead of starting a browser driver locally. |
| * |
| * The `SELENIUM_SERVER_JAR` environment variable may be set to the path of a |
| * standalone Selenium server on the local machine that should be used for |
| * WebDriver sessions. When set, the WebDriver builder provided by each |
| * {@linkplain TestEnvironment} will automatically be configured to use the |
| * started server instead of using a browser driver directly. It should only be |
| * necessary to set the `SELENIUM_SERVER_JAR` when testing locally against |
| * browsers not natively supported by the WebDriver |
| * {@link ../index.Builder Builder}. |
| * |
| * When either of the `SELENIUM_REMOTE_URL` or `SELENIUM_SERVER_JAR` environment |
| * variables are set, the `SELENIUM_BROWSER` variable must also be set. |
| * |
| * @param {boolean=} force whether to force this module to re-initialize and |
| * scan `process.env` again to determine which browsers to run tests |
| * against. |
| */ |
| function init(force = false) { |
| if (wasInit && !force) { |
| return |
| } |
| wasInit = true |
| |
| // If force re-init, kill the current server if there is one. |
| if (seleniumServer) { |
| seleniumServer.kill() |
| seleniumServer = null |
| } |
| |
| seleniumJar = process.env['SELENIUM_SERVER_JAR'] |
| seleniumUrl = process.env['SELENIUM_REMOTE_URL'] |
| if (seleniumJar) { |
| info(`Using Selenium server jar: ${seleniumJar}`) |
| } |
| |
| if (seleniumUrl) { |
| info(`Using Selenium remote end: ${seleniumUrl}`) |
| } |
| |
| if (seleniumJar && seleniumUrl) { |
| throw Error( |
| 'Ambiguous test configuration: both SELENIUM_REMOTE_URL' + |
| ' && SELENIUM_SERVER_JAR environment variables are set', |
| ) |
| } |
| |
| const envBrowsers = getBrowsersToTestFromEnv() |
| if ((seleniumJar || seleniumUrl) && envBrowsers.length === 0) { |
| throw Error( |
| 'Ambiguous test configuration: when either the SELENIUM_REMOTE_URL or' + |
| ' SELENIUM_SERVER_JAR environment variable is set, the' + |
| ' SELENIUM_BROWSER variable must also be set.', |
| ) |
| } |
| |
| targetBrowsers = envBrowsers.length > 0 ? envBrowsers : getAvailableBrowsers() |
| info(`Running tests against [${targetBrowsers.map((b) => b.name).join(', ')}]`) |
| |
| after(function () { |
| if (seleniumServer) { |
| return seleniumServer.kill() |
| } |
| }) |
| } |
| |
| const TARGET_MAP = /** !WeakMap<!Environment, !TargetBrowser> */ new WeakMap() |
| const URL_MAP = /** !WeakMap<!Environment, ?(string|remote.SeleniumServer)> */ new WeakMap() |
| |
| /** |
| * Defines the environment a {@linkplain suite test suite} is running against. |
| * @final |
| */ |
| class Environment { |
| /** |
| * @param {!TargetBrowser} browser the browser targetted in this environment. |
| * @param {?(string|remote.SeleniumServer)=} url remote URL of an existing |
| * Selenium server to test against. |
| */ |
| constructor(browser, url = undefined) { |
| browser = /** @type {!TargetBrowser} */ (Object.seal(Object.assign({}, browser))) |
| |
| TARGET_MAP.set(this, browser) |
| URL_MAP.set(this, url || null) |
| } |
| |
| /** @return {!TargetBrowser} the target browser for this test environment. */ |
| get browser() { |
| return TARGET_MAP.get(this) |
| } |
| |
| /** |
| * Returns a predicate function that will suppress tests in this environment |
| * if the {@linkplain #browser current browser} is in the list of |
| * `browsersToIgnore`. |
| * |
| * @param {...(string|!Browser)} browsersToIgnore the browsers that should |
| * be ignored. |
| * @return {function(): boolean} a new predicate function. |
| */ |
| browsers(...browsersToIgnore) { |
| return () => browsersToIgnore.indexOf(this.browser.name) !== -1 |
| } |
| |
| /** |
| * @return {!Builder} a new WebDriver builder configured to target this |
| * environment's {@linkplain #browser browser}. |
| */ |
| builder() { |
| const browser = this.browser |
| const urlOrServer = URL_MAP.get(this) |
| |
| const builder = new Builder() |
| builder.disableEnvironmentOverrides() |
| |
| const realBuild = builder.build |
| builder.build = function () { |
| builder.forBrowser(browser.name, browser.version, browser.platform) |
| |
| if (browser.capabilities) { |
| builder.getCapabilities().merge(browser.capabilities) |
| } |
| |
| if (browser.name === 'firefox') { |
| builder.setCapability('moz:debuggerAddress', true) |
| } |
| |
| if (typeof urlOrServer === 'string') { |
| builder.usingServer(urlOrServer) |
| } else if (urlOrServer) { |
| builder.usingServer(urlOrServer.address()) |
| } |
| return realBuild.call(builder) |
| } |
| |
| return builder |
| } |
| } |
| |
| /** |
| * Configuration options for a {@linkplain ./index.suite test suite}. |
| * @record |
| */ |
| function SuiteOptions() {} |
| |
| /** |
| * The browsers to run the test suite against. |
| * @type {!Array<!(Browser|TargetBrowser)>} |
| */ |
| SuiteOptions.prototype.browsers |
| |
| let inSuite = false |
| |
| /** |
| * Defines a test suite by calling the provided function once for each of the |
| * target browsers. If a suite is not limited to a specific set of browsers in |
| * the provided {@linkplain ./index.SuiteOptions suite options}, the suite will |
| * be configured to run against each of the {@linkplain ./index.init runtime |
| * target browsers}. |
| * |
| * Sample usage: |
| * |
| * const {By, Key, until} = require('selenium-webdriver'); |
| * const {suite} = require('selenium-webdriver/testing'); |
| * |
| * suite(function(env) { |
| * describe('Google Search', function() { |
| * let driver; |
| * |
| * before(async function() { |
| * driver = await env.builder().build(); |
| * }); |
| * |
| * after(() => driver.quit()); |
| * |
| * it('demo', async function() { |
| * await driver.get('http://www.google.com/ncr'); |
| * |
| * let q = await driver.findElement(By.name('q')); |
| * await q.sendKeys('webdriver', Key.RETURN); |
| * await driver.wait( |
| * until.titleIs('webdriver - Google Search'), 1000); |
| * }); |
| * }); |
| * }); |
| * |
| * By default, this example suite will run against every WebDriver-enabled |
| * browser on the current system. Alternatively, the `SELENIUM_BROWSER` |
| * environment variable may be used to run against a specific browser: |
| * |
| * SELENIUM_BROWSER=firefox mocha -t 120000 example_test.js |
| * |
| * @param {function(!Environment)} fn the function to call to build the test |
| * suite. |
| * @param {SuiteOptions=} options configuration options. |
| */ |
| function suite(fn, options = undefined) { |
| if (inSuite) { |
| throw Error('Calls to suite() may not be nested') |
| } |
| try { |
| init() |
| inSuite = true |
| |
| const suiteBrowsers = new Map() |
| if (options && options.browsers) { |
| for (let browser of options.browsers) { |
| if (typeof browser === 'string') { |
| suiteBrowsers.set(browser, { name: browser }) |
| } else { |
| suiteBrowsers.set(browser.name, browser) |
| } |
| } |
| } |
| |
| for (let browser of targetBrowsers) { |
| if (suiteBrowsers.size > 0 && !suiteBrowsers.has(browser.name)) { |
| continue |
| } |
| |
| describe(`[${browser.name}]`, function () { |
| if (!seleniumUrl && seleniumJar && !seleniumServer) { |
| seleniumServer = new remote.SeleniumServer(seleniumJar) |
| |
| const startTimeout = 65 * 1000 |
| // eslint-disable-next-line no-inner-declarations |
| function startSelenium() { |
| if (typeof this.timeout === 'function') { |
| this.timeout(startTimeout) // For mocha. |
| } |
| |
| info(`Starting selenium server ${seleniumJar}`) |
| return seleniumServer.start(60 * 1000) |
| } |
| |
| const /** !Function */ beforeHook = global.beforeAll || global.before |
| beforeHook(startSelenium, startTimeout) |
| } |
| |
| fn(new Environment(browser, seleniumUrl || seleniumServer)) |
| }) |
| } |
| } finally { |
| inSuite = false |
| } |
| } |
| |
| /** |
| * Returns an object with wrappers for the standard mocha/jasmine test |
| * functions: `describe` and `it`, which will redirect to `xdescribe` and `xit`, |
| * respectively, if provided predicate function returns false. |
| * |
| * Sample usage: |
| * |
| * const {Browser} = require('selenium-webdriver'); |
| * const {suite, ignore} = require('selenium-webdriver/testing'); |
| * |
| * suite(function(env) { |
| * |
| * // Skip tests the current environment targets Chrome. |
| * ignore(env.browsers(Browser.CHROME)). |
| * describe('something', async function() { |
| * let driver = await env.builder().build(); |
| * // etc. |
| * }); |
| * }); |
| * |
| * @param {function(): boolean} predicateFn A predicate to call to determine |
| * if the test should be suppressed. This function MUST be synchronous. |
| * @return {{describe: !Function, it: !Function}} an object with wrapped |
| * versions of the `describe` and `it` test functions. |
| */ |
| function ignore(predicateFn) { |
| const isJasmine = global.jasmine && typeof global.jasmine === 'object' |
| |
| const hooks = { |
| describe: getTestHook('describe'), |
| xdescribe: getTestHook('xdescribe'), |
| it: getTestHook('it'), |
| xit: getTestHook('xit'), |
| } |
| hooks.fdescribe = isJasmine ? getTestHook('fdescribe') : hooks.describe.only |
| hooks.fit = isJasmine ? getTestHook('fit') : hooks.it.only |
| |
| let describe = wrap(hooks.xdescribe, hooks.describe) |
| let fdescribe = wrap(hooks.xdescribe, hooks.fdescribe) |
| //eslint-disable-next-line no-only-tests/no-only-tests |
| describe.only = fdescribe |
| |
| let it = wrap(hooks.xit, hooks.it) |
| let fit = wrap(hooks.xit, hooks.fit) |
| //eslint-disable-next-line no-only-tests/no-only-tests |
| it.only = fit |
| |
| return { describe, it } |
| |
| function wrap(onSkip, onRun) { |
| return function (...args) { |
| if (predicateFn()) { |
| onSkip(...args) |
| } else { |
| onRun(...args) |
| } |
| } |
| } |
| } |
| |
| /** |
| * @param {string} name |
| * @return {!Function} |
| * @throws {TypeError} |
| */ |
| function getTestHook(name) { |
| let fn = global[name] |
| let type = typeof fn |
| if (type !== 'function') { |
| throw TypeError( |
| `Expected global.${name} to be a function, but is ${type}.` + |
| ' This can happen if you try using this module when running with' + |
| ' node directly instead of using jasmine or mocha', |
| ) |
| } |
| return fn |
| } |
| |
| // PUBLIC API |
| |
| module.exports = { |
| Environment, |
| SuiteOptions, |
| init, |
| ignore, |
| suite, |
| } |