@japa/runner
Version:
A simple yet powerful testing framework for Node.js
354 lines (350 loc) • 13.6 kB
JavaScript
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 };