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 webdriver.promise.ControlFlow to
31 * simplify writing asynchronous tests:
32 * <pre><code>
33 * var webdriver = require('selenium-webdriver'),
34 * remote = require('selenium-webdriver/remote'),
35 * test = require('selenium-webdriver/testing');
36 *
37 * test.describe('Google Search', function() {
38 * var driver, server;
39 *
40 * test.before(function() {
41 * server = new remote.SeleniumServer({
42 * jar: 'path/to/selenium-server-standalone.jar'
43 * });
44 * server.start();
45 *
46 * driver = new webdriver.Builder().
47 * withCapabilities({'browserName': 'firefox'}).
48 * usingServer(server.address()).
49 * build();
50 * });
51 *
52 * test.after(function() {
53 * driver.quit();
54 * server.stop();
55 * });
56 *
57 * test.it('should append query to title', function() {
58 * driver.get('http://www.google.com');
59 * driver.findElement(webdriver.By.name('q')).sendKeys('webdriver');
60 * driver.findElement(webdriver.By.name('btnG')).click();
61 * driver.wait(function() {
62 * return driver.getTitle().then(function(title) {
63 * return 'webdriver - Google Search' === title;
64 * });
65 * }, 1000, 'Waiting for title to update');
66 * });
67 * });
68 * </code></pre>
69 *
70 * <p>You may conditionally suppress a test function using the exported
71 * "ignore" function. If the provided predicate returns true, the attached
72 * test case will be skipped:
73 * <pre><code>
74 * test.ignore(maybe()).it('is flaky', function() {
75 * if (Math.random() < 0.5) throw Error();
76 * });
77 *
78 * function maybe() { return Math.random() < 0.5; }
79 * </code></pre>
80 */
81
82var flow = require('..').promise.controlFlow();
83
84
85/**
86 * Wraps a function so that all passed arguments are ignored.
87 * @param {!Function} fn The function to wrap.
88 * @return {!Function} The wrapped function.
89 */
90function seal(fn) {
91 return function() {
92 fn();
93 };
94}
95
96
97/**
98 * Wraps a function on Mocha's BDD interface so it runs inside a
99 * webdriver.promise.ControlFlow and waits for the flow to complete before
100 * continuing.
101 * @param {!Function} globalFn The function to wrap.
102 * @return {!Function} The new function.
103 */
104function wrapped(globalFn) {
105 return function() {
106 switch (arguments.length) {
107 case 1:
108 globalFn(asyncTestFn(arguments[0]));
109 break;
110
111 case 2:
112 globalFn(arguments[0], asyncTestFn(arguments[1]));
113 break;
114
115 default:
116 throw Error('Invalid # arguments: ' + arguments.length);
117 }
118 };
119
120 function asyncTestFn(fn) {
121 return function(done) {
122 this.timeout(0);
123 var timeout = this.timeout;
124 this.timeout = undefined; // Do not let tests change the timeout.
125 try {
126 flow.execute(fn.bind(this)).then(seal(done), done);
127 } finally {
128 this.timeout = timeout;
129 }
130 };
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;