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 * <a href="http://visionmedia.github.io/mocha/">Mocha's BDD interface</a>:
19 * <ul>
20 * <li>after
21 * <li>afterEach
22 * <li>before
23 * <li>beforeEach
24 * <li>it
25 * <li>it.only
26 * <li>it.skip
27 * <li>xit
28 * </ul>
29 *
30 * <p>The provided wrappers leverage the {@link webdriver.promise.ControlFlow}
31 * to simplify writing asynchronous tests:
32 * <pre><code>
33 * var By = require('selenium-webdriver').By,
34 * until = require('selenium-webdriver').until,
35 * firefox = require('selenium-webdriver/firefox'),
36 * test = require('selenium-webdriver/testing');
37 *
38 * test.describe('Google Search', function() {
39 * var driver;
40 *
41 * test.before(function() {
42 * driver = new firefox.Driver();
43 * });
44 *
45 * test.after(function() {
46 * driver.quit();
47 * });
48 *
49 * test.it('should append query to title', function() {
50 * driver.get('http://www.google.com/ncr');
51 * driver.findElement(By.name('q')).sendKeys('webdriver');
52 * driver.findElement(By.name('btnG')).click();
53 * driver.wait(until.titleIs('webdriver - Google Search'), 1000);
54 * });
55 * });
56 * </code></pre>
57 *
58 * <p>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 * <pre><code>
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 * </code></pre>
68 */
69
70var promise = require('..').promise;
71var flow = promise.controlFlow();
72
73
74/**
75 * Wraps a function so that all passed arguments are ignored.
76 * @param {!Function} fn The function to wrap.
77 * @return {!Function} The wrapped function.
78 */
79function seal(fn) {
80 return function() {
81 fn();
82 };
83}
84
85
86/**
87 * Wraps a function on Mocha's BDD interface so it runs inside a
88 * webdriver.promise.ControlFlow and waits for the flow to complete before
89 * continuing.
90 * @param {!Function} globalFn The function to wrap.
91 * @return {!Function} The new function.
92 */
93function wrapped(globalFn) {
94 return function() {
95 if (arguments.length === 1) {
96 return globalFn(asyncTestFn(arguments[0]));
97 }
98 else if (arguments.length === 2) {
99 return globalFn(arguments[0], asyncTestFn(arguments[1]));
100 }
101 else {
102 throw Error('Invalid # arguments: ' + arguments.length);
103 }
104 };
105
106 function asyncTestFn(fn) {
107 var ret = function(done) {
108 function cleanupBeforeCallback() {
109 flow.reset();
110 return cleanupBeforeCallback.mochaCallback.apply(this, arguments);
111 }
112 // We set this as an attribute of the callback function to allow us to
113 // test this properly.
114 cleanupBeforeCallback.mochaCallback = this.runnable().callback;
115
116 this.runnable().callback = cleanupBeforeCallback;
117
118 var testFn = fn.bind(this);
119 flow.execute(function() {
120 var done = promise.defer();
121 promise.asap(testFn(done.reject), done.fulfill, done.reject);
122 return done.promise;
123 }).then(seal(done), done);
124 };
125
126 ret.toString = function() {
127 return fn.toString();
128 };
129
130 return ret;
131 }
132}
133
134
135/**
136 * Ignores the test chained to this function if the provided predicate returns
137 * true.
138 * @param {function(): boolean} predicateFn A predicate to call to determine
139 * if the test should be suppressed. This function MUST be synchronous.
140 * @return {!Object} An object with wrapped versions of {@link #it()} and
141 * {@link #describe()} that ignore tests as indicated by the predicate.
142 */
143function ignore(predicateFn) {
144 var describe = wrap(exports.xdescribe, exports.describe);
145 describe.only = wrap(exports.xdescribe, exports.describe.only);
146
147 var it = wrap(exports.xit, exports.it);
148 it.only = wrap(exports.xit, exports.it.only);
149
150 return {
151 describe: describe,
152 it: it
153 };
154
155 function wrap(onSkip, onRun) {
156 return function(title, fn) {
157 if (predicateFn()) {
158 onSkip(title, fn);
159 } else {
160 onRun(title, fn);
161 }
162 };
163 }
164}
165
166
167// PUBLIC API
168
169/**
170 * Registers a new test suite.
171 * @param {string} name The suite name.
172 * @param {function()=} fn The suite function, or {@code undefined} to define
173 * a pending test suite.
174 */
175exports.describe = global.describe;
176
177/**
178 * Defines a suppressed test suite.
179 * @param {string} name The suite name.
180 * @param {function()=} fn The suite function, or {@code undefined} to define
181 * a pending test suite.
182 */
183exports.xdescribe = global.xdescribe;
184exports.describe.skip = global.describe.skip;
185
186/**
187 * Register a function to call after the current suite finishes.
188 * @param {function()} fn .
189 */
190exports.after = wrapped(global.after);
191
192/**
193 * Register a function to call after each test in a suite.
194 * @param {function()} fn .
195 */
196exports.afterEach = wrapped(global.afterEach);
197
198/**
199 * Register a function to call before the current suite starts.
200 * @param {function()} fn .
201 */
202exports.before = wrapped(global.before);
203
204/**
205 * Register a function to call before each test in a suite.
206 * @param {function()} fn .
207 */
208exports.beforeEach = wrapped(global.beforeEach);
209
210/**
211 * Add a test to the current suite.
212 * @param {string} name The test name.
213 * @param {function()=} fn The test function, or {@code undefined} to define
214 * a pending test case.
215 */
216exports.it = wrapped(global.it);
217
218/**
219 * An alias for {@link #it()} that flags the test as the only one that should
220 * be run within the current suite.
221 * @param {string} name The test name.
222 * @param {function()=} fn The test function, or {@code undefined} to define
223 * a pending test case.
224 */
225exports.iit = exports.it.only = wrapped(global.it.only);
226
227/**
228 * Adds a test to the current suite while suppressing it so it is not run.
229 * @param {string} name The test name.
230 * @param {function()=} fn The test function, or {@code undefined} to define
231 * a pending test case.
232 */
233exports.xit = exports.it.skip = wrapped(global.xit);
234
235exports.ignore = ignore;