--- /dev/null
+// Copyright 2013 Selenium committers
+// Copyright 2013 Software Freedom Conservancy
+//
+// Licensed 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
+ *
+ * The provided wrappers leverage the {@link webdriver.promise.ControlFlow}
+ * to simplify writing asynchronous tests:
+ *
+ * var By = require('selenium-webdriver').By,
+ * until = require('selenium-webdriver').until,
+ * firefox = require('selenium-webdriver/firefox'),
+ * test = require('selenium-webdriver/testing');
+ *
+ * test.describe('Google Search', function() {
+ * var driver;
+ *
+ * test.before(function() {
+ * driver = new firefox.Driver();
+ * });
+ *
+ * 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; }
+ */
+
+var promise = require('..').promise;
+var flow = promise.controlFlow();
+
+
+/**
+ * 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(makeAsyncTestFn(arguments[0]));
+ }
+ else if (arguments.length === 2) {
+ return globalFn(arguments[0], makeAsyncTestFn(arguments[1]));
+ }
+ else {
+ throw Error('Invalid # arguments: ' + arguments.length);
+ }
+ };
+}
+
+/**
+ * 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) {
+ var async = fn.length > 0; // if test function expects a callback, its "async"
+
+ var ret = function(done) {
+ var runnable = this.runnable();
+ var mochaCallback = runnable.callback;
+ runnable.callback = function() {
+ flow.reset();
+ return mochaCallback.apply(this, arguments);
+ };
+
+ var testFn = fn.bind(this);
+ flow.execute(function controlFlowExecute() {
+ return new promise.Promise(function(fulfill, reject) {
+ if (async) {
+ // If testFn is async (it expects a done callback), resolve the promise of this
+ // test whenever that callback says to. Any promises returned from testFn are
+ // ignored.
+ testFn(function testFnDoneCallback(err) {
+ if (err) {
+ reject(err);
+ } else {
+ fulfill();
+ }
+ });
+ } else {
+ // Without a callback, testFn can return a promise, or it will
+ // be assumed to have completed synchronously
+ fulfill(testFn());
+ }
+ }, 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);
+ }
+ };
+ }
+}
+
+
+// PUBLIC API
+
+/**
+ * Registers a new test suite.
+ * @param {string} name The suite name.
+ * @param {function()=} fn The suite function, or {@code undefined} to define
+ * a pending test suite.
+ */
+exports.describe = global.describe;
+
+/**
+ * Defines a suppressed test suite.
+ * @param {string} name The suite name.
+ * @param {function()=} fn The suite function, or {@code undefined} to define
+ * a pending test suite.
+ */
+exports.xdescribe = global.xdescribe;
+exports.describe.skip = global.describe.skip;
+
+/**
+ * Register a function to call after the current suite finishes.
+ * @param {function()} fn .
+ */
+exports.after = wrapped(global.after);
+
+/**
+ * Register a function to call after each test in a suite.
+ * @param {function()} fn .
+ */
+exports.afterEach = wrapped(global.afterEach);
+
+/**
+ * Register a function to call before the current suite starts.
+ * @param {function()} fn .
+ */
+exports.before = wrapped(global.before);
+
+/**
+ * Register a function to call before each test in a suite.
+ * @param {function()} fn .
+ */
+exports.beforeEach = wrapped(global.beforeEach);
+
+/**
+ * Add a test to the current suite.
+ * @param {string} name The test name.
+ * @param {function()=} fn The test function, or {@code undefined} to define
+ * a pending test case.
+ */
+exports.it = wrapped(global.it);
+
+/**
+ * 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()=} fn The test function, or {@code undefined} to define
+ * a pending test case.
+ */
+exports.iit = exports.it.only = wrapped(global.it.only);
+
+/**
+ * Adds a test to the current suite while suppressing it so it is not run.
+ * @param {string} name The test name.
+ * @param {function()=} fn The test function, or {@code undefined} to define
+ * a pending test case.
+ */
+exports.xit = exports.it.skip = wrapped(global.xit);
+
+exports.ignore = ignore;