123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597 |
- const os = require('os');
- const path = require('path');
- const util = require('util');
- const glob = require('glob');
- const Loader = require('./loader');
- const ExitHandler = require('./exit_handler');
- const ConsoleSpecFilter = require('./filters/console_spec_filter');
- /**
- * Options for the {@link Jasmine} constructor
- * @name JasmineOptions
- * @interface
- */
- /**
- * The path to the project's base directory. This can be absolute or relative
- * to the current working directory. If it isn't specified, the current working
- * directory will be used.
- * @name JasmineOptions#projectBaseDir
- * @type (string | undefined)
- */
- /**
- * Whether to create the globals (describe, it, etc) that make up Jasmine's
- * spec-writing interface. If it is set to false, the spec-writing interface
- * can be accessed via jasmine-core's `noGlobals` method, e.g.:
- *
- * `const {describe, it, expect, jasmine} = require('jasmine-core').noGlobals();`
- *
- * @name JasmineOptions#globals
- * @type (boolean | undefined)
- * @default true
- */
- /**
- * @classdesc Configures, builds, and executes a Jasmine test suite
- * @param {(JasmineOptions | undefined)} options
- * @constructor
- * @name Jasmine
- * @example
- * const Jasmine = require('jasmine');
- * const jasmine = new Jasmine();
- */
- class Jasmine {
- constructor(options) {
- options = options || {};
- this.loader = options.loader || new Loader();
- this.isWindows_ = (options.platform || os.platform)() === 'win32';
- const jasmineCore = options.jasmineCore || require('jasmine-core');
- if (options.globals === false) {
- this.jasmine = jasmineCore.noGlobals().jasmine;
- } else {
- this.jasmine = jasmineCore.boot(jasmineCore);
- }
- if (options.projectBaseDir) {
- this.validatePath_(options.projectBaseDir);
- this.projectBaseDir = options.projectBaseDir;
- } else {
- this.projectBaseDir = (options.getcwd || path.resolve)();
- }
- this.specDir = '';
- this.specFiles = [];
- this.helperFiles = [];
- this.requires = [];
- /**
- * The Jasmine environment.
- * @name Jasmine#env
- * @readonly
- * @see {@link https://jasmine.github.io/api/edge/Env.html|Env}
- * @type {Env}
- */
- this.env = this.jasmine.getEnv({suppressLoadErrors: true});
- this.reportersCount = 0;
- this.exit = process.exit;
- this.showingColors = true;
- this.alwaysListPendingSpecs_ = true;
- this.reporter = new module.exports.ConsoleReporter();
- this.addReporter(this.reporter);
- this.defaultReporterConfigured = false;
- /**
- * @function
- * @name Jasmine#coreVersion
- * @return {string} The version of jasmine-core in use
- */
- this.coreVersion = function() {
- return jasmineCore.version();
- };
- /**
- * Whether to cause the Node process to exit when the suite finishes executing.
- *
- * @name Jasmine#exitOnCompletion
- * @type {boolean}
- * @default true
- */
- this.exitOnCompletion = true;
- }
- /**
- * Sets whether to randomize the order of specs.
- * @function
- * @name Jasmine#randomizeTests
- * @param {boolean} value Whether to randomize
- */
- randomizeTests(value) {
- this.env.configure({random: value});
- }
- /**
- * Sets the random seed.
- * @function
- * @name Jasmine#seed
- * @param {number} seed The random seed
- */
- seed(value) {
- this.env.configure({seed: value});
- }
- /**
- * Sets whether to show colors in the console reporter.
- * @function
- * @name Jasmine#showColors
- * @param {boolean} value Whether to show colors
- */
- showColors(value) {
- this.showingColors = value;
- }
- /**
- * Sets whether the console reporter should list pending specs even when there
- * are failures.
- * @name Jasmine#alwaysListPendingSpecs
- * @param value {boolean}
- */
- alwaysListPendingSpecs(value) {
- this.alwaysListPendingSpecs_ = value;
- }
- /**
- * Adds a spec file to the list that will be loaded when the suite is executed.
- * @function
- * @name Jasmine#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 Jasmine#addHelperFile
- * @param {string} filePath The path to the file to be loaded.
- */
- addHelperFile(filePath) {
- this.helperFiles.push(filePath);
- }
- /**
- * Add a custom reporter to the Jasmine environment.
- * @function
- * @name Jasmine#addReporter
- * @param {Reporter} reporter The reporter to add
- * @see custom_reporter
- */
- addReporter(reporter) {
- this.env.addReporter(reporter);
- this.reportersCount++;
- }
- /**
- * Clears all registered reporters.
- * @function
- * @name Jasmine#clearReporters
- */
- clearReporters() {
- this.env.clearReporters();
- this.reportersCount = 0;
- }
- /**
- * Provide a fallback reporter if no other reporters have been specified.
- * @function
- * @name Jasmine#provideFallbackReporter
- * @param reporter The fallback reporter
- * @see custom_reporter
- */
- provideFallbackReporter(reporter) {
- this.env.provideFallbackReporter(reporter);
- }
- /**
- * Configures the default reporter that is installed if no other reporter is
- * specified.
- * @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;
- }
- /**
- * Add custom matchers for the current scope of specs.
- *
- * _Note:_ This is only callable from within a {@link beforeEach}, {@link it}, or {@link beforeAll}.
- * @function
- * @name Jasmine#addMatchers
- * @param {Object} matchers - Keys from this object will be the new matcher names.
- * @see custom_matcher
- */
- addMatchers(matchers) {
- this.env.addMatchers(matchers);
- }
- async loadSpecs() {
- await this._loadFiles(this.specFiles);
- }
- async loadHelpers() {
- await this._loadFiles(this.helperFiles);
- }
- async _loadFiles(files) {
- for (const file of files) {
- await this.loader.load(file);
- }
- }
- async loadRequires() {
- await this._loadFiles(this.requires);
- }
- /**
- * Loads configuration from the specified file. The file can be a JSON file or
- * any JS file that's loadable via require and provides a Jasmine config
- * as its default export.
- * @param {string} [configFilePath=spec/support/jasmine.json]
- * @return Promise
- */
- async loadConfigFile(configFilePath) {
- if (configFilePath) {
- await this.loadSpecificConfigFile_(configFilePath);
- } else {
- for (const ext of ['json', 'js']) {
- try {
- await this.loadSpecificConfigFile_(`spec/support/jasmine.${ext}`);
- } catch (e) {
- if (e.code !== 'MODULE_NOT_FOUND' && e.code !== 'ERR_MODULE_NOT_FOUND') {
- throw e;
- }
- }
- }
- }
- }
- async loadSpecificConfigFile_(relativePath) {
- const absolutePath = path.resolve(this.projectBaseDir, relativePath);
- const config = await this.loader.load(absolutePath);
- this.loadConfig(config);
- }
- /**
- * Loads configuration from the specified object.
- * @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;
- this.validatePath_(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.env.configure(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 via require() 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|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 (const r of config.reporters) {
- this.addReporter(r);
- }
- }
- }
- addRequires(requires) {
- const jasmineRunner = this;
- requires.forEach(function(r) {
- jasmineRunner.requires.push(r);
- });
- }
- /**
- * Sets whether to cause specs to only have one expectation failure.
- * @function
- * @name Jasmine#stopSpecOnExpectationFailure
- * @param {boolean} value Whether to cause specs to only have one expectation
- * failure
- */
- stopSpecOnExpectationFailure(value) {
- this.env.configure({stopSpecOnExpectationFailure: value});
- }
- /**
- * Sets whether to stop execution of the suite after the first spec failure.
- * @function
- * @name Jasmine#stopOnSpecFailure
- * @param {boolean} value Whether to stop execution of the suite after the
- * first spec failure
- */
- stopOnSpecFailure(value) {
- this.env.configure({stopOnSpecFailure: value});
- }
- 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);
- }
- /**
- * Runs the test suite.
- *
- * _Note_: Set {@link Jasmine#exitOnCompletion|exitOnCompletion} to false if you
- * intend to use the returned promise. Otherwise, the Node process will
- * ordinarily exit before the promise is settled.
- * @param {Array.<string>} [files] Spec files to run instead of the previously
- * configured set
- * @param {string} [filterString] Regex used to filter specs. If specified, only
- * specs with matching full names will be run.
- * @return {Promise<JasmineDoneInfo>} Promise that is resolved when the suite completes.
- */
- async execute(files, filterString) {
- await this.loadRequires();
- await this.loadHelpers();
- if (!this.defaultReporterConfigured) {
- this.configureDefaultReporter({
- showColors: this.showingColors,
- alwaysListPendingSpecs: this.alwaysListPendingSpecs_
- });
- }
- if (filterString) {
- const specFilter = new ConsoleSpecFilter({
- filterString: filterString
- });
- this.env.configure({specFilter: function(spec) {
- return specFilter.matches(spec.getFullName());
- }});
- }
- if (files && files.length > 0) {
- this.specDir = '';
- this.specFiles = [];
- this.addMatchingSpecFiles(files);
- }
- await this.loadSpecs();
- const prematureExitHandler = new ExitHandler(() => this.exit(4));
- prematureExitHandler.install();
- const overallResult = await this.env.execute();
- await this.flushOutput();
- prematureExitHandler.uninstall();
- if (this.exitOnCompletion) {
- this.exit(exitCodeForStatus(overallResult.overallStatus));
- }
- return overallResult;
- }
- validatePath_(path) {
- if (this.isWindows_ && path.includes('\\')) {
- const fixed = path.replace(/\\/g, '/');
- console.warn('Backslashes in ' +
- 'file paths behave inconsistently between platforms and might not be ' +
- 'treated as directory separators in a future version. Consider ' +
- `changing ${path} to ${fixed}.`);
- }
- }
- }
- /**
- * Adds files that match the specified patterns to the list of spec files.
- * @function
- * @name Jasmine#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.
- */
- Jasmine.prototype.addMatchingSpecFiles = addFiles('specFiles');
- /**
- * Adds files that match the specified patterns to the list of helper files.
- * @function
- * @name Jasmine#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.
- */
- Jasmine.prototype.addMatchingHelperFiles = addFiles('helperFiles');
- function addFiles(kind) {
- return function (files) {
- for (const f of files) {
- this.validatePath_(f);
- }
- const jasmineRunner = this;
- const fileArr = this[kind];
- const {includeFiles, excludeFiles} = files.reduce(function(ongoing, file) {
- const hasNegation = file.startsWith('!');
- if (hasNegation) {
- file = file.substring(1);
- }
- if (!path.isAbsolute(file)) {
- file = path.join(jasmineRunner.projectBaseDir, jasmineRunner.specDir, file);
- }
- return {
- includeFiles: ongoing.includeFiles.concat(!hasNegation ? [file] : []),
- excludeFiles: ongoing.excludeFiles.concat(hasNegation ? [file] : [])
- };
- }, { includeFiles: [], excludeFiles: [] });
- includeFiles.forEach(function(file) {
- const filePaths = glob
- .sync(file, { ignore: excludeFiles })
- .filter(function(filePath) {
- // glob will always output '/' as a segment separator but the fileArr may use \ on windows
- // fileArr needs to be checked for both versions
- return fileArr.indexOf(filePath) === -1 && fileArr.indexOf(path.normalize(filePath)) === -1;
- });
- filePaths.forEach(function(filePath) {
- fileArr.push(filePath);
- });
- });
- };
- }
- function exitCodeForStatus(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 = Jasmine;
- module.exports.ConsoleReporter = require('./reporters/console_reporter');
|