| 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');
 |