@japa/runner
Version:
A simple yet powerful testing framework for Node.js
251 lines (250 loc) • 8.91 kB
JavaScript
import { a as GlobalHooks, c as debug_default, i as CliParser, n as createTestGroup, o as Planner, r as ConfigManager, s as validator_default, t as createTest } from "./create_test-C7_T6lrm.js";
import { a as Emitter, c as Runner, i as printPinnedTests, l as Suite, n as dateTimeDoubles, t as colors } from "./helpers-C1bD1eod.js";
import "./main-CjKU6911.js";
import { fileURLToPath } from "node:url";
import { ErrorsPrinter } from "@japa/errors-printer";
import { join } from "node:path";
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
import findCacheDirectory from "find-cache-directory";
const CACHE_DIR = findCacheDirectory({ name: "@japa/runner" });
const SUMMARY_FILE = CACHE_DIR ? join(CACHE_DIR, "summary.json") : void 0;
async function getFailedTests() {
try {
const summary = await readFile(SUMMARY_FILE, "utf-8");
return JSON.parse(summary);
} catch (error) {
if (error.code === "ENOENT") return {};
throw new Error("Unable to read failed tests cache file", { cause: error });
}
}
async function cacheFailedTests(tests) {
await mkdir(CACHE_DIR, { recursive: true });
await writeFile(SUMMARY_FILE, JSON.stringify({ tests }));
}
const retryPlugin = async function retry({ config, cliArgs: cliArgs$1 }) {
if (!SUMMARY_FILE) return;
config.teardown.push(async (runner) => {
await cacheFailedTests(runner.getSummary().failedTestsTitles);
});
if (cliArgs$1.failed) try {
const { tests } = await getFailedTests();
if (!tests || !tests.length) {
console.log(colors.bgYellow().black(" No failing tests found. Running all the tests "));
return;
}
config.filters.tests = tests;
} catch (error) {
console.log(colors.bgRed().black(" Unable to read failed tests. Running all the tests "));
console.log(colors.red(error));
}
};
var ExceptionsManager = class {
#exceptionsBuffer = [];
#rejectionsBuffer = [];
#state = "watching";
#errorsPrinter = new ErrorsPrinter({
stackLinesCount: 2,
framesMaxLimit: 4
});
hasErrors = false;
monitor() {
process.on("uncaughtException", async (error) => {
debug_default("received uncaught exception %O", error);
this.hasErrors = true;
if (this.#state === "watching") this.#exceptionsBuffer.push(error);
else {
this.#errorsPrinter.printSectionBorder("[Unhandled Error]");
await this.#errorsPrinter.printError(error);
process.exitCode = 1;
}
});
process.on("unhandledRejection", async (error) => {
debug_default("received unhandled rejection %O", error);
this.hasErrors = true;
if (this.#state === "watching") this.#rejectionsBuffer.push(error);
else {
this.#errorsPrinter.printSectionBorder("[Unhandled Rejection]");
await this.#errorsPrinter.printError(error);
process.exitCode = 1;
}
});
}
async report() {
if (this.#state === "reporting") return;
this.#state = "reporting";
if (this.#exceptionsBuffer.length) {
let exceptionsCount = this.#exceptionsBuffer.length;
let exceptionsIndex = this.#exceptionsBuffer.length;
this.#errorsPrinter.printSectionHeader("Unhandled Errors");
for (let exception of this.#exceptionsBuffer) {
await this.#errorsPrinter.printError(exception);
this.#errorsPrinter.printSectionBorder(`[${++exceptionsIndex}/${exceptionsCount}]`);
}
this.#exceptionsBuffer = [];
}
if (this.#rejectionsBuffer.length) {
let rejectionsCount = this.#exceptionsBuffer.length;
let rejectionsIndex = this.#exceptionsBuffer.length;
this.#errorsPrinter.printSectionBorder("Unhandled Rejections");
for (let rejection of this.#rejectionsBuffer) {
await this.#errorsPrinter.printError(rejection);
this.#errorsPrinter.printSectionBorder(`[${++rejectionsIndex}/${rejectionsCount}]`);
}
this.#rejectionsBuffer = [];
}
}
};
const emitter = new Emitter();
let activeTest;
let cliArgs = {};
let runnerConfig;
const executionPlanState = { phase: "idle" };
function test(title, callback) {
validator_default.ensureIsInPlanningPhase(executionPlanState.phase);
const debuggingError = /* @__PURE__ */ new Error();
const testInstance = createTest(title, emitter, runnerConfig.refiner, debuggingError, executionPlanState);
testInstance.setup((t) => {
activeTest = t;
return () => {
activeTest = void 0;
};
});
if (callback) testInstance.run(callback, debuggingError);
return testInstance;
}
test.group = function(title, callback) {
validator_default.ensureIsInPlanningPhase(executionPlanState.phase);
const group = createTestGroup(title, emitter, runnerConfig.refiner, executionPlanState);
executionPlanState.group = group;
if (cliArgs.bail && cliArgs.bailLayer === "group") executionPlanState.group.bail(true);
callback(executionPlanState.group);
executionPlanState.group = void 0;
return group;
};
test.macro = function(callback) {
return (...args) => {
if (!activeTest) throw new Error("Cannot invoke macro outside of the test callback");
return callback(activeTest, ...args);
};
};
function getActiveTest() {
return activeTest;
}
function getActiveTestOrFail() {
if (!activeTest) throw new Error("Cannot access active test outside of a test callback");
return activeTest;
}
function processCLIArgs(argv) {
cliArgs = new CliParser().parse(argv);
}
function configure(options) {
runnerConfig = new ConfigManager(options, cliArgs).hydrate();
}
async function run() {
if (cliArgs.help) {
console.log(new CliParser().getHelp());
return;
}
validator_default.ensureIsConfigured(runnerConfig);
executionPlanState.phase = "planning";
const runner = new Runner(emitter);
if (cliArgs.bail && cliArgs.bailLayer === "") runner.bail(true);
const globalHooks = new GlobalHooks();
const exceptionsManager = new ExceptionsManager();
try {
await retryPlugin({
config: runnerConfig,
runner,
emitter,
cliArgs
});
for (let plugin of runnerConfig.plugins) {
debug_default("executing \"%s\" plugin", plugin.name || "anonymous");
await plugin({
runner,
emitter,
cliArgs,
config: runnerConfig
});
}
const { config, reporters, suites, refinerFilters } = await new Planner(runnerConfig).plan();
reporters.forEach((reporter) => {
debug_default("registering \"%s\" reporter", reporter.name);
runner.registerReporter(reporter);
});
refinerFilters.forEach((filter) => {
debug_default("apply %s filters \"%O\" ", filter.layer, filter.filters);
config.refiner.add(filter.layer, filter.filters);
});
config.refiner.matchAllTags(cliArgs.matchAll ?? false);
runner.onSuite(config.configureSuite);
debug_default("executing global hooks");
globalHooks.apply(config);
if (!cliArgs.listPinned) await globalHooks.setup(runner);
for (let suite of suites) {
debug_default("initiating suite %s", suite.name);
executionPlanState.suite = new Suite(suite.name, emitter, config.refiner);
executionPlanState.retries = suite.retries;
executionPlanState.timeout = suite.timeout;
if (typeof suite.configure === "function") suite.configure(executionPlanState.suite);
if (cliArgs.bail && cliArgs.bailLayer === "suite") {
debug_default("enabling bail mode for the suite %s", suite.name);
executionPlanState.suite.bail(true);
}
runner.add(executionPlanState.suite);
for (let fileURL of suite.filesURLs) {
executionPlanState.file = fileURLToPath(fileURL);
debug_default("importing test file %s", executionPlanState.file);
await config.importer(fileURL);
}
executionPlanState.suite = void 0;
}
if (cliArgs.listPinned) {
printPinnedTests(runner);
if (config.forceExit) {
debug_default("force exiting process");
process.exit();
}
return;
}
executionPlanState.phase = "executing";
exceptionsManager.monitor();
await runner.start();
await runner.exec();
await globalHooks.teardown(null, runner);
await runner.end();
await exceptionsManager.report();
const summary = runner.getSummary();
if (summary.hasError || exceptionsManager.hasErrors) {
debug_default("updating exit code to 1. summary.hasError %s, process.hasError", summary.hasError, exceptionsManager.hasErrors);
process.exitCode = 1;
}
if (config.forceExit) {
debug_default("force exiting process");
process.exit();
}
} catch (error) {
debug_default("error running tests %O", error);
await globalHooks.teardown(error, runner);
await new ErrorsPrinter().printError(error);
await exceptionsManager.report();
process.exitCode = 1;
if (runnerConfig.forceExit) {
debug_default("force exiting process");
process.exit();
}
}
}
const timeTravel = test.macro(($test, durationOrTime) => {
$test.cleanup(() => {
dateTimeDoubles.reset();
});
dateTimeDoubles.travelTo(durationOrTime);
});
const freezeTime = test.macro(($test, date) => {
$test.cleanup(() => {
dateTimeDoubles.reset();
});
dateTimeDoubles.freeze(date);
});
export { configure, freezeTime, getActiveTest, getActiveTestOrFail, processCLIArgs, run, test, timeTravel };