UNPKG

mocha

Version:

simple, flexible, fun test framework

390 lines (369 loc) 11.4 kB
"use strict"; /** * Definition for Mocha's default ("run tests") command * * @module * @private */ const symbols = require("log-symbols"); const pc = require("picocolors"); const Mocha = require("../mocha"); const { createUnsupportedError, createInvalidArgumentValueError, createMissingArgumentError, } = require("../errors"); const { list, handleRequires, validateLegacyPlugin, runMocha, } = require("./run-helpers"); const { ONE_AND_DONES, ONE_AND_DONE_ARGS } = require("./one-and-dones"); const debug = require("debug")("mocha:cli:run"); const defaults = require("../mocharc.json"); const { types, aliases } = require("./run-option-metadata"); /** * Logical option groups * @constant */ const GROUPS = { FILES: "File Handling", FILTERS: "Test Filters", NODEJS: "Node.js & V8", OUTPUT: "Reporting & Output", RULES: "Rules & Behavior", CONFIG: "Configuration", }; exports.command = ["$0 [spec..]", "inspect"]; exports.describe = "Run tests with Mocha"; exports.builder = (yargs) => yargs .options({ "allow-uncaught": { description: "Allow uncaught errors to propagate", group: GROUPS.RULES, }, "async-only": { description: "Require all tests to use a callback (async) or return a Promise", group: GROUPS.RULES, }, bail: { description: 'Abort ("bail") after first test failure', group: GROUPS.RULES, }, "check-leaks": { description: "Check for global variable leaks", group: GROUPS.RULES, }, color: { description: "Force-enable color output", group: GROUPS.OUTPUT, }, config: { config: true, defaultDescription: "(nearest rc file)", description: "Path to config file", group: GROUPS.CONFIG, }, delay: { description: "Delay initial execution of root suite", group: GROUPS.RULES, }, diff: { default: true, description: "Show diff on failure", group: GROUPS.OUTPUT, }, "dry-run": { description: "Report tests without executing them", group: GROUPS.RULES, }, exit: { description: "Force Mocha to quit after tests complete", group: GROUPS.RULES, }, extension: { default: defaults.extension, description: "File extension(s) to load", group: GROUPS.FILES, requiresArg: true, coerce: list, }, "pass-on-failing-test-suite": { default: false, description: "Not fail test run if tests were failed", group: GROUPS.RULES, }, "fail-zero": { description: "Fail test run if no test(s) encountered", group: GROUPS.RULES, }, fgrep: { conflicts: "grep", description: "Only run tests containing this string", group: GROUPS.FILTERS, requiresArg: true, }, file: { defaultDescription: "(none)", description: "Specify file(s) to be loaded prior to root suite execution", group: GROUPS.FILES, normalize: true, requiresArg: true, }, "forbid-only": { description: "Fail if exclusive test(s) encountered", group: GROUPS.RULES, }, "forbid-pending": { description: "Fail if pending test(s) encountered", group: GROUPS.RULES, }, "full-trace": { description: "Display full stack traces", group: GROUPS.OUTPUT, }, global: { coerce: list, description: "List of allowed global variables", group: GROUPS.RULES, requiresArg: true, }, grep: { coerce: (value) => (!value ? null : value), conflicts: "fgrep", description: "Only run tests matching this string or regexp", group: GROUPS.FILTERS, requiresArg: true, }, ignore: { defaultDescription: "(none)", description: "Ignore file(s) or glob pattern(s)", group: GROUPS.FILES, requiresArg: true, }, "inline-diffs": { description: "Display actual/expected differences inline within each string", group: GROUPS.OUTPUT, }, invert: { description: "Inverts --grep and --fgrep matches", group: GROUPS.FILTERS, }, jobs: { description: "Number of concurrent jobs for --parallel; use 1 to run in serial", defaultDescription: "(number of CPU cores - 1)", requiresArg: true, group: GROUPS.RULES, }, "list-interfaces": { conflicts: Array.from(ONE_AND_DONE_ARGS).filter( (arg) => arg !== "list-interfaces", ), description: "List built-in user interfaces & exit", }, "list-reporters": { conflicts: Array.from(ONE_AND_DONE_ARGS).filter( (arg) => arg !== "list-reporters", ), description: "List built-in reporters & exit", }, "no-colors": { description: "Force-disable color output", group: GROUPS.OUTPUT, hidden: true, }, "node-option": { description: 'Node or V8 option (no leading "--")', group: GROUPS.CONFIG, }, package: { description: "Path to package.json for config", group: GROUPS.CONFIG, normalize: true, requiresArg: true, }, parallel: { description: "Run tests in parallel", group: GROUPS.RULES, }, "posix-exit-codes": { description: "Use POSIX and UNIX shell exit codes as Mocha's return value", group: GROUPS.RULES, }, recursive: { description: "Look for tests in subdirectories", group: GROUPS.FILES, }, reporter: { default: defaults.reporter, description: "Specify reporter to use", group: GROUPS.OUTPUT, requiresArg: true, }, "reporter-option": { coerce: (opts) => list(opts).reduce((acc, opt) => { const pair = opt.split("="); if (pair.length > 2 || !pair.length) { throw createInvalidArgumentValueError( `invalid reporter option '${opt}'`, "--reporter-option", opt, 'expected "key=value" format', ); } acc[pair[0]] = pair.length === 2 ? pair[1] : true; return acc; }, {}), description: "Reporter-specific options (<k=v,[k1=v1,..]>)", group: GROUPS.OUTPUT, requiresArg: true, }, require: { defaultDescription: "(none)", description: "Require module", group: GROUPS.FILES, requiresArg: true, }, retries: { description: "Retry failed tests this many times", group: GROUPS.RULES, }, slow: { default: defaults.slow, description: 'Specify "slow" test threshold (in milliseconds)', group: GROUPS.RULES, }, sort: { description: "Sort test files", group: GROUPS.FILES, }, timeout: { default: defaults.timeout, description: "Specify test timeout threshold (in milliseconds)", group: GROUPS.RULES, }, ui: { default: defaults.ui, description: "Specify user interface", group: GROUPS.RULES, requiresArg: true, }, watch: { description: "Watch files in the current working directory for changes", group: GROUPS.FILES, }, "watch-files": { description: "List of paths or globs to watch", group: GROUPS.FILES, requiresArg: true, coerce: list, }, "watch-ignore": { description: "List of paths or globs to exclude from watching", group: GROUPS.FILES, requiresArg: true, coerce: list, default: defaults["watch-ignore"], }, }) .positional("spec", { default: ["test"], description: "One or more files, directories, or globs to test", type: "array", }) .check((argv) => { // "one-and-dones"; let yargs handle help and version Object.keys(ONE_AND_DONES).forEach((opt) => { if (argv[opt]) { ONE_AND_DONES[opt].call(null, yargs); process.exit(); } }); // yargs.implies() isn't flexible enough to handle this if (argv.invert && !("fgrep" in argv || "grep" in argv)) { throw createMissingArgumentError( '"--invert" requires one of "--fgrep <str>" or "--grep <regexp>"', "--fgrep|--grep", "string|regexp", ); } if (argv.parallel) { // yargs.conflicts() can't deal with `--file foo.js --no-parallel`, either if (argv.file) { throw createUnsupportedError( "--parallel runs test files in a non-deterministic order, and is mutually exclusive with --file", ); } // or this if (argv.sort) { throw createUnsupportedError( "--parallel runs test files in a non-deterministic order, and is mutually exclusive with --sort", ); } if (argv.reporter === "progress") { throw createUnsupportedError( "--reporter=progress is mutually exclusive with --parallel", ); } if (argv.reporter === "markdown") { throw createUnsupportedError( "--reporter=markdown is mutually exclusive with --parallel", ); } if (argv.reporter === "json-stream") { throw createUnsupportedError( "--reporter=json-stream is mutually exclusive with --parallel", ); } } if (argv.compilers) { throw createUnsupportedError( `--compilers is DEPRECATED and no longer supported. See https://github.com/mochajs/mocha/wiki/compilers-deprecation for migration information.`, ); } if (argv.opts) { throw createUnsupportedError( `--opts: configuring Mocha via 'mocha.opts' is DEPRECATED and no longer supported. Please use a configuration file instead.`, ); } return true; }) .middleware(async (argv, yargs) => { // currently a failing middleware does not work nicely with yargs' `fail()`. try { // load requires first, because it can impact "plugin" validation const plugins = await handleRequires(argv.require); validateLegacyPlugin(argv, "reporter", Mocha.reporters); validateLegacyPlugin(argv, "ui", Mocha.interfaces); Object.assign(argv, plugins); } catch (err) { // this could be a bad --require, bad reporter, ui, etc. console.error(`\n${symbols.error} ${pc.red("ERROR:")}`, err); yargs.exit(1); } }) .array(types.array) .boolean(types.boolean) .string(types.string) .number(types.number) .alias(aliases); exports.handler = async function (argv) { debug("post-yargs config", argv); const mocha = new Mocha(argv); try { await runMocha(mocha, argv); } catch (err) { console.error("\n Exception during run:", err); process.exit(1); } };