UNPKG

@japa/runner

Version:

A simple yet powerful testing framework for Node.js

354 lines (350 loc) 13.6 kB
import { d as TestContext, o as Group, s as Refiner, t as colors, u as Test } from "./helpers-C1bD1eod.js"; import { i as spec, n as github, r as ndjson, t as dot } from "./main-CjKU6911.js"; import { pathToFileURL } from "node:url"; import { debuglog } from "node:util"; import { join } from "node:path"; import string from "@poppinss/string"; import { glob } from "node:fs/promises"; import Hooks from "@poppinss/hooks"; import getopts from "getopts"; import { isRunningInAIAgent } from "@poppinss/utils"; var debug_default = debuglog("japa:runner"); var Validator = class { ensureIsConfigured(config) { if (!config) throw new Error(`Cannot run tests. Make sure to call "configure" method before the "run" method`); } ensureIsInPlanningPhase(phase) { if (phase !== "planning") throw new Error(`Cannot import japa test file directly. It must be imported by calling the "japa.run" method`); } 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`); } 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(); } 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(); const FILE_SUFFIX_EXPRESSION = /(\.spec|\.test)?\.[js|ts|jsx|tsx|mjs|mts|cjs|cts]+$/; var FilesManager = class { async getFiles(cwd, files, excludes) { if (Array.isArray(files) || typeof files === "string") { const matchingFiles = glob(files, { withFileTypes: false, cwd, exclude: excludes }); return (await Array.fromAsync(matchingFiles)).sort((current, next) => { return current.localeCompare(next, void 0, { numeric: true, sensitivity: "base" }); }).map((file) => pathToFileURL(join(cwd, file))); } return await files(); } grep(files, filters) { return files.filter((file) => { const filename = string.toUnixSlash(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)); }); }); }); } }; var Planner = class { #config; #fileManager = new FilesManager(); constructor(config) { validator_default.validateActivatedReporters(config); validator_default.validateSuitesFilter(config); validator_default.validateSuitesForUniqueness(config); this.#config = config; } #getActivatedReporters() { return this.#config.reporters.activated.map((activated) => { return this.#config.reporters.list.find(({ name }) => activated === name); }); } 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; } 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; } #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; }, []); } async plan() { const suites = await this.#getSuites(); return { reporters: this.#getActivatedReporters(), suites, refinerFilters: this.#getRefinerFilters(), config: this.#config }; } }; var GlobalHooks = class { #hooks = new Hooks(); #setupRunner; #teardownRunner; apply(config) { config.setup.forEach((hook) => this.#hooks.add("setup", hook)); config.teardown.forEach((hook) => this.#hooks.add("teardown", hook)); } async setup(runner) { this.#setupRunner = this.#hooks.runner("setup"); this.#teardownRunner = this.#hooks.runner("teardown"); await this.#setupRunner.run(runner); } 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); } } }; const OPTIONS = { string: [ "tests", "groups", "tags", "files", "timeout", "retries", "reporters", "bailLayer" ], boolean: [ "help", "matchAll", "failed", "bail", "listPinned" ], alias: { forceExit: "force-exit", matchAll: "match-all", listPinned: "list-pinned", bailLayer: "bail-layer", help: "h" } }; const 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("--list-pinned")} ${colors.dim("List pinned tests")} ${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 { parse(argv) { return getopts(argv, OPTIONS); } getHelp() { return GET_HELP(); } }; const NOOP = () => {}; const DEFAULTS = { files: [], timeout: 2e3, retries: 0, forceExit: false, plugins: [], reporters: { activated: isRunningInAIAgent() ? ["dot"] : ["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; } #processAsArray(value, splitByComma) { return Array.isArray(value) ? value : splitByComma ? value.split(",").map((item) => item.trim()) : [value]; } #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; } #getCLITimeout() { if (this.#cliArgs.timeout) { const value = Number(this.#cliArgs.timeout); if (!Number.isNaN(value)) return value; } } #getCLIRetries() { if (this.#cliArgs.retries) { const value = Number(this.#cliArgs.retries); if (!Number.isNaN(value)) return value; } } #getCLIForceExit() { if (this.#cliArgs.forceExit) return true; } #getCLIReporters() { if (this.#cliArgs.reporters) return this.#processAsArray(this.#cliArgs.reporters, true); } 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 }; } }; const contextBuilder = (testInstance) => new TestContext(testInstance); function createTest(title, emitter, refiner, debuggingError, 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; testInstance.options.meta.abort = (message) => { debuggingError.message = message; throw debuggingError; }; 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 { GlobalHooks as a, debug_default as c, CliParser as i, createTestGroup as n, Planner as o, ConfigManager as r, validator_default as s, createTest as t };