UNPKG

jasmine

Version:

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

479 lines (410 loc) 13.4 kB
const path = require('path'); const fs = require('fs'); const os = require('os'); const unWindows = require('./unWindows'); exports = module.exports = Command; const subCommands = { init: { description: 'initialize jasmine', action: initJasmine }, examples: { description: 'install examples', action: installExamples }, help: { description: 'show help', action: help, alias: '-h' }, version: { description: 'show jasmine and jasmine-core versions', action: version, alias: '-v' }, enumerate: { description: 'enumerate suites and specs', action: enumerate } }; function Command(projectBaseDir, examplesDir, deps) { const {print, platform, terminalColumns, Jasmine, ParallelRunner} = deps; const isWindows = platform() === 'win32'; this.projectBaseDir = isWindows ? unWindows(projectBaseDir) : projectBaseDir; this.specDir = `${this.projectBaseDir}/spec`; const command = this; this.run = async function(args) { setEnvironmentVariables(args); let commandToRun; Object.keys(subCommands).forEach(function(cmd) { const commandObject = subCommands[cmd]; if (args.indexOf(cmd) >= 0) { commandToRun = commandObject; } else if (commandObject.alias && args.indexOf(commandObject.alias) >= 0) { commandToRun = commandObject; } }); if (commandToRun) { await commandToRun.action({ Jasmine, projectBaseDir, specDir: command.specDir, examplesDir: examplesDir, print, terminalColumns }); } else { const options = parseOptions(args, isWindows); if (options.usageErrors.length > 0) { process.exitCode = 1; for (const e of options.usageErrors) { print(e); } print(''); help({print, terminalColumns}); } else { await runJasmine(Jasmine, ParallelRunner, projectBaseDir, options); } } }; } function isFileArg(arg) { return arg.indexOf('--') !== 0 && !isEnvironmentVariable(arg); } function parseOptions(argv, isWindows) { let files = []; let helpers = []; let requires = []; let unknownOptions = []; let usageErrors = []; let color = undefined; let reporter; let configPath; let filterRegex; let filterPath; let failFast; let random; let seed; let numWorkers = 1; let verbose = false; for (const arg of argv) { if (arg === '--no-color') { color = false; } else if (arg === '--color') { color = true; } else if (arg.match("^--filter=")) { filterRegex = arg.match("^--filter=(.*)")[1]; } else if (arg.match("^--filter-path=(.*)")) { const json = arg.match("^--filter-path=(.*)")[1]; try { filterPath = JSON.parse(json); } catch { usageErrors.push('Invalid filter path: ' + json); } if (!(filterPath instanceof Array)) { usageErrors.push('Invalid filter path: ' + json); } } else if (arg.match("^--helper=")) { helpers.push(arg.match("^--helper=(.*)")[1]); } else if (arg.match("^--require=")) { requires.push(arg.match("^--require=(.*)")[1]); } else if (arg === '--fail-fast') { failFast = true; } else if (arg.match("^--random=")) { random = arg.match("^--random=(.*)")[1] === 'true'; } else if (arg.match("^--seed=")) { seed = arg.match("^--seed=(.*)")[1]; } else if (arg.match("^--config=")) { configPath = arg.match("^--config=(.*)")[1]; } else if (arg.match("^--reporter=")) { reporter = arg.match("^--reporter=(.*)")[1]; } else if (arg.match("^--parallel=(.*)")) { const w = arg.match("^--parallel=(.*)")[1]; if (w === 'auto') { // A reasonable default in most situations numWorkers = os.cpus().length -1; } else { numWorkers = parseFloat(w); if (isNaN(numWorkers) || numWorkers < 2 || numWorkers !== Math.floor(numWorkers)) { usageErrors.push('Argument to --parallel= must be an integer greater than 1'); } } } else if (arg === '--verbose') { verbose = true; } else if (arg === '--') { break; } else if (isFileArg(arg)) { files.push(isWindows ? unWindows(arg) : arg); } else if (!isEnvironmentVariable(arg)) { unknownOptions.push(arg); } } if (numWorkers > 1 && filterPath) { usageErrors.push('--filter-path is not supported in parallel mode.'); } if (unknownOptions.length > 0) { usageErrors.push('Unknown options: ' + unknownOptions.join(', ')); } return { color, configPath, filterRegex, filterPath, failFast, helpers, requires, reporter, files, random, seed, numWorkers, verbose, usageErrors }; } async function runJasmine(Jasmine, ParallelRunner, projectBaseDir, options) { let runner; if (options.numWorkers > 1) { runner = new ParallelRunner({ projectBaseDir, numWorkers: options.numWorkers }); } else { runner = new Jasmine({ projectBaseDir }); } runner.verbose(options.verbose); await runner.loadConfigFile(options.configPath || process.env.JASMINE_CONFIG_PATH); if (options.failFast !== undefined) { runner.configureEnv({ stopSpecOnExpectationFailure: options.failFast, stopOnSpecFailure: options.failFast }); } if (options.seed !== undefined) { runner.seed(options.seed); } if (options.random !== undefined) { runner.randomizeTests(options.random); } if (options.helpers !== undefined && options.helpers.length) { runner.addMatchingHelperFiles(options.helpers); } if (options.requires !== undefined && options.requires.length) { runner.addRequires(options.requires); } if (options.reporter !== undefined) { await registerReporter(options.reporter, runner); } runner.showColors(options.color); let filter; if (options.filterRegex) { filter = options.filterRegex; } else if (options.filterPath) { filter = {path: options.filterPath}; } try { await runner.execute(options.files, filter); } catch (error) { console.error(error); process.exit(1); } } async function registerReporter(reporterModuleName, runner) { let Reporter; try { Reporter = await runner.loader.load(resolveReporter(reporterModuleName)); } catch (e) { throw new Error('Failed to load reporter module '+ reporterModuleName + '\nUnderlying error: ' + e.stack + '\n(end underlying error)'); } let reporter; try { reporter = new Reporter(); } catch (e) { throw new Error('Failed to instantiate reporter from '+ reporterModuleName + '\nUnderlying error: ' + e.stack + '\n(end underlying error)'); } runner.clearReporters(); runner.addReporter(reporter); } function resolveReporter(nameOrPath) { if (nameOrPath.startsWith('./') || nameOrPath.startsWith('../')) { return path.resolve(nameOrPath); } else { return nameOrPath; } } function initJasmine(options) { const print = options.print; const destDir = path.join(options.specDir, 'support/'); makeDirStructure(destDir); const destPath = path.join(destDir, 'jasmine.mjs'); if (fs.existsSync(destPath)) { print('spec/support/jasmine.mjs already exists in your project.'); } else { const contents = fs.readFileSync( path.join(__dirname, '../lib/examples/jasmine.mjs'), 'utf-8'); fs.writeFileSync(destPath, contents); } } function installExamples(options) { const specDir = options.specDir; const projectBaseDir = options.projectBaseDir; const examplesDir = options.examplesDir; makeDirStructure(path.join(specDir, 'support')); makeDirStructure(path.join(specDir, 'jasmine_examples')); makeDirStructure(path.join(specDir, 'helpers', 'jasmine_examples')); makeDirStructure(path.join(projectBaseDir, 'lib', 'jasmine_examples')); copyFiles( path.join(examplesDir, 'spec', 'helpers', 'jasmine_examples'), path.join(specDir, 'helpers', 'jasmine_examples'), new RegExp(/[Hh]elper\.js/) ); copyFiles( path.join(examplesDir, 'lib', 'jasmine_examples'), path.join(projectBaseDir, 'lib', 'jasmine_examples'), new RegExp(/\.js/) ); copyFiles( path.join(examplesDir, 'spec', 'jasmine_examples'), path.join(specDir, 'jasmine_examples'), new RegExp(/[Ss]pec.js/) ); } function help(deps) { const print = deps.print; let terminalColumns = deps.terminalColumns || 80; print(wrap(terminalColumns, 'Usage: jasmine [command] [options] [files] [--]')); print(''); print('Commands:'); Object.keys(subCommands).forEach(function(cmd) { let commandNameText = cmd; if(subCommands[cmd].alias) { commandNameText = commandNameText + ',' + subCommands[cmd].alias; } print(wrapWithIndent(terminalColumns, lPad(commandNameText, 10) + ' ', subCommands[cmd].description)); }); print(''); print(wrap(terminalColumns, 'If no command is given, Jasmine specs will be run.')); print(''); print(''); print('Options:'); const options = [ { syntax: '--parallel=N', help: 'Run in parallel with N workers' }, { syntax: '--parallel=auto', help: 'Run in parallel with an automatically chosen number of workers' }, { syntax: '--no-color', help: 'turn off color in spec output' }, { syntax: '--color', help: 'force turn on color in spec output' }, { syntax: '--filter=', help: 'filter specs to run only those that match the given regular expression' }, { syntax: '--filter-path=', help: 'run only the spec or suite that matches ' + 'the given path, e.g. ' + '--filter-path=\'["parent suite name","child suite name","spec name"]\'' }, { syntax: '--helper=', help: 'load helper files that match the given string' }, { syntax: '--require=', help: 'load module that matches the given string' }, { syntax: '--fail-fast', help: 'stop Jasmine execution on spec failure' }, { syntax: '--config=', help: 'path to the Jasmine configuration file' }, { syntax: '--reporter=', help: 'path to reporter to use instead of the default Jasmine reporter' }, { syntax: '--verbose', help: 'print information that may be useful for debugging configuration' }, { syntax: '--', help: 'marker to signal the end of options meant for Jasmine' }, ]; for (const o of options) { print(wrapWithIndent(terminalColumns, lPad(o.syntax, 18) + ' ', o.help)); } print(''); print(wrap(terminalColumns, 'The given arguments take precedence over options in your jasmine.json.')); print(wrap(terminalColumns, 'The path to your optional jasmine.json can also be configured by setting the JASMINE_CONFIG_PATH environment variable.')); } function wrapWithIndent(cols, prefix, suffix) { const lines = wrap2(cols - prefix.length, suffix); const indent = lPad('', prefix.length); return prefix + lines.join('\n' + indent); } function wrap(cols, input) { return wrap2(cols, input).join('\n'); } function wrap2(cols, input) { let lines = []; let start = 0; while (start < input.length) { const splitAt = indexOfLastSpaceInRange(start, start + cols, input); if (splitAt === -1 || input.length - start <= cols) { lines.push(input.substring(start)); break; } else { lines.push(input.substring(start, splitAt)); start = splitAt + 1; } } return lines; } function indexOfLastSpaceInRange(start, end, s) { for (let i = end; i >= start; i--) { if (s[i] === ' ') { return i; } } return -1; } function version(options) { const print = options.print; print('jasmine v' + require('../package.json').version); const jasmine = new options.Jasmine(); print('jasmine-core v' + jasmine.coreVersion()); } async function enumerate(options) { const runner = new options.Jasmine({projectBaseDir: options.projectBaseDir}); await runner.loadConfigFile(options.configPath || process.env.JASMINE_CONFIG_PATH); if (options.helpers !== undefined && options.helpers.length) { runner.addMatchingHelperFiles(options.helpers); } if (options.requires !== undefined && options.requires.length) { runner.addRequires(options.requires); } const suites = await runner.enumerate(); options.print(JSON.stringify(suites, null, ' ')); } function lPad(str, length) { if (str.length >= length) { return str; } else { return lPad(' ' + str, length); } } function copyFiles(srcDir, destDir, pattern) { const srcDirFiles = fs.readdirSync(srcDir); srcDirFiles.forEach(function(file) { if (file.search(pattern) !== -1) { fs.writeFileSync(path.join(destDir, file), fs.readFileSync(path.join(srcDir, file))); } }); } function makeDirStructure(absolutePath) { const splitPath = absolutePath.split(path.sep); splitPath.forEach(function(dir, index) { if(index > 1) { const fullPath = path.join(splitPath.slice(0, index).join('/'), dir); if (!fs.existsSync(fullPath)) { fs.mkdirSync(fullPath); } } }); } function isEnvironmentVariable(arg) { if (arg.match(/^--/)) { return false; } return arg.match(/(.*)=(.*)/); } function setEnvironmentVariables(args) { args.forEach(function (arg) { const regExpMatch = isEnvironmentVariable(arg); if(regExpMatch) { const key = regExpMatch[1]; const value = regExpMatch[2]; process.env[key] = value; } }); }