index.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  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. * @fileoverview Provides wrappers around the following global functions from
  19. * [Mocha's BDD interface](https://github.com/mochajs/mocha):
  20. *
  21. * - after
  22. * - afterEach
  23. * - before
  24. * - beforeEach
  25. * - it
  26. * - it.only
  27. * - it.skip
  28. * - xit
  29. *
  30. * Each of the wrapped functions support generator functions. If the generator
  31. * {@linkplain ../lib/promise.consume yields a promise}, the test will wait
  32. * for that promise to resolve before invoking the next iteration of the
  33. * generator:
  34. *
  35. * test.it('generators', function*() {
  36. * let x = yield Promise.resolve(1);
  37. * assert.equal(x, 1);
  38. * });
  39. *
  40. * The provided wrappers leverage the {@link webdriver.promise.ControlFlow}
  41. * to simplify writing asynchronous tests:
  42. *
  43. * var {Builder, By, Key, until} = require('selenium-webdriver');
  44. * var test = require('selenium-webdriver/testing');
  45. *
  46. * test.describe('Google Search', function() {
  47. * var driver;
  48. *
  49. * test.before(function() {
  50. * driver = new Builder().forBrowser('firefox').build();
  51. * });
  52. *
  53. * test.after(function() {
  54. * driver.quit();
  55. * });
  56. *
  57. * test.it('should append query to title', function() {
  58. * driver.get('http://www.google.com/ncr');
  59. * driver.findElement(By.name('q')).sendKeys('webdriver', Key.RETURN);
  60. * driver.wait(until.titleIs('webdriver - Google Search'), 1000);
  61. * });
  62. * });
  63. *
  64. * You may conditionally suppress a test function using the exported
  65. * "ignore" function. If the provided predicate returns true, the attached
  66. * test case will be skipped:
  67. *
  68. * test.ignore(maybe()).it('is flaky', function() {
  69. * if (Math.random() < 0.5) throw Error();
  70. * });
  71. *
  72. * function maybe() { return Math.random() < 0.5; }
  73. */
  74. 'use strict';
  75. const promise = require('..').promise;
  76. const flow = (function() {
  77. const initial = process.env['SELENIUM_PROMISE_MANAGER'];
  78. try {
  79. process.env['SELENIUM_PROMISE_MANAGER'] = '1';
  80. return promise.controlFlow();
  81. } finally {
  82. if (initial === undefined) {
  83. delete process.env['SELENIUM_PROMISE_MANAGER'];
  84. } else {
  85. process.env['SELENIUM_PROMISE_MANAGER'] = initial;
  86. }
  87. }
  88. })();
  89. /**
  90. * Wraps a function so that all passed arguments are ignored.
  91. * @param {!Function} fn The function to wrap.
  92. * @return {!Function} The wrapped function.
  93. */
  94. function seal(fn) {
  95. return function() {
  96. fn();
  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. */
  106. function wrapped(globalFn) {
  107. return function() {
  108. if (arguments.length === 1) {
  109. return globalFn(wrapArgument(arguments[0]));
  110. } else if (arguments.length === 2) {
  111. return globalFn(arguments[0], wrapArgument(arguments[1]));
  112. } else {
  113. throw Error('Invalid # arguments: ' + arguments.length);
  114. }
  115. };
  116. }
  117. function wrapArgument(value) {
  118. if (typeof value === 'function') {
  119. return makeAsyncTestFn(value);
  120. }
  121. return value;
  122. }
  123. /**
  124. * Make a wrapper to invoke caller's test function, fn. Run the test function
  125. * within a ControlFlow.
  126. *
  127. * Should preserve the semantics of Mocha's Runnable.prototype.run (See
  128. * https://github.com/mochajs/mocha/blob/master/lib/runnable.js#L192)
  129. *
  130. * @param {!Function} fn
  131. * @return {!Function}
  132. */
  133. function makeAsyncTestFn(fn) {
  134. const isAsync = fn.length > 0;
  135. const isGenerator = promise.isGenerator(fn);
  136. if (isAsync && isGenerator) {
  137. throw TypeError(
  138. 'generator-based tests must not take a callback; for async testing,'
  139. + ' return a promise (or yield on a promise)');
  140. }
  141. var ret = /** @type {function(this: mocha.Context)}*/ (function(done) {
  142. const runTest = (resolve, reject) => {
  143. try {
  144. if (isAsync) {
  145. fn.call(this, err => err ? reject(err) : resolve());
  146. } else if (isGenerator) {
  147. resolve(promise.consume(fn, this));
  148. } else {
  149. resolve(fn.call(this));
  150. }
  151. } catch (ex) {
  152. reject(ex);
  153. }
  154. };
  155. if (!promise.USE_PROMISE_MANAGER) {
  156. new Promise(runTest).then(seal(done), done);
  157. return;
  158. }
  159. var runnable = this.runnable();
  160. var mochaCallback = runnable.callback;
  161. runnable.callback = function() {
  162. flow.reset();
  163. return mochaCallback.apply(this, arguments);
  164. };
  165. flow.execute(function controlFlowExecute() {
  166. return new promise.Promise(function(fulfill, reject) {
  167. return runTest(fulfill, reject);
  168. }, flow);
  169. }, runnable.fullTitle()).then(seal(done), done);
  170. });
  171. ret.toString = function() {
  172. return fn.toString();
  173. };
  174. return ret;
  175. }
  176. /**
  177. * Ignores the test chained to this function if the provided predicate returns
  178. * true.
  179. * @param {function(): boolean} predicateFn A predicate to call to determine
  180. * if the test should be suppressed. This function MUST be synchronous.
  181. * @return {!Object} An object with wrapped versions of {@link #it()} and
  182. * {@link #describe()} that ignore tests as indicated by the predicate.
  183. */
  184. function ignore(predicateFn) {
  185. var describe = wrap(exports.xdescribe, exports.describe);
  186. describe.only = wrap(exports.xdescribe, exports.describe.only);
  187. var it = wrap(exports.xit, exports.it);
  188. it.only = wrap(exports.xit, exports.it.only);
  189. return {
  190. describe: describe,
  191. it: it
  192. };
  193. function wrap(onSkip, onRun) {
  194. return function(title, fn) {
  195. if (predicateFn()) {
  196. onSkip(title, fn);
  197. } else {
  198. onRun(title, fn);
  199. }
  200. };
  201. }
  202. }
  203. /**
  204. * @param {string} name
  205. * @return {!Function}
  206. * @throws {TypeError}
  207. */
  208. function getMochaGlobal(name) {
  209. let fn = global[name];
  210. let type = typeof fn;
  211. if (type !== 'function') {
  212. throw TypeError(
  213. `Expected global.${name} to be a function, but is ${type}. `
  214. + 'This can happen if you try using this module when running '
  215. + 'with node directly instead of using the mocha executable');
  216. }
  217. return fn;
  218. }
  219. const WRAPPED = {
  220. after: null,
  221. afterEach: null,
  222. before: null,
  223. beforeEach: null,
  224. it: null,
  225. itOnly: null,
  226. xit: null
  227. };
  228. function wrapIt() {
  229. if (!WRAPPED.it) {
  230. let it = getMochaGlobal('it');
  231. WRAPPED.it = wrapped(it);
  232. WRAPPED.itOnly = wrapped(it.only);
  233. }
  234. }
  235. // PUBLIC API
  236. /**
  237. * @return {!promise.ControlFlow} the control flow instance used by this module
  238. * to coordinate test actions.
  239. */
  240. exports.controlFlow = function(){
  241. return flow;
  242. };
  243. /**
  244. * Registers a new test suite.
  245. * @param {string} name The suite name.
  246. * @param {function()=} opt_fn The suite function, or `undefined` to define
  247. * a pending test suite.
  248. */
  249. exports.describe = function(name, opt_fn) {
  250. let fn = getMochaGlobal('describe');
  251. return opt_fn ? fn(name, opt_fn) : fn(name);
  252. };
  253. /**
  254. * An alias for {@link #describe()} that marks the suite as exclusive,
  255. * suppressing all other test suites.
  256. * @param {string} name The suite name.
  257. * @param {function()=} opt_fn The suite function, or `undefined` to define
  258. * a pending test suite.
  259. */
  260. exports.describe.only = function(name, opt_fn) {
  261. let desc = getMochaGlobal('describe');
  262. return opt_fn ? desc.only(name, opt_fn) : desc.only(name);
  263. };
  264. /**
  265. * Defines a suppressed test suite.
  266. * @param {string} name The suite name.
  267. * @param {function()=} opt_fn The suite function, or `undefined` to define
  268. * a pending test suite.
  269. */
  270. exports.describe.skip = function(name, opt_fn) {
  271. let fn = getMochaGlobal('describe');
  272. return opt_fn ? fn.skip(name, opt_fn) : fn.skip(name);
  273. };
  274. /**
  275. * Defines a suppressed test suite.
  276. * @param {string} name The suite name.
  277. * @param {function()=} opt_fn The suite function, or `undefined` to define
  278. * a pending test suite.
  279. */
  280. exports.xdescribe = function(name, opt_fn) {
  281. let fn = getMochaGlobal('xdescribe');
  282. return opt_fn ? fn(name, opt_fn) : fn(name);
  283. };
  284. /**
  285. * Register a function to call after the current suite finishes.
  286. * @param {function()} fn .
  287. */
  288. exports.after = function(fn) {
  289. if (!WRAPPED.after) {
  290. WRAPPED.after = wrapped(getMochaGlobal('after'));
  291. }
  292. WRAPPED.after(fn);
  293. };
  294. /**
  295. * Register a function to call after each test in a suite.
  296. * @param {function()} fn .
  297. */
  298. exports.afterEach = function(fn) {
  299. if (!WRAPPED.afterEach) {
  300. WRAPPED.afterEach = wrapped(getMochaGlobal('afterEach'));
  301. }
  302. WRAPPED.afterEach(fn);
  303. };
  304. /**
  305. * Register a function to call before the current suite starts.
  306. * @param {function()} fn .
  307. */
  308. exports.before = function(fn) {
  309. if (!WRAPPED.before) {
  310. WRAPPED.before = wrapped(getMochaGlobal('before'));
  311. }
  312. WRAPPED.before(fn);
  313. };
  314. /**
  315. * Register a function to call before each test in a suite.
  316. * @param {function()} fn .
  317. */
  318. exports.beforeEach = function(fn) {
  319. if (!WRAPPED.beforeEach) {
  320. WRAPPED.beforeEach = wrapped(getMochaGlobal('beforeEach'));
  321. }
  322. WRAPPED.beforeEach(fn);
  323. };
  324. /**
  325. * Add a test to the current suite.
  326. * @param {string} name The test name.
  327. * @param {function()=} opt_fn The test function, or `undefined` to define
  328. * a pending test case.
  329. */
  330. exports.it = function(name, opt_fn) {
  331. wrapIt();
  332. if (opt_fn) {
  333. WRAPPED.it(name, opt_fn);
  334. } else {
  335. WRAPPED.it(name);
  336. }
  337. };
  338. /**
  339. * An alias for {@link #it()} that flags the test as the only one that should
  340. * be run within the current suite.
  341. * @param {string} name The test name.
  342. * @param {function()=} opt_fn The test function, or `undefined` to define
  343. * a pending test case.
  344. */
  345. exports.it.only = function(name, opt_fn) {
  346. wrapIt();
  347. if (opt_fn) {
  348. WRAPPED.itOnly(name, opt_fn);
  349. } else {
  350. WRAPPED.itOnly(name);
  351. }
  352. };
  353. /**
  354. * Adds a test to the current suite while suppressing it so it is not run.
  355. * @param {string} name The test name.
  356. * @param {function()=} opt_fn The test function, or `undefined` to define
  357. * a pending test case.
  358. */
  359. exports.xit = function(name, opt_fn) {
  360. if (!WRAPPED.xit) {
  361. WRAPPED.xit = wrapped(getMochaGlobal('xit'));
  362. }
  363. if (opt_fn) {
  364. WRAPPED.xit(name, opt_fn);
  365. } else {
  366. WRAPPED.xit(name);
  367. }
  368. };
  369. exports.it.skip = exports.xit;
  370. exports.ignore = ignore;