UNPKG

10.7 kBJavaScriptView Raw
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 * Each of the wrapped functions support generator functions. If the generator
32 * {@linkplain ../lib/promise.consume yields a promise}, the test will wait
33 * for that promise to resolve before invoking the next iteration of the
34 * generator:
35 *
36 * test.it('generators', function*() {
37 * let x = yield Promise.resolve(1);
38 * assert.equal(x, 1);
39 * });
40 *
41 * The provided wrappers leverage the {@link webdriver.promise.ControlFlow}
42 * to simplify writing asynchronous tests:
43 *
44 * var By = require('selenium-webdriver').By,
45 * until = require('selenium-webdriver').until,
46 * firefox = require('selenium-webdriver/firefox'),
47 * test = require('selenium-webdriver/testing');
48 *
49 * test.describe('Google Search', function() {
50 * var driver;
51 *
52 * test.before(function() {
53 * driver = new firefox.Driver();
54 * });
55 *
56 * test.after(function() {
57 * driver.quit();
58 * });
59 *
60 * test.it('should append query to title', function() {
61 * driver.get('http://www.google.com/ncr');
62 * driver.findElement(By.name('q')).sendKeys('webdriver');
63 * driver.findElement(By.name('btnG')).click();
64 * driver.wait(until.titleIs('webdriver - Google Search'), 1000);
65 * });
66 * });
67 *
68 * You may conditionally suppress a test function using the exported
69 * "ignore" function. If the provided predicate returns true, the attached
70 * test case will be skipped:
71 *
72 * test.ignore(maybe()).it('is flaky', function() {
73 * if (Math.random() < 0.5) throw Error();
74 * });
75 *
76 * function maybe() { return Math.random() < 0.5; }
77 */
78
79'use strict';
80
81const promise = require('..').promise;
82const flow = (function() {
83 const initial = process.env['SELENIUM_PROMISE_MANAGER'];
84 try {
85 process.env['SELENIUM_PROMISE_MANAGER'] = '1';
86 return promise.controlFlow();
87 } finally {
88 if (initial === undefined) {
89 delete process.env['SELENIUM_PROMISE_MANAGER'];
90 } else {
91 process.env['SELENIUM_PROMISE_MANAGER'] = initial;
92 }
93 }
94})();
95
96
97/**
98 * Wraps a function so that all passed arguments are ignored.
99 * @param {!Function} fn The function to wrap.
100 * @return {!Function} The wrapped function.
101 */
102function seal(fn) {
103 return function() {
104 fn();
105 };
106}
107
108
109/**
110 * Wraps a function on Mocha's BDD interface so it runs inside a
111 * webdriver.promise.ControlFlow and waits for the flow to complete before
112 * continuing.
113 * @param {!Function} globalFn The function to wrap.
114 * @return {!Function} The new function.
115 */
116function wrapped(globalFn) {
117 return function() {
118 if (arguments.length === 1) {
119 return globalFn(wrapArgument(arguments[0]));
120
121 } else if (arguments.length === 2) {
122 return globalFn(arguments[0], wrapArgument(arguments[1]));
123
124 } else {
125 throw Error('Invalid # arguments: ' + arguments.length);
126 }
127 };
128}
129
130
131function wrapArgument(value) {
132 if (typeof value === 'function') {
133 return makeAsyncTestFn(value);
134 }
135 return value;
136}
137
138
139/**
140 * Make a wrapper to invoke caller's test function, fn. Run the test function
141 * within a ControlFlow.
142 *
143 * Should preserve the semantics of Mocha's Runnable.prototype.run (See
144 * https://github.com/mochajs/mocha/blob/master/lib/runnable.js#L192)
145 *
146 * @param {!Function} fn
147 * @return {!Function}
148 */
149function makeAsyncTestFn(fn) {
150 const isAsync = fn.length > 0;
151 const isGenerator = promise.isGenerator(fn);
152 if (isAsync && isGenerator) {
153 throw TypeError(
154 'generator-based tests must not take a callback; for async testing,'
155 + ' return a promise (or yield on a promise)');
156 }
157
158 var ret = /** @type {function(this: mocha.Context)}*/ (function(done) {
159 const runTest = (resolve, reject) => {
160 try {
161 if (isAsync) {
162 fn.call(this, err => err ? reject(err) : resolve());
163 } else if (isGenerator) {
164 resolve(promise.consume(fn, this));
165 } else {
166 resolve(fn.call(this));
167 }
168 } catch (ex) {
169 reject(ex);
170 }
171 };
172
173 if (!promise.USE_PROMISE_MANAGER) {
174 new Promise(runTest).then(seal(done), done);
175 return;
176 }
177
178 var runnable = this.runnable();
179 var mochaCallback = runnable.callback;
180 runnable.callback = function() {
181 flow.reset();
182 return mochaCallback.apply(this, arguments);
183 };
184
185 flow.execute(function controlFlowExecute() {
186 return new promise.Promise(function(fulfill, reject) {
187 return runTest(fulfill, reject);
188 }, flow);
189 }, runnable.fullTitle()).then(seal(done), done);
190 });
191
192 ret.toString = function() {
193 return fn.toString();
194 };
195
196 return ret;
197}
198
199
200/**
201 * Ignores the test chained to this function if the provided predicate returns
202 * true.
203 * @param {function(): boolean} predicateFn A predicate to call to determine
204 * if the test should be suppressed. This function MUST be synchronous.
205 * @return {!Object} An object with wrapped versions of {@link #it()} and
206 * {@link #describe()} that ignore tests as indicated by the predicate.
207 */
208function ignore(predicateFn) {
209 var describe = wrap(exports.xdescribe, exports.describe);
210 describe.only = wrap(exports.xdescribe, exports.describe.only);
211
212 var it = wrap(exports.xit, exports.it);
213 it.only = wrap(exports.xit, exports.it.only);
214
215 return {
216 describe: describe,
217 it: it
218 };
219
220 function wrap(onSkip, onRun) {
221 return function(title, fn) {
222 if (predicateFn()) {
223 onSkip(title, fn);
224 } else {
225 onRun(title, fn);
226 }
227 };
228 }
229}
230
231
232/**
233 * @param {string} name
234 * @return {!Function}
235 * @throws {TypeError}
236 */
237function getMochaGlobal(name) {
238 let fn = global[name];
239 let type = typeof fn;
240 if (type !== 'function') {
241 throw TypeError(
242 `Expected global.${name} to be a function, but is ${type}. `
243 + 'This can happen if you try using this module when running '
244 + 'with node directly instead of using the mocha executable');
245 }
246 return fn;
247}
248
249
250const WRAPPED = {
251 after: null,
252 afterEach: null,
253 before: null,
254 beforeEach: null,
255 it: null,
256 itOnly: null,
257 xit: null
258};
259
260
261function wrapIt() {
262 if (!WRAPPED.it) {
263 let it = getMochaGlobal('it');
264 WRAPPED.it = wrapped(it);
265 WRAPPED.itOnly = wrapped(it.only);
266 }
267}
268
269
270
271// PUBLIC API
272
273
274/**
275 * @return {!promise.ControlFlow} the control flow instance used by this module
276 * to coordinate test actions.
277 */
278exports.controlFlow = function(){
279 return flow;
280};
281
282
283/**
284 * Registers a new test suite.
285 * @param {string} name The suite name.
286 * @param {function()=} opt_fn The suite function, or `undefined` to define
287 * a pending test suite.
288 */
289exports.describe = function(name, opt_fn) {
290 let fn = getMochaGlobal('describe');
291 return opt_fn ? fn(name, opt_fn) : fn(name);
292};
293
294
295/**
296 * Defines a suppressed test suite.
297 * @param {string} name The suite name.
298 * @param {function()=} opt_fn The suite function, or `undefined` to define
299 * a pending test suite.
300 */
301exports.describe.skip = function(name, opt_fn) {
302 let fn = getMochaGlobal('describe');
303 return opt_fn ? fn.skip(name, opt_fn) : fn.skip(name);
304};
305
306
307/**
308 * Defines a suppressed test suite.
309 * @param {string} name The suite name.
310 * @param {function()=} opt_fn The suite function, or `undefined` to define
311 * a pending test suite.
312 */
313exports.xdescribe = function(name, opt_fn) {
314 let fn = getMochaGlobal('xdescribe');
315 return opt_fn ? fn(name, opt_fn) : fn(name);
316};
317
318
319/**
320 * Register a function to call after the current suite finishes.
321 * @param {function()} fn .
322 */
323exports.after = function(fn) {
324 if (!WRAPPED.after) {
325 WRAPPED.after = wrapped(getMochaGlobal('after'));
326 }
327 WRAPPED.after(fn);
328};
329
330
331/**
332 * Register a function to call after each test in a suite.
333 * @param {function()} fn .
334 */
335exports.afterEach = function(fn) {
336 if (!WRAPPED.afterEach) {
337 WRAPPED.afterEach = wrapped(getMochaGlobal('afterEach'));
338 }
339 WRAPPED.afterEach(fn);
340};
341
342
343/**
344 * Register a function to call before the current suite starts.
345 * @param {function()} fn .
346 */
347exports.before = function(fn) {
348 if (!WRAPPED.before) {
349 WRAPPED.before = wrapped(getMochaGlobal('before'));
350 }
351 WRAPPED.before(fn);
352};
353
354/**
355 * Register a function to call before each test in a suite.
356 * @param {function()} fn .
357 */
358exports.beforeEach = function(fn) {
359 if (!WRAPPED.beforeEach) {
360 WRAPPED.beforeEach = wrapped(getMochaGlobal('beforeEach'));
361 }
362 WRAPPED.beforeEach(fn);
363};
364
365/**
366 * Add a test to the current suite.
367 * @param {string} name The test name.
368 * @param {function()=} opt_fn The test function, or `undefined` to define
369 * a pending test case.
370 */
371exports.it = function(name, opt_fn) {
372 wrapIt();
373 if (opt_fn) {
374 WRAPPED.it(name, opt_fn);
375 } else {
376 WRAPPED.it(name);
377 }
378};
379
380/**
381 * An alias for {@link #it()} that flags the test as the only one that should
382 * be run within the current suite.
383 * @param {string} name The test name.
384 * @param {function()=} opt_fn The test function, or `undefined` to define
385 * a pending test case.
386 */
387exports.it.only = function(name, opt_fn) {
388 wrapIt();
389 if (opt_fn) {
390 WRAPPED.itOnly(name, opt_fn);
391 } else {
392 WRAPPED.itOnly(name);
393 }
394};
395
396
397/**
398 * Adds a test to the current suite while suppressing it so it is not run.
399 * @param {string} name The test name.
400 * @param {function()=} opt_fn The test function, or `undefined` to define
401 * a pending test case.
402 */
403exports.xit = function(name, opt_fn) {
404 if (!WRAPPED.xit) {
405 WRAPPED.xit = wrapped(getMochaGlobal('xit'));
406 }
407 if (opt_fn) {
408 WRAPPED.xit(name, opt_fn);
409 } else {
410 WRAPPED.xit(name);
411 }
412};
413
414
415exports.it.skip = exports.xit;
416exports.ignore = ignore;