UNPKG

jasmine

Version:

CLI for Jasmine, a simple JavaScript testing framework for browsers and Node

571 lines (510 loc) 17.9 kB
const path = require('path'); const util = require('util'); const glob = require("glob"); const ConsoleReporter = require('./reporters/console_reporter'); const Loader = require("./loader"); const GlobalSetupOrTeardownRunner = require('./global_setup_or_teardown_runner'); const unWindows = require('./unWindows'); /** * @classdesc Defines common methods and properties of {@link Jasmine} and * {@link ParallelRunner}.<br> * Note: This should be considered an interface. It's only documented as a class * due to jsdoc limitations. You can safely assume that these members are * available on both runner classes, but the inheritance structure itself is an * implementation detail that may change at any time. * @constructor * @hideconstructor * @name Runner */ class RunnerBase { constructor(options) { this.loader = options.loader || new Loader(); let baseDir; if (options.projectBaseDir) { baseDir = options.projectBaseDir; } else { baseDir = (options.getcwd || path.resolve)(); } this.projectBaseDir = unWindows(baseDir); this.specFiles = []; this.helperFiles = []; this.requires = []; this.specDir = ''; this.reporter_ = new (options.ConsoleReporter || ConsoleReporter)(); this.defaultReporterConfigured = false; this.showingColors = true; this.alwaysListPendingSpecs_ = true; this.globalSetupOrTeardownRunner_ = options.globalSetupOrTeardownRunner || new GlobalSetupOrTeardownRunner(); this.globals_ = options.globals; } /** * Sets whether to show colors in the console reporter. * @function * @name Runner#showColors * @param {boolean} value Whether to show colors */ showColors(value) { this.showingColors = value; } /** * Sets whether to run in verbose mode, which prints information that may * be useful for debugging configuration problems. * @function * @name Runner#verbose * @param {boolean} value Whether to run in verbose mode */ verbose(isVerbose) { this.isVerbose_ = isVerbose; } /** * Sets whether the console reporter should list pending specs even when there * are failures. * @name Runner#alwaysListPendingSpecs * @function * @param value {boolean} */ alwaysListPendingSpecs(value) { this.alwaysListPendingSpecs_ = value; } /** * Loads configuration from the specified file. The file can be a JSON file or * any JS file that's loadable as a module and provides a Jasmine config * as its default export. * * The config file will be loaded via dynamic import() unless this Jasmine * instance has already been configured with {jsLoader: 'require'}. Dynamic * import() supports ES modules as well as nearly all CommonJS modules. * @name Runner#loadConfigFile * @function * @param {string} [configFilePath=spec/support/jasmine.json] * @return Promise */ async loadConfigFile(configFilePath) { if (this.isVerbose_) { console.log(`Project base dir: ${this.projectBaseDir}`); } if (configFilePath) { if (this.isVerbose_) { console.log(`Loading config file ${configFilePath} because it was explicitly specified`); } await this.loadSpecificConfigFile_(configFilePath); } else { let numFound = 0; for (const ext of ['mjs', 'json', 'js']) { const candidate = `spec/support/jasmine.${ext}`; try { await this.loadSpecificConfigFile_(candidate); numFound++; if (ext === 'mjs') { break; } // Otherwise continue possibly loading other files, for backwards // compatibility. } catch (e) { if (e.code !== 'MODULE_NOT_FOUND' // CommonJS && e.code !== 'ERR_MODULE_NOT_FOUND' // ESM && e.code !== 'ENOENT') { // Testdouble.js, maybe other ESM loaders too throw e; } if (this.isVerbose_) { console.log(`Tried to load config file ${candidate} but it does not exist (${e.code})`); } } } if (numFound > 1) { console.warn( 'Deprecation warning: Jasmine found and loaded both jasmine.js ' + 'and jasmine.json\n' + 'config files. In a future version, only the first file found ' + 'will be loaded.' ); } else if (numFound === 0 && this.isVerbose_) { console.log('Did not find any config files.'); } } } async loadSpecificConfigFile_(relativePath) { const absolutePath = path.resolve(this.projectBaseDir, relativePath); const config = await this.loader.load(absolutePath); if (this.isVerbose_) { console.log(`Loaded config file ${absolutePath}`); } this.loadConfig(config); } /** * Loads configuration from the specified object. * @name Runner#loadConfig * @function * @param {Configuration} config */ loadConfig(config) { /** * @interface Configuration */ const envConfig = {...config.env}; /** * The directory that spec files are contained in, relative to the project * base directory. * @name Configuration#spec_dir * @type string | undefined */ this.specDir = config.spec_dir || this.specDir; /** * Whether to fail specs that contain no expectations. * @name Configuration#failSpecWithNoExpectations * @type boolean | undefined * @default false */ if (config.failSpecWithNoExpectations !== undefined) { envConfig.failSpecWithNoExpectations = config.failSpecWithNoExpectations; } /** * Whether to stop each spec on the first expectation failure. * @name Configuration#stopSpecOnExpectationFailure * @type boolean | undefined * @default false */ if (config.stopSpecOnExpectationFailure !== undefined) { envConfig.stopSpecOnExpectationFailure = config.stopSpecOnExpectationFailure; } /** * Whether to stop suite execution on the first spec failure. * @name Configuration#stopOnSpecFailure * @type boolean | undefined * @default false */ if (config.stopOnSpecFailure !== undefined) { envConfig.stopOnSpecFailure = config.stopOnSpecFailure; } /** * Whether the default reporter should list pending specs even if there are * failures. * @name Configuration#alwaysListPendingSpecs * @type boolean | undefined * @default true */ if (config.alwaysListPendingSpecs !== undefined) { this.alwaysListPendingSpecs(config.alwaysListPendingSpecs); } /** * Whether to run specs in a random order. * @name Configuration#random * @type boolean | undefined * @default true */ if (config.random !== undefined) { envConfig.random = config.random; } if (config.verboseDeprecations !== undefined) { envConfig.verboseDeprecations = config.verboseDeprecations; } /** * Specifies how to load files with names ending in .js. Valid values are * "require" and "import". "import" should be safe in all cases, and is * required if your project contains ES modules with filenames ending in .js. * @name Configuration#jsLoader * @type string | undefined * @default "require" */ if (config.jsLoader === 'import' || config.jsLoader === undefined) { this.loader.alwaysImport = true; } else if (config.jsLoader === 'require') { this.loader.alwaysImport = false; } else { throw new Error(`"${config.jsLoader}" is not a valid value for the ` + 'jsLoader configuration property. Valid values are "import", ' + '"require", and undefined.'); } if (Object.keys(envConfig).length > 0) { this.configureEnv(envConfig); } /** * An array of helper file paths or {@link https://github.com/isaacs/node-glob#glob-primer|globs} * that match helper files. Each path or glob will be evaluated relative to * the spec directory. Helpers are loaded before specs. * @name Configuration#helpers * @type string[] | undefined */ if(config.helpers) { this.addMatchingHelperFiles(config.helpers); } /** * An array of module names to load at the start of execution. * @name Configuration#requires * @type string[] | undefined */ if(config.requires) { this.addRequires(config.requires); } /** * An array of spec file paths or {@link https://github.com/isaacs/node-glob#glob-primer|globs} * that match helper files. Each path or glob will be evaluated relative to * the spec directory. * @name Configuration#spec_files * @type string[] | undefined */ if(config.spec_files) { this.addMatchingSpecFiles(config.spec_files); } /** * An array of reporters. Each object in the array will be passed to * {@link Jasmine#addReporter} or {@link ParallelRunner#addReporter}. * * This provides a middle ground between the --reporter= CLI option and full * programmatic usage. Note that because reporters are objects with methods, * this option can only be used in JavaScript config files * (e.g `spec/support/jasmine.js`), not JSON. * @name Configuration#reporters * @type Reporter[] | undefined * @see custom_reporter */ if (config.reporters) { for (let i = 0; i < config.reporters.length; i++) { this.addReporter( config.reporters[i], `the reporter in position ${i} of the configuration's reporters array` ); } } /** * A function that will be called exactly once, even in parallel mode, * before the test suite runs. This is intended to be used to initialize * out-of-process state such as starting up an external service. * * If the globalSetup function is async or otherwise returns a promise, * Jasmine will wait up to {@link Configuration#globalSetupTimeout} * milliseconds for it to finish before running specs. Callbacks are not * supported. * * globalSetup may be run in a different process from the specs. In-process * side effects that it causes, including changes to the Jasmine * environment, are not guaranteed to affect any or all specs. Use either * beforeEach or beforeAll for in-process setup. * * @name Configuration#globalSetup * @type Function | undefined */ if (config.globalSetup) { this.globalSetup_ = config.globalSetup; } /** * The number of milliseconds to wait for an asynchronous * {@link Configuration#globalSetup} to complete. * * @name Configuration#globalSetupTimeout * @type Number | undefined * @default 5000 */ if (config.globalSetupTimeout) { this.globalSetupTimeout_ = config.globalSetupTimeout; } /** * A function that will be called exactly once, even in parallel mode, * after the test suite runs. This is intended to be used to clean up * out-of-process state such as shutting down an external service. * * If the globalTeardown function is async or otherwise returns a promise, * Jasmine will wait up to {@link Configuration#globalTeardownTimeout} * milliseconds for it to finish. Callbacks are not supported. * * globalTeardown may be run in a different process from the specs. * In-process side effects caused by specs, including changes to the Jasmine * environment, are not guaranteed to be visible to globalTeardown. Use * either afterEach or afterAll for in-process cleanup. * * @name Configuration#globalTeardown * @type Function | undefined */ if (config.globalTeardown) { this.globalTeardown_ = config.globalTeardown; } /** * The number of milliseconds to wait for an asynchronous * {@link Configuration#globalTeardown} to complete. * * @name Configuration#globalTeardownTimeout * @type Number | undefined * @default 5000 */ if (config.globalTeardownTimeout) { this.globalTeardownTimeout_ = config.globalTeardownTimeout; } } /** * Adds a spec file to the list that will be loaded when the suite is executed. * @function * @name Runner#addSpecFile * @param {string} filePath The path to the file to be loaded. */ addSpecFile(filePath) { this.specFiles.push(filePath); } /** * Adds a helper file to the list that will be loaded when the suite is executed. * @function * @name Runner#addHelperFile * @param {string} filePath The path to the file to be loaded. */ addHelperFile(filePath) { this.helperFiles.push(filePath); } addRequires(requires) { const jasmineRunner = this; requires.forEach(function(r) { jasmineRunner.requires.push(r); }); } /** * Configures the default reporter that is installed if no other reporter is * specified. * @name Runner#configureDefaultReporter * @function * @param {ConsoleReporterOptions} options */ configureDefaultReporter(options) { options.print = options.print || function() { process.stdout.write(util.format.apply(this, arguments)); }; options.showColors = options.hasOwnProperty('showColors') ? options.showColors : true; this.reporter_.setOptions(options); this.defaultReporterConfigured = true; } async withinGlobalSetup_(fn) { let ok = false; await this.runGlobalSetup_(); try { await fn(); ok = true; } finally { if (ok) { await this.runGlobalTeardown_(); } else { // Allow the error from fn (which executes the suite) to propagate. // It's probably more important than any error from global teardown. try { await this.runGlobalTeardown_(); } catch (e) { console.error(e); } } } } async runGlobalSetup_() { if (this.globalSetup_) { await this.globalSetupOrTeardownRunner_.run( 'globalSetup', this.globalSetup_, this.globalSetupTimeout_ ); } } async runGlobalTeardown_() { if (this.globalTeardown_) { await this.globalSetupOrTeardownRunner_.run( 'globalTeardown', this.globalTeardown_, this.globalTeardownTimeout_ ); } } async flushOutput() { // Ensure that all data has been written to stdout and stderr, // then exit with an appropriate status code. Otherwise, we // might exit before all previous writes have actually been // written when Jasmine is piped to another process that isn't // reading quickly enough. var streams = [process.stdout, process.stderr]; var promises = streams.map(stream => { return new Promise(resolve => stream.write('', null, resolve)); }); return Promise.all(promises); } } /** * Adds files that match the specified patterns to the list of spec files. * @function * @name Runner#addMatchingSpecFiles * @param {Array<string>} patterns An array of spec file paths * or {@link https://github.com/isaacs/node-glob#glob-primer|globs} that match * spec files. Each path or glob will be evaluated relative to the spec directory. */ RunnerBase.prototype.addMatchingSpecFiles = addFiles('specFiles'); /** * Adds files that match the specified patterns to the list of helper files. * @function * @name Runner#addMatchingHelperFiles * @param {Array<string>} patterns An array of helper file paths * or {@link https://github.com/isaacs/node-glob#glob-primer|globs} that match * helper files. Each path or glob will be evaluated relative to the spec directory. */ RunnerBase.prototype.addMatchingHelperFiles = addFiles('helperFiles'); /** * Whether to cause the Node process to exit when the suite finishes executing. * * @name Runner#exitOnCompletion * @type {boolean} * @default true */ /** * Add a custom reporter to the Jasmine environment. * @function * @name Runner#addReporter * @param {Reporter} reporter The reporter to add * @see custom_reporter */ /** * Clears all registered reporters. * @function * @name Runner#clearReporters */ function addFiles(kind) { return function (files) { const fileArr = this[kind]; const {includeFiles, excludeFiles} = files.reduce((ongoing, file) => { const hasNegation = file.startsWith('!'); if (hasNegation) { file = file.substring(1); } return { includeFiles: ongoing.includeFiles.concat(!hasNegation ? [file] : []), excludeFiles: ongoing.excludeFiles.concat(hasNegation ? [file] : []) }; }, { includeFiles: [], excludeFiles: [] }); const baseDir = `${this.projectBaseDir}/${this.specDir}`; includeFiles.forEach(function(file) { const filePaths = glob .sync(file, { cwd: baseDir, ignore: excludeFiles }) .map(function(f) { if (path.isAbsolute(f)) { return f; } else { return unWindows(path.join(baseDir, f)); } }) .filter(function(filePath) { return fileArr.indexOf(filePath) === -1; }) // Sort file paths consistently. Glob <9 did this but Glob >= 9 doesn't. // Jasmine doesn't care about the order, but users might. .sort(); filePaths.forEach(function(filePath) { fileArr.push(filePath); }); }); if (this.isVerbose_) { console.log(`File glob for ${kind}: ${files}`); console.log(`Resulting ${kind}: [${fileArr}]`); } }; } RunnerBase.exitCodeForStatus = function(status) { switch (status) { case 'passed': return 0; case 'incomplete': return 2; case 'failed': return 3; default: console.error(`Unrecognized overall status: ${status}`); return 1; } }; module.exports = RunnerBase;