suite.js 15 KB

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