Path: blob/trunk/javascript/selenium-webdriver/testing/index.js
2884 views
// Licensed to the Software Freedom Conservancy (SFC) under one1// or more contributor license agreements. See the NOTICE file2// distributed with this work for additional information3// regarding copyright ownership. The SFC licenses this file4// to you under the Apache License, Version 2.0 (the5// "License"); you may not use this file except in compliance6// with the License. You may obtain a copy of the License at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing,11// software distributed under the License is distributed on an12// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13// KIND, either express or implied. See the License for the14// specific language governing permissions and limitations15// under the License.1617/**18* @fileoverview Provides extensions for19* [Jasmine](https://jasmine.github.io) and [Mocha](https://mochajs.org).20*21* You may conditionally suppress a test function using the exported22* "ignore" function. If the provided predicate returns true, the attached23* test case will be skipped:24*25* test.ignore(maybe()).it('is flaky', function() {26* if (Math.random() < 0.5) throw Error();27* });28*29* function maybe() { return Math.random() < 0.5; }30*/3132'use strict'3334const fs = require('node:fs')35const path = require('node:path')36const { isatty } = require('node:tty')37const chrome = require('../chrome')38const edge = require('../edge')39const firefox = require('../firefox')40const ie = require('../ie')41const remote = require('../remote')42const safari = require('../safari')43const { Browser } = require('../lib/capabilities')44const { Builder } = require('../index')45const { getBinaryPaths } = require('../common/driverFinder')4647let runfiles48try {49// Attempt to require @bazel/runfiles50runfiles = require('@bazel/runfiles').runfiles51} catch {52// Fall through53}5455/**56* Describes a browser targeted by a {@linkplain suite test suite}.57* @record58*/59function TargetBrowser() {}6061/**62* The {@linkplain Browser name} of the targeted browser.63* @type {string}64*/65TargetBrowser.prototype.name6667/**68* The specific version of the targeted browser, if any.69* @type {(string|undefined)}70*/71TargetBrowser.prototype.version7273/**74* The specific {@linkplain ../lib/capabilities.Platform platform} for the75* targeted browser, if any.76* @type {(string|undefined)}.77*/78TargetBrowser.prototype.platform7980/** @suppress {checkTypes} */81function color(c, s) {82return isatty(process.stdout) ? `\u001b[${c}m${s}\u001b[0m` : s83}8485function green(s) {86return color(32, s)87}8889function cyan(s) {90return color(36, s)91}9293function info(msg) {94console.info(`${green('[INFO]')} ${msg}`)95}9697function warn(msg) {98console.warn(`${cyan('[WARNING]')} ${msg}`)99}100101/**102* Extracts the browsers for a test suite to target from the `SELENIUM_BROWSER`103* environment variable.104*105* @return {{name: string, version: string, platform: string}}[] the browsers to target.106*/107function getBrowsersToTestFromEnv() {108let browsers = process.env['SELENIUM_BROWSER']109if (!browsers) {110return []111}112return browsers.split(',').map((spec) => {113const parts = spec.split(/:/, 3)114let name = parts[0]115if (name === 'ie') {116name = Browser.INTERNET_EXPLORER117} else if (name === 'edge') {118name = Browser.EDGE119}120let version = parts[1]121let platform = parts[2]122return { name, version, platform }123})124}125126/**127* @return {!Array<!TargetBrowser>} the browsers available for testing on this128* system.129*/130function getAvailableBrowsers() {131info(`Searching for WebDriver executables installed on the current system...`)132133let targets = [134[getBinaryPaths(new chrome.Options()), Browser.CHROME],135[getBinaryPaths(new edge.Options()), Browser.EDGE],136[getBinaryPaths(new firefox.Options()), Browser.FIREFOX],137]138if (process.platform === 'win32') {139targets.push([getBinaryPaths(new ie.Options()), Browser.INTERNET_EXPLORER])140}141if (process.platform === 'darwin') {142targets.push([getBinaryPaths(new safari.Options()), Browser.SAFARI])143}144145let availableBrowsers = []146for (let pair of targets) {147const driverPath = pair[0].driverPath148const browserPath = pair[0].browserPath149const name = pair[1]150const capabilities = pair[2]151if (driverPath.length > 0 && browserPath && browserPath.length > 0) {152info(`... located ${name}`)153availableBrowsers.push({ name, capabilities })154}155}156157if (availableBrowsers.length === 0) {158warn(`Unable to locate any WebDriver executables for testing`)159}160161return availableBrowsers162}163164let wasInit165let targetBrowsers166let seleniumJar167let seleniumUrl168let seleniumServer169170/**171* Initializes this module by determining which browsers a172* {@linkplain ./index.suite test suite} should run against. The default173* behavior is to run tests against every browser with a WebDriver executables174* (chromedriver, firefoxdriver, etc.) are installed on the system by `PATH`.175*176* Specific browsers can be selected at runtime by setting the177* `SELENIUM_BROWSER` environment variable. This environment variable has the178* same semantics as with the WebDriver {@link ../index.Builder Builder},179* except you may use a comma-delimited list to run against multiple browsers:180*181* SELENIUM_BROWSER=chrome,firefox mocha --recursive tests/182*183* The `SELENIUM_REMOTE_URL` environment variable may be set to configure tests184* to run against an externally managed (usually remote) Selenium server. When185* set, the WebDriver builder provided by each186* {@linkplain TestEnvironment#builder TestEnvironment} will automatically be187* configured to use this server instead of starting a browser driver locally.188*189* The `SELENIUM_SERVER_JAR` environment variable may be set to the path of a190* standalone Selenium server on the local machine that should be used for191* WebDriver sessions. When set, the WebDriver builder provided by each192* {@linkplain TestEnvironment} will automatically be configured to use the193* started server instead of using a browser driver directly. It should only be194* necessary to set the `SELENIUM_SERVER_JAR` when testing locally against195* browsers not natively supported by the WebDriver196* {@link ../index.Builder Builder}.197*198* When either of the `SELENIUM_REMOTE_URL` or `SELENIUM_SERVER_JAR` environment199* variables are set, the `SELENIUM_BROWSER` variable must also be set.200*201* @param {boolean=} force whether to force this module to re-initialize and202* scan `process.env` again to determine which browsers to run tests203* against.204*/205function init(force = false) {206if (wasInit && !force) {207return208}209wasInit = true210211// If force re-init, kill the current server if there is one.212if (seleniumServer) {213seleniumServer.kill()214seleniumServer = null215}216217seleniumJar = process.env['SELENIUM_SERVER_JAR']218seleniumUrl = process.env['SELENIUM_REMOTE_URL']219if (seleniumJar) {220info(`Using Selenium server jar: ${seleniumJar}`)221}222223if (seleniumUrl) {224info(`Using Selenium remote end: ${seleniumUrl}`)225}226227if (seleniumJar && seleniumUrl) {228throw Error(229'Ambiguous test configuration: both SELENIUM_REMOTE_URL' +230' && SELENIUM_SERVER_JAR environment variables are set',231)232}233234const envBrowsers = getBrowsersToTestFromEnv()235if ((seleniumJar || seleniumUrl) && envBrowsers.length === 0) {236throw Error(237'Ambiguous test configuration: when either the SELENIUM_REMOTE_URL or' +238' SELENIUM_SERVER_JAR environment variable is set, the' +239' SELENIUM_BROWSER variable must also be set.',240)241}242243targetBrowsers = envBrowsers.length > 0 ? envBrowsers : getAvailableBrowsers()244info(`Running tests against [${targetBrowsers.map((b) => b.name).join(', ')}]`)245246after(function () {247if (seleniumServer) {248return seleniumServer.kill()249}250})251}252253const TARGET_MAP = /** !WeakMap<!Environment, !TargetBrowser> */ new WeakMap()254const URL_MAP = /** !WeakMap<!Environment, ?(string|remote.SeleniumServer)> */ new WeakMap()255256/**257* Defines the environment a {@linkplain suite test suite} is running against.258* @final259*/260class Environment {261/**262* @param {!TargetBrowser} browser the browser targeted in this environment.263* @param {?(string|remote.SeleniumServer)=} url remote URL of an existing264* Selenium server to test against.265*/266constructor(browser, url = undefined) {267browser = /** @type {!TargetBrowser} */ (Object.seal(Object.assign({}, browser)))268269TARGET_MAP.set(this, browser)270URL_MAP.set(this, url || null)271}272273/** @return {!TargetBrowser} the target browser for this test environment. */274get browser() {275return TARGET_MAP.get(this)276}277278/**279* Returns a predicate function that will suppress tests in this environment280* if the {@linkplain #browser current browser} is in the list of281* `browsersToIgnore`.282*283* @param {...(string|!Browser)} browsersToIgnore the browsers that should284* be ignored.285* @return {function(): boolean} a new predicate function.286*/287browsers(...browsersToIgnore) {288return () => browsersToIgnore.indexOf(this.browser.name) !== -1289}290291/**292* @return {!Builder} a new WebDriver builder configured to target this293* environment's {@linkplain #browser browser}.294*/295builder() {296const browser = this.browser297const urlOrServer = URL_MAP.get(this)298299const builder = new Builder()300301// Sniff the environment variables for paths to use for the common browsers302// Chrome303if ('SE_CHROMEDRIVER' in process.env) {304const found = locate(process.env.SE_CHROMEDRIVER)305const service = new chrome.ServiceBuilder(found)306builder.setChromeService(service)307}308if ('SE_CHROME' in process.env) {309const binary = locate(process.env.SE_CHROME)310const options = new chrome.Options()311options.setChromeBinaryPath(binary)312options.setAcceptInsecureCerts(true)313options.addArguments('disable-infobars', 'disable-breakpad', 'disable-dev-shm-usage', 'no-sandbox')314builder.setChromeOptions(options)315}316// Edge317// Firefox318if ('SE_GECKODRIVER' in process.env) {319const found = locate(process.env.SE_GECKODRIVER)320const service = new firefox.ServiceBuilder(found)321builder.setFirefoxService(service)322}323if ('SE_FIREFOX' in process.env) {324const binary = locate(process.env.SE_FIREFOX)325const options = new firefox.Options()326options.enableBidi()327options.setBinary(binary)328builder.setFirefoxOptions(options)329}330331builder.disableEnvironmentOverrides()332333const realBuild = builder.build334builder.build = function () {335builder.forBrowser(browser.name, browser.version, browser.platform)336337if (browser.capabilities) {338builder.getCapabilities().merge(browser.capabilities)339}340341if (browser.name === 'firefox') {342builder.setCapability('moz:debuggerAddress', true)343}344345// Enable BiDi for supporting browsers.346if (browser.name === Browser.FIREFOX || browser.name === Browser.CHROME || browser.name === Browser.EDGE) {347builder.setCapability('webSocketUrl', true)348builder.setCapability('unhandledPromptBehavior', 'ignore')349}350351if (typeof urlOrServer === 'string') {352builder.usingServer(urlOrServer)353} else if (urlOrServer) {354builder.usingServer(urlOrServer.address())355}356return realBuild.call(builder)357}358359return builder360}361}362363/**364* Configuration options for a {@linkplain ./index.suite test suite}.365* @record366*/367function SuiteOptions() {}368369/**370* The browsers to run the test suite against.371* @type {!Array<!(Browser|TargetBrowser)>}372*/373SuiteOptions.prototype.browsers374375let inSuite = false376377/**378* Defines a test suite by calling the provided function once for each of the379* target browsers. If a suite is not limited to a specific set of browsers in380* the provided {@linkplain ./index.SuiteOptions suite options}, the suite will381* be configured to run against each of the {@linkplain ./index.init runtime382* target browsers}.383*384* Sample usage:385*386* const {By, Key, until} = require('selenium-webdriver');387* const {suite} = require('selenium-webdriver/testing');388*389* suite(function(env) {390* describe('Google Search', function() {391* let driver;392*393* before(async function() {394* driver = await env.builder().build();395* });396*397* after(() => driver.quit());398*399* it('demo', async function() {400* await driver.get('http://www.google.com/ncr');401*402* let q = await driver.findElement(By.name('q'));403* await q.sendKeys('webdriver', Key.RETURN);404* await driver.wait(405* until.titleIs('webdriver - Google Search'), 1000);406* });407* });408* });409*410* By default, this example suite will run against every WebDriver-enabled411* browser on the current system. Alternatively, the `SELENIUM_BROWSER`412* environment variable may be used to run against a specific browser:413*414* SELENIUM_BROWSER=firefox mocha -t 120000 example_test.js415*416* @param {function(!Environment)} fn the function to call to build the test417* suite.418* @param {SuiteOptions=} options configuration options.419*/420function suite(fn, options = undefined) {421if (inSuite) {422throw Error('Calls to suite() may not be nested')423}424try {425init()426inSuite = true427428const suiteBrowsers = new Map()429if (options && options.browsers) {430for (let browser of options.browsers) {431if (typeof browser === 'string') {432suiteBrowsers.set(browser, { name: browser })433} else {434suiteBrowsers.set(browser.name, browser)435}436}437}438439for (let browser of targetBrowsers) {440if (suiteBrowsers.size > 0 && !suiteBrowsers.has(browser.name)) {441continue442}443444describe(`[${browser.name}]`, function () {445if (!seleniumUrl && seleniumJar && !seleniumServer) {446seleniumServer = new remote.SeleniumServer(seleniumJar)447448const startTimeout = 65 * 1000449450function startSelenium() {451if (typeof this.timeout === 'function') {452this.timeout(startTimeout) // For mocha.453}454455info(`Starting selenium server ${seleniumJar}`)456return seleniumServer.start(60 * 1000)457}458459const /** !Function */ beforeHook = global.beforeAll || global.before460beforeHook(startSelenium, startTimeout)461}462463fn(new Environment(browser, seleniumUrl || seleniumServer))464})465}466} finally {467inSuite = false468}469}470471/**472* Returns an object with wrappers for the standard mocha/jasmine test473* functions: `describe` and `it`, which will redirect to `xdescribe` and `xit`,474* respectively, if provided predicate function returns false.475*476* Sample usage:477*478* const {Browser} = require('selenium-webdriver');479* const {suite, ignore} = require('selenium-webdriver/testing');480*481* suite(function(env) {482*483* // Skip tests the current environment targets Chrome.484* ignore(env.browsers(Browser.CHROME)).485* describe('something', async function() {486* let driver = await env.builder().build();487* // etc.488* });489* });490*491* @param {function(): boolean} predicateFn A predicate to call to determine492* if the test should be suppressed. This function MUST be synchronous.493* @return {{describe: !Function, it: !Function}} an object with wrapped494* versions of the `describe` and `it` test functions.495*/496function ignore(predicateFn) {497const isJasmine = global.jasmine && typeof global.jasmine === 'object'498499const hooks = {500describe: getTestHook('describe'),501xdescribe: getTestHook('xdescribe'),502it: getTestHook('it'),503xit: getTestHook('xit'),504}505hooks.fdescribe = isJasmine ? getTestHook('fdescribe') : hooks.describe.only506hooks.fit = isJasmine ? getTestHook('fit') : hooks.it.only507508let describe = wrap(hooks.xdescribe, hooks.describe)509let fdescribe = wrap(hooks.xdescribe, hooks.fdescribe)510//eslint-disable-next-line no-only-tests/no-only-tests511describe.only = fdescribe512513let it = wrap(hooks.xit, hooks.it)514let fit = wrap(hooks.xit, hooks.fit)515//eslint-disable-next-line no-only-tests/no-only-tests516it.only = fit517518return { describe, it }519520function wrap(onSkip, onRun) {521return function (...args) {522if (predicateFn()) {523onSkip(...args)524} else {525onRun(...args)526}527}528}529}530531/**532* @param {string} name533* @return {!Function}534* @throws {TypeError}535*/536function getTestHook(name) {537let fn = global[name]538let type = typeof fn539if (type !== 'function') {540throw TypeError(541`Expected global.${name} to be a function, but is ${type}.` +542' This can happen if you try using this module when running with' +543' node directly instead of using jasmine or mocha',544)545}546return fn547}548549function locate(fileLike) {550if (fs.existsSync(fileLike)) {551return fileLike552}553554if (!runfiles) {555throw new Error('Unable to find ' + fileLike)556}557558try {559return runfiles.resolve(fileLike)560} catch {561// Fall through562}563564// Is the item in the workspace?565try {566return runfiles.resolveWorkspaceRelative(fileLike)567} catch {568// Fall through569}570571// Find the repo mapping file572let repoMappingFile573try {574repoMappingFile = runfiles.resolve('_repo_mapping')575} catch {576throw new Error('Unable to locate (no repo mapping file): ' + fileLike)577}578const lines = fs.readFileSync(repoMappingFile, { encoding: 'utf8' }).split('\n')579580// Build a map of "repo we declared we need" to "path"581const mapping = {}582for (const line of lines) {583if (line.startsWith(',')) {584const parts = line.split(',', 3)585mapping[parts[1]] = parts[2]586}587}588589// Get the first segment of the path590const pathSegments = fileLike.split('/')591if (!pathSegments.length) {592throw new Error('Unable to locate ' + fileLike)593}594595pathSegments[0] = mapping[pathSegments[0]] ? mapping[pathSegments[0]] : '_main'596597try {598return runfiles.resolve(path.join(...pathSegments))599} catch {600// Fall through601}602603throw new Error('Unable to find ' + fileLike)604}605606// PUBLIC API607608module.exports = {609Environment,610SuiteOptions,611init,612ignore,613suite,614}615616617