jasmine.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  1. const os = require('os');
  2. const path = require('path');
  3. const util = require('util');
  4. const glob = require('glob');
  5. const Loader = require('./loader');
  6. const ExitHandler = require('./exit_handler');
  7. const ConsoleSpecFilter = require('./filters/console_spec_filter');
  8. /**
  9. * Options for the {@link Jasmine} constructor
  10. * @name JasmineOptions
  11. * @interface
  12. */
  13. /**
  14. * The path to the project's base directory. This can be absolute or relative
  15. * to the current working directory. If it isn't specified, the current working
  16. * directory will be used.
  17. * @name JasmineOptions#projectBaseDir
  18. * @type (string | undefined)
  19. */
  20. /**
  21. * Whether to create the globals (describe, it, etc) that make up Jasmine's
  22. * spec-writing interface. If it is set to false, the spec-writing interface
  23. * can be accessed via jasmine-core's `noGlobals` method, e.g.:
  24. *
  25. * `const {describe, it, expect, jasmine} = require('jasmine-core').noGlobals();`
  26. *
  27. * @name JasmineOptions#globals
  28. * @type (boolean | undefined)
  29. * @default true
  30. */
  31. /**
  32. * @classdesc Configures, builds, and executes a Jasmine test suite
  33. * @param {(JasmineOptions | undefined)} options
  34. * @constructor
  35. * @name Jasmine
  36. * @example
  37. * const Jasmine = require('jasmine');
  38. * const jasmine = new Jasmine();
  39. */
  40. class Jasmine {
  41. constructor(options) {
  42. options = options || {};
  43. this.loader = options.loader || new Loader();
  44. this.isWindows_ = (options.platform || os.platform)() === 'win32';
  45. const jasmineCore = options.jasmineCore || require('jasmine-core');
  46. if (options.globals === false) {
  47. this.jasmine = jasmineCore.noGlobals().jasmine;
  48. } else {
  49. this.jasmine = jasmineCore.boot(jasmineCore);
  50. }
  51. if (options.projectBaseDir) {
  52. this.validatePath_(options.projectBaseDir);
  53. this.projectBaseDir = options.projectBaseDir;
  54. } else {
  55. this.projectBaseDir = (options.getcwd || path.resolve)();
  56. }
  57. this.specDir = '';
  58. this.specFiles = [];
  59. this.helperFiles = [];
  60. this.requires = [];
  61. /**
  62. * The Jasmine environment.
  63. * @name Jasmine#env
  64. * @readonly
  65. * @see {@link https://jasmine.github.io/api/edge/Env.html|Env}
  66. * @type {Env}
  67. */
  68. this.env = this.jasmine.getEnv({suppressLoadErrors: true});
  69. this.reportersCount = 0;
  70. this.exit = process.exit;
  71. this.showingColors = true;
  72. this.alwaysListPendingSpecs_ = true;
  73. this.reporter = new module.exports.ConsoleReporter();
  74. this.addReporter(this.reporter);
  75. this.defaultReporterConfigured = false;
  76. /**
  77. * @function
  78. * @name Jasmine#coreVersion
  79. * @return {string} The version of jasmine-core in use
  80. */
  81. this.coreVersion = function() {
  82. return jasmineCore.version();
  83. };
  84. /**
  85. * Whether to cause the Node process to exit when the suite finishes executing.
  86. *
  87. * @name Jasmine#exitOnCompletion
  88. * @type {boolean}
  89. * @default true
  90. */
  91. this.exitOnCompletion = true;
  92. }
  93. /**
  94. * Sets whether to randomize the order of specs.
  95. * @function
  96. * @name Jasmine#randomizeTests
  97. * @param {boolean} value Whether to randomize
  98. */
  99. randomizeTests(value) {
  100. this.env.configure({random: value});
  101. }
  102. /**
  103. * Sets the random seed.
  104. * @function
  105. * @name Jasmine#seed
  106. * @param {number} seed The random seed
  107. */
  108. seed(value) {
  109. this.env.configure({seed: value});
  110. }
  111. /**
  112. * Sets whether to show colors in the console reporter.
  113. * @function
  114. * @name Jasmine#showColors
  115. * @param {boolean} value Whether to show colors
  116. */
  117. showColors(value) {
  118. this.showingColors = value;
  119. }
  120. /**
  121. * Sets whether the console reporter should list pending specs even when there
  122. * are failures.
  123. * @name Jasmine#alwaysListPendingSpecs
  124. * @param value {boolean}
  125. */
  126. alwaysListPendingSpecs(value) {
  127. this.alwaysListPendingSpecs_ = value;
  128. }
  129. /**
  130. * Adds a spec file to the list that will be loaded when the suite is executed.
  131. * @function
  132. * @name Jasmine#addSpecFile
  133. * @param {string} filePath The path to the file to be loaded.
  134. */
  135. addSpecFile(filePath) {
  136. this.specFiles.push(filePath);
  137. }
  138. /**
  139. * Adds a helper file to the list that will be loaded when the suite is executed.
  140. * @function
  141. * @name Jasmine#addHelperFile
  142. * @param {string} filePath The path to the file to be loaded.
  143. */
  144. addHelperFile(filePath) {
  145. this.helperFiles.push(filePath);
  146. }
  147. /**
  148. * Add a custom reporter to the Jasmine environment.
  149. * @function
  150. * @name Jasmine#addReporter
  151. * @param {Reporter} reporter The reporter to add
  152. * @see custom_reporter
  153. */
  154. addReporter(reporter) {
  155. this.env.addReporter(reporter);
  156. this.reportersCount++;
  157. }
  158. /**
  159. * Clears all registered reporters.
  160. * @function
  161. * @name Jasmine#clearReporters
  162. */
  163. clearReporters() {
  164. this.env.clearReporters();
  165. this.reportersCount = 0;
  166. }
  167. /**
  168. * Provide a fallback reporter if no other reporters have been specified.
  169. * @function
  170. * @name Jasmine#provideFallbackReporter
  171. * @param reporter The fallback reporter
  172. * @see custom_reporter
  173. */
  174. provideFallbackReporter(reporter) {
  175. this.env.provideFallbackReporter(reporter);
  176. }
  177. /**
  178. * Configures the default reporter that is installed if no other reporter is
  179. * specified.
  180. * @param {ConsoleReporterOptions} options
  181. */
  182. configureDefaultReporter(options) {
  183. options.print = options.print || function() {
  184. process.stdout.write(util.format.apply(this, arguments));
  185. };
  186. options.showColors = options.hasOwnProperty('showColors') ? options.showColors : true;
  187. this.reporter.setOptions(options);
  188. this.defaultReporterConfigured = true;
  189. }
  190. /**
  191. * Add custom matchers for the current scope of specs.
  192. *
  193. * _Note:_ This is only callable from within a {@link beforeEach}, {@link it}, or {@link beforeAll}.
  194. * @function
  195. * @name Jasmine#addMatchers
  196. * @param {Object} matchers - Keys from this object will be the new matcher names.
  197. * @see custom_matcher
  198. */
  199. addMatchers(matchers) {
  200. this.env.addMatchers(matchers);
  201. }
  202. async loadSpecs() {
  203. await this._loadFiles(this.specFiles);
  204. }
  205. async loadHelpers() {
  206. await this._loadFiles(this.helperFiles);
  207. }
  208. async _loadFiles(files) {
  209. for (const file of files) {
  210. await this.loader.load(file);
  211. }
  212. }
  213. async loadRequires() {
  214. await this._loadFiles(this.requires);
  215. }
  216. /**
  217. * Loads configuration from the specified file. The file can be a JSON file or
  218. * any JS file that's loadable via require and provides a Jasmine config
  219. * as its default export.
  220. * @param {string} [configFilePath=spec/support/jasmine.json]
  221. * @return Promise
  222. */
  223. async loadConfigFile(configFilePath) {
  224. if (configFilePath) {
  225. await this.loadSpecificConfigFile_(configFilePath);
  226. } else {
  227. for (const ext of ['json', 'js']) {
  228. try {
  229. await this.loadSpecificConfigFile_(`spec/support/jasmine.${ext}`);
  230. } catch (e) {
  231. if (e.code !== 'MODULE_NOT_FOUND' && e.code !== 'ERR_MODULE_NOT_FOUND') {
  232. throw e;
  233. }
  234. }
  235. }
  236. }
  237. }
  238. async loadSpecificConfigFile_(relativePath) {
  239. const absolutePath = path.resolve(this.projectBaseDir, relativePath);
  240. const config = await this.loader.load(absolutePath);
  241. this.loadConfig(config);
  242. }
  243. /**
  244. * Loads configuration from the specified object.
  245. * @param {Configuration} config
  246. */
  247. loadConfig(config) {
  248. /**
  249. * @interface Configuration
  250. */
  251. const envConfig = {...config.env};
  252. /**
  253. * The directory that spec files are contained in, relative to the project
  254. * base directory.
  255. * @name Configuration#spec_dir
  256. * @type string | undefined
  257. */
  258. this.specDir = config.spec_dir || this.specDir;
  259. this.validatePath_(this.specDir);
  260. /**
  261. * Whether to fail specs that contain no expectations.
  262. * @name Configuration#failSpecWithNoExpectations
  263. * @type boolean | undefined
  264. * @default false
  265. */
  266. if (config.failSpecWithNoExpectations !== undefined) {
  267. envConfig.failSpecWithNoExpectations = config.failSpecWithNoExpectations;
  268. }
  269. /**
  270. * Whether to stop each spec on the first expectation failure.
  271. * @name Configuration#stopSpecOnExpectationFailure
  272. * @type boolean | undefined
  273. * @default false
  274. */
  275. if (config.stopSpecOnExpectationFailure !== undefined) {
  276. envConfig.stopSpecOnExpectationFailure = config.stopSpecOnExpectationFailure;
  277. }
  278. /**
  279. * Whether to stop suite execution on the first spec failure.
  280. * @name Configuration#stopOnSpecFailure
  281. * @type boolean | undefined
  282. * @default false
  283. */
  284. if (config.stopOnSpecFailure !== undefined) {
  285. envConfig.stopOnSpecFailure = config.stopOnSpecFailure;
  286. }
  287. /**
  288. * Whether the default reporter should list pending specs even if there are
  289. * failures.
  290. * @name Configuration#alwaysListPendingSpecs
  291. * @type boolean | undefined
  292. * @default true
  293. */
  294. if (config.alwaysListPendingSpecs !== undefined) {
  295. this.alwaysListPendingSpecs(config.alwaysListPendingSpecs);
  296. }
  297. /**
  298. * Whether to run specs in a random order.
  299. * @name Configuration#random
  300. * @type boolean | undefined
  301. * @default true
  302. */
  303. if (config.random !== undefined) {
  304. envConfig.random = config.random;
  305. }
  306. if (config.verboseDeprecations !== undefined) {
  307. envConfig.verboseDeprecations = config.verboseDeprecations;
  308. }
  309. /**
  310. * Specifies how to load files with names ending in .js. Valid values are
  311. * "require" and "import". "import" should be safe in all cases, and is
  312. * required if your project contains ES modules with filenames ending in .js.
  313. * @name Configuration#jsLoader
  314. * @type string | undefined
  315. * @default "require"
  316. */
  317. if (config.jsLoader === 'import' || config.jsLoader === undefined) {
  318. this.loader.alwaysImport = true;
  319. } else if (config.jsLoader === 'require') {
  320. this.loader.alwaysImport = false;
  321. } else {
  322. throw new Error(`"${config.jsLoader}" is not a valid value for the ` +
  323. 'jsLoader configuration property. Valid values are "import", ' +
  324. '"require", and undefined.');
  325. }
  326. if (Object.keys(envConfig).length > 0) {
  327. this.env.configure(envConfig);
  328. }
  329. /**
  330. * An array of helper file paths or {@link https://github.com/isaacs/node-glob#glob-primer|globs}
  331. * that match helper files. Each path or glob will be evaluated relative to
  332. * the spec directory. Helpers are loaded before specs.
  333. * @name Configuration#helpers
  334. * @type string[] | undefined
  335. */
  336. if(config.helpers) {
  337. this.addMatchingHelperFiles(config.helpers);
  338. }
  339. /**
  340. * An array of module names to load via require() at the start of execution.
  341. * @name Configuration#requires
  342. * @type string[] | undefined
  343. */
  344. if(config.requires) {
  345. this.addRequires(config.requires);
  346. }
  347. /**
  348. * An array of spec file paths or {@link https://github.com/isaacs/node-glob#glob-primer|globs}
  349. * that match helper files. Each path or glob will be evaluated relative to
  350. * the spec directory.
  351. * @name Configuration#spec_files
  352. * @type string[] | undefined
  353. */
  354. if(config.spec_files) {
  355. this.addMatchingSpecFiles(config.spec_files);
  356. }
  357. /**
  358. * An array of reporters. Each object in the array will be passed to
  359. * {@link Jasmine#addReporter|addReporter}.
  360. *
  361. * This provides a middle ground between the --reporter= CLI option and full
  362. * programmatic usage. Note that because reporters are objects with methods,
  363. * this option can only be used in JavaScript config files
  364. * (e.g `spec/support/jasmine.js`), not JSON.
  365. * @name Configuration#reporters
  366. * @type Reporter[] | undefined
  367. * @see custom_reporter
  368. */
  369. if (config.reporters) {
  370. for (const r of config.reporters) {
  371. this.addReporter(r);
  372. }
  373. }
  374. }
  375. addRequires(requires) {
  376. const jasmineRunner = this;
  377. requires.forEach(function(r) {
  378. jasmineRunner.requires.push(r);
  379. });
  380. }
  381. /**
  382. * Sets whether to cause specs to only have one expectation failure.
  383. * @function
  384. * @name Jasmine#stopSpecOnExpectationFailure
  385. * @param {boolean} value Whether to cause specs to only have one expectation
  386. * failure
  387. */
  388. stopSpecOnExpectationFailure(value) {
  389. this.env.configure({stopSpecOnExpectationFailure: value});
  390. }
  391. /**
  392. * Sets whether to stop execution of the suite after the first spec failure.
  393. * @function
  394. * @name Jasmine#stopOnSpecFailure
  395. * @param {boolean} value Whether to stop execution of the suite after the
  396. * first spec failure
  397. */
  398. stopOnSpecFailure(value) {
  399. this.env.configure({stopOnSpecFailure: value});
  400. }
  401. async flushOutput() {
  402. // Ensure that all data has been written to stdout and stderr,
  403. // then exit with an appropriate status code. Otherwise, we
  404. // might exit before all previous writes have actually been
  405. // written when Jasmine is piped to another process that isn't
  406. // reading quickly enough.
  407. var streams = [process.stdout, process.stderr];
  408. var promises = streams.map(stream => {
  409. return new Promise(resolve => stream.write('', null, resolve));
  410. });
  411. return Promise.all(promises);
  412. }
  413. /**
  414. * Runs the test suite.
  415. *
  416. * _Note_: Set {@link Jasmine#exitOnCompletion|exitOnCompletion} to false if you
  417. * intend to use the returned promise. Otherwise, the Node process will
  418. * ordinarily exit before the promise is settled.
  419. * @param {Array.<string>} [files] Spec files to run instead of the previously
  420. * configured set
  421. * @param {string} [filterString] Regex used to filter specs. If specified, only
  422. * specs with matching full names will be run.
  423. * @return {Promise<JasmineDoneInfo>} Promise that is resolved when the suite completes.
  424. */
  425. async execute(files, filterString) {
  426. await this.loadRequires();
  427. await this.loadHelpers();
  428. if (!this.defaultReporterConfigured) {
  429. this.configureDefaultReporter({
  430. showColors: this.showingColors,
  431. alwaysListPendingSpecs: this.alwaysListPendingSpecs_
  432. });
  433. }
  434. if (filterString) {
  435. const specFilter = new ConsoleSpecFilter({
  436. filterString: filterString
  437. });
  438. this.env.configure({specFilter: function(spec) {
  439. return specFilter.matches(spec.getFullName());
  440. }});
  441. }
  442. if (files && files.length > 0) {
  443. this.specDir = '';
  444. this.specFiles = [];
  445. this.addMatchingSpecFiles(files);
  446. }
  447. await this.loadSpecs();
  448. const prematureExitHandler = new ExitHandler(() => this.exit(4));
  449. prematureExitHandler.install();
  450. const overallResult = await this.env.execute();
  451. await this.flushOutput();
  452. prematureExitHandler.uninstall();
  453. if (this.exitOnCompletion) {
  454. this.exit(exitCodeForStatus(overallResult.overallStatus));
  455. }
  456. return overallResult;
  457. }
  458. validatePath_(path) {
  459. if (this.isWindows_ && path.includes('\\')) {
  460. const fixed = path.replace(/\\/g, '/');
  461. console.warn('Backslashes in ' +
  462. 'file paths behave inconsistently between platforms and might not be ' +
  463. 'treated as directory separators in a future version. Consider ' +
  464. `changing ${path} to ${fixed}.`);
  465. }
  466. }
  467. }
  468. /**
  469. * Adds files that match the specified patterns to the list of spec files.
  470. * @function
  471. * @name Jasmine#addMatchingSpecFiles
  472. * @param {Array<string>} patterns An array of spec file paths
  473. * or {@link https://github.com/isaacs/node-glob#glob-primer|globs} that match
  474. * spec files. Each path or glob will be evaluated relative to the spec directory.
  475. */
  476. Jasmine.prototype.addMatchingSpecFiles = addFiles('specFiles');
  477. /**
  478. * Adds files that match the specified patterns to the list of helper files.
  479. * @function
  480. * @name Jasmine#addMatchingHelperFiles
  481. * @param {Array<string>} patterns An array of helper file paths
  482. * or {@link https://github.com/isaacs/node-glob#glob-primer|globs} that match
  483. * helper files. Each path or glob will be evaluated relative to the spec directory.
  484. */
  485. Jasmine.prototype.addMatchingHelperFiles = addFiles('helperFiles');
  486. function addFiles(kind) {
  487. return function (files) {
  488. for (const f of files) {
  489. this.validatePath_(f);
  490. }
  491. const jasmineRunner = this;
  492. const fileArr = this[kind];
  493. const {includeFiles, excludeFiles} = files.reduce(function(ongoing, file) {
  494. const hasNegation = file.startsWith('!');
  495. if (hasNegation) {
  496. file = file.substring(1);
  497. }
  498. if (!path.isAbsolute(file)) {
  499. file = path.join(jasmineRunner.projectBaseDir, jasmineRunner.specDir, file);
  500. }
  501. return {
  502. includeFiles: ongoing.includeFiles.concat(!hasNegation ? [file] : []),
  503. excludeFiles: ongoing.excludeFiles.concat(hasNegation ? [file] : [])
  504. };
  505. }, { includeFiles: [], excludeFiles: [] });
  506. includeFiles.forEach(function(file) {
  507. const filePaths = glob
  508. .sync(file, { ignore: excludeFiles })
  509. .filter(function(filePath) {
  510. // glob will always output '/' as a segment separator but the fileArr may use \ on windows
  511. // fileArr needs to be checked for both versions
  512. return fileArr.indexOf(filePath) === -1 && fileArr.indexOf(path.normalize(filePath)) === -1;
  513. });
  514. filePaths.forEach(function(filePath) {
  515. fileArr.push(filePath);
  516. });
  517. });
  518. };
  519. }
  520. function exitCodeForStatus(status) {
  521. switch (status) {
  522. case 'passed':
  523. return 0;
  524. case 'incomplete':
  525. return 2;
  526. case 'failed':
  527. return 3;
  528. default:
  529. console.error(`Unrecognized overall status: ${status}`);
  530. return 1;
  531. }
  532. }
  533. module.exports = Jasmine;
  534. module.exports.ConsoleReporter = require('./reporters/console_reporter');