| // 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, Key, 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', Key.RETURN); |
| * 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; |