mirror of
https://github.com/soconnor0919/beenpad.git
synced 2026-02-05 00:06:40 -05:00
2112 lines
78 KiB
JavaScript
2112 lines
78 KiB
JavaScript
import { processError } from '@vitest/utils/error';
|
|
import { isObject, createDefer, assertTypes, toArray, isNegativeNaN, objectAttr, shuffle } from '@vitest/utils/helpers';
|
|
import { getSafeTimers } from '@vitest/utils/timers';
|
|
import { format, formatRegExp, objDisplay } from '@vitest/utils/display';
|
|
import { c as createChainable, e as createTaskName, f as findTestFileStackTrace, b as createFileTask, a as calculateSuiteHash, s as someTasksAreOnly, i as interpretTaskModes, l as limitConcurrency, p as partitionSuiteChildren, r as hasTests, q as hasFailed } from './chunk-tasks.js';
|
|
import '@vitest/utils/source-map';
|
|
import 'pathe';
|
|
|
|
class PendingError extends Error {
|
|
code = "VITEST_PENDING";
|
|
taskId;
|
|
constructor(message, task, note) {
|
|
super(message);
|
|
this.message = message;
|
|
this.note = note;
|
|
this.taskId = task.id;
|
|
}
|
|
}
|
|
class TestRunAbortError extends Error {
|
|
name = "TestRunAbortError";
|
|
reason;
|
|
constructor(message, reason) {
|
|
super(message);
|
|
this.reason = reason;
|
|
}
|
|
}
|
|
|
|
// use WeakMap here to make the Test and Suite object serializable
|
|
const fnMap = new WeakMap();
|
|
const testFixtureMap = new WeakMap();
|
|
const hooksMap = new WeakMap();
|
|
function setFn(key, fn) {
|
|
fnMap.set(key, fn);
|
|
}
|
|
function getFn(key) {
|
|
return fnMap.get(key);
|
|
}
|
|
function setTestFixture(key, fixture) {
|
|
testFixtureMap.set(key, fixture);
|
|
}
|
|
function getTestFixture(key) {
|
|
return testFixtureMap.get(key);
|
|
}
|
|
function setHooks(key, hooks) {
|
|
hooksMap.set(key, hooks);
|
|
}
|
|
function getHooks(key) {
|
|
return hooksMap.get(key);
|
|
}
|
|
|
|
function mergeScopedFixtures(testFixtures, scopedFixtures) {
|
|
const scopedFixturesMap = scopedFixtures.reduce((map, fixture) => {
|
|
map[fixture.prop] = fixture;
|
|
return map;
|
|
}, {});
|
|
const newFixtures = {};
|
|
testFixtures.forEach((fixture) => {
|
|
const useFixture = scopedFixturesMap[fixture.prop] || { ...fixture };
|
|
newFixtures[useFixture.prop] = useFixture;
|
|
});
|
|
for (const fixtureKep in newFixtures) {
|
|
var _fixture$deps;
|
|
const fixture = newFixtures[fixtureKep];
|
|
// if the fixture was define before the scope, then its dep
|
|
// will reference the original fixture instead of the scope
|
|
fixture.deps = (_fixture$deps = fixture.deps) === null || _fixture$deps === void 0 ? void 0 : _fixture$deps.map((dep) => newFixtures[dep.prop]);
|
|
}
|
|
return Object.values(newFixtures);
|
|
}
|
|
function mergeContextFixtures(fixtures, context, runner) {
|
|
const fixtureOptionKeys = [
|
|
"auto",
|
|
"injected",
|
|
"scope"
|
|
];
|
|
const fixtureArray = Object.entries(fixtures).map(([prop, value]) => {
|
|
const fixtureItem = { value };
|
|
if (Array.isArray(value) && value.length >= 2 && isObject(value[1]) && Object.keys(value[1]).some((key) => fixtureOptionKeys.includes(key))) {
|
|
var _runner$injectValue;
|
|
// fixture with options
|
|
Object.assign(fixtureItem, value[1]);
|
|
const userValue = value[0];
|
|
fixtureItem.value = fixtureItem.injected ? ((_runner$injectValue = runner.injectValue) === null || _runner$injectValue === void 0 ? void 0 : _runner$injectValue.call(runner, prop)) ?? userValue : userValue;
|
|
}
|
|
fixtureItem.scope = fixtureItem.scope || "test";
|
|
if (fixtureItem.scope === "worker" && !runner.getWorkerContext) {
|
|
fixtureItem.scope = "file";
|
|
}
|
|
fixtureItem.prop = prop;
|
|
fixtureItem.isFn = typeof fixtureItem.value === "function";
|
|
return fixtureItem;
|
|
});
|
|
if (Array.isArray(context.fixtures)) {
|
|
context.fixtures = context.fixtures.concat(fixtureArray);
|
|
} else {
|
|
context.fixtures = fixtureArray;
|
|
}
|
|
// Update dependencies of fixture functions
|
|
fixtureArray.forEach((fixture) => {
|
|
if (fixture.isFn) {
|
|
const usedProps = getUsedProps(fixture.value);
|
|
if (usedProps.length) {
|
|
fixture.deps = context.fixtures.filter(({ prop }) => prop !== fixture.prop && usedProps.includes(prop));
|
|
}
|
|
// test can access anything, so we ignore it
|
|
if (fixture.scope !== "test") {
|
|
var _fixture$deps2;
|
|
(_fixture$deps2 = fixture.deps) === null || _fixture$deps2 === void 0 ? void 0 : _fixture$deps2.forEach((dep) => {
|
|
if (!dep.isFn) {
|
|
// non fn fixtures are always resolved and available to anyone
|
|
return;
|
|
}
|
|
// worker scope can only import from worker scope
|
|
if (fixture.scope === "worker" && dep.scope === "worker") {
|
|
return;
|
|
}
|
|
// file scope an import from file and worker scopes
|
|
if (fixture.scope === "file" && dep.scope !== "test") {
|
|
return;
|
|
}
|
|
throw new SyntaxError(`cannot use the ${dep.scope} fixture "${dep.prop}" inside the ${fixture.scope} fixture "${fixture.prop}"`);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
return context;
|
|
}
|
|
const fixtureValueMaps = new Map();
|
|
const cleanupFnArrayMap = new Map();
|
|
async function callFixtureCleanup(context) {
|
|
const cleanupFnArray = cleanupFnArrayMap.get(context) ?? [];
|
|
for (const cleanup of cleanupFnArray.reverse()) {
|
|
await cleanup();
|
|
}
|
|
cleanupFnArrayMap.delete(context);
|
|
}
|
|
function withFixtures(runner, fn, testContext) {
|
|
return (hookContext) => {
|
|
const context = hookContext || testContext;
|
|
if (!context) {
|
|
return fn({});
|
|
}
|
|
const fixtures = getTestFixture(context);
|
|
if (!(fixtures === null || fixtures === void 0 ? void 0 : fixtures.length)) {
|
|
return fn(context);
|
|
}
|
|
const usedProps = getUsedProps(fn);
|
|
const hasAutoFixture = fixtures.some(({ auto }) => auto);
|
|
if (!usedProps.length && !hasAutoFixture) {
|
|
return fn(context);
|
|
}
|
|
if (!fixtureValueMaps.get(context)) {
|
|
fixtureValueMaps.set(context, new Map());
|
|
}
|
|
const fixtureValueMap = fixtureValueMaps.get(context);
|
|
if (!cleanupFnArrayMap.has(context)) {
|
|
cleanupFnArrayMap.set(context, []);
|
|
}
|
|
const cleanupFnArray = cleanupFnArrayMap.get(context);
|
|
const usedFixtures = fixtures.filter(({ prop, auto }) => auto || usedProps.includes(prop));
|
|
const pendingFixtures = resolveDeps(usedFixtures);
|
|
if (!pendingFixtures.length) {
|
|
return fn(context);
|
|
}
|
|
async function resolveFixtures() {
|
|
for (const fixture of pendingFixtures) {
|
|
// fixture could be already initialized during "before" hook
|
|
if (fixtureValueMap.has(fixture)) {
|
|
continue;
|
|
}
|
|
const resolvedValue = await resolveFixtureValue(runner, fixture, context, cleanupFnArray);
|
|
context[fixture.prop] = resolvedValue;
|
|
fixtureValueMap.set(fixture, resolvedValue);
|
|
if (fixture.scope === "test") {
|
|
cleanupFnArray.unshift(() => {
|
|
fixtureValueMap.delete(fixture);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return resolveFixtures().then(() => fn(context));
|
|
};
|
|
}
|
|
const globalFixturePromise = new WeakMap();
|
|
function resolveFixtureValue(runner, fixture, context, cleanupFnArray) {
|
|
var _runner$getWorkerCont;
|
|
const fileContext = getFileContext(context.task.file);
|
|
const workerContext = (_runner$getWorkerCont = runner.getWorkerContext) === null || _runner$getWorkerCont === void 0 ? void 0 : _runner$getWorkerCont.call(runner);
|
|
if (!fixture.isFn) {
|
|
var _fixture$prop;
|
|
fileContext[_fixture$prop = fixture.prop] ?? (fileContext[_fixture$prop] = fixture.value);
|
|
if (workerContext) {
|
|
var _fixture$prop2;
|
|
workerContext[_fixture$prop2 = fixture.prop] ?? (workerContext[_fixture$prop2] = fixture.value);
|
|
}
|
|
return fixture.value;
|
|
}
|
|
if (fixture.scope === "test") {
|
|
return resolveFixtureFunction(fixture.value, context, cleanupFnArray);
|
|
}
|
|
// in case the test runs in parallel
|
|
if (globalFixturePromise.has(fixture)) {
|
|
return globalFixturePromise.get(fixture);
|
|
}
|
|
let fixtureContext;
|
|
if (fixture.scope === "worker") {
|
|
if (!workerContext) {
|
|
throw new TypeError("[@vitest/runner] The worker context is not available in the current test runner. Please, provide the `getWorkerContext` method when initiating the runner.");
|
|
}
|
|
fixtureContext = workerContext;
|
|
} else {
|
|
fixtureContext = fileContext;
|
|
}
|
|
if (fixture.prop in fixtureContext) {
|
|
return fixtureContext[fixture.prop];
|
|
}
|
|
if (!cleanupFnArrayMap.has(fixtureContext)) {
|
|
cleanupFnArrayMap.set(fixtureContext, []);
|
|
}
|
|
const cleanupFnFileArray = cleanupFnArrayMap.get(fixtureContext);
|
|
const promise = resolveFixtureFunction(fixture.value, fixtureContext, cleanupFnFileArray).then((value) => {
|
|
fixtureContext[fixture.prop] = value;
|
|
globalFixturePromise.delete(fixture);
|
|
return value;
|
|
});
|
|
globalFixturePromise.set(fixture, promise);
|
|
return promise;
|
|
}
|
|
async function resolveFixtureFunction(fixtureFn, context, cleanupFnArray) {
|
|
// wait for `use` call to extract fixture value
|
|
const useFnArgPromise = createDefer();
|
|
let isUseFnArgResolved = false;
|
|
const fixtureReturn = fixtureFn(context, async (useFnArg) => {
|
|
// extract `use` argument
|
|
isUseFnArgResolved = true;
|
|
useFnArgPromise.resolve(useFnArg);
|
|
// suspend fixture teardown by holding off `useReturnPromise` resolution until cleanup
|
|
const useReturnPromise = createDefer();
|
|
cleanupFnArray.push(async () => {
|
|
// start teardown by resolving `use` Promise
|
|
useReturnPromise.resolve();
|
|
// wait for finishing teardown
|
|
await fixtureReturn;
|
|
});
|
|
await useReturnPromise;
|
|
}).catch((e) => {
|
|
// treat fixture setup error as test failure
|
|
if (!isUseFnArgResolved) {
|
|
useFnArgPromise.reject(e);
|
|
return;
|
|
}
|
|
// otherwise re-throw to avoid silencing error during cleanup
|
|
throw e;
|
|
});
|
|
return useFnArgPromise;
|
|
}
|
|
function resolveDeps(fixtures, depSet = new Set(), pendingFixtures = []) {
|
|
fixtures.forEach((fixture) => {
|
|
if (pendingFixtures.includes(fixture)) {
|
|
return;
|
|
}
|
|
if (!fixture.isFn || !fixture.deps) {
|
|
pendingFixtures.push(fixture);
|
|
return;
|
|
}
|
|
if (depSet.has(fixture)) {
|
|
throw new Error(`Circular fixture dependency detected: ${fixture.prop} <- ${[...depSet].reverse().map((d) => d.prop).join(" <- ")}`);
|
|
}
|
|
depSet.add(fixture);
|
|
resolveDeps(fixture.deps, depSet, pendingFixtures);
|
|
pendingFixtures.push(fixture);
|
|
depSet.clear();
|
|
});
|
|
return pendingFixtures;
|
|
}
|
|
function getUsedProps(fn) {
|
|
let fnString = filterOutComments(fn.toString());
|
|
// match lowered async function and strip it off
|
|
// example code on esbuild-try https://esbuild.github.io/try/#YgAwLjI0LjAALS1zdXBwb3J0ZWQ6YXN5bmMtYXdhaXQ9ZmFsc2UAZQBlbnRyeS50cwBjb25zdCBvID0gewogIGYxOiBhc3luYyAoKSA9PiB7fSwKICBmMjogYXN5bmMgKGEpID0+IHt9LAogIGYzOiBhc3luYyAoYSwgYikgPT4ge30sCiAgZjQ6IGFzeW5jIGZ1bmN0aW9uKGEpIHt9LAogIGY1OiBhc3luYyBmdW5jdGlvbiBmZihhKSB7fSwKICBhc3luYyBmNihhKSB7fSwKCiAgZzE6IGFzeW5jICgpID0+IHt9LAogIGcyOiBhc3luYyAoeyBhIH0pID0+IHt9LAogIGczOiBhc3luYyAoeyBhIH0sIGIpID0+IHt9LAogIGc0OiBhc3luYyBmdW5jdGlvbiAoeyBhIH0pIHt9LAogIGc1OiBhc3luYyBmdW5jdGlvbiBnZyh7IGEgfSkge30sCiAgYXN5bmMgZzYoeyBhIH0pIHt9LAoKICBoMTogYXN5bmMgKCkgPT4ge30sCiAgLy8gY29tbWVudCBiZXR3ZWVuCiAgaDI6IGFzeW5jIChhKSA9PiB7fSwKfQ
|
|
// __async(this, null, function*
|
|
// __async(this, arguments, function*
|
|
// __async(this, [_0, _1], function*
|
|
if (/__async\((?:this|null), (?:null|arguments|\[[_0-9, ]*\]), function\*/.test(fnString)) {
|
|
fnString = fnString.split(/__async\((?:this|null),/)[1];
|
|
}
|
|
const match = fnString.match(/[^(]*\(([^)]*)/);
|
|
if (!match) {
|
|
return [];
|
|
}
|
|
const args = splitByComma(match[1]);
|
|
if (!args.length) {
|
|
return [];
|
|
}
|
|
let first = args[0];
|
|
if ("__VITEST_FIXTURE_INDEX__" in fn) {
|
|
first = args[fn.__VITEST_FIXTURE_INDEX__];
|
|
if (!first) {
|
|
return [];
|
|
}
|
|
}
|
|
if (!(first[0] === "{" && first.endsWith("}"))) {
|
|
throw new Error(`The first argument inside a fixture must use object destructuring pattern, e.g. ({ test } => {}). Instead, received "${first}".`);
|
|
}
|
|
const _first = first.slice(1, -1).replace(/\s/g, "");
|
|
const props = splitByComma(_first).map((prop) => {
|
|
return prop.replace(/:.*|=.*/g, "");
|
|
});
|
|
const last = props.at(-1);
|
|
if (last && last.startsWith("...")) {
|
|
throw new Error(`Rest parameters are not supported in fixtures, received "${last}".`);
|
|
}
|
|
return props;
|
|
}
|
|
function filterOutComments(s) {
|
|
const result = [];
|
|
let commentState = "none";
|
|
for (let i = 0; i < s.length; ++i) {
|
|
if (commentState === "singleline") {
|
|
if (s[i] === "\n") {
|
|
commentState = "none";
|
|
}
|
|
} else if (commentState === "multiline") {
|
|
if (s[i - 1] === "*" && s[i] === "/") {
|
|
commentState = "none";
|
|
}
|
|
} else if (commentState === "none") {
|
|
if (s[i] === "/" && s[i + 1] === "/") {
|
|
commentState = "singleline";
|
|
} else if (s[i] === "/" && s[i + 1] === "*") {
|
|
commentState = "multiline";
|
|
i += 2;
|
|
} else {
|
|
result.push(s[i]);
|
|
}
|
|
}
|
|
}
|
|
return result.join("");
|
|
}
|
|
function splitByComma(s) {
|
|
const result = [];
|
|
const stack = [];
|
|
let start = 0;
|
|
for (let i = 0; i < s.length; i++) {
|
|
if (s[i] === "{" || s[i] === "[") {
|
|
stack.push(s[i] === "{" ? "}" : "]");
|
|
} else if (s[i] === stack.at(-1)) {
|
|
stack.pop();
|
|
} else if (!stack.length && s[i] === ",") {
|
|
const token = s.substring(start, i).trim();
|
|
if (token) {
|
|
result.push(token);
|
|
}
|
|
start = i + 1;
|
|
}
|
|
}
|
|
const lastToken = s.substring(start).trim();
|
|
if (lastToken) {
|
|
result.push(lastToken);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
let _test;
|
|
function setCurrentTest(test) {
|
|
_test = test;
|
|
}
|
|
function getCurrentTest() {
|
|
return _test;
|
|
}
|
|
const tests = [];
|
|
function addRunningTest(test) {
|
|
tests.push(test);
|
|
return () => {
|
|
tests.splice(tests.indexOf(test));
|
|
};
|
|
}
|
|
function getRunningTests() {
|
|
return tests;
|
|
}
|
|
|
|
function getDefaultHookTimeout() {
|
|
return getRunner().config.hookTimeout;
|
|
}
|
|
const CLEANUP_TIMEOUT_KEY = Symbol.for("VITEST_CLEANUP_TIMEOUT");
|
|
const CLEANUP_STACK_TRACE_KEY = Symbol.for("VITEST_CLEANUP_STACK_TRACE");
|
|
function getBeforeHookCleanupCallback(hook, result, context) {
|
|
if (typeof result === "function") {
|
|
const timeout = CLEANUP_TIMEOUT_KEY in hook && typeof hook[CLEANUP_TIMEOUT_KEY] === "number" ? hook[CLEANUP_TIMEOUT_KEY] : getDefaultHookTimeout();
|
|
const stackTraceError = CLEANUP_STACK_TRACE_KEY in hook && hook[CLEANUP_STACK_TRACE_KEY] instanceof Error ? hook[CLEANUP_STACK_TRACE_KEY] : undefined;
|
|
return withTimeout(result, timeout, true, stackTraceError, (_, error) => {
|
|
if (context) {
|
|
abortContextSignal(context, error);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Registers a callback function to be executed once before all tests within the current suite.
|
|
* This hook is useful for scenarios where you need to perform setup operations that are common to all tests in a suite, such as initializing a database connection or setting up a test environment.
|
|
*
|
|
* **Note:** The `beforeAll` hooks are executed in the order they are defined one after another. You can configure this by changing the `sequence.hooks` option in the config file.
|
|
*
|
|
* @param {Function} fn - The callback function to be executed before all tests.
|
|
* @param {number} [timeout] - Optional timeout in milliseconds for the hook. If not provided, the default hook timeout from the runner's configuration is used.
|
|
* @returns {void}
|
|
* @example
|
|
* ```ts
|
|
* // Example of using beforeAll to set up a database connection
|
|
* beforeAll(async () => {
|
|
* await database.connect();
|
|
* });
|
|
* ```
|
|
*/
|
|
function beforeAll(fn, timeout = getDefaultHookTimeout()) {
|
|
assertTypes(fn, "\"beforeAll\" callback", ["function"]);
|
|
const stackTraceError = new Error("STACK_TRACE_ERROR");
|
|
return getCurrentSuite().on("beforeAll", Object.assign(withTimeout(fn, timeout, true, stackTraceError), {
|
|
[CLEANUP_TIMEOUT_KEY]: timeout,
|
|
[CLEANUP_STACK_TRACE_KEY]: stackTraceError
|
|
}));
|
|
}
|
|
/**
|
|
* Registers a callback function to be executed once after all tests within the current suite have completed.
|
|
* This hook is useful for scenarios where you need to perform cleanup operations after all tests in a suite have run, such as closing database connections or cleaning up temporary files.
|
|
*
|
|
* **Note:** The `afterAll` hooks are running in reverse order of their registration. You can configure this by changing the `sequence.hooks` option in the config file.
|
|
*
|
|
* @param {Function} fn - The callback function to be executed after all tests.
|
|
* @param {number} [timeout] - Optional timeout in milliseconds for the hook. If not provided, the default hook timeout from the runner's configuration is used.
|
|
* @returns {void}
|
|
* @example
|
|
* ```ts
|
|
* // Example of using afterAll to close a database connection
|
|
* afterAll(async () => {
|
|
* await database.disconnect();
|
|
* });
|
|
* ```
|
|
*/
|
|
function afterAll(fn, timeout) {
|
|
assertTypes(fn, "\"afterAll\" callback", ["function"]);
|
|
return getCurrentSuite().on("afterAll", withTimeout(fn, timeout ?? getDefaultHookTimeout(), true, new Error("STACK_TRACE_ERROR")));
|
|
}
|
|
/**
|
|
* Registers a callback function to be executed before each test within the current suite.
|
|
* This hook is useful for scenarios where you need to reset or reinitialize the test environment before each test runs, such as resetting database states, clearing caches, or reinitializing variables.
|
|
*
|
|
* **Note:** The `beforeEach` hooks are executed in the order they are defined one after another. You can configure this by changing the `sequence.hooks` option in the config file.
|
|
*
|
|
* @param {Function} fn - The callback function to be executed before each test. This function receives an `TestContext` parameter if additional test context is needed.
|
|
* @param {number} [timeout] - Optional timeout in milliseconds for the hook. If not provided, the default hook timeout from the runner's configuration is used.
|
|
* @returns {void}
|
|
* @example
|
|
* ```ts
|
|
* // Example of using beforeEach to reset a database state
|
|
* beforeEach(async () => {
|
|
* await database.reset();
|
|
* });
|
|
* ```
|
|
*/
|
|
function beforeEach(fn, timeout = getDefaultHookTimeout()) {
|
|
assertTypes(fn, "\"beforeEach\" callback", ["function"]);
|
|
const stackTraceError = new Error("STACK_TRACE_ERROR");
|
|
const runner = getRunner();
|
|
return getCurrentSuite().on("beforeEach", Object.assign(withTimeout(withFixtures(runner, fn), timeout ?? getDefaultHookTimeout(), true, stackTraceError, abortIfTimeout), {
|
|
[CLEANUP_TIMEOUT_KEY]: timeout,
|
|
[CLEANUP_STACK_TRACE_KEY]: stackTraceError
|
|
}));
|
|
}
|
|
/**
|
|
* Registers a callback function to be executed after each test within the current suite has completed.
|
|
* This hook is useful for scenarios where you need to clean up or reset the test environment after each test runs, such as deleting temporary files, clearing test-specific database entries, or resetting mocked functions.
|
|
*
|
|
* **Note:** The `afterEach` hooks are running in reverse order of their registration. You can configure this by changing the `sequence.hooks` option in the config file.
|
|
*
|
|
* @param {Function} fn - The callback function to be executed after each test. This function receives an `TestContext` parameter if additional test context is needed.
|
|
* @param {number} [timeout] - Optional timeout in milliseconds for the hook. If not provided, the default hook timeout from the runner's configuration is used.
|
|
* @returns {void}
|
|
* @example
|
|
* ```ts
|
|
* // Example of using afterEach to delete temporary files created during a test
|
|
* afterEach(async () => {
|
|
* await fileSystem.deleteTempFiles();
|
|
* });
|
|
* ```
|
|
*/
|
|
function afterEach(fn, timeout) {
|
|
assertTypes(fn, "\"afterEach\" callback", ["function"]);
|
|
const runner = getRunner();
|
|
return getCurrentSuite().on("afterEach", withTimeout(withFixtures(runner, fn), timeout ?? getDefaultHookTimeout(), true, new Error("STACK_TRACE_ERROR"), abortIfTimeout));
|
|
}
|
|
/**
|
|
* Registers a callback function to be executed when a test fails within the current suite.
|
|
* This function allows for custom actions to be performed in response to test failures, such as logging, cleanup, or additional diagnostics.
|
|
*
|
|
* **Note:** The `onTestFailed` hooks are running in reverse order of their registration. You can configure this by changing the `sequence.hooks` option in the config file.
|
|
*
|
|
* @param {Function} fn - The callback function to be executed upon a test failure. The function receives the test result (including errors).
|
|
* @param {number} [timeout] - Optional timeout in milliseconds for the hook. If not provided, the default hook timeout from the runner's configuration is used.
|
|
* @throws {Error} Throws an error if the function is not called within a test.
|
|
* @returns {void}
|
|
* @example
|
|
* ```ts
|
|
* // Example of using onTestFailed to log failure details
|
|
* onTestFailed(({ errors }) => {
|
|
* console.log(`Test failed: ${test.name}`, errors);
|
|
* });
|
|
* ```
|
|
*/
|
|
const onTestFailed = createTestHook("onTestFailed", (test, handler, timeout) => {
|
|
test.onFailed || (test.onFailed = []);
|
|
test.onFailed.push(withTimeout(handler, timeout ?? getDefaultHookTimeout(), true, new Error("STACK_TRACE_ERROR"), abortIfTimeout));
|
|
});
|
|
/**
|
|
* Registers a callback function to be executed when the current test finishes, regardless of the outcome (pass or fail).
|
|
* This function is ideal for performing actions that should occur after every test execution, such as cleanup, logging, or resetting shared resources.
|
|
*
|
|
* This hook is useful if you have access to a resource in the test itself and you want to clean it up after the test finishes. It is a more compact way to clean up resources than using the combination of `beforeEach` and `afterEach`.
|
|
*
|
|
* **Note:** The `onTestFinished` hooks are running in reverse order of their registration. You can configure this by changing the `sequence.hooks` option in the config file.
|
|
*
|
|
* **Note:** The `onTestFinished` hook is not called if the test is canceled with a dynamic `ctx.skip()` call.
|
|
*
|
|
* @param {Function} fn - The callback function to be executed after a test finishes. The function can receive parameters providing details about the completed test, including its success or failure status.
|
|
* @param {number} [timeout] - Optional timeout in milliseconds for the hook. If not provided, the default hook timeout from the runner's configuration is used.
|
|
* @throws {Error} Throws an error if the function is not called within a test.
|
|
* @returns {void}
|
|
* @example
|
|
* ```ts
|
|
* // Example of using onTestFinished for cleanup
|
|
* const db = await connectToDatabase();
|
|
* onTestFinished(async () => {
|
|
* await db.disconnect();
|
|
* });
|
|
* ```
|
|
*/
|
|
const onTestFinished = createTestHook("onTestFinished", (test, handler, timeout) => {
|
|
test.onFinished || (test.onFinished = []);
|
|
test.onFinished.push(withTimeout(handler, timeout ?? getDefaultHookTimeout(), true, new Error("STACK_TRACE_ERROR"), abortIfTimeout));
|
|
});
|
|
function createTestHook(name, handler) {
|
|
return (fn, timeout) => {
|
|
assertTypes(fn, `"${name}" callback`, ["function"]);
|
|
const current = getCurrentTest();
|
|
if (!current) {
|
|
throw new Error(`Hook ${name}() can only be called inside a test`);
|
|
}
|
|
return handler(current, fn, timeout);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates a suite of tests, allowing for grouping and hierarchical organization of tests.
|
|
* Suites can contain both tests and other suites, enabling complex test structures.
|
|
*
|
|
* @param {string} name - The name of the suite, used for identification and reporting.
|
|
* @param {Function} fn - A function that defines the tests and suites within this suite.
|
|
* @example
|
|
* ```ts
|
|
* // Define a suite with two tests
|
|
* suite('Math operations', () => {
|
|
* test('should add two numbers', () => {
|
|
* expect(add(1, 2)).toBe(3);
|
|
* });
|
|
*
|
|
* test('should subtract two numbers', () => {
|
|
* expect(subtract(5, 2)).toBe(3);
|
|
* });
|
|
* });
|
|
* ```
|
|
* @example
|
|
* ```ts
|
|
* // Define nested suites
|
|
* suite('String operations', () => {
|
|
* suite('Trimming', () => {
|
|
* test('should trim whitespace from start and end', () => {
|
|
* expect(' hello '.trim()).toBe('hello');
|
|
* });
|
|
* });
|
|
*
|
|
* suite('Concatenation', () => {
|
|
* test('should concatenate two strings', () => {
|
|
* expect('hello' + ' ' + 'world').toBe('hello world');
|
|
* });
|
|
* });
|
|
* });
|
|
* ```
|
|
*/
|
|
const suite = createSuite();
|
|
/**
|
|
* Defines a test case with a given name and test function. The test function can optionally be configured with test options.
|
|
*
|
|
* @param {string | Function} name - The name of the test or a function that will be used as a test name.
|
|
* @param {TestOptions | TestFunction} [optionsOrFn] - Optional. The test options or the test function if no explicit name is provided.
|
|
* @param {number | TestOptions | TestFunction} [optionsOrTest] - Optional. The test function or options, depending on the previous parameters.
|
|
* @throws {Error} If called inside another test function.
|
|
* @example
|
|
* ```ts
|
|
* // Define a simple test
|
|
* test('should add two numbers', () => {
|
|
* expect(add(1, 2)).toBe(3);
|
|
* });
|
|
* ```
|
|
* @example
|
|
* ```ts
|
|
* // Define a test with options
|
|
* test('should subtract two numbers', { retry: 3 }, () => {
|
|
* expect(subtract(5, 2)).toBe(3);
|
|
* });
|
|
* ```
|
|
*/
|
|
const test = createTest(function(name, optionsOrFn, optionsOrTest) {
|
|
if (getCurrentTest()) {
|
|
throw new Error("Calling the test function inside another test function is not allowed. Please put it inside \"describe\" or \"suite\" so it can be properly collected.");
|
|
}
|
|
getCurrentSuite().test.fn.call(this, formatName(name), optionsOrFn, optionsOrTest);
|
|
});
|
|
/**
|
|
* Creates a suite of tests, allowing for grouping and hierarchical organization of tests.
|
|
* Suites can contain both tests and other suites, enabling complex test structures.
|
|
*
|
|
* @param {string} name - The name of the suite, used for identification and reporting.
|
|
* @param {Function} fn - A function that defines the tests and suites within this suite.
|
|
* @example
|
|
* ```ts
|
|
* // Define a suite with two tests
|
|
* describe('Math operations', () => {
|
|
* test('should add two numbers', () => {
|
|
* expect(add(1, 2)).toBe(3);
|
|
* });
|
|
*
|
|
* test('should subtract two numbers', () => {
|
|
* expect(subtract(5, 2)).toBe(3);
|
|
* });
|
|
* });
|
|
* ```
|
|
* @example
|
|
* ```ts
|
|
* // Define nested suites
|
|
* describe('String operations', () => {
|
|
* describe('Trimming', () => {
|
|
* test('should trim whitespace from start and end', () => {
|
|
* expect(' hello '.trim()).toBe('hello');
|
|
* });
|
|
* });
|
|
*
|
|
* describe('Concatenation', () => {
|
|
* test('should concatenate two strings', () => {
|
|
* expect('hello' + ' ' + 'world').toBe('hello world');
|
|
* });
|
|
* });
|
|
* });
|
|
* ```
|
|
*/
|
|
const describe = suite;
|
|
/**
|
|
* Defines a test case with a given name and test function. The test function can optionally be configured with test options.
|
|
*
|
|
* @param {string | Function} name - The name of the test or a function that will be used as a test name.
|
|
* @param {TestOptions | TestFunction} [optionsOrFn] - Optional. The test options or the test function if no explicit name is provided.
|
|
* @param {number | TestOptions | TestFunction} [optionsOrTest] - Optional. The test function or options, depending on the previous parameters.
|
|
* @throws {Error} If called inside another test function.
|
|
* @example
|
|
* ```ts
|
|
* // Define a simple test
|
|
* it('adds two numbers', () => {
|
|
* expect(add(1, 2)).toBe(3);
|
|
* });
|
|
* ```
|
|
* @example
|
|
* ```ts
|
|
* // Define a test with options
|
|
* it('subtracts two numbers', { retry: 3 }, () => {
|
|
* expect(subtract(5, 2)).toBe(3);
|
|
* });
|
|
* ```
|
|
*/
|
|
const it = test;
|
|
let runner;
|
|
let defaultSuite;
|
|
let currentTestFilepath;
|
|
function assert(condition, message) {
|
|
if (!condition) {
|
|
throw new Error(`Vitest failed to find ${message}. This is a bug in Vitest. Please, open an issue with reproduction.`);
|
|
}
|
|
}
|
|
function getDefaultSuite() {
|
|
assert(defaultSuite, "the default suite");
|
|
return defaultSuite;
|
|
}
|
|
function getRunner() {
|
|
assert(runner, "the runner");
|
|
return runner;
|
|
}
|
|
function createDefaultSuite(runner) {
|
|
const config = runner.config.sequence;
|
|
const collector = suite("", { concurrent: config.concurrent }, () => {});
|
|
// no parent suite for top-level tests
|
|
delete collector.suite;
|
|
return collector;
|
|
}
|
|
function clearCollectorContext(file, currentRunner) {
|
|
if (!defaultSuite) {
|
|
defaultSuite = createDefaultSuite(currentRunner);
|
|
}
|
|
defaultSuite.file = file;
|
|
runner = currentRunner;
|
|
currentTestFilepath = file.filepath;
|
|
collectorContext.tasks.length = 0;
|
|
defaultSuite.clear();
|
|
collectorContext.currentSuite = defaultSuite;
|
|
}
|
|
function getCurrentSuite() {
|
|
const currentSuite = collectorContext.currentSuite || defaultSuite;
|
|
assert(currentSuite, "the current suite");
|
|
return currentSuite;
|
|
}
|
|
function createSuiteHooks() {
|
|
return {
|
|
beforeAll: [],
|
|
afterAll: [],
|
|
beforeEach: [],
|
|
afterEach: []
|
|
};
|
|
}
|
|
function parseArguments(optionsOrFn, timeoutOrTest) {
|
|
if (timeoutOrTest != null && typeof timeoutOrTest === "object") {
|
|
throw new TypeError(`Signature "test(name, fn, { ... })" was deprecated in Vitest 3 and removed in Vitest 4. Please, provide options as a second argument instead.`);
|
|
}
|
|
let options = {};
|
|
let fn;
|
|
// it('', () => {}, 1000)
|
|
if (typeof timeoutOrTest === "number") {
|
|
options = { timeout: timeoutOrTest };
|
|
} else if (typeof optionsOrFn === "object") {
|
|
options = optionsOrFn;
|
|
}
|
|
if (typeof optionsOrFn === "function") {
|
|
if (typeof timeoutOrTest === "function") {
|
|
throw new TypeError("Cannot use two functions as arguments. Please use the second argument for options.");
|
|
}
|
|
fn = optionsOrFn;
|
|
} else if (typeof timeoutOrTest === "function") {
|
|
fn = timeoutOrTest;
|
|
}
|
|
return {
|
|
options,
|
|
handler: fn
|
|
};
|
|
}
|
|
// implementations
|
|
function createSuiteCollector(name, factory = () => {}, mode, each, suiteOptions, parentCollectorFixtures) {
|
|
const tasks = [];
|
|
let suite;
|
|
initSuite(true);
|
|
const task = function(name = "", options = {}) {
|
|
var _collectorContext$cur, _collectorContext$cur2, _collectorContext$cur3;
|
|
const timeout = (options === null || options === void 0 ? void 0 : options.timeout) ?? runner.config.testTimeout;
|
|
const currentSuite = (_collectorContext$cur = collectorContext.currentSuite) === null || _collectorContext$cur === void 0 ? void 0 : _collectorContext$cur.suite;
|
|
const task = {
|
|
id: "",
|
|
name,
|
|
fullName: createTaskName([(currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.fullName) ?? ((_collectorContext$cur2 = collectorContext.currentSuite) === null || _collectorContext$cur2 === void 0 || (_collectorContext$cur2 = _collectorContext$cur2.file) === null || _collectorContext$cur2 === void 0 ? void 0 : _collectorContext$cur2.fullName), name]),
|
|
fullTestName: createTaskName([currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.fullTestName, name]),
|
|
suite: currentSuite,
|
|
each: options.each,
|
|
fails: options.fails,
|
|
context: undefined,
|
|
type: "test",
|
|
file: (currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.file) ?? ((_collectorContext$cur3 = collectorContext.currentSuite) === null || _collectorContext$cur3 === void 0 ? void 0 : _collectorContext$cur3.file),
|
|
timeout,
|
|
retry: options.retry ?? runner.config.retry,
|
|
repeats: options.repeats,
|
|
mode: options.only ? "only" : options.skip ? "skip" : options.todo ? "todo" : "run",
|
|
meta: options.meta ?? Object.create(null),
|
|
annotations: [],
|
|
artifacts: []
|
|
};
|
|
const handler = options.handler;
|
|
if (task.mode === "run" && !handler) {
|
|
task.mode = "todo";
|
|
}
|
|
if (options.concurrent || !options.sequential && runner.config.sequence.concurrent) {
|
|
task.concurrent = true;
|
|
}
|
|
task.shuffle = suiteOptions === null || suiteOptions === void 0 ? void 0 : suiteOptions.shuffle;
|
|
const context = createTestContext(task, runner);
|
|
// create test context
|
|
Object.defineProperty(task, "context", {
|
|
value: context,
|
|
enumerable: false
|
|
});
|
|
setTestFixture(context, options.fixtures);
|
|
// custom can be called from any place, let's assume the limit is 15 stacks
|
|
const limit = Error.stackTraceLimit;
|
|
Error.stackTraceLimit = 15;
|
|
const stackTraceError = new Error("STACK_TRACE_ERROR");
|
|
Error.stackTraceLimit = limit;
|
|
if (handler) {
|
|
setFn(task, withTimeout(withAwaitAsyncAssertions(withFixtures(runner, handler, context), task), timeout, false, stackTraceError, (_, error) => abortIfTimeout([context], error)));
|
|
}
|
|
if (runner.config.includeTaskLocation) {
|
|
const error = stackTraceError.stack;
|
|
const stack = findTestFileStackTrace(currentTestFilepath, error);
|
|
if (stack) {
|
|
task.location = {
|
|
line: stack.line,
|
|
column: stack.column
|
|
};
|
|
}
|
|
}
|
|
tasks.push(task);
|
|
return task;
|
|
};
|
|
const test = createTest(function(name, optionsOrFn, timeoutOrTest) {
|
|
let { options, handler } = parseArguments(optionsOrFn, timeoutOrTest);
|
|
// inherit repeats, retry, timeout from suite
|
|
if (typeof suiteOptions === "object") {
|
|
options = Object.assign({}, suiteOptions, options);
|
|
}
|
|
// inherit concurrent / sequential from suite
|
|
options.concurrent = this.concurrent || !this.sequential && (options === null || options === void 0 ? void 0 : options.concurrent);
|
|
options.sequential = this.sequential || !this.concurrent && (options === null || options === void 0 ? void 0 : options.sequential);
|
|
const test = task(formatName(name), {
|
|
...this,
|
|
...options,
|
|
handler
|
|
});
|
|
test.type = "test";
|
|
});
|
|
let collectorFixtures = parentCollectorFixtures;
|
|
const collector = {
|
|
type: "collector",
|
|
name,
|
|
mode,
|
|
suite,
|
|
options: suiteOptions,
|
|
test,
|
|
tasks,
|
|
collect,
|
|
task,
|
|
clear,
|
|
on: addHook,
|
|
fixtures() {
|
|
return collectorFixtures;
|
|
},
|
|
scoped(fixtures) {
|
|
const parsed = mergeContextFixtures(fixtures, { fixtures: collectorFixtures }, runner);
|
|
if (parsed.fixtures) {
|
|
collectorFixtures = parsed.fixtures;
|
|
}
|
|
}
|
|
};
|
|
function addHook(name, ...fn) {
|
|
getHooks(suite)[name].push(...fn);
|
|
}
|
|
function initSuite(includeLocation) {
|
|
var _collectorContext$cur4, _collectorContext$cur5, _collectorContext$cur6;
|
|
if (typeof suiteOptions === "number") {
|
|
suiteOptions = { timeout: suiteOptions };
|
|
}
|
|
const currentSuite = (_collectorContext$cur4 = collectorContext.currentSuite) === null || _collectorContext$cur4 === void 0 ? void 0 : _collectorContext$cur4.suite;
|
|
suite = {
|
|
id: "",
|
|
type: "suite",
|
|
name,
|
|
fullName: createTaskName([(currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.fullName) ?? ((_collectorContext$cur5 = collectorContext.currentSuite) === null || _collectorContext$cur5 === void 0 || (_collectorContext$cur5 = _collectorContext$cur5.file) === null || _collectorContext$cur5 === void 0 ? void 0 : _collectorContext$cur5.fullName), name]),
|
|
fullTestName: createTaskName([currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.fullTestName, name]),
|
|
suite: currentSuite,
|
|
mode,
|
|
each,
|
|
file: (currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.file) ?? ((_collectorContext$cur6 = collectorContext.currentSuite) === null || _collectorContext$cur6 === void 0 ? void 0 : _collectorContext$cur6.file),
|
|
shuffle: suiteOptions === null || suiteOptions === void 0 ? void 0 : suiteOptions.shuffle,
|
|
tasks: [],
|
|
meta: Object.create(null),
|
|
concurrent: suiteOptions === null || suiteOptions === void 0 ? void 0 : suiteOptions.concurrent
|
|
};
|
|
if (runner && includeLocation && runner.config.includeTaskLocation) {
|
|
const limit = Error.stackTraceLimit;
|
|
Error.stackTraceLimit = 15;
|
|
const error = new Error("stacktrace").stack;
|
|
Error.stackTraceLimit = limit;
|
|
const stack = findTestFileStackTrace(currentTestFilepath, error);
|
|
if (stack) {
|
|
suite.location = {
|
|
line: stack.line,
|
|
column: stack.column
|
|
};
|
|
}
|
|
}
|
|
setHooks(suite, createSuiteHooks());
|
|
}
|
|
function clear() {
|
|
tasks.length = 0;
|
|
initSuite(false);
|
|
}
|
|
async function collect(file) {
|
|
if (!file) {
|
|
throw new TypeError("File is required to collect tasks.");
|
|
}
|
|
if (factory) {
|
|
await runWithSuite(collector, () => factory(test));
|
|
}
|
|
const allChildren = [];
|
|
for (const i of tasks) {
|
|
allChildren.push(i.type === "collector" ? await i.collect(file) : i);
|
|
}
|
|
suite.tasks = allChildren;
|
|
return suite;
|
|
}
|
|
collectTask(collector);
|
|
return collector;
|
|
}
|
|
function withAwaitAsyncAssertions(fn, task) {
|
|
return (async (...args) => {
|
|
const fnResult = await fn(...args);
|
|
// some async expect will be added to this array, in case user forget to await them
|
|
if (task.promises) {
|
|
const result = await Promise.allSettled(task.promises);
|
|
const errors = result.map((r) => r.status === "rejected" ? r.reason : undefined).filter(Boolean);
|
|
if (errors.length) {
|
|
throw errors;
|
|
}
|
|
}
|
|
return fnResult;
|
|
});
|
|
}
|
|
function createSuite() {
|
|
function suiteFn(name, factoryOrOptions, optionsOrFactory) {
|
|
var _currentSuite$options;
|
|
let mode = this.only ? "only" : this.skip ? "skip" : this.todo ? "todo" : "run";
|
|
const currentSuite = collectorContext.currentSuite || defaultSuite;
|
|
let { options, handler: factory } = parseArguments(factoryOrOptions, optionsOrFactory);
|
|
if (mode === "run" && !factory) {
|
|
mode = "todo";
|
|
}
|
|
const isConcurrentSpecified = options.concurrent || this.concurrent || options.sequential === false;
|
|
const isSequentialSpecified = options.sequential || this.sequential || options.concurrent === false;
|
|
// inherit options from current suite
|
|
options = {
|
|
...currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.options,
|
|
...options,
|
|
shuffle: this.shuffle ?? options.shuffle ?? (currentSuite === null || currentSuite === void 0 || (_currentSuite$options = currentSuite.options) === null || _currentSuite$options === void 0 ? void 0 : _currentSuite$options.shuffle) ?? (runner === null || runner === void 0 ? void 0 : runner.config.sequence.shuffle)
|
|
};
|
|
// inherit concurrent / sequential from suite
|
|
const isConcurrent = isConcurrentSpecified || options.concurrent && !isSequentialSpecified;
|
|
const isSequential = isSequentialSpecified || options.sequential && !isConcurrentSpecified;
|
|
options.concurrent = isConcurrent && !isSequential;
|
|
options.sequential = isSequential && !isConcurrent;
|
|
return createSuiteCollector(formatName(name), factory, mode, this.each, options, currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.fixtures());
|
|
}
|
|
suiteFn.each = function(cases, ...args) {
|
|
const suite = this.withContext();
|
|
this.setContext("each", true);
|
|
if (Array.isArray(cases) && args.length) {
|
|
cases = formatTemplateString(cases, args);
|
|
}
|
|
return (name, optionsOrFn, fnOrOptions) => {
|
|
const _name = formatName(name);
|
|
const arrayOnlyCases = cases.every(Array.isArray);
|
|
const { options, handler } = parseArguments(optionsOrFn, fnOrOptions);
|
|
const fnFirst = typeof optionsOrFn === "function";
|
|
cases.forEach((i, idx) => {
|
|
const items = Array.isArray(i) ? i : [i];
|
|
if (fnFirst) {
|
|
if (arrayOnlyCases) {
|
|
suite(formatTitle(_name, items, idx), handler ? () => handler(...items) : undefined, options.timeout);
|
|
} else {
|
|
suite(formatTitle(_name, items, idx), handler ? () => handler(i) : undefined, options.timeout);
|
|
}
|
|
} else {
|
|
if (arrayOnlyCases) {
|
|
suite(formatTitle(_name, items, idx), options, handler ? () => handler(...items) : undefined);
|
|
} else {
|
|
suite(formatTitle(_name, items, idx), options, handler ? () => handler(i) : undefined);
|
|
}
|
|
}
|
|
});
|
|
this.setContext("each", undefined);
|
|
};
|
|
};
|
|
suiteFn.for = function(cases, ...args) {
|
|
if (Array.isArray(cases) && args.length) {
|
|
cases = formatTemplateString(cases, args);
|
|
}
|
|
return (name, optionsOrFn, fnOrOptions) => {
|
|
const name_ = formatName(name);
|
|
const { options, handler } = parseArguments(optionsOrFn, fnOrOptions);
|
|
cases.forEach((item, idx) => {
|
|
suite(formatTitle(name_, toArray(item), idx), options, handler ? () => handler(item) : undefined);
|
|
});
|
|
};
|
|
};
|
|
suiteFn.skipIf = (condition) => condition ? suite.skip : suite;
|
|
suiteFn.runIf = (condition) => condition ? suite : suite.skip;
|
|
return createChainable([
|
|
"concurrent",
|
|
"sequential",
|
|
"shuffle",
|
|
"skip",
|
|
"only",
|
|
"todo"
|
|
], suiteFn);
|
|
}
|
|
function createTaskCollector(fn, context) {
|
|
const taskFn = fn;
|
|
taskFn.each = function(cases, ...args) {
|
|
const test = this.withContext();
|
|
this.setContext("each", true);
|
|
if (Array.isArray(cases) && args.length) {
|
|
cases = formatTemplateString(cases, args);
|
|
}
|
|
return (name, optionsOrFn, fnOrOptions) => {
|
|
const _name = formatName(name);
|
|
const arrayOnlyCases = cases.every(Array.isArray);
|
|
const { options, handler } = parseArguments(optionsOrFn, fnOrOptions);
|
|
const fnFirst = typeof optionsOrFn === "function";
|
|
cases.forEach((i, idx) => {
|
|
const items = Array.isArray(i) ? i : [i];
|
|
if (fnFirst) {
|
|
if (arrayOnlyCases) {
|
|
test(formatTitle(_name, items, idx), handler ? () => handler(...items) : undefined, options.timeout);
|
|
} else {
|
|
test(formatTitle(_name, items, idx), handler ? () => handler(i) : undefined, options.timeout);
|
|
}
|
|
} else {
|
|
if (arrayOnlyCases) {
|
|
test(formatTitle(_name, items, idx), options, handler ? () => handler(...items) : undefined);
|
|
} else {
|
|
test(formatTitle(_name, items, idx), options, handler ? () => handler(i) : undefined);
|
|
}
|
|
}
|
|
});
|
|
this.setContext("each", undefined);
|
|
};
|
|
};
|
|
taskFn.for = function(cases, ...args) {
|
|
const test = this.withContext();
|
|
if (Array.isArray(cases) && args.length) {
|
|
cases = formatTemplateString(cases, args);
|
|
}
|
|
return (name, optionsOrFn, fnOrOptions) => {
|
|
const _name = formatName(name);
|
|
const { options, handler } = parseArguments(optionsOrFn, fnOrOptions);
|
|
cases.forEach((item, idx) => {
|
|
// monkey-patch handler to allow parsing fixture
|
|
const handlerWrapper = handler ? (ctx) => handler(item, ctx) : undefined;
|
|
if (handlerWrapper) {
|
|
handlerWrapper.__VITEST_FIXTURE_INDEX__ = 1;
|
|
handlerWrapper.toString = () => handler.toString();
|
|
}
|
|
test(formatTitle(_name, toArray(item), idx), options, handlerWrapper);
|
|
});
|
|
};
|
|
};
|
|
taskFn.skipIf = function(condition) {
|
|
return condition ? this.skip : this;
|
|
};
|
|
taskFn.runIf = function(condition) {
|
|
return condition ? this : this.skip;
|
|
};
|
|
taskFn.scoped = function(fixtures) {
|
|
const collector = getCurrentSuite();
|
|
collector.scoped(fixtures);
|
|
};
|
|
taskFn.extend = function(fixtures) {
|
|
const _context = mergeContextFixtures(fixtures, context || {}, runner);
|
|
const originalWrapper = fn;
|
|
return createTest(function(name, optionsOrFn, optionsOrTest) {
|
|
const collector = getCurrentSuite();
|
|
const scopedFixtures = collector.fixtures();
|
|
const context = { ...this };
|
|
if (scopedFixtures) {
|
|
context.fixtures = mergeScopedFixtures(context.fixtures || [], scopedFixtures);
|
|
}
|
|
originalWrapper.call(context, formatName(name), optionsOrFn, optionsOrTest);
|
|
}, _context);
|
|
};
|
|
taskFn.beforeEach = beforeEach;
|
|
taskFn.afterEach = afterEach;
|
|
taskFn.beforeAll = beforeAll;
|
|
taskFn.afterAll = afterAll;
|
|
const _test = createChainable([
|
|
"concurrent",
|
|
"sequential",
|
|
"skip",
|
|
"only",
|
|
"todo",
|
|
"fails"
|
|
], taskFn);
|
|
if (context) {
|
|
_test.mergeContext(context);
|
|
}
|
|
return _test;
|
|
}
|
|
function createTest(fn, context) {
|
|
return createTaskCollector(fn, context);
|
|
}
|
|
function formatName(name) {
|
|
return typeof name === "string" ? name : typeof name === "function" ? name.name || "<anonymous>" : String(name);
|
|
}
|
|
function formatTitle(template, items, idx) {
|
|
if (template.includes("%#") || template.includes("%$")) {
|
|
// '%#' match index of the test case
|
|
template = template.replace(/%%/g, "__vitest_escaped_%__").replace(/%#/g, `${idx}`).replace(/%\$/g, `${idx + 1}`).replace(/__vitest_escaped_%__/g, "%%");
|
|
}
|
|
const count = template.split("%").length - 1;
|
|
if (template.includes("%f")) {
|
|
const placeholders = template.match(/%f/g) || [];
|
|
placeholders.forEach((_, i) => {
|
|
if (isNegativeNaN(items[i]) || Object.is(items[i], -0)) {
|
|
// Replace the i-th occurrence of '%f' with '-%f'
|
|
let occurrence = 0;
|
|
template = template.replace(/%f/g, (match) => {
|
|
occurrence++;
|
|
return occurrence === i + 1 ? "-%f" : match;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
const isObjectItem = isObject(items[0]);
|
|
function formatAttribute(s) {
|
|
return s.replace(/\$([$\w.]+)/g, (_, key) => {
|
|
var _runner$config;
|
|
const isArrayKey = /^\d+$/.test(key);
|
|
if (!isObjectItem && !isArrayKey) {
|
|
return `$${key}`;
|
|
}
|
|
const arrayElement = isArrayKey ? objectAttr(items, key) : undefined;
|
|
const value = isObjectItem ? objectAttr(items[0], key, arrayElement) : arrayElement;
|
|
return objDisplay(value, { truncate: runner === null || runner === void 0 || (_runner$config = runner.config) === null || _runner$config === void 0 || (_runner$config = _runner$config.chaiConfig) === null || _runner$config === void 0 ? void 0 : _runner$config.truncateThreshold });
|
|
});
|
|
}
|
|
let output = "";
|
|
let i = 0;
|
|
handleRegexMatch(
|
|
template,
|
|
formatRegExp,
|
|
// format "%"
|
|
(match) => {
|
|
if (i < count) {
|
|
output += format(match[0], items[i++]);
|
|
} else {
|
|
output += match[0];
|
|
}
|
|
},
|
|
// format "$"
|
|
(nonMatch) => {
|
|
output += formatAttribute(nonMatch);
|
|
}
|
|
);
|
|
return output;
|
|
}
|
|
// based on https://github.com/unocss/unocss/blob/2e74b31625bbe3b9c8351570749aa2d3f799d919/packages/autocomplete/src/parse.ts#L11
|
|
function handleRegexMatch(input, regex, onMatch, onNonMatch) {
|
|
let lastIndex = 0;
|
|
for (const m of input.matchAll(regex)) {
|
|
if (lastIndex < m.index) {
|
|
onNonMatch(input.slice(lastIndex, m.index));
|
|
}
|
|
onMatch(m);
|
|
lastIndex = m.index + m[0].length;
|
|
}
|
|
if (lastIndex < input.length) {
|
|
onNonMatch(input.slice(lastIndex));
|
|
}
|
|
}
|
|
function formatTemplateString(cases, args) {
|
|
const header = cases.join("").trim().replace(/ /g, "").split("\n").map((i) => i.split("|"))[0];
|
|
const res = [];
|
|
for (let i = 0; i < Math.floor(args.length / header.length); i++) {
|
|
const oneCase = {};
|
|
for (let j = 0; j < header.length; j++) {
|
|
oneCase[header[j]] = args[i * header.length + j];
|
|
}
|
|
res.push(oneCase);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
const now$2 = Date.now;
|
|
const collectorContext = {
|
|
tasks: [],
|
|
currentSuite: null
|
|
};
|
|
function collectTask(task) {
|
|
var _collectorContext$cur;
|
|
(_collectorContext$cur = collectorContext.currentSuite) === null || _collectorContext$cur === void 0 ? void 0 : _collectorContext$cur.tasks.push(task);
|
|
}
|
|
async function runWithSuite(suite, fn) {
|
|
const prev = collectorContext.currentSuite;
|
|
collectorContext.currentSuite = suite;
|
|
await fn();
|
|
collectorContext.currentSuite = prev;
|
|
}
|
|
function withTimeout(fn, timeout, isHook = false, stackTraceError, onTimeout) {
|
|
if (timeout <= 0 || timeout === Number.POSITIVE_INFINITY) {
|
|
return fn;
|
|
}
|
|
const { setTimeout, clearTimeout } = getSafeTimers();
|
|
// this function name is used to filter error in test/cli/test/fails.test.ts
|
|
return (function runWithTimeout(...args) {
|
|
const startTime = now$2();
|
|
const runner = getRunner();
|
|
runner._currentTaskStartTime = startTime;
|
|
runner._currentTaskTimeout = timeout;
|
|
return new Promise((resolve_, reject_) => {
|
|
var _timer$unref;
|
|
const timer = setTimeout(() => {
|
|
clearTimeout(timer);
|
|
rejectTimeoutError();
|
|
}, timeout);
|
|
// `unref` might not exist in browser
|
|
(_timer$unref = timer.unref) === null || _timer$unref === void 0 ? void 0 : _timer$unref.call(timer);
|
|
function rejectTimeoutError() {
|
|
const error = makeTimeoutError(isHook, timeout, stackTraceError);
|
|
onTimeout === null || onTimeout === void 0 ? void 0 : onTimeout(args, error);
|
|
reject_(error);
|
|
}
|
|
function resolve(result) {
|
|
runner._currentTaskStartTime = undefined;
|
|
runner._currentTaskTimeout = undefined;
|
|
clearTimeout(timer);
|
|
// if test/hook took too long in microtask, setTimeout won't be triggered,
|
|
// but we still need to fail the test, see
|
|
// https://github.com/vitest-dev/vitest/issues/2920
|
|
if (now$2() - startTime >= timeout) {
|
|
rejectTimeoutError();
|
|
return;
|
|
}
|
|
resolve_(result);
|
|
}
|
|
function reject(error) {
|
|
runner._currentTaskStartTime = undefined;
|
|
runner._currentTaskTimeout = undefined;
|
|
clearTimeout(timer);
|
|
reject_(error);
|
|
}
|
|
// sync test/hook will be caught by try/catch
|
|
try {
|
|
const result = fn(...args);
|
|
// the result is a thenable, we don't wrap this in Promise.resolve
|
|
// to avoid creating new promises
|
|
if (typeof result === "object" && result != null && typeof result.then === "function") {
|
|
result.then(resolve, reject);
|
|
} else {
|
|
resolve(result);
|
|
}
|
|
}
|
|
// user sync test/hook throws an error
|
|
catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
const abortControllers = new WeakMap();
|
|
function abortIfTimeout([context], error) {
|
|
if (context) {
|
|
abortContextSignal(context, error);
|
|
}
|
|
}
|
|
function abortContextSignal(context, error) {
|
|
const abortController = abortControllers.get(context);
|
|
abortController === null || abortController === void 0 ? void 0 : abortController.abort(error);
|
|
}
|
|
function createTestContext(test, runner) {
|
|
var _runner$extendTaskCon;
|
|
const context = function() {
|
|
throw new Error("done() callback is deprecated, use promise instead");
|
|
};
|
|
let abortController = abortControllers.get(context);
|
|
if (!abortController) {
|
|
abortController = new AbortController();
|
|
abortControllers.set(context, abortController);
|
|
}
|
|
context.signal = abortController.signal;
|
|
context.task = test;
|
|
context.skip = (condition, note) => {
|
|
if (condition === false) {
|
|
// do nothing
|
|
return undefined;
|
|
}
|
|
test.result ?? (test.result = { state: "skip" });
|
|
test.result.pending = true;
|
|
throw new PendingError("test is skipped; abort execution", test, typeof condition === "string" ? condition : note);
|
|
};
|
|
context.annotate = ((message, type, attachment) => {
|
|
if (test.result && test.result.state !== "run") {
|
|
throw new Error(`Cannot annotate tests outside of the test run. The test "${test.name}" finished running with the "${test.result.state}" state already.`);
|
|
}
|
|
const annotation = {
|
|
message,
|
|
type: typeof type === "object" || type === undefined ? "notice" : type
|
|
};
|
|
const annotationAttachment = typeof type === "object" ? type : attachment;
|
|
if (annotationAttachment) {
|
|
annotation.attachment = annotationAttachment;
|
|
manageArtifactAttachment(annotation.attachment);
|
|
}
|
|
return recordAsyncOperation(test, recordArtifact(test, {
|
|
type: "internal:annotation",
|
|
annotation
|
|
}).then(async ({ annotation }) => {
|
|
if (!runner.onTestAnnotate) {
|
|
throw new Error(`Test runner doesn't support test annotations.`);
|
|
}
|
|
await finishSendTasksUpdate(runner);
|
|
const resolvedAnnotation = await runner.onTestAnnotate(test, annotation);
|
|
test.annotations.push(resolvedAnnotation);
|
|
return resolvedAnnotation;
|
|
}));
|
|
});
|
|
context.onTestFailed = (handler, timeout) => {
|
|
test.onFailed || (test.onFailed = []);
|
|
test.onFailed.push(withTimeout(handler, timeout ?? runner.config.hookTimeout, true, new Error("STACK_TRACE_ERROR"), (_, error) => abortController.abort(error)));
|
|
};
|
|
context.onTestFinished = (handler, timeout) => {
|
|
test.onFinished || (test.onFinished = []);
|
|
test.onFinished.push(withTimeout(handler, timeout ?? runner.config.hookTimeout, true, new Error("STACK_TRACE_ERROR"), (_, error) => abortController.abort(error)));
|
|
};
|
|
return ((_runner$extendTaskCon = runner.extendTaskContext) === null || _runner$extendTaskCon === void 0 ? void 0 : _runner$extendTaskCon.call(runner, context)) || context;
|
|
}
|
|
function makeTimeoutError(isHook, timeout, stackTraceError) {
|
|
const message = `${isHook ? "Hook" : "Test"} timed out in ${timeout}ms.\nIf this is a long-running ${isHook ? "hook" : "test"}, pass a timeout value as the last argument or configure it globally with "${isHook ? "hookTimeout" : "testTimeout"}".`;
|
|
const error = new Error(message);
|
|
if (stackTraceError === null || stackTraceError === void 0 ? void 0 : stackTraceError.stack) {
|
|
error.stack = stackTraceError.stack.replace(error.message, stackTraceError.message);
|
|
}
|
|
return error;
|
|
}
|
|
const fileContexts = new WeakMap();
|
|
function getFileContext(file) {
|
|
const context = fileContexts.get(file);
|
|
if (!context) {
|
|
throw new Error(`Cannot find file context for ${file.name}`);
|
|
}
|
|
return context;
|
|
}
|
|
function setFileContext(file, context) {
|
|
fileContexts.set(file, context);
|
|
}
|
|
|
|
async function runSetupFiles(config, files, runner) {
|
|
if (config.sequence.setupFiles === "parallel") {
|
|
await Promise.all(files.map(async (fsPath) => {
|
|
await runner.importFile(fsPath, "setup");
|
|
}));
|
|
} else {
|
|
for (const fsPath of files) {
|
|
await runner.importFile(fsPath, "setup");
|
|
}
|
|
}
|
|
}
|
|
|
|
const now$1 = globalThis.performance ? globalThis.performance.now.bind(globalThis.performance) : Date.now;
|
|
async function collectTests(specs, runner) {
|
|
const files = [];
|
|
const config = runner.config;
|
|
const $ = runner.trace;
|
|
for (const spec of specs) {
|
|
const filepath = typeof spec === "string" ? spec : spec.filepath;
|
|
await $("collect_spec", { "code.file.path": filepath }, async () => {
|
|
var _runner$onCollectStar;
|
|
const testLocations = typeof spec === "string" ? undefined : spec.testLocations;
|
|
const file = createFileTask(filepath, config.root, config.name, runner.pool, runner.viteEnvironment);
|
|
setFileContext(file, Object.create(null));
|
|
file.shuffle = config.sequence.shuffle;
|
|
(_runner$onCollectStar = runner.onCollectStart) === null || _runner$onCollectStar === void 0 ? void 0 : _runner$onCollectStar.call(runner, file);
|
|
clearCollectorContext(file, runner);
|
|
try {
|
|
var _runner$getImportDura;
|
|
const setupFiles = toArray(config.setupFiles);
|
|
if (setupFiles.length) {
|
|
const setupStart = now$1();
|
|
await runSetupFiles(config, setupFiles, runner);
|
|
const setupEnd = now$1();
|
|
file.setupDuration = setupEnd - setupStart;
|
|
} else {
|
|
file.setupDuration = 0;
|
|
}
|
|
const collectStart = now$1();
|
|
await runner.importFile(filepath, "collect");
|
|
const durations = (_runner$getImportDura = runner.getImportDurations) === null || _runner$getImportDura === void 0 ? void 0 : _runner$getImportDura.call(runner);
|
|
if (durations) {
|
|
file.importDurations = durations;
|
|
}
|
|
const defaultTasks = await getDefaultSuite().collect(file);
|
|
const fileHooks = createSuiteHooks();
|
|
mergeHooks(fileHooks, getHooks(defaultTasks));
|
|
for (const c of [...defaultTasks.tasks, ...collectorContext.tasks]) {
|
|
if (c.type === "test" || c.type === "suite") {
|
|
file.tasks.push(c);
|
|
} else if (c.type === "collector") {
|
|
const suite = await c.collect(file);
|
|
if (suite.name || suite.tasks.length) {
|
|
mergeHooks(fileHooks, getHooks(suite));
|
|
file.tasks.push(suite);
|
|
}
|
|
} else {
|
|
// check that types are exhausted
|
|
c;
|
|
}
|
|
}
|
|
setHooks(file, fileHooks);
|
|
file.collectDuration = now$1() - collectStart;
|
|
} catch (e) {
|
|
var _runner$getImportDura2;
|
|
const error = processError(e);
|
|
file.result = {
|
|
state: "fail",
|
|
errors: [error]
|
|
};
|
|
const durations = (_runner$getImportDura2 = runner.getImportDurations) === null || _runner$getImportDura2 === void 0 ? void 0 : _runner$getImportDura2.call(runner);
|
|
if (durations) {
|
|
file.importDurations = durations;
|
|
}
|
|
}
|
|
calculateSuiteHash(file);
|
|
const hasOnlyTasks = someTasksAreOnly(file);
|
|
interpretTaskModes(file, config.testNamePattern, testLocations, hasOnlyTasks, false, config.allowOnly);
|
|
if (file.mode === "queued") {
|
|
file.mode = "run";
|
|
}
|
|
files.push(file);
|
|
});
|
|
}
|
|
return files;
|
|
}
|
|
function mergeHooks(baseHooks, hooks) {
|
|
for (const _key in hooks) {
|
|
const key = _key;
|
|
baseHooks[key].push(...hooks[key]);
|
|
}
|
|
return baseHooks;
|
|
}
|
|
|
|
const now = globalThis.performance ? globalThis.performance.now.bind(globalThis.performance) : Date.now;
|
|
const unixNow = Date.now;
|
|
const { clearTimeout, setTimeout } = getSafeTimers();
|
|
function updateSuiteHookState(task, name, state, runner) {
|
|
if (!task.result) {
|
|
task.result = { state: "run" };
|
|
}
|
|
if (!task.result.hooks) {
|
|
task.result.hooks = {};
|
|
}
|
|
const suiteHooks = task.result.hooks;
|
|
if (suiteHooks) {
|
|
suiteHooks[name] = state;
|
|
let event = state === "run" ? "before-hook-start" : "before-hook-end";
|
|
if (name === "afterAll" || name === "afterEach") {
|
|
event = state === "run" ? "after-hook-start" : "after-hook-end";
|
|
}
|
|
updateTask(event, task, runner);
|
|
}
|
|
}
|
|
function getSuiteHooks(suite, name, sequence) {
|
|
const hooks = getHooks(suite)[name];
|
|
if (sequence === "stack" && (name === "afterAll" || name === "afterEach")) {
|
|
return hooks.slice().reverse();
|
|
}
|
|
return hooks;
|
|
}
|
|
async function callTestHooks(runner, test, hooks, sequence) {
|
|
if (sequence === "stack") {
|
|
hooks = hooks.slice().reverse();
|
|
}
|
|
if (!hooks.length) {
|
|
return;
|
|
}
|
|
const context = test.context;
|
|
const onTestFailed = test.context.onTestFailed;
|
|
const onTestFinished = test.context.onTestFinished;
|
|
context.onTestFailed = () => {
|
|
throw new Error(`Cannot call "onTestFailed" inside a test hook.`);
|
|
};
|
|
context.onTestFinished = () => {
|
|
throw new Error(`Cannot call "onTestFinished" inside a test hook.`);
|
|
};
|
|
if (sequence === "parallel") {
|
|
try {
|
|
await Promise.all(hooks.map((fn) => fn(test.context)));
|
|
} catch (e) {
|
|
failTask(test.result, e, runner.config.diffOptions);
|
|
}
|
|
} else {
|
|
for (const fn of hooks) {
|
|
try {
|
|
await fn(test.context);
|
|
} catch (e) {
|
|
failTask(test.result, e, runner.config.diffOptions);
|
|
}
|
|
}
|
|
}
|
|
context.onTestFailed = onTestFailed;
|
|
context.onTestFinished = onTestFinished;
|
|
}
|
|
async function callSuiteHook(suite, currentTask, name, runner, args) {
|
|
const sequence = runner.config.sequence.hooks;
|
|
const callbacks = [];
|
|
// stop at file level
|
|
const parentSuite = "filepath" in suite ? null : suite.suite || suite.file;
|
|
if (name === "beforeEach" && parentSuite) {
|
|
callbacks.push(...await callSuiteHook(parentSuite, currentTask, name, runner, args));
|
|
}
|
|
const hooks = getSuiteHooks(suite, name, sequence);
|
|
if (hooks.length > 0) {
|
|
updateSuiteHookState(currentTask, name, "run", runner);
|
|
}
|
|
async function runHook(hook) {
|
|
return getBeforeHookCleanupCallback(hook, await hook(...args), name === "beforeEach" ? args[0] : undefined);
|
|
}
|
|
if (sequence === "parallel") {
|
|
callbacks.push(...await Promise.all(hooks.map((hook) => runHook(hook))));
|
|
} else {
|
|
for (const hook of hooks) {
|
|
callbacks.push(await runHook(hook));
|
|
}
|
|
}
|
|
if (hooks.length > 0) {
|
|
updateSuiteHookState(currentTask, name, "pass", runner);
|
|
}
|
|
if (name === "afterEach" && parentSuite) {
|
|
callbacks.push(...await callSuiteHook(parentSuite, currentTask, name, runner, args));
|
|
}
|
|
return callbacks;
|
|
}
|
|
const packs = new Map();
|
|
const eventsPacks = [];
|
|
const pendingTasksUpdates = [];
|
|
function sendTasksUpdate(runner) {
|
|
if (packs.size) {
|
|
var _runner$onTaskUpdate;
|
|
const taskPacks = Array.from(packs).map(([id, task]) => {
|
|
return [
|
|
id,
|
|
task[0],
|
|
task[1]
|
|
];
|
|
});
|
|
const p = (_runner$onTaskUpdate = runner.onTaskUpdate) === null || _runner$onTaskUpdate === void 0 ? void 0 : _runner$onTaskUpdate.call(runner, taskPacks, eventsPacks);
|
|
if (p) {
|
|
pendingTasksUpdates.push(p);
|
|
// remove successful promise to not grow array indefnitely,
|
|
// but keep rejections so finishSendTasksUpdate can handle them
|
|
p.then(() => pendingTasksUpdates.splice(pendingTasksUpdates.indexOf(p), 1), () => {});
|
|
}
|
|
eventsPacks.length = 0;
|
|
packs.clear();
|
|
}
|
|
}
|
|
async function finishSendTasksUpdate(runner) {
|
|
sendTasksUpdate(runner);
|
|
await Promise.all(pendingTasksUpdates);
|
|
}
|
|
function throttle(fn, ms) {
|
|
let last = 0;
|
|
let pendingCall;
|
|
return function call(...args) {
|
|
const now = unixNow();
|
|
if (now - last > ms) {
|
|
last = now;
|
|
clearTimeout(pendingCall);
|
|
pendingCall = undefined;
|
|
return fn.apply(this, args);
|
|
}
|
|
// Make sure fn is still called even if there are no further calls
|
|
pendingCall ?? (pendingCall = setTimeout(() => call.bind(this)(...args), ms));
|
|
};
|
|
}
|
|
// throttle based on summary reporter's DURATION_UPDATE_INTERVAL_MS
|
|
const sendTasksUpdateThrottled = throttle(sendTasksUpdate, 100);
|
|
function updateTask(event, task, runner) {
|
|
eventsPacks.push([
|
|
task.id,
|
|
event,
|
|
undefined
|
|
]);
|
|
packs.set(task.id, [task.result, task.meta]);
|
|
sendTasksUpdateThrottled(runner);
|
|
}
|
|
async function callCleanupHooks(runner, cleanups) {
|
|
const sequence = runner.config.sequence.hooks;
|
|
if (sequence === "stack") {
|
|
cleanups = cleanups.slice().reverse();
|
|
}
|
|
if (sequence === "parallel") {
|
|
await Promise.all(cleanups.map(async (fn) => {
|
|
if (typeof fn !== "function") {
|
|
return;
|
|
}
|
|
await fn();
|
|
}));
|
|
} else {
|
|
for (const fn of cleanups) {
|
|
if (typeof fn !== "function") {
|
|
continue;
|
|
}
|
|
await fn();
|
|
}
|
|
}
|
|
}
|
|
async function runTest(test, runner) {
|
|
var _runner$onBeforeRunTa, _test$result, _runner$onAfterRunTas;
|
|
await ((_runner$onBeforeRunTa = runner.onBeforeRunTask) === null || _runner$onBeforeRunTa === void 0 ? void 0 : _runner$onBeforeRunTa.call(runner, test));
|
|
if (test.mode !== "run" && test.mode !== "queued") {
|
|
updateTask("test-prepare", test, runner);
|
|
updateTask("test-finished", test, runner);
|
|
return;
|
|
}
|
|
if (((_test$result = test.result) === null || _test$result === void 0 ? void 0 : _test$result.state) === "fail") {
|
|
// should not be possible to get here, I think this is just copy pasted from suite
|
|
// TODO: maybe someone fails tests in `beforeAll` hooks?
|
|
// https://github.com/vitest-dev/vitest/pull/7069
|
|
updateTask("test-failed-early", test, runner);
|
|
return;
|
|
}
|
|
const start = now();
|
|
test.result = {
|
|
state: "run",
|
|
startTime: unixNow(),
|
|
retryCount: 0
|
|
};
|
|
updateTask("test-prepare", test, runner);
|
|
const cleanupRunningTest = addRunningTest(test);
|
|
setCurrentTest(test);
|
|
const suite = test.suite || test.file;
|
|
const $ = runner.trace;
|
|
const repeats = test.repeats ?? 0;
|
|
for (let repeatCount = 0; repeatCount <= repeats; repeatCount++) {
|
|
const retry = test.retry ?? 0;
|
|
for (let retryCount = 0; retryCount <= retry; retryCount++) {
|
|
var _test$onFinished, _test$onFailed, _runner$onAfterRetryT, _test$result2, _test$result3;
|
|
let beforeEachCleanups = [];
|
|
try {
|
|
var _runner$onBeforeTryTa, _runner$onAfterTryTas;
|
|
await ((_runner$onBeforeTryTa = runner.onBeforeTryTask) === null || _runner$onBeforeTryTa === void 0 ? void 0 : _runner$onBeforeTryTa.call(runner, test, {
|
|
retry: retryCount,
|
|
repeats: repeatCount
|
|
}));
|
|
test.result.repeatCount = repeatCount;
|
|
beforeEachCleanups = await $("test.beforeEach", () => callSuiteHook(suite, test, "beforeEach", runner, [test.context, suite]));
|
|
if (runner.runTask) {
|
|
await $("test.callback", () => runner.runTask(test));
|
|
} else {
|
|
const fn = getFn(test);
|
|
if (!fn) {
|
|
throw new Error("Test function is not found. Did you add it using `setFn`?");
|
|
}
|
|
await $("test.callback", () => fn());
|
|
}
|
|
await ((_runner$onAfterTryTas = runner.onAfterTryTask) === null || _runner$onAfterTryTas === void 0 ? void 0 : _runner$onAfterTryTas.call(runner, test, {
|
|
retry: retryCount,
|
|
repeats: repeatCount
|
|
}));
|
|
if (test.result.state !== "fail") {
|
|
if (!test.repeats) {
|
|
test.result.state = "pass";
|
|
} else if (test.repeats && retry === retryCount) {
|
|
test.result.state = "pass";
|
|
}
|
|
}
|
|
} catch (e) {
|
|
failTask(test.result, e, runner.config.diffOptions);
|
|
}
|
|
try {
|
|
var _runner$onTaskFinishe;
|
|
await ((_runner$onTaskFinishe = runner.onTaskFinished) === null || _runner$onTaskFinishe === void 0 ? void 0 : _runner$onTaskFinishe.call(runner, test));
|
|
} catch (e) {
|
|
failTask(test.result, e, runner.config.diffOptions);
|
|
}
|
|
try {
|
|
await $("test.afterEach", () => callSuiteHook(suite, test, "afterEach", runner, [test.context, suite]));
|
|
if (beforeEachCleanups.length) {
|
|
await $("test.cleanup", () => callCleanupHooks(runner, beforeEachCleanups));
|
|
}
|
|
await callFixtureCleanup(test.context);
|
|
} catch (e) {
|
|
failTask(test.result, e, runner.config.diffOptions);
|
|
}
|
|
if ((_test$onFinished = test.onFinished) === null || _test$onFinished === void 0 ? void 0 : _test$onFinished.length) {
|
|
await $("test.onFinished", () => callTestHooks(runner, test, test.onFinished, "stack"));
|
|
}
|
|
if (test.result.state === "fail" && ((_test$onFailed = test.onFailed) === null || _test$onFailed === void 0 ? void 0 : _test$onFailed.length)) {
|
|
await $("test.onFailed", () => callTestHooks(runner, test, test.onFailed, runner.config.sequence.hooks));
|
|
}
|
|
test.onFailed = undefined;
|
|
test.onFinished = undefined;
|
|
await ((_runner$onAfterRetryT = runner.onAfterRetryTask) === null || _runner$onAfterRetryT === void 0 ? void 0 : _runner$onAfterRetryT.call(runner, test, {
|
|
retry: retryCount,
|
|
repeats: repeatCount
|
|
}));
|
|
// skipped with new PendingError
|
|
if (((_test$result2 = test.result) === null || _test$result2 === void 0 ? void 0 : _test$result2.pending) || ((_test$result3 = test.result) === null || _test$result3 === void 0 ? void 0 : _test$result3.state) === "skip") {
|
|
var _test$result4;
|
|
test.mode = "skip";
|
|
test.result = {
|
|
state: "skip",
|
|
note: (_test$result4 = test.result) === null || _test$result4 === void 0 ? void 0 : _test$result4.note,
|
|
pending: true,
|
|
duration: now() - start
|
|
};
|
|
updateTask("test-finished", test, runner);
|
|
setCurrentTest(undefined);
|
|
cleanupRunningTest();
|
|
return;
|
|
}
|
|
if (test.result.state === "pass") {
|
|
break;
|
|
}
|
|
if (retryCount < retry) {
|
|
// reset state when retry test
|
|
test.result.state = "run";
|
|
test.result.retryCount = (test.result.retryCount ?? 0) + 1;
|
|
}
|
|
// update retry info
|
|
updateTask("test-retried", test, runner);
|
|
}
|
|
}
|
|
// if test is marked to be failed, flip the result
|
|
if (test.fails) {
|
|
if (test.result.state === "pass") {
|
|
const error = processError(new Error("Expect test to fail"));
|
|
test.result.state = "fail";
|
|
test.result.errors = [error];
|
|
} else {
|
|
test.result.state = "pass";
|
|
test.result.errors = undefined;
|
|
}
|
|
}
|
|
cleanupRunningTest();
|
|
setCurrentTest(undefined);
|
|
test.result.duration = now() - start;
|
|
await ((_runner$onAfterRunTas = runner.onAfterRunTask) === null || _runner$onAfterRunTas === void 0 ? void 0 : _runner$onAfterRunTas.call(runner, test));
|
|
updateTask("test-finished", test, runner);
|
|
}
|
|
function failTask(result, err, diffOptions) {
|
|
if (err instanceof PendingError) {
|
|
result.state = "skip";
|
|
result.note = err.note;
|
|
result.pending = true;
|
|
return;
|
|
}
|
|
result.state = "fail";
|
|
const errors = Array.isArray(err) ? err : [err];
|
|
for (const e of errors) {
|
|
const error = processError(e, diffOptions);
|
|
result.errors ?? (result.errors = []);
|
|
result.errors.push(error);
|
|
}
|
|
}
|
|
function markTasksAsSkipped(suite, runner) {
|
|
suite.tasks.forEach((t) => {
|
|
t.mode = "skip";
|
|
t.result = {
|
|
...t.result,
|
|
state: "skip"
|
|
};
|
|
updateTask("test-finished", t, runner);
|
|
if (t.type === "suite") {
|
|
markTasksAsSkipped(t, runner);
|
|
}
|
|
});
|
|
}
|
|
async function runSuite(suite, runner) {
|
|
var _runner$onBeforeRunSu, _suite$result;
|
|
await ((_runner$onBeforeRunSu = runner.onBeforeRunSuite) === null || _runner$onBeforeRunSu === void 0 ? void 0 : _runner$onBeforeRunSu.call(runner, suite));
|
|
if (((_suite$result = suite.result) === null || _suite$result === void 0 ? void 0 : _suite$result.state) === "fail") {
|
|
markTasksAsSkipped(suite, runner);
|
|
// failed during collection
|
|
updateTask("suite-failed-early", suite, runner);
|
|
return;
|
|
}
|
|
const start = now();
|
|
const mode = suite.mode;
|
|
suite.result = {
|
|
state: mode === "skip" || mode === "todo" ? mode : "run",
|
|
startTime: unixNow()
|
|
};
|
|
const $ = runner.trace;
|
|
updateTask("suite-prepare", suite, runner);
|
|
let beforeAllCleanups = [];
|
|
if (suite.mode === "skip") {
|
|
suite.result.state = "skip";
|
|
updateTask("suite-finished", suite, runner);
|
|
} else if (suite.mode === "todo") {
|
|
suite.result.state = "todo";
|
|
updateTask("suite-finished", suite, runner);
|
|
} else {
|
|
var _runner$onAfterRunSui;
|
|
try {
|
|
try {
|
|
beforeAllCleanups = await $("suite.beforeAll", () => callSuiteHook(suite, suite, "beforeAll", runner, [suite]));
|
|
} catch (e) {
|
|
markTasksAsSkipped(suite, runner);
|
|
throw e;
|
|
}
|
|
if (runner.runSuite) {
|
|
await runner.runSuite(suite);
|
|
} else {
|
|
for (let tasksGroup of partitionSuiteChildren(suite)) {
|
|
if (tasksGroup[0].concurrent === true) {
|
|
await Promise.all(tasksGroup.map((c) => runSuiteChild(c, runner)));
|
|
} else {
|
|
const { sequence } = runner.config;
|
|
if (suite.shuffle) {
|
|
// run describe block independently from tests
|
|
const suites = tasksGroup.filter((group) => group.type === "suite");
|
|
const tests = tasksGroup.filter((group) => group.type === "test");
|
|
const groups = shuffle([suites, tests], sequence.seed);
|
|
tasksGroup = groups.flatMap((group) => shuffle(group, sequence.seed));
|
|
}
|
|
for (const c of tasksGroup) {
|
|
await runSuiteChild(c, runner);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
failTask(suite.result, e, runner.config.diffOptions);
|
|
}
|
|
try {
|
|
await $("suite.afterAll", () => callSuiteHook(suite, suite, "afterAll", runner, [suite]));
|
|
if (beforeAllCleanups.length) {
|
|
await $("suite.cleanup", () => callCleanupHooks(runner, beforeAllCleanups));
|
|
}
|
|
if (suite.file === suite) {
|
|
const context = getFileContext(suite);
|
|
await callFixtureCleanup(context);
|
|
}
|
|
} catch (e) {
|
|
failTask(suite.result, e, runner.config.diffOptions);
|
|
}
|
|
if (suite.mode === "run" || suite.mode === "queued") {
|
|
if (!runner.config.passWithNoTests && !hasTests(suite)) {
|
|
var _suite$result$errors;
|
|
suite.result.state = "fail";
|
|
if (!((_suite$result$errors = suite.result.errors) === null || _suite$result$errors === void 0 ? void 0 : _suite$result$errors.length)) {
|
|
const error = processError(new Error(`No test found in suite ${suite.name}`));
|
|
suite.result.errors = [error];
|
|
}
|
|
} else if (hasFailed(suite)) {
|
|
suite.result.state = "fail";
|
|
} else {
|
|
suite.result.state = "pass";
|
|
}
|
|
}
|
|
suite.result.duration = now() - start;
|
|
await ((_runner$onAfterRunSui = runner.onAfterRunSuite) === null || _runner$onAfterRunSui === void 0 ? void 0 : _runner$onAfterRunSui.call(runner, suite));
|
|
updateTask("suite-finished", suite, runner);
|
|
}
|
|
}
|
|
let limitMaxConcurrency;
|
|
async function runSuiteChild(c, runner) {
|
|
const $ = runner.trace;
|
|
if (c.type === "test") {
|
|
return limitMaxConcurrency(() => {
|
|
var _c$location, _c$location2;
|
|
return $("run.test", {
|
|
"vitest.test.id": c.id,
|
|
"vitest.test.name": c.name,
|
|
"vitest.test.mode": c.mode,
|
|
"vitest.test.timeout": c.timeout,
|
|
"code.file.path": c.file.filepath,
|
|
"code.line.number": (_c$location = c.location) === null || _c$location === void 0 ? void 0 : _c$location.line,
|
|
"code.column.number": (_c$location2 = c.location) === null || _c$location2 === void 0 ? void 0 : _c$location2.column
|
|
}, () => runTest(c, runner));
|
|
});
|
|
} else if (c.type === "suite") {
|
|
var _c$location3, _c$location4;
|
|
return $("run.suite", {
|
|
"vitest.suite.id": c.id,
|
|
"vitest.suite.name": c.name,
|
|
"vitest.suite.mode": c.mode,
|
|
"code.file.path": c.file.filepath,
|
|
"code.line.number": (_c$location3 = c.location) === null || _c$location3 === void 0 ? void 0 : _c$location3.line,
|
|
"code.column.number": (_c$location4 = c.location) === null || _c$location4 === void 0 ? void 0 : _c$location4.column
|
|
}, () => runSuite(c, runner));
|
|
}
|
|
}
|
|
async function runFiles(files, runner) {
|
|
limitMaxConcurrency ?? (limitMaxConcurrency = limitConcurrency(runner.config.maxConcurrency));
|
|
for (const file of files) {
|
|
if (!file.tasks.length && !runner.config.passWithNoTests) {
|
|
var _file$result;
|
|
if (!((_file$result = file.result) === null || _file$result === void 0 || (_file$result = _file$result.errors) === null || _file$result === void 0 ? void 0 : _file$result.length)) {
|
|
const error = processError(new Error(`No test suite found in file ${file.filepath}`));
|
|
file.result = {
|
|
state: "fail",
|
|
errors: [error]
|
|
};
|
|
}
|
|
}
|
|
await runner.trace("run.spec", {
|
|
"code.file.path": file.filepath,
|
|
"vitest.suite.tasks.length": file.tasks.length
|
|
}, () => runSuite(file, runner));
|
|
}
|
|
}
|
|
const workerRunners = new WeakSet();
|
|
function defaultTrace(_, attributes, cb) {
|
|
if (typeof attributes === "function") {
|
|
return attributes();
|
|
}
|
|
return cb();
|
|
}
|
|
async function startTests(specs, runner) {
|
|
var _runner$cancel;
|
|
runner.trace ?? (runner.trace = defaultTrace);
|
|
const cancel = (_runner$cancel = runner.cancel) === null || _runner$cancel === void 0 ? void 0 : _runner$cancel.bind(runner);
|
|
// Ideally, we need to have an event listener for this, but only have a runner here.
|
|
// Adding another onCancel felt wrong (maybe it needs to be refactored)
|
|
runner.cancel = (reason) => {
|
|
// We intentionally create only one error since there is only one test run that can be cancelled
|
|
const error = new TestRunAbortError("The test run was aborted by the user.", reason);
|
|
getRunningTests().forEach((test) => abortContextSignal(test.context, error));
|
|
return cancel === null || cancel === void 0 ? void 0 : cancel(reason);
|
|
};
|
|
if (!workerRunners.has(runner)) {
|
|
var _runner$onCleanupWork;
|
|
(_runner$onCleanupWork = runner.onCleanupWorkerContext) === null || _runner$onCleanupWork === void 0 ? void 0 : _runner$onCleanupWork.call(runner, async () => {
|
|
var _runner$getWorkerCont;
|
|
const context = (_runner$getWorkerCont = runner.getWorkerContext) === null || _runner$getWorkerCont === void 0 ? void 0 : _runner$getWorkerCont.call(runner);
|
|
if (context) {
|
|
await callFixtureCleanup(context);
|
|
}
|
|
});
|
|
workerRunners.add(runner);
|
|
}
|
|
try {
|
|
var _runner$onBeforeColle, _runner$onCollected, _runner$onBeforeRunFi, _runner$onAfterRunFil;
|
|
const paths = specs.map((f) => typeof f === "string" ? f : f.filepath);
|
|
await ((_runner$onBeforeColle = runner.onBeforeCollect) === null || _runner$onBeforeColle === void 0 ? void 0 : _runner$onBeforeColle.call(runner, paths));
|
|
const files = await collectTests(specs, runner);
|
|
await ((_runner$onCollected = runner.onCollected) === null || _runner$onCollected === void 0 ? void 0 : _runner$onCollected.call(runner, files));
|
|
await ((_runner$onBeforeRunFi = runner.onBeforeRunFiles) === null || _runner$onBeforeRunFi === void 0 ? void 0 : _runner$onBeforeRunFi.call(runner, files));
|
|
await runFiles(files, runner);
|
|
await ((_runner$onAfterRunFil = runner.onAfterRunFiles) === null || _runner$onAfterRunFil === void 0 ? void 0 : _runner$onAfterRunFil.call(runner, files));
|
|
await finishSendTasksUpdate(runner);
|
|
return files;
|
|
} finally {
|
|
runner.cancel = cancel;
|
|
}
|
|
}
|
|
async function publicCollect(specs, runner) {
|
|
var _runner$onBeforeColle2, _runner$onCollected2;
|
|
runner.trace ?? (runner.trace = defaultTrace);
|
|
const paths = specs.map((f) => typeof f === "string" ? f : f.filepath);
|
|
await ((_runner$onBeforeColle2 = runner.onBeforeCollect) === null || _runner$onBeforeColle2 === void 0 ? void 0 : _runner$onBeforeColle2.call(runner, paths));
|
|
const files = await collectTests(specs, runner);
|
|
await ((_runner$onCollected2 = runner.onCollected) === null || _runner$onCollected2 === void 0 ? void 0 : _runner$onCollected2.call(runner, files));
|
|
return files;
|
|
}
|
|
|
|
/**
|
|
* @experimental
|
|
* @advanced
|
|
*
|
|
* Records a custom test artifact during test execution.
|
|
*
|
|
* This function allows you to attach structured data, files, or metadata to a test.
|
|
*
|
|
* Vitest automatically injects the source location where the artifact was created and manages any attachments you include.
|
|
*
|
|
* @param task - The test task context, typically accessed via `this.task` in custom matchers or `context.task` in tests
|
|
* @param artifact - The artifact to record. Must extend {@linkcode TestArtifactBase}
|
|
*
|
|
* @returns A promise that resolves to the recorded artifact with location injected
|
|
*
|
|
* @throws {Error} If called after the test has finished running
|
|
* @throws {Error} If the test runner doesn't support artifacts
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* // In a custom assertion
|
|
* async function toHaveValidSchema(this: MatcherState, actual: unknown) {
|
|
* const validation = validateSchema(actual)
|
|
*
|
|
* await recordArtifact(this.task, {
|
|
* type: 'my-plugin:schema-validation',
|
|
* passed: validation.valid,
|
|
* errors: validation.errors,
|
|
* })
|
|
*
|
|
* return { pass: validation.valid, message: () => '...' }
|
|
* }
|
|
* ```
|
|
*/
|
|
async function recordArtifact(task, artifact) {
|
|
const runner = getRunner();
|
|
if (task.result && task.result.state !== "run") {
|
|
throw new Error(`Cannot record a test artifact outside of the test run. The test "${task.name}" finished running with the "${task.result.state}" state already.`);
|
|
}
|
|
const stack = findTestFileStackTrace(task.file.filepath, new Error("STACK_TRACE").stack);
|
|
if (stack) {
|
|
artifact.location = {
|
|
file: stack.file,
|
|
line: stack.line,
|
|
column: stack.column
|
|
};
|
|
if (artifact.type === "internal:annotation") {
|
|
artifact.annotation.location = artifact.location;
|
|
}
|
|
}
|
|
if (Array.isArray(artifact.attachments)) {
|
|
for (const attachment of artifact.attachments) {
|
|
manageArtifactAttachment(attachment);
|
|
}
|
|
}
|
|
// annotations won't resolve as artifacts for backwards compatibility until next major
|
|
if (artifact.type === "internal:annotation") {
|
|
return artifact;
|
|
}
|
|
if (!runner.onTestArtifactRecord) {
|
|
throw new Error(`Test runner doesn't support test artifacts.`);
|
|
}
|
|
await finishSendTasksUpdate(runner);
|
|
const resolvedArtifact = await runner.onTestArtifactRecord(task, artifact);
|
|
task.artifacts.push(resolvedArtifact);
|
|
return resolvedArtifact;
|
|
}
|
|
const table = [];
|
|
for (let i = 65; i < 91; i++) {
|
|
table.push(String.fromCharCode(i));
|
|
}
|
|
for (let i = 97; i < 123; i++) {
|
|
table.push(String.fromCharCode(i));
|
|
}
|
|
for (let i = 0; i < 10; i++) {
|
|
table.push(i.toString(10));
|
|
}
|
|
table.push("+", "/");
|
|
function encodeUint8Array(bytes) {
|
|
let base64 = "";
|
|
const len = bytes.byteLength;
|
|
for (let i = 0; i < len; i += 3) {
|
|
if (len === i + 1) {
|
|
const a = (bytes[i] & 252) >> 2;
|
|
const b = (bytes[i] & 3) << 4;
|
|
base64 += table[a];
|
|
base64 += table[b];
|
|
base64 += "==";
|
|
} else if (len === i + 2) {
|
|
const a = (bytes[i] & 252) >> 2;
|
|
const b = (bytes[i] & 3) << 4 | (bytes[i + 1] & 240) >> 4;
|
|
const c = (bytes[i + 1] & 15) << 2;
|
|
base64 += table[a];
|
|
base64 += table[b];
|
|
base64 += table[c];
|
|
base64 += "=";
|
|
} else {
|
|
const a = (bytes[i] & 252) >> 2;
|
|
const b = (bytes[i] & 3) << 4 | (bytes[i + 1] & 240) >> 4;
|
|
const c = (bytes[i + 1] & 15) << 2 | (bytes[i + 2] & 192) >> 6;
|
|
const d = bytes[i + 2] & 63;
|
|
base64 += table[a];
|
|
base64 += table[b];
|
|
base64 += table[c];
|
|
base64 += table[d];
|
|
}
|
|
}
|
|
return base64;
|
|
}
|
|
/**
|
|
* Records an async operation associated with a test task.
|
|
*
|
|
* This function tracks promises that should be awaited before a test completes.
|
|
* The promise is automatically removed from the test's promise list once it settles.
|
|
*/
|
|
function recordAsyncOperation(test, promise) {
|
|
// if promise is explicitly awaited, remove it from the list
|
|
promise = promise.finally(() => {
|
|
if (!test.promises) {
|
|
return;
|
|
}
|
|
const index = test.promises.indexOf(promise);
|
|
if (index !== -1) {
|
|
test.promises.splice(index, 1);
|
|
}
|
|
});
|
|
// record promise
|
|
if (!test.promises) {
|
|
test.promises = [];
|
|
}
|
|
test.promises.push(promise);
|
|
return promise;
|
|
}
|
|
/**
|
|
* Validates and prepares a test attachment for serialization.
|
|
*
|
|
* This function ensures attachments have either `body` or `path` set (but not both), and converts `Uint8Array` bodies to base64-encoded strings for easier serialization.
|
|
*
|
|
* @param attachment - The attachment to validate and prepare
|
|
*
|
|
* @throws {TypeError} If neither `body` nor `path` is provided
|
|
* @throws {TypeError} If both `body` and `path` are provided
|
|
*/
|
|
function manageArtifactAttachment(attachment) {
|
|
if (attachment.body == null && !attachment.path) {
|
|
throw new TypeError(`Test attachment requires "body" or "path" to be set. Both are missing.`);
|
|
}
|
|
if (attachment.body && attachment.path) {
|
|
throw new TypeError(`Test attachment requires only one of "body" or "path" to be set. Both are specified.`);
|
|
}
|
|
// convert to a string so it's easier to serialise
|
|
if (attachment.body instanceof Uint8Array) {
|
|
attachment.body = encodeUint8Array(attachment.body);
|
|
}
|
|
}
|
|
|
|
export { afterAll, afterEach, beforeAll, beforeEach, publicCollect as collectTests, createTaskCollector, describe, getCurrentSuite, getCurrentTest, getFn, getHooks, it, onTestFailed, onTestFinished, recordArtifact, setFn, setHooks, startTests, suite, test, updateTask };
|