jasmine
Version:
CLI for Jasmine, a simple JavaScript testing framework for browsers and Node
571 lines (510 loc) • 17.9 kB
JavaScript
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;