'use strict' const path = require('path') const yargs = require('yargs') const fs = require('graceful-fs') const Server = require('./server') const helper = require('./helper') const constant = require('./constants') const cfg = require('./config') function processArgs (argv, options, fs, path) { Object.getOwnPropertyNames(argv).forEach(function (name) { let argumentValue = argv[name] if (name !== '_' && name !== '$0') { if (Array.isArray(argumentValue)) { argumentValue = argumentValue.pop() // If the same argument is defined multiple times, override. } options[helper.dashToCamel(name)] = argumentValue } }) if (helper.isString(options.autoWatch)) { options.autoWatch = options.autoWatch === 'true' } if (helper.isString(options.colors)) { options.colors = options.colors === 'true' } if (helper.isString(options.failOnEmptyTestSuite)) { options.failOnEmptyTestSuite = options.failOnEmptyTestSuite === 'true' } if (helper.isString(options.failOnFailingTestSuite)) { options.failOnFailingTestSuite = options.failOnFailingTestSuite === 'true' } if (helper.isString(options.formatError)) { let required try { required = require(options.formatError) } catch (err) { console.error('Could not require formatError: ' + options.formatError, err) } // support exports.formatError and module.exports = function options.formatError = required.formatError || required if (!helper.isFunction(options.formatError)) { console.error(`Format error must be a function, got: ${typeof options.formatError}`) process.exit(1) } } if (helper.isString(options.logLevel)) { const logConstant = constant['LOG_' + options.logLevel.toUpperCase()] if (helper.isDefined(logConstant)) { options.logLevel = logConstant } else { console.error('Log level must be one of disable, error, warn, info, or debug.') process.exit(1) } } else if (helper.isDefined(options.logLevel)) { console.error('Log level must be one of disable, error, warn, info, or debug.') process.exit(1) } if (helper.isString(options.singleRun)) { options.singleRun = options.singleRun === 'true' } if (helper.isString(options.browsers)) { options.browsers = options.browsers.split(',') } if (options.reportSlowerThan === false) { options.reportSlowerThan = 0 } if (helper.isString(options.reporters)) { options.reporters = options.reporters.split(',') } if (helper.isString(options.removedFiles)) { options.removedFiles = options.removedFiles.split(',') } if (helper.isString(options.addedFiles)) { options.addedFiles = options.addedFiles.split(',') } if (helper.isString(options.changedFiles)) { options.changedFiles = options.changedFiles.split(',') } if (helper.isString(options.refresh)) { options.refresh = options.refresh === 'true' } let configFile = argv.configFile if (!configFile) { // default config file (if exists) if (fs.existsSync('./karma.conf.js')) { configFile = './karma.conf.js' } else if (fs.existsSync('./karma.conf.coffee')) { configFile = './karma.conf.coffee' } else if (fs.existsSync('./karma.conf.ts')) { configFile = './karma.conf.ts' } else if (fs.existsSync('./.config/karma.conf.js')) { configFile = './.config/karma.conf.js' } else if (fs.existsSync('./.config/karma.conf.coffee')) { configFile = './.config/karma.conf.coffee' } else if (fs.existsSync('./.config/karma.conf.ts')) { configFile = './.config/karma.conf.ts' } } options.configFile = configFile ? path.resolve(configFile) : null if (options.cmd === 'run') { options.clientArgs = parseClientArgs(process.argv) } return options } function parseClientArgs (argv) { // extract any args after '--' as clientArgs let clientArgs = [] argv = argv.slice(2) const idx = argv.indexOf('--') if (idx !== -1) { clientArgs = argv.slice(idx + 1) } return clientArgs } // return only args that occur before `--` function argsBeforeDoubleDash (argv) { const idx = argv.indexOf('--') return idx === -1 ? argv : argv.slice(0, idx) } function describeRoot () { return yargs .usage('Karma - Spectacular Test Runner for JavaScript.\n\n' + 'Run --help with particular command to see its description and available options.\n\n' + 'Usage:\n' + ' $0 ') .command('init [configFile]', 'Initialize a config file.', describeInit) .command('start [configFile]', 'Start the server / do a single run.', describeStart) .command('run [configFile]', 'Trigger a test run.', describeRun) .command('stop [configFile]', 'Stop the server.', describeStop) .command('completion', 'Shell completion for karma.', describeCompletion) .demandCommand(1, 'Command not specified.') .strictCommands() .describe('help', 'Print usage and options.') .describe('version', 'Print current version.') } function describeInit (yargs) { yargs .usage('Karma - Spectacular Test Runner for JavaScript.\n\n' + 'INIT - Initialize a config file.\n\n' + 'Usage:\n' + ' $0 init [configFile]') .strictCommands(false) .version(false) .positional('configFile', { describe: 'Name of the generated Karma configuration file', type: 'string' }) .describe('log-level', ' Level of logging.') .describe('colors', 'Use colors when reporting and printing logs.') .describe('no-colors', 'Do not use colors when reporting or printing logs.') } function describeStart (yargs) { yargs .usage('Karma - Spectacular Test Runner for JavaScript.\n\n' + 'START - Start the server / do a single run.\n\n' + 'Usage:\n' + ' $0 start [configFile]') .strictCommands(false) .version(false) .positional('configFile', { describe: 'Path to the Karma configuration file', type: 'string' }) .describe('port', ' Port where the server is running.') .describe('auto-watch', 'Auto watch source files and run on change.') .describe('detached', 'Detach the server.') .describe('no-auto-watch', 'Do not watch source files.') .describe('log-level', ' Level of logging.') .describe('colors', 'Use colors when reporting and printing logs.') .describe('no-colors', 'Do not use colors when reporting or printing logs.') .describe('reporters', 'List of reporters (available: dots, progress, junit, growl, coverage).') .describe('browsers', 'List of browsers to start (eg. --browsers Chrome,ChromeCanary,Firefox).') .describe('capture-timeout', ' Kill browser if does not capture in given time [ms].') .describe('single-run', 'Run the test when browsers captured and exit.') .describe('no-single-run', 'Disable single-run.') .describe('report-slower-than', ' Report tests that are slower than given time [ms].') .describe('fail-on-empty-test-suite', 'Fail on empty test suite.') .describe('no-fail-on-empty-test-suite', 'Do not fail on empty test suite.') .describe('fail-on-failing-test-suite', 'Fail on failing test suite.') .describe('no-fail-on-failing-test-suite', 'Do not fail on failing test suite.') .option('format-error', { describe: 'A path to a file that exports the format function.', type: 'string' }) } function describeRun (yargs) { yargs .usage('Karma - Spectacular Test Runner for JavaScript.\n\n' + 'RUN - Run the tests (requires running server).\n\n' + 'Usage:\n' + ' $0 run [configFile] [-- ]') .strictCommands(false) .version(false) .positional('configFile', { describe: 'Path to the Karma configuration file', type: 'string' }) .describe('port', ' Port where the server is listening.') .describe('no-refresh', 'Do not re-glob all the patterns.') .describe('fail-on-empty-test-suite', 'Fail on empty test suite.') .describe('no-fail-on-empty-test-suite', 'Do not fail on empty test suite.') .describe('log-level', ' Level of logging.') .describe('colors', 'Use colors when reporting and printing logs.') .describe('no-colors', 'Do not use colors when reporting or printing logs.') .option('removed-files', { describe: 'Comma-separated paths to removed files. Useful when automatic file watching is disabled.', type: 'string' }) .option('changed-files', { describe: 'Comma-separated paths to changed files. Useful when automatic file watching is disabled.', type: 'string' }) .option('added-files', { describe: 'Comma-separated paths to added files. Useful when automatic file watching is disabled.', type: 'string' }) } function describeStop (yargs) { yargs .usage('Karma - Spectacular Test Runner for JavaScript.\n\n' + 'STOP - Stop the server (requires running server).\n\n' + 'Usage:\n' + ' $0 stop [configFile]') .strictCommands(false) .version(false) .positional('configFile', { describe: 'Path to the Karma configuration file', type: 'string' }) .describe('port', ' Port where the server is listening.') .describe('log-level', ' Level of logging.') } function describeCompletion (yargs) { yargs .usage('Karma - Spectacular Test Runner for JavaScript.\n\n' + 'COMPLETION - Bash/ZSH completion for karma.\n\n' + 'Installation:\n' + ' $0 completion >> ~/.bashrc') .version(false) } function printRunnerProgress (data) { process.stdout.write(data) } exports.process = () => { const argv = describeRoot().parse(argsBeforeDoubleDash(process.argv.slice(2))) return processArgs(argv, { cmd: argv._.shift() }, fs, path) } exports.run = async () => { const cliOptions = exports.process() const cmd = cliOptions.cmd // prevent config from changing the command const cmdNeedsConfig = cmd === 'start' || cmd === 'run' || cmd === 'stop' if (cmdNeedsConfig) { let config try { config = await cfg.parseConfig( cliOptions.configFile, cliOptions, { promiseConfig: true, throwErrors: true } ) } catch (karmaConfigException) { // The reject reason/exception isn't used to log a message since // parseConfig already calls a configured logger method with an almost // identical message. // The `run` function is a private application, not a public API. We don't // need to worry about process.exit vs throw vs promise rejection here. process.exit(1) } switch (cmd) { case 'start': { const server = new Server(config) await server.start() return server } case 'run': return require('./runner') .run(config) .on('progress', printRunnerProgress) case 'stop': return require('./stopper').stop(config) } } else { switch (cmd) { case 'init': return require('./init').init(cliOptions) case 'completion': return require('./completion').completion(cliOptions) } } } // just for testing exports.processArgs = processArgs exports.parseClientArgs = parseClientArgs exports.argsBeforeDoubleDash = argsBeforeDoubleDash