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 * 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 flow = require('..').promise.controlFlow();
84
85
86/**
87 * Wraps a function so that all passed arguments are ignored.
88 * @param {!Function} fn The function to wrap.
89 * @return {!Function} The wrapped function.
90 */
91function seal(fn) {
92 return function() {
93 fn();
94 };
95}
96
97
98/**
99 * Wraps a function on Mocha's BDD interface so it runs inside a
100 * webdriver.promise.ControlFlow and waits for the flow to complete before
101 * continuing.
102 * @param {!Function} globalFn The function to wrap.
103 * @return {!Function} The new function.
104 */
105function wrapped(globalFn) {
106 return function() {
107 switch (arguments.length) {
108 case 1:
109 globalFn(asyncTestFn(arguments[0]));
110 break;
111
112 case 2:
113 globalFn(arguments[0], asyncTestFn(arguments[1]));
114 break;
115
116 default:
117 throw Error('Invalid # arguments: ' + arguments.length);
118 }
119 };
120
121 function asyncTestFn(fn) {
122 var ret = function(done) {
123 this.timeout(0);
124 var timeout = this.timeout;
125 this.timeout = undefined; // Do not let tests change the timeout.
126 try {
127 flow.execute(fn.bind(this)).then(seal(done), done);
128 } finally {
129 this.timeout = timeout;
130 }
131 };
132
133 ret.toString = function() {
134 return fn.toString();
135 };
136
137 return ret;
138 }
139}
140
141
142/**
143 * Ignores the test chained to this function if the provided predicate returns
144 * true.
145 * @param {function(): boolean} predicateFn A predicate to call to determine
146 * if the test should be suppressed. This function MUST be synchronous.
147 * @return {!Object} An object with wrapped versions of {@link #it()} and
148 * {@link #describe()} that ignore tests as indicated by the predicate.
149 */
150function ignore(predicateFn) {
151 var describe = wrap(exports.xdescribe, exports.describe);
152 describe.only = wrap(exports.xdescribe, exports.describe.only);
153
154 var it = wrap(exports.xit, exports.it);
155 it.only = wrap(exports.xit, exports.it.only);
156
157 return {
158 describe: describe,
159 it: it
160 };
161
162 function wrap(onSkip, onRun) {
163 return function(title, fn) {
164 if (predicateFn()) {
165 onSkip(title, fn);
166 } else {
167 onRun(title, fn);
168 }
169 };
170 }
171}
172
173
174// PUBLIC API
175
176/**
177 * Registers a new test suite.
178 * @param {string} name The suite name.
179 * @param {function()=} fn The suite function, or {@code undefined} to define
180 * a pending test suite.
181 */
182exports.describe = global.describe;
183
184/**
185 * Defines a suppressed test suite.
186 * @param {string} name The suite name.
187 * @param {function()=} fn The suite function, or {@code undefined} to define
188 * a pending test suite.
189 */
190exports.xdescribe = global.xdescribe;
191exports.describe.skip = global.describe.skip;
192
193/**
194 * Register a function to call after the current suite finishes.
195 * @param {function()} fn .
196 */
197exports.after = wrapped(global.after);
198
199/**
200 * Register a function to call after each test in a suite.
201 * @param {function()} fn .
202 */
203exports.afterEach = wrapped(global.afterEach);
204
205/**
206 * Register a function to call before the current suite starts.
207 * @param {function()} fn .
208 */
209exports.before = wrapped(global.before);
210
211/**
212 * Register a function to call before each test in a suite.
213 * @param {function()} fn .
214 */
215exports.beforeEach = wrapped(global.beforeEach);
216
217/**
218 * Add a test to the current suite.
219 * @param {string} name The test name.
220 * @param {function()=} fn The test function, or {@code undefined} to define
221 * a pending test case.
222 */
223exports.it = wrapped(global.it);
224
225/**
226 * An alias for {@link #it()} that flags the test as the only one that should
227 * be run within the current suite.
228 * @param {string} name The test name.
229 * @param {function()=} fn The test function, or {@code undefined} to define
230 * a pending test case.
231 */
232exports.iit = exports.it.only = wrapped(global.it.only);
233
234/**
235 * Adds a test to the current suite while suppressing it so it is not run.
236 * @param {string} name The test name.
237 * @param {function()=} fn The test function, or {@code undefined} to define
238 * a pending test case.
239 */
240exports.xit = exports.it.skip = wrapped(global.xit);
241
242exports.ignore = ignore;