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