// 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 wrappers around the following global functions from * [Mocha's BDD interface](https://github.com/mochajs/mocha): * * - after * - afterEach * - before * - beforeEach * - it * - it.only * - it.skip * - xit * * Each of the wrapped functions support generator functions. If the generator * {@linkplain ../lib/promise.consume yields a promise}, the test will wait * for that promise to resolve before invoking the next iteration of the * generator: * * test.it('generators', function*() { * let x = yield Promise.resolve(1); * assert.equal(x, 1); * }); * * The provided wrappers leverage the {@link webdriver.promise.ControlFlow} * to simplify writing asynchronous tests: * * var {Builder, By, until} = require('selenium-webdriver'); * var test = require('selenium-webdriver/testing'); * * test.describe('Google Search', function() { * var driver; * * test.before(function() { * driver = new Builder().forBrowser('firefox').build(); * }); * * test.after(function() { * driver.quit(); * }); * * test.it('should append query to title', function() { * driver.get('http://www.google.com/ncr'); * driver.findElement(By.name('q')).sendKeys('webdriver'); * driver.findElement(By.name('btnG')).click(); * driver.wait(until.titleIs('webdriver - Google Search'), 1000); * }); * }); * * 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 promise = require('..').promise; const flow = (function() { const initial = process.env['SELENIUM_PROMISE_MANAGER']; try { process.env['SELENIUM_PROMISE_MANAGER'] = '1'; return promise.controlFlow(); } finally { if (initial === undefined) { delete process.env['SELENIUM_PROMISE_MANAGER']; } else { process.env['SELENIUM_PROMISE_MANAGER'] = initial; } } })(); /** * Wraps a function so that all passed arguments are ignored. * @param {!Function} fn The function to wrap. * @return {!Function} The wrapped function. */ function seal(fn) { return function() { fn(); }; } /** * Wraps a function on Mocha's BDD interface so it runs inside a * webdriver.promise.ControlFlow and waits for the flow to complete before * continuing. * @param {!Function} globalFn The function to wrap. * @return {!Function} The new function. */ function wrapped(globalFn) { return function() { if (arguments.length === 1) { return globalFn(wrapArgument(arguments[0])); } else if (arguments.length === 2) { return globalFn(arguments[0], wrapArgument(arguments[1])); } else { throw Error('Invalid # arguments: ' + arguments.length); } }; } function wrapArgument(value) { if (typeof value === 'function') { return makeAsyncTestFn(value); } return value; } /** * Make a wrapper to invoke caller's test function, fn. Run the test function * within a ControlFlow. * * Should preserve the semantics of Mocha's Runnable.prototype.run (See * https://github.com/mochajs/mocha/blob/master/lib/runnable.js#L192) * * @param {!Function} fn * @return {!Function} */ function makeAsyncTestFn(fn) { const isAsync = fn.length > 0; const isGenerator = promise.isGenerator(fn); if (isAsync && isGenerator) { throw TypeError( 'generator-based tests must not take a callback; for async testing,' + ' return a promise (or yield on a promise)'); } var ret = /** @type {function(this: mocha.Context)}*/ (function(done) { const runTest = (resolve, reject) => { try { if (isAsync) { fn.call(this, err => err ? reject(err) : resolve()); } else if (isGenerator) { resolve(promise.consume(fn, this)); } else { resolve(fn.call(this)); } } catch (ex) { reject(ex); } }; if (!promise.USE_PROMISE_MANAGER) { new Promise(runTest).then(seal(done), done); return; } var runnable = this.runnable(); var mochaCallback = runnable.callback; runnable.callback = function() { flow.reset(); return mochaCallback.apply(this, arguments); }; flow.execute(function controlFlowExecute() { return new promise.Promise(function(fulfill, reject) { return runTest(fulfill, reject); }, flow); }, runnable.fullTitle()).then(seal(done), done); }); ret.toString = function() { return fn.toString(); }; return ret; } /** * Ignores the test chained to this function if the provided predicate returns * true. * @param {function(): boolean} predicateFn A predicate to call to determine * if the test should be suppressed. This function MUST be synchronous. * @return {!Object} An object with wrapped versions of {@link #it()} and * {@link #describe()} that ignore tests as indicated by the predicate. */ function ignore(predicateFn) { var describe = wrap(exports.xdescribe, exports.describe); describe.only = wrap(exports.xdescribe, exports.describe.only); var it = wrap(exports.xit, exports.it); it.only = wrap(exports.xit, exports.it.only); return { describe: describe, it: it }; function wrap(onSkip, onRun) { return function(title, fn) { if (predicateFn()) { onSkip(title, fn); } else { onRun(title, fn); } }; } } /** * @param {string} name * @return {!Function} * @throws {TypeError} */ function getMochaGlobal(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 the mocha executable'); } return fn; } const WRAPPED = { after: null, afterEach: null, before: null, beforeEach: null, it: null, itOnly: null, xit: null }; function wrapIt() { if (!WRAPPED.it) { let it = getMochaGlobal('it'); WRAPPED.it = wrapped(it); WRAPPED.itOnly = wrapped(it.only); } } // PUBLIC API /** * @return {!promise.ControlFlow} the control flow instance used by this module * to coordinate test actions. */ exports.controlFlow = function(){ return flow; }; /** * Registers a new test suite. * @param {string} name The suite name. * @param {function()=} opt_fn The suite function, or `undefined` to define * a pending test suite. */ exports.describe = function(name, opt_fn) { let fn = getMochaGlobal('describe'); return opt_fn ? fn(name, opt_fn) : fn(name); }; /** * An alias for {@link #describe()} that marks the suite as exclusive, * suppressing all other test suites. * @param {string} name The suite name. * @param {function()=} opt_fn The suite function, or `undefined` to define * a pending test suite. */ exports.describe.only = function(name, opt_fn) { let desc = getMochaGlobal('describe'); return opt_fn ? desc.only(name, opt_fn) : desc.only(name); }; /** * Defines a suppressed test suite. * @param {string} name The suite name. * @param {function()=} opt_fn The suite function, or `undefined` to define * a pending test suite. */ exports.describe.skip = function(name, opt_fn) { let fn = getMochaGlobal('describe'); return opt_fn ? fn.skip(name, opt_fn) : fn.skip(name); }; /** * Defines a suppressed test suite. * @param {string} name The suite name. * @param {function()=} opt_fn The suite function, or `undefined` to define * a pending test suite. */ exports.xdescribe = function(name, opt_fn) { let fn = getMochaGlobal('xdescribe'); return opt_fn ? fn(name, opt_fn) : fn(name); }; /** * Register a function to call after the current suite finishes. * @param {function()} fn . */ exports.after = function(fn) { if (!WRAPPED.after) { WRAPPED.after = wrapped(getMochaGlobal('after')); } WRAPPED.after(fn); }; /** * Register a function to call after each test in a suite. * @param {function()} fn . */ exports.afterEach = function(fn) { if (!WRAPPED.afterEach) { WRAPPED.afterEach = wrapped(getMochaGlobal('afterEach')); } WRAPPED.afterEach(fn); }; /** * Register a function to call before the current suite starts. * @param {function()} fn . */ exports.before = function(fn) { if (!WRAPPED.before) { WRAPPED.before = wrapped(getMochaGlobal('before')); } WRAPPED.before(fn); }; /** * Register a function to call before each test in a suite. * @param {function()} fn . */ exports.beforeEach = function(fn) { if (!WRAPPED.beforeEach) { WRAPPED.beforeEach = wrapped(getMochaGlobal('beforeEach')); } WRAPPED.beforeEach(fn); }; /** * Add a test to the current suite. * @param {string} name The test name. * @param {function()=} opt_fn The test function, or `undefined` to define * a pending test case. */ exports.it = function(name, opt_fn) { wrapIt(); if (opt_fn) { WRAPPED.it(name, opt_fn); } else { WRAPPED.it(name); } }; /** * An alias for {@link #it()} that flags the test as the only one that should * be run within the current suite. * @param {string} name The test name. * @param {function()=} opt_fn The test function, or `undefined` to define * a pending test case. */ exports.it.only = function(name, opt_fn) { wrapIt(); if (opt_fn) { WRAPPED.itOnly(name, opt_fn); } else { WRAPPED.itOnly(name); } }; /** * Adds a test to the current suite while suppressing it so it is not run. * @param {string} name The test name. * @param {function()=} opt_fn The test function, or `undefined` to define * a pending test case. */ exports.xit = function(name, opt_fn) { if (!WRAPPED.xit) { WRAPPED.xit = wrapped(getMochaGlobal('xit')); } if (opt_fn) { WRAPPED.xit(name, opt_fn); } else { WRAPPED.xit(name); } }; exports.it.skip = exports.xit; exports.ignore = ignore;