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 | ;
|
80 |
|
81 | const promise = require('..').promise;
|
82 | const 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 | */
|
102 | function 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 | */
|
116 | function 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 |
|
131 | function 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 | */
|
149 | function 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 | */
|
208 | function 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 | */
|
237 | function 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 |
|
250 | const 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 |
|
261 | function 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 | */
|
278 | exports.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 | */
|
289 | exports.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 | */
|
301 | exports.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 | */
|
313 | exports.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 | */
|
323 | exports.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 | */
|
335 | exports.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 | */
|
347 | exports.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 | */
|
358 | exports.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 | */
|
371 | exports.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 | */
|
387 | exports.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 | */
|
403 | exports.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 |
|
415 | exports.it.skip = exports.xit;
|
416 | exports.ignore = ignore;
|