testing/index.js

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