testing/index.js

1// Licensed to the Software Freedom Conservancy (SFC) under one
2// or more contributor license agreements. See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership. The SFC licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License. You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied. See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18/**
19 * @fileoverview Provides wrappers around the following global functions from
20 * [Mocha's BDD interface](https://github.com/mochajs/mocha):
21 *
22 * - after
23 * - afterEach
24 * - before
25 * - beforeEach
26 * - it
27 * - it.only
28 * - it.skip
29 * - xit
30 *
31 * The provided wrappers leverage the {@link webdriver.promise.ControlFlow}
32 * to simplify writing asynchronous tests:
33 *
34 * var By = require('selenium-webdriver').By,
35 * until = require('selenium-webdriver').until,
36 * firefox = require('selenium-webdriver/firefox'),
37 * test = require('selenium-webdriver/testing');
38 *
39 * test.describe('Google Search', function() {
40 * var driver;
41 *
42 * test.before(function() {
43 * driver = new firefox.Driver();
44 * });
45 *
46 * test.after(function() {
47 * driver.quit();
48 * });
49 *
50 * test.it('should append query to title', function() {
51 * driver.get('http://www.google.com/ncr');
52 * driver.findElement(By.name('q')).sendKeys('webdriver');
53 * driver.findElement(By.name('btnG')).click();
54 * driver.wait(until.titleIs('webdriver - Google Search'), 1000);
55 * });
56 * });
57 *
58 * You may conditionally suppress a test function using the exported
59 * "ignore" function. If the provided predicate returns true, the attached
60 * test case will be skipped:
61 *
62 * test.ignore(maybe()).it('is flaky', function() {
63 * if (Math.random() < 0.5) throw Error();
64 * });
65 *
66 * function maybe() { return Math.random() < 0.5; }
67 */
68
69var promise = require('..').promise;
70var flow = promise.controlFlow();
71
72
73/**
74 * Wraps a function so that all passed arguments are ignored.
75 * @param {!Function} fn The function to wrap.
76 * @return {!Function} The wrapped function.
77 */
78function seal(fn) {
79 return function() {
80 fn();
81 };
82}
83
84
85/**
86 * Wraps a function on Mocha's BDD interface so it runs inside a
87 * webdriver.promise.ControlFlow and waits for the flow to complete before
88 * continuing.
89 * @param {!Function} globalFn The function to wrap.
90 * @return {!Function} The new function.
91 */
92function wrapped(globalFn) {
93 return function() {
94 if (arguments.length === 1) {
95 return globalFn(makeAsyncTestFn(arguments[0]));
96 }
97 else if (arguments.length === 2) {
98 return globalFn(arguments[0], makeAsyncTestFn(arguments[1]));
99 }
100 else {
101 throw Error('Invalid # arguments: ' + arguments.length);
102 }
103 };
104}
105
106/**
107 * Make a wrapper to invoke caller's test function, fn. Run the test function
108 * within a ControlFlow.
109 *
110 * Should preserve the semantics of Mocha's Runnable.prototype.run (See
111 * https://github.com/mochajs/mocha/blob/master/lib/runnable.js#L192)
112 *
113 * @param {Function} fn
114 * @return {Function}
115 */
116function makeAsyncTestFn(fn) {
117 var async = fn.length > 0; // if test function expects a callback, its "async"
118
119 var ret = function(done) {
120 var runnable = this.runnable();
121 var mochaCallback = runnable.callback;
122 runnable.callback = function() {
123 flow.reset();
124 return mochaCallback.apply(this, arguments);
125 };
126
127 var testFn = fn.bind(this);
128 flow.execute(function controlFlowExecute() {
129 return new promise.Promise(function(fulfill, reject) {
130 if (async) {
131 // If testFn is async (it expects a done callback), resolve the promise of this
132 // test whenever that callback says to. Any promises returned from testFn are
133 // ignored.
134 testFn(function testFnDoneCallback(err) {
135 if (err) {
136 reject(err);
137 } else {
138 fulfill();
139 }
140 });
141 } else {
142 // Without a callback, testFn can return a promise, or it will
143 // be assumed to have completed synchronously
144 fulfill(testFn());
145 }
146 }, flow);
147 }, runnable.fullTitle()).then(seal(done), done);
148 };
149
150 ret.toString = function() {
151 return fn.toString();
152 };
153
154 return ret;
155}
156
157
158/**
159 * Ignores the test chained to this function if the provided predicate returns
160 * true.
161 * @param {function(): boolean} predicateFn A predicate to call to determine
162 * if the test should be suppressed. This function MUST be synchronous.
163 * @return {!Object} An object with wrapped versions of {@link #it()} and
164 * {@link #describe()} that ignore tests as indicated by the predicate.
165 */
166function ignore(predicateFn) {
167 var describe = wrap(exports.xdescribe, exports.describe);
168 describe.only = wrap(exports.xdescribe, exports.describe.only);
169
170 var it = wrap(exports.xit, exports.it);
171 it.only = wrap(exports.xit, exports.it.only);
172
173 return {
174 describe: describe,
175 it: it
176 };
177
178 function wrap(onSkip, onRun) {
179 return function(title, fn) {
180 if (predicateFn()) {
181 onSkip(title, fn);
182 } else {
183 onRun(title, fn);
184 }
185 };
186 }
187}
188
189
190// PUBLIC API
191
192/**
193 * Registers a new test suite.
194 * @param {string} name The suite name.
195 * @param {function()=} fn The suite function, or {@code undefined} to define
196 * a pending test suite.
197 */
198exports.describe = global.describe;
199
200/**
201 * Defines a suppressed test suite.
202 * @param {string} name The suite name.
203 * @param {function()=} fn The suite function, or {@code undefined} to define
204 * a pending test suite.
205 */
206exports.xdescribe = global.xdescribe;
207exports.describe.skip = global.describe.skip;
208
209/**
210 * Register a function to call after the current suite finishes.
211 * @param {function()} fn .
212 */
213exports.after = wrapped(global.after);
214
215/**
216 * Register a function to call after each test in a suite.
217 * @param {function()} fn .
218 */
219exports.afterEach = wrapped(global.afterEach);
220
221/**
222 * Register a function to call before the current suite starts.
223 * @param {function()} fn .
224 */
225exports.before = wrapped(global.before);
226
227/**
228 * Register a function to call before each test in a suite.
229 * @param {function()} fn .
230 */
231exports.beforeEach = wrapped(global.beforeEach);
232
233/**
234 * Add a test to the current suite.
235 * @param {string} name The test name.
236 * @param {function()=} fn The test function, or {@code undefined} to define
237 * a pending test case.
238 */
239exports.it = wrapped(global.it);
240
241/**
242 * An alias for {@link #it()} that flags the test as the only one that should
243 * be run within the current suite.
244 * @param {string} name The test name.
245 * @param {function()=} fn The test function, or {@code undefined} to define
246 * a pending test case.
247 */
248exports.iit = exports.it.only = wrapped(global.it.only);
249
250/**
251 * Adds a test to the current suite while suppressing it so it is not run.
252 * @param {string} name The test name.
253 * @param {function()=} fn The test function, or {@code undefined} to define
254 * a pending test case.
255 */
256exports.xit = exports.it.skip = wrapped(global.xit);
257
258exports.ignore = ignore;