suite.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. 'use strict';
  2. /**
  3. * Module dependencies.
  4. */
  5. var EventEmitter = require('events').EventEmitter;
  6. var Hook = require('./hook');
  7. var utils = require('./utils');
  8. var inherits = utils.inherits;
  9. var debug = require('debug')('mocha:suite');
  10. var milliseconds = require('ms');
  11. var errors = require('./errors');
  12. var createInvalidArgumentTypeError = errors.createInvalidArgumentTypeError;
  13. /**
  14. * Expose `Suite`.
  15. */
  16. exports = module.exports = Suite;
  17. /**
  18. * Create a new `Suite` with the given `title` and parent `Suite`.
  19. *
  20. * @public
  21. * @param {Suite} parent - Parent suite (required!)
  22. * @param {string} title - Title
  23. * @return {Suite}
  24. */
  25. Suite.create = function(parent, title) {
  26. var suite = new Suite(title, parent.ctx);
  27. suite.parent = parent;
  28. title = suite.fullTitle();
  29. parent.addSuite(suite);
  30. return suite;
  31. };
  32. /**
  33. * Constructs a new `Suite` instance with the given `title`, `ctx`, and `isRoot`.
  34. *
  35. * @public
  36. * @class
  37. * @extends EventEmitter
  38. * @see {@link https://nodejs.org/api/events.html#events_class_eventemitter|EventEmitter}
  39. * @param {string} title - Suite title.
  40. * @param {Context} parentContext - Parent context instance.
  41. * @param {boolean} [isRoot=false] - Whether this is the root suite.
  42. */
  43. function Suite(title, parentContext, isRoot) {
  44. if (!utils.isString(title)) {
  45. throw createInvalidArgumentTypeError(
  46. 'Suite argument "title" must be a string. Received type "' +
  47. typeof title +
  48. '"',
  49. 'title',
  50. 'string'
  51. );
  52. }
  53. this.title = title;
  54. function Context() {}
  55. Context.prototype = parentContext;
  56. this.ctx = new Context();
  57. this.suites = [];
  58. this.tests = [];
  59. this.pending = false;
  60. this._beforeEach = [];
  61. this._beforeAll = [];
  62. this._afterEach = [];
  63. this._afterAll = [];
  64. this.root = isRoot === true;
  65. this._timeout = 2000;
  66. this._enableTimeouts = true;
  67. this._slow = 75;
  68. this._bail = false;
  69. this._retries = -1;
  70. this._onlyTests = [];
  71. this._onlySuites = [];
  72. this.delayed = false;
  73. this.on('newListener', function(event) {
  74. if (deprecatedEvents[event]) {
  75. utils.deprecate(
  76. 'Event "' +
  77. event +
  78. '" is deprecated. Please let the Mocha team know about your use case: https://git.io/v6Lwm'
  79. );
  80. }
  81. });
  82. }
  83. /**
  84. * Inherit from `EventEmitter.prototype`.
  85. */
  86. inherits(Suite, EventEmitter);
  87. /**
  88. * Return a clone of this `Suite`.
  89. *
  90. * @private
  91. * @return {Suite}
  92. */
  93. Suite.prototype.clone = function() {
  94. var suite = new Suite(this.title);
  95. debug('clone');
  96. suite.ctx = this.ctx;
  97. suite.timeout(this.timeout());
  98. suite.retries(this.retries());
  99. suite.enableTimeouts(this.enableTimeouts());
  100. suite.slow(this.slow());
  101. suite.bail(this.bail());
  102. return suite;
  103. };
  104. /**
  105. * Set or get timeout `ms` or short-hand such as "2s".
  106. *
  107. * @private
  108. * @param {number|string} ms
  109. * @return {Suite|number} for chaining
  110. */
  111. Suite.prototype.timeout = function(ms) {
  112. if (!arguments.length) {
  113. return this._timeout;
  114. }
  115. if (ms.toString() === '0') {
  116. this._enableTimeouts = false;
  117. }
  118. if (typeof ms === 'string') {
  119. ms = milliseconds(ms);
  120. }
  121. debug('timeout %d', ms);
  122. this._timeout = parseInt(ms, 10);
  123. return this;
  124. };
  125. /**
  126. * Set or get number of times to retry a failed test.
  127. *
  128. * @private
  129. * @param {number|string} n
  130. * @return {Suite|number} for chaining
  131. */
  132. Suite.prototype.retries = function(n) {
  133. if (!arguments.length) {
  134. return this._retries;
  135. }
  136. debug('retries %d', n);
  137. this._retries = parseInt(n, 10) || 0;
  138. return this;
  139. };
  140. /**
  141. * Set or get timeout to `enabled`.
  142. *
  143. * @private
  144. * @param {boolean} enabled
  145. * @return {Suite|boolean} self or enabled
  146. */
  147. Suite.prototype.enableTimeouts = function(enabled) {
  148. if (!arguments.length) {
  149. return this._enableTimeouts;
  150. }
  151. debug('enableTimeouts %s', enabled);
  152. this._enableTimeouts = enabled;
  153. return this;
  154. };
  155. /**
  156. * Set or get slow `ms` or short-hand such as "2s".
  157. *
  158. * @private
  159. * @param {number|string} ms
  160. * @return {Suite|number} for chaining
  161. */
  162. Suite.prototype.slow = function(ms) {
  163. if (!arguments.length) {
  164. return this._slow;
  165. }
  166. if (typeof ms === 'string') {
  167. ms = milliseconds(ms);
  168. }
  169. debug('slow %d', ms);
  170. this._slow = ms;
  171. return this;
  172. };
  173. /**
  174. * Set or get whether to bail after first error.
  175. *
  176. * @private
  177. * @param {boolean} bail
  178. * @return {Suite|number} for chaining
  179. */
  180. Suite.prototype.bail = function(bail) {
  181. if (!arguments.length) {
  182. return this._bail;
  183. }
  184. debug('bail %s', bail);
  185. this._bail = bail;
  186. return this;
  187. };
  188. /**
  189. * Check if this suite or its parent suite is marked as pending.
  190. *
  191. * @private
  192. */
  193. Suite.prototype.isPending = function() {
  194. return this.pending || (this.parent && this.parent.isPending());
  195. };
  196. /**
  197. * Generic hook-creator.
  198. * @private
  199. * @param {string} title - Title of hook
  200. * @param {Function} fn - Hook callback
  201. * @returns {Hook} A new hook
  202. */
  203. Suite.prototype._createHook = function(title, fn) {
  204. var hook = new Hook(title, fn);
  205. hook.parent = this;
  206. hook.timeout(this.timeout());
  207. hook.retries(this.retries());
  208. hook.enableTimeouts(this.enableTimeouts());
  209. hook.slow(this.slow());
  210. hook.ctx = this.ctx;
  211. hook.file = this.file;
  212. return hook;
  213. };
  214. /**
  215. * Run `fn(test[, done])` before running tests.
  216. *
  217. * @private
  218. * @param {string} title
  219. * @param {Function} fn
  220. * @return {Suite} for chaining
  221. */
  222. Suite.prototype.beforeAll = function(title, fn) {
  223. if (this.isPending()) {
  224. return this;
  225. }
  226. if (typeof title === 'function') {
  227. fn = title;
  228. title = fn.name;
  229. }
  230. title = '"before all" hook' + (title ? ': ' + title : '');
  231. var hook = this._createHook(title, fn);
  232. this._beforeAll.push(hook);
  233. this.emit(constants.EVENT_SUITE_ADD_HOOK_BEFORE_ALL, hook);
  234. return this;
  235. };
  236. /**
  237. * Run `fn(test[, done])` after running tests.
  238. *
  239. * @private
  240. * @param {string} title
  241. * @param {Function} fn
  242. * @return {Suite} for chaining
  243. */
  244. Suite.prototype.afterAll = function(title, fn) {
  245. if (this.isPending()) {
  246. return this;
  247. }
  248. if (typeof title === 'function') {
  249. fn = title;
  250. title = fn.name;
  251. }
  252. title = '"after all" hook' + (title ? ': ' + title : '');
  253. var hook = this._createHook(title, fn);
  254. this._afterAll.push(hook);
  255. this.emit(constants.EVENT_SUITE_ADD_HOOK_AFTER_ALL, hook);
  256. return this;
  257. };
  258. /**
  259. * Run `fn(test[, done])` before each test case.
  260. *
  261. * @private
  262. * @param {string} title
  263. * @param {Function} fn
  264. * @return {Suite} for chaining
  265. */
  266. Suite.prototype.beforeEach = function(title, fn) {
  267. if (this.isPending()) {
  268. return this;
  269. }
  270. if (typeof title === 'function') {
  271. fn = title;
  272. title = fn.name;
  273. }
  274. title = '"before each" hook' + (title ? ': ' + title : '');
  275. var hook = this._createHook(title, fn);
  276. this._beforeEach.push(hook);
  277. this.emit(constants.EVENT_SUITE_ADD_HOOK_BEFORE_EACH, hook);
  278. return this;
  279. };
  280. /**
  281. * Run `fn(test[, done])` after each test case.
  282. *
  283. * @private
  284. * @param {string} title
  285. * @param {Function} fn
  286. * @return {Suite} for chaining
  287. */
  288. Suite.prototype.afterEach = function(title, fn) {
  289. if (this.isPending()) {
  290. return this;
  291. }
  292. if (typeof title === 'function') {
  293. fn = title;
  294. title = fn.name;
  295. }
  296. title = '"after each" hook' + (title ? ': ' + title : '');
  297. var hook = this._createHook(title, fn);
  298. this._afterEach.push(hook);
  299. this.emit(constants.EVENT_SUITE_ADD_HOOK_AFTER_EACH, hook);
  300. return this;
  301. };
  302. /**
  303. * Add a test `suite`.
  304. *
  305. * @private
  306. * @param {Suite} suite
  307. * @return {Suite} for chaining
  308. */
  309. Suite.prototype.addSuite = function(suite) {
  310. suite.parent = this;
  311. suite.root = false;
  312. suite.timeout(this.timeout());
  313. suite.retries(this.retries());
  314. suite.enableTimeouts(this.enableTimeouts());
  315. suite.slow(this.slow());
  316. suite.bail(this.bail());
  317. this.suites.push(suite);
  318. this.emit(constants.EVENT_SUITE_ADD_SUITE, suite);
  319. return this;
  320. };
  321. /**
  322. * Add a `test` to this suite.
  323. *
  324. * @private
  325. * @param {Test} test
  326. * @return {Suite} for chaining
  327. */
  328. Suite.prototype.addTest = function(test) {
  329. test.parent = this;
  330. test.timeout(this.timeout());
  331. test.retries(this.retries());
  332. test.enableTimeouts(this.enableTimeouts());
  333. test.slow(this.slow());
  334. test.ctx = this.ctx;
  335. this.tests.push(test);
  336. this.emit(constants.EVENT_SUITE_ADD_TEST, test);
  337. return this;
  338. };
  339. /**
  340. * Return the full title generated by recursively concatenating the parent's
  341. * full title.
  342. *
  343. * @memberof Suite
  344. * @public
  345. * @return {string}
  346. */
  347. Suite.prototype.fullTitle = function() {
  348. return this.titlePath().join(' ');
  349. };
  350. /**
  351. * Return the title path generated by recursively concatenating the parent's
  352. * title path.
  353. *
  354. * @memberof Suite
  355. * @public
  356. * @return {string}
  357. */
  358. Suite.prototype.titlePath = function() {
  359. var result = [];
  360. if (this.parent) {
  361. result = result.concat(this.parent.titlePath());
  362. }
  363. if (!this.root) {
  364. result.push(this.title);
  365. }
  366. return result;
  367. };
  368. /**
  369. * Return the total number of tests.
  370. *
  371. * @memberof Suite
  372. * @public
  373. * @return {number}
  374. */
  375. Suite.prototype.total = function() {
  376. return (
  377. this.suites.reduce(function(sum, suite) {
  378. return sum + suite.total();
  379. }, 0) + this.tests.length
  380. );
  381. };
  382. /**
  383. * Iterates through each suite recursively to find all tests. Applies a
  384. * function in the format `fn(test)`.
  385. *
  386. * @private
  387. * @param {Function} fn
  388. * @return {Suite}
  389. */
  390. Suite.prototype.eachTest = function(fn) {
  391. this.tests.forEach(fn);
  392. this.suites.forEach(function(suite) {
  393. suite.eachTest(fn);
  394. });
  395. return this;
  396. };
  397. /**
  398. * This will run the root suite if we happen to be running in delayed mode.
  399. * @private
  400. */
  401. Suite.prototype.run = function run() {
  402. if (this.root) {
  403. this.emit(constants.EVENT_ROOT_SUITE_RUN);
  404. }
  405. };
  406. /**
  407. * Determines whether a suite has an `only` test or suite as a descendant.
  408. *
  409. * @private
  410. * @returns {Boolean}
  411. */
  412. Suite.prototype.hasOnly = function hasOnly() {
  413. return (
  414. this._onlyTests.length > 0 ||
  415. this._onlySuites.length > 0 ||
  416. this.suites.some(function(suite) {
  417. return suite.hasOnly();
  418. })
  419. );
  420. };
  421. /**
  422. * Filter suites based on `isOnly` logic.
  423. *
  424. * @private
  425. * @returns {Boolean}
  426. */
  427. Suite.prototype.filterOnly = function filterOnly() {
  428. if (this._onlyTests.length) {
  429. // If the suite contains `only` tests, run those and ignore any nested suites.
  430. this.tests = this._onlyTests;
  431. this.suites = [];
  432. } else {
  433. // Otherwise, do not run any of the tests in this suite.
  434. this.tests = [];
  435. this._onlySuites.forEach(function(onlySuite) {
  436. // If there are other `only` tests/suites nested in the current `only` suite, then filter that `only` suite.
  437. // Otherwise, all of the tests on this `only` suite should be run, so don't filter it.
  438. if (onlySuite.hasOnly()) {
  439. onlySuite.filterOnly();
  440. }
  441. });
  442. // Run the `only` suites, as well as any other suites that have `only` tests/suites as descendants.
  443. var onlySuites = this._onlySuites;
  444. this.suites = this.suites.filter(function(childSuite) {
  445. return onlySuites.indexOf(childSuite) !== -1 || childSuite.filterOnly();
  446. });
  447. }
  448. // Keep the suite only if there is something to run
  449. return this.tests.length > 0 || this.suites.length > 0;
  450. };
  451. /**
  452. * Adds a suite to the list of subsuites marked `only`.
  453. *
  454. * @private
  455. * @param {Suite} suite
  456. */
  457. Suite.prototype.appendOnlySuite = function(suite) {
  458. this._onlySuites.push(suite);
  459. };
  460. /**
  461. * Adds a test to the list of tests marked `only`.
  462. *
  463. * @private
  464. * @param {Test} test
  465. */
  466. Suite.prototype.appendOnlyTest = function(test) {
  467. this._onlyTests.push(test);
  468. };
  469. /**
  470. * Returns the array of hooks by hook name; see `HOOK_TYPE_*` constants.
  471. * @private
  472. */
  473. Suite.prototype.getHooks = function getHooks(name) {
  474. return this['_' + name];
  475. };
  476. /**
  477. * Cleans up the references to all the deferred functions
  478. * (before/after/beforeEach/afterEach) and tests of a Suite.
  479. * These must be deleted otherwise a memory leak can happen,
  480. * as those functions may reference variables from closures,
  481. * thus those variables can never be garbage collected as long
  482. * as the deferred functions exist.
  483. *
  484. * @private
  485. */
  486. Suite.prototype.cleanReferences = function cleanReferences() {
  487. function cleanArrReferences(arr) {
  488. for (var i = 0; i < arr.length; i++) {
  489. delete arr[i].fn;
  490. }
  491. }
  492. if (Array.isArray(this._beforeAll)) {
  493. cleanArrReferences(this._beforeAll);
  494. }
  495. if (Array.isArray(this._beforeEach)) {
  496. cleanArrReferences(this._beforeEach);
  497. }
  498. if (Array.isArray(this._afterAll)) {
  499. cleanArrReferences(this._afterAll);
  500. }
  501. if (Array.isArray(this._afterEach)) {
  502. cleanArrReferences(this._afterEach);
  503. }
  504. for (var i = 0; i < this.tests.length; i++) {
  505. delete this.tests[i].fn;
  506. }
  507. };
  508. var constants = utils.defineConstants(
  509. /**
  510. * {@link Suite}-related constants.
  511. * @public
  512. * @memberof Suite
  513. * @alias constants
  514. * @readonly
  515. * @static
  516. * @enum {string}
  517. */
  518. {
  519. /**
  520. * Event emitted after a test file has been loaded Not emitted in browser.
  521. */
  522. EVENT_FILE_POST_REQUIRE: 'post-require',
  523. /**
  524. * Event emitted before a test file has been loaded. In browser, this is emitted once an interface has been selected.
  525. */
  526. EVENT_FILE_PRE_REQUIRE: 'pre-require',
  527. /**
  528. * Event emitted immediately after a test file has been loaded. Not emitted in browser.
  529. */
  530. EVENT_FILE_REQUIRE: 'require',
  531. /**
  532. * Event emitted when `global.run()` is called (use with `delay` option)
  533. */
  534. EVENT_ROOT_SUITE_RUN: 'run',
  535. /**
  536. * Namespace for collection of a `Suite`'s "after all" hooks
  537. */
  538. HOOK_TYPE_AFTER_ALL: 'afterAll',
  539. /**
  540. * Namespace for collection of a `Suite`'s "after each" hooks
  541. */
  542. HOOK_TYPE_AFTER_EACH: 'afterEach',
  543. /**
  544. * Namespace for collection of a `Suite`'s "before all" hooks
  545. */
  546. HOOK_TYPE_BEFORE_ALL: 'beforeAll',
  547. /**
  548. * Namespace for collection of a `Suite`'s "before all" hooks
  549. */
  550. HOOK_TYPE_BEFORE_EACH: 'beforeEach',
  551. // the following events are all deprecated
  552. /**
  553. * Emitted after an "after all" `Hook` has been added to a `Suite`. Deprecated
  554. */
  555. EVENT_SUITE_ADD_HOOK_AFTER_ALL: 'afterAll',
  556. /**
  557. * Emitted after an "after each" `Hook` has been added to a `Suite` Deprecated
  558. */
  559. EVENT_SUITE_ADD_HOOK_AFTER_EACH: 'afterEach',
  560. /**
  561. * Emitted after an "before all" `Hook` has been added to a `Suite` Deprecated
  562. */
  563. EVENT_SUITE_ADD_HOOK_BEFORE_ALL: 'beforeAll',
  564. /**
  565. * Emitted after an "before each" `Hook` has been added to a `Suite` Deprecated
  566. */
  567. EVENT_SUITE_ADD_HOOK_BEFORE_EACH: 'beforeEach',
  568. /**
  569. * Emitted after a child `Suite` has been added to a `Suite`. Deprecated
  570. */
  571. EVENT_SUITE_ADD_SUITE: 'suite',
  572. /**
  573. * Emitted after a `Test` has been added to a `Suite`. Deprecated
  574. */
  575. EVENT_SUITE_ADD_TEST: 'test'
  576. }
  577. );
  578. /**
  579. * @summary There are no known use cases for these events.
  580. * @desc This is a `Set`-like object having all keys being the constant's string value and the value being `true`.
  581. * @todo Remove eventually
  582. * @type {Object<string,boolean>}
  583. * @ignore
  584. */
  585. var deprecatedEvents = Object.keys(constants)
  586. .filter(function(constant) {
  587. return constant.substring(0, 15) === 'EVENT_SUITE_ADD';
  588. })
  589. .reduce(function(acc, constant) {
  590. acc[constants[constant]] = true;
  591. return acc;
  592. }, utils.createMap());
  593. Suite.constants = constants;