UNPKG

@japa/runner

Version:

A simple yet powerful testing framework for Node.js

514 lines (501 loc) 15.9 kB
import { dot, github, ndjson, spec } from "./chunk-U3BSXCEH.js"; import { Group, Refiner, Test, TestContext, colors } from "./chunk-PCBL2VZP.js"; // src/debug.ts import { debuglog } from "util"; var debug_default = debuglog("japa:runner"); // src/validator.ts var Validator = class { /** * Ensures the japa is configured. Otherwise raises an exception */ ensureIsConfigured(config) { if (!config) { throw new Error( `Cannot run tests. Make sure to call "configure" method before the "run" method` ); } } /** * Ensures the japa is in planning phase */ ensureIsInPlanningPhase(phase) { if (phase !== "planning") { throw new Error( `Cannot import japa test file directly. It must be imported by calling the "japa.run" method` ); } } /** * Ensures the suites filter uses a subset of the user configured suites. */ validateSuitesFilter(config) { if (!config.filters.suites || !config.filters.suites.length) { return; } if (!("suites" in config) || !config.suites.length) { throw new Error(`Cannot apply suites filter. You have not configured any test suites`); } const suites = config.suites.map(({ name }) => name); const unknownSuites = config.filters.suites.filter((suite) => !suites.includes(suite)); if (unknownSuites.length) { throw new Error(`Cannot apply suites filter. "${unknownSuites[0]}" suite is not configured`); } } /** * Ensure there are unique suites */ validateSuitesForUniqueness(config) { if (!("suites" in config)) { return; } const suites = /* @__PURE__ */ new Set(); config.suites.forEach(({ name }) => { if (suites.has(name)) { throw new Error(`Duplicate suite "${name}"`); } suites.add(name); }); suites.clear(); } /** * Ensure the activated reporters are in the list of defined * reporters */ validateActivatedReporters(config) { const reportersList = config.reporters.list.map(({ name }) => name); const unknownReporters = config.reporters.activated.filter( (name) => !reportersList.includes(name) ); if (unknownReporters.length) { throw new Error( `Invalid reporter "${unknownReporters[0]}". Make sure to register it first inside the "reporters.list" array` ); } } }; var validator_default = new Validator(); // src/files_manager.ts import slash from "slash"; import fastGlob from "fast-glob"; import { pathToFileURL } from "url"; var FILE_SUFFIX_EXPRESSION = /(\.spec|\.test)?\.[js|ts|jsx|tsx|mjs|mts|cjs|cts]+$/; var FilesManager = class { /** * Returns a collection of files from the user defined * glob or the implementation function */ async getFiles(cwd, files, excludes) { if (Array.isArray(files) || typeof files === "string") { const testFiles = await fastGlob(files, { absolute: true, onlyFiles: true, cwd, ignore: excludes }); return testFiles.map((file) => pathToFileURL(file)); } return await files(); } /** * Applies file name filter on a collection of file * URLs */ grep(files, filters) { return files.filter((file) => { const filename = slash(file.pathname); const filenameWithoutTestSuffix = filename.replace(FILE_SUFFIX_EXPRESSION, ""); return !!filters.find((filter) => { if (filename.endsWith(filter)) { return true; } const filterSegments = filter.split("/").reverse(); const fileSegments = filenameWithoutTestSuffix.split("/").reverse(); return filterSegments.every((segment, index) => { return fileSegments[index] && (segment === "*" || fileSegments[index].endsWith(segment)); }); }); }); } }; // src/planner.ts var Planner = class { #config; #fileManager = new FilesManager(); constructor(config) { validator_default.validateActivatedReporters(config); validator_default.validateSuitesFilter(config); validator_default.validateSuitesForUniqueness(config); this.#config = config; } /** * Returns a list of reporters based upon the activated * reporters list. */ #getActivatedReporters() { return this.#config.reporters.activated.map((activated) => { return this.#config.reporters.list.find(({ name }) => activated === name); }); } /** * A generic method to collect files from the user defined * files glob and apply the files filter */ async #collectFiles(files) { let filesURLs = await this.#fileManager.getFiles(this.#config.cwd, files, this.#config.exclude); if (this.#config.filters.files && this.#config.filters.files.length) { filesURLs = this.#fileManager.grep(filesURLs, this.#config.filters.files); } return filesURLs; } /** * Returns a collection of suites and their associated * test files by applying all the filters */ async #getSuites() { let suites = []; let suitesFilters = this.#config.filters.suites || []; if ("files" in this.#config) { suites.push({ name: "default", files: this.#config.files, timeout: this.#config.timeout, retries: this.#config.retries, filesURLs: await this.#collectFiles(this.#config.files) }); } if ("suites" in this.#config) { for (let suite of this.#config.suites) { if (!suitesFilters.length || suitesFilters.includes(suite.name)) { suites.push({ ...suite, filesURLs: await this.#collectFiles(suite.files) }); } } } return suites; } /** * Returns a list of filters to the passed to the refiner */ #getRefinerFilters() { return Object.keys(this.#config.filters).reduce( (result, layer) => { if (layer === "tests" || layer === "tags" || layer === "groups") { result.push({ layer, filters: this.#config.filters[layer] }); } return result; }, [] ); } /** * Creates a plan for running the tests */ async plan() { const suites = await this.#getSuites(); const reporters = this.#getActivatedReporters(); const refinerFilters = this.#getRefinerFilters(); return { reporters, suites, refinerFilters, config: this.#config }; } }; // src/hooks.ts import Hooks from "@poppinss/hooks"; var GlobalHooks = class { #hooks = new Hooks(); #setupRunner; #teardownRunner; /** * Apply hooks from the config */ apply(config) { config.setup.forEach((hook) => this.#hooks.add("setup", hook)); config.teardown.forEach((hook) => this.#hooks.add("teardown", hook)); } /** * Perform setup */ async setup(runner) { this.#setupRunner = this.#hooks.runner("setup"); this.#teardownRunner = this.#hooks.runner("teardown"); await this.#setupRunner.run(runner); } /** * Perform cleanup */ async teardown(error, runner) { if (this.#setupRunner) { await this.#setupRunner.cleanup(error, runner); } if (this.#teardownRunner) { if (!error) { await this.#teardownRunner.run(runner); } await this.#teardownRunner.cleanup(error, runner); } } }; // src/cli_parser.ts import getopts from "getopts"; var OPTIONS = { string: ["tests", "groups", "tags", "files", "timeout", "retries", "reporters", "bailLayer"], boolean: ["help", "matchAll", "failed", "bail"], alias: { forceExit: "force-exit", matchAll: "match-all", bailLayer: "bail-layer", help: "h" } }; var GET_HELP = () => ` ${colors.yellow("@japa/runner v2.3.0")} ${colors.green("--tests")} ${colors.dim("Filter tests by the test title")} ${colors.green("--groups")} ${colors.dim("Filter tests by the group title")} ${colors.green("--tags")} ${colors.dim("Filter tests by tags")} ${colors.green("--match-all")} ${colors.dim("Run tests that matches all the supplied tags")} ${colors.green("--files")} ${colors.dim("Filter tests by the file name")} ${colors.green("--force-exit")} ${colors.dim("Forcefully exit the process")} ${colors.green("--timeout")} ${colors.dim("Define default timeout for all tests")} ${colors.green("--retries")} ${colors.dim("Define default retries for all tests")} ${colors.green("--reporters")} ${colors.dim("Activate one or more test reporters")} ${colors.green("--failed")} ${colors.dim("Run tests failed during the last run")} ${colors.green("--bail")} ${colors.dim("Exit early when a test fails")} ${colors.green("--bail-layer")} ${colors.dim('Specify at which layer to enable the bail mode. Can be "group" or "suite"')} ${colors.green("-h, --help")} ${colors.dim("View help")} ${colors.yellow("Examples:")} ${colors.dim('node bin/test.js --tags="@github"')} ${colors.dim('node bin/test.js --tags="~@github"')} ${colors.dim('node bin/test.js --tags="@github,@slow,@integration" --match-all')} ${colors.dim("node bin/test.js --force-exit")} ${colors.dim('node bin/test.js --files="user"')} ${colors.dim('node bin/test.js --files="functional/user"')} ${colors.dim('node bin/test.js --files="unit/user"')} ${colors.dim("node bin/test.js --failed")} ${colors.dim("node bin/test.js --bail")} ${colors.dim("node bin/test.js --bail=group")} ${colors.yellow("Notes:")} - When groups and tests filters are applied together. We will first filter the tests by group title and then apply the tests filter. - The timeout defined on test object takes precedence over the ${colors.green("--timeout")} flag. - The retries defined on test object takes precedence over the ${colors.green("--retries")} flag. - The ${colors.green("--files")} flag checks for the file names ending with the filter substring. - The ${colors.green("--tags")} filter runs tests that has one or more of the supplied tags. - You can use the ${colors.green("--match-all")} flag to run tests that has all the supplied tags. `; var CliParser = class { /** * Parses command-line arguments */ parse(argv) { return getopts(argv, OPTIONS); } /** * Returns the help string */ getHelp() { return GET_HELP(); } }; // src/config_manager.ts var NOOP = () => { }; var DEFAULTS = { files: [], timeout: 2e3, retries: 0, forceExit: false, plugins: [], reporters: { activated: ["spec"].concat(process.env.GITHUB_ACTIONS === "true" ? ["github"] : []), list: [spec(), ndjson(), dot(), github()] }, importer: (filePath) => import(filePath.href), configureSuite: () => { } }; var ConfigManager = class { #config; #cliArgs; constructor(config, cliArgs) { this.#config = config; this.#cliArgs = cliArgs; } /** * Processes a CLI argument and converts it to an * array of strings */ #processAsArray(value, splitByComma) { return Array.isArray(value) ? value : splitByComma ? value.split(",").map((item) => item.trim()) : [value]; } /** * Returns a copy of filters based upon the CLI * arguments. */ #getCLIFilters() { const filters = {}; if (this.#cliArgs.tags) { filters.tags = this.#processAsArray(this.#cliArgs.tags, true); } if (this.#cliArgs.tests) { filters.tests = this.#processAsArray(this.#cliArgs.tests, false); } if (this.#cliArgs.files) { filters.files = this.#processAsArray(this.#cliArgs.files, true); } if (this.#cliArgs.groups) { filters.groups = this.#processAsArray(this.#cliArgs.groups, false); } if (this.#cliArgs._ && this.#cliArgs._.length) { filters.suites = this.#processAsArray(this.#cliArgs._, true); } return filters; } /** * Returns the timeout from the CLI args */ #getCLITimeout() { if (this.#cliArgs.timeout) { const value = Number(this.#cliArgs.timeout); if (!Number.isNaN(value)) { return value; } } } /** * Returns the retries from the CLI args */ #getCLIRetries() { if (this.#cliArgs.retries) { const value = Number(this.#cliArgs.retries); if (!Number.isNaN(value)) { return value; } } } /** * Returns the forceExit property from the CLI args */ #getCLIForceExit() { if (this.#cliArgs.forceExit) { return true; } } /** * Returns reporters selected using the commandline * --reporter flag */ #getCLIReporters() { if (this.#cliArgs.reporters) { return this.#processAsArray(this.#cliArgs.reporters, true); } } /** * Hydrates the config with user defined options and the * command-line flags. */ hydrate() { const cliFilters = this.#getCLIFilters(); const cliRetries = this.#getCLIRetries(); const cliTimeout = this.#getCLITimeout(); const cliReporters = this.#getCLIReporters(); const cliForceExit = this.#getCLIForceExit(); debug_default("filters applied using CLI flags %O", cliFilters); const baseConfig = { cwd: this.#config.cwd ?? process.cwd(), exclude: this.#config.exclude || ["node_modules/**", ".git/**", "coverage/**"], filters: Object.assign({}, this.#config.filters ?? {}, cliFilters), importer: this.#config.importer ?? DEFAULTS.importer, refiner: this.#config.refiner ?? new Refiner(), retries: cliRetries ?? this.#config.retries ?? DEFAULTS.retries, timeout: cliTimeout ?? this.#config.timeout ?? DEFAULTS.timeout, plugins: this.#config.plugins ?? DEFAULTS.plugins, forceExit: cliForceExit ?? this.#config.forceExit ?? DEFAULTS.forceExit, reporters: this.#config.reporters ? { activated: this.#config.reporters.activated, list: this.#config.reporters.list || DEFAULTS.reporters.list } : DEFAULTS.reporters, configureSuite: this.#config.configureSuite ?? DEFAULTS.configureSuite, setup: this.#config.setup || [], teardown: this.#config.teardown || [] }; if (cliReporters) { baseConfig.reporters.activated = cliReporters; } if ("files" in this.#config) { return { files: this.#config.files, ...baseConfig }; } return { suites: this.#config.suites.map((suite) => { return { name: suite.name, files: suite.files, timeout: cliTimeout ?? suite.timeout ?? baseConfig.timeout, retries: cliRetries ?? suite.retries ?? baseConfig.retries, configure: suite.configure || NOOP }; }), ...baseConfig }; } }; // src/create_test.ts var contextBuilder = (testInstance) => new TestContext(testInstance); function createTest(title, emitter, refiner, options) { const testInstance = new Test(title, contextBuilder, emitter, refiner, options.group); testInstance.options.meta.suite = options.suite; testInstance.options.meta.group = options.group; testInstance.options.meta.fileName = options.file; if (options.timeout !== void 0) { testInstance.timeout(options.timeout); } if (options.retries !== void 0) { testInstance.retry(options.retries); } if (options.group) { options.group.add(testInstance); } else if (options.suite) { options.suite.add(testInstance); } return testInstance; } function createTestGroup(title, emitter, refiner, options) { if (options.group) { throw new Error("Nested groups are not supported by Japa"); } const group = new Group(title, emitter, refiner); group.options.meta.suite = options.suite; group.options.meta.fileName = options.file; if (options.suite) { options.suite.add(group); } return group; } export { debug_default, validator_default, Planner, GlobalHooks, CliParser, ConfigManager, createTest, createTestGroup };