UNPKG

@japa/runner

Version:

A simple yet powerful testing framework for Node.js

251 lines (250 loc) 8.91 kB
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 };