UNPKG

allure-vitest

Version:
445 lines 16.2 kB
import { Status } from "allure-js-commons"; import { getMessageAndTraceFromError, getStatusFromError } from "allure-js-commons/sdk"; import { getGlobalTestRuntime } from "allure-js-commons/sdk/runtime"; import { chai } from "vitest"; import { markAsMatcherMessage } from "./matcherMessages.js"; import { getCurrentTask } from "./runtime.js"; const ALLURE_VITEST_EXPECT_PATCHED = Symbol.for("allure.vitest.expectPatched"); const ALLURE_VITEST_EXPECT_EXTEND_PATCHED = Symbol.for("allure.vitest.expectExtendPatched"); const ALLURE_VITEST_EXPECT_WRAPPED = Symbol.for("allure.vitest.expectWrapped"); const MAX_VALUE_LENGTH = 160; const MAX_VALUE_DEPTH = 2; const ACTIVE_MATCHER_STEP_FLAG = "allure-vitest-active-matcher-step"; const JEST_ASYMMETRIC_MATCHER = Symbol.for("jest.asymmetricMatcher"); // Vitest doesn't expose a public marker for "this Chai assertion came from Vitest expect". // Its expect() implementation marks those assertions through withTest() with this flag; // we only read it and let Vitest remain the owner of the flag. const VITEST_TEST_FLAG = "vitest-test"; const LANGUAGE_CHAINS = new Set([ "to", "be", "been", "is", "and", "has", "have", "with", "that", "which", "at", "of", "same", "but", "does", "still", "also", ]); const MODIFIER_CHAINS = new Set([ "not", "deep", "nested", "own", "ordered", "any", "all", "itself", "resolves", "rejects", ]); const SKIPPED_ASSERTION_METHODS = new Set(["assert", "constructor", "withContext", "withTest"]); const SKIPPED_ASSERTION_PROPERTIES = new Set(["_obj", "__flags", "__methods", "callable", "iterable", "numeric"]); const ASYMMETRIC_MATCHER_FACTORIES = new Map([ ["ArrayContaining", "arrayContaining"], ["ObjectContaining", "objectContaining"], ["SchemaMatching", "schemaMatching"], ["StringContaining", "stringContaining"], ["StringMatching", "stringMatching"], ]); const isWrapped = (value) => typeof value === "function" && Boolean(value[ALLURE_VITEST_EXPECT_WRAPPED]); const isAsymmetricMatcher = (value) => Boolean(value && typeof value === "object" && value.$$typeof === JEST_ASYMMETRIC_MATCHER); const markWrapped = (value) => { Object.defineProperty(value, ALLURE_VITEST_EXPECT_WRAPPED, { value: true, }); return value; }; const limitString = (value, maxLength) => value.length > maxLength ? `${value.slice(0, maxLength)}... <truncated>` : value; const formatFunction = (value) => { if (value.prototype instanceof Error) { return value.name || "Error"; } return value.name ? `[Function ${value.name}]` : "[Function]"; }; const getAsymmetricMatcherName = (value) => { try { return value.toString?.(); } catch { return undefined; } }; const isMatcherPath = (value) => /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(value); function formatAsymmetricMatcher(value) { const constructorName = value.constructor?.name; const inversePrefix = value.inverse ? "not." : ""; if (constructorName === "Any") { const sample = value.sample; const type = typeof sample === "function" && sample.name ? sample.name : formatValue(sample); return `expect.${inversePrefix}any(${type})`; } if (constructorName === "Anything") { return `expect.${inversePrefix}anything()`; } if (constructorName === "CloseTo") { const args = [value.sample, value.precision] .filter((arg) => arg !== undefined) .map(formatValue) .join(", "); return `expect.${inversePrefix}closeTo(${args})`; } if (constructorName === "CustomMatcher") { const matcherName = getAsymmetricMatcherName(value); if (matcherName && isMatcherPath(matcherName)) { const args = Array.isArray(value.sample) ? value.sample.map(formatValue).join(", ") : ""; return `expect.${matcherName}(${args})`; } } const factoryName = constructorName ? ASYMMETRIC_MATCHER_FACTORIES.get(constructorName) : undefined; if (factoryName) { return `expect.${inversePrefix}${factoryName}(${formatValue(value.sample)})`; } const matcherName = getAsymmetricMatcherName(value); if (matcherName) { return matcherName; } return "expect.asymmetricMatcher"; } const createJsonReplacer = () => { const parents = []; return function (_, value) { if (typeof value === "bigint") { return `${value}n`; } if (typeof value === "function") { return formatFunction(value); } if (typeof value === "symbol") { return value.toString(); } if (value instanceof RegExp) { return value.toString(); } if (value instanceof Error) { return { name: value.name, message: value.message, }; } if (isAsymmetricMatcher(value)) { return formatAsymmetricMatcher(value); } if (!value || typeof value !== "object") { return value; } while (parents.length > 0 && !Object.is(parents[parents.length - 1], this)) { parents.pop(); } if (parents.includes(value)) { return "[Circular]"; } if (parents.length >= MAX_VALUE_DEPTH) { return Array.isArray(value) ? "[Array]" : "[Object]"; } parents.push(value); return value; }; }; const formatValue = (value) => { if (typeof value === "string") { return limitString(JSON.stringify(value), MAX_VALUE_LENGTH); } if (typeof value === "number" || typeof value === "boolean" || value === null || value === undefined) { return String(value); } if (typeof value === "bigint") { return `${value}n`; } if (typeof value === "symbol") { return value.toString(); } if (typeof value === "function") { return formatFunction(value); } if (isAsymmetricMatcher(value)) { return limitString(formatAsymmetricMatcher(value), MAX_VALUE_LENGTH); } try { return limitString(JSON.stringify(value, createJsonReplacer()) ?? String(value), MAX_VALUE_LENGTH); } catch { return limitString(String(value), MAX_VALUE_LENGTH); } }; const formatArguments = (args) => args.map(formatValue).join(", "); const getCurrentVitestTask = () => { const task = getCurrentTask(); return task?.meta ? task : undefined; }; // In concurrent tests, Vitest's process-global getCurrentTest() can be reset after an awaited gap, // so expect() may skip withTest() even though the assertion is still running inside a known test task. // Keep that recovery narrow to Vitest/Jest-style matcher names so raw Chai methods like equal() // remain ignored. const canRecoverVitestTaskFromAsyncContext = (assertionName) => assertionName.startsWith("to"); const getVitestTask = (utils, assertion, assertionName) => { const flaggedTask = utils.flag(assertion, VITEST_TEST_FLAG); const currentTask = getCurrentVitestTask(); if (!flaggedTask?.meta) { return canRecoverVitestTaskFromAsyncContext(assertionName) ? currentTask : undefined; } return currentTask ?? flaggedTask; }; const countErrors = (task) => task?.result?.errors?.length ?? 0; const getNewSoftAssertionError = (task, beforeErrors) => { const errors = task?.result?.errors; return errors && errors.length > beforeErrors ? errors[errors.length - 1] : undefined; }; const sendMatcherMessage = (message) => { const runtime = getGlobalTestRuntime(); const matcherMessage = markAsMatcherMessage(message); if (runtime.sendMessageSync) { runtime.sendMessageSync(matcherMessage); return; } void runtime.sendMessage?.(matcherMessage); }; const startMatcherStep = (name) => { sendMatcherMessage({ type: "step_start", data: { name, start: Date.now(), }, }); let stopped = false; return (status, error) => { if (stopped) { return; } stopped = true; sendMatcherMessage({ type: "step_stop", data: { status, stop: Date.now(), statusDetails: error ? getMessageAndTraceFromError(error) : undefined, }, }); }; }; const isPromise = (value) => value instanceof Promise; const isThenable = (value) => Boolean(value && (typeof value === "object" || typeof value === "function") && typeof value.then === "function"); const observePromise = (promise, stop) => { void promise.then(() => stop(Status.PASSED), (error) => { stop(getStatusFromError(error), error); }); return promise; }; const observeThenable = (thenable, stop) => { try { void thenable.then(() => stop(Status.PASSED), (error) => { stop(getStatusFromError(error), error); }); } catch (error) { stop(getStatusFromError(error), error); } return thenable; }; const restoreActiveMatcherFlag = (utils, assertion, previousValue) => { utils.flag(assertion, ACTIVE_MATCHER_STEP_FLAG, previousValue); }; const isMatcherStepActive = (utils, assertion) => Boolean(utils.flag(assertion, ACTIVE_MATCHER_STEP_FLAG)); const runWithNestedMatcherStepsSuppressed = (utils, assertion, body) => { const previousValue = utils.flag(assertion, ACTIVE_MATCHER_STEP_FLAG); utils.flag(assertion, ACTIVE_MATCHER_STEP_FLAG, true); try { const result = body(); if (isPromise(result)) { void result.then(() => restoreActiveMatcherFlag(utils, assertion, previousValue), () => restoreActiveMatcherFlag(utils, assertion, previousValue)); return result; } if (isThenable(result)) { try { void result.then(() => restoreActiveMatcherFlag(utils, assertion, previousValue), () => restoreActiveMatcherFlag(utils, assertion, previousValue)); } catch { restoreActiveMatcherFlag(utils, assertion, previousValue); } return result; } restoreActiveMatcherFlag(utils, assertion, previousValue); return result; } catch (error) { restoreActiveMatcherFlag(utils, assertion, previousValue); throw error; } }; const buildMatcherStepName = (utils, assertion, assertionName, args, isProperty) => { const actual = formatValue(utils.flag(assertion, "object")); const promise = utils.flag(assertion, "promise"); const parts = []; if (promise) { parts.push(promise); } if (utils.flag(assertion, "negate")) { parts.push("not"); } parts.push(assertionName); const argsSuffix = isProperty ? "" : `(${formatArguments(args)})`; return `expect(${actual}).${parts.join(".")}${argsSuffix}`; }; const runMatcherStep = (task, name, body) => { const stop = startMatcherStep(name); const beforeErrors = countErrors(task); try { const result = body(); if (isPromise(result)) { return observePromise(result, stop); } if (isThenable(result)) { return observeThenable(result, stop); } const softAssertionError = getNewSoftAssertionError(task, beforeErrors); if (softAssertionError) { stop(getStatusFromError(softAssertionError), softAssertionError); } else { stop(Status.PASSED); } return result; } catch (error) { stop(getStatusFromError(error), error); throw error; } }; const wrapAssertionMethod = (utils, prototype, assertionName, descriptor) => { const original = descriptor.value; if (isWrapped(original)) { return; } const wrapped = markWrapped(function (...args) { if (isMatcherStepActive(utils, this)) { return original.apply(this, args); } const task = getVitestTask(utils, this, assertionName); if (!task || utils.flag(this, "poll")) { return original.apply(this, args); } return runMatcherStep(task, buildMatcherStepName(utils, this, assertionName, args, false), () => runWithNestedMatcherStepsSuppressed(utils, this, () => original.apply(this, args))); }); Object.defineProperty(prototype, assertionName, { ...descriptor, value: wrapped, }); }; const wrapAssertionProperty = (utils, prototype, assertionName, descriptor) => { const originalGet = descriptor.get; if (!originalGet || isWrapped(originalGet)) { return; } const wrappedGet = markWrapped(function () { if (isMatcherStepActive(utils, this)) { return originalGet.call(this); } const task = getVitestTask(utils, this, assertionName); if (!task || utils.flag(this, "poll")) { return originalGet.call(this); } return runMatcherStep(task, buildMatcherStepName(utils, this, assertionName, [], true), () => runWithNestedMatcherStepsSuppressed(utils, this, () => originalGet.call(this))); }); Object.defineProperty(prototype, assertionName, { ...descriptor, get: wrappedGet, }); }; const wrapDescriptor = (utils, prototype, assertionName) => { if (SKIPPED_ASSERTION_METHODS.has(assertionName) || SKIPPED_ASSERTION_PROPERTIES.has(assertionName)) { return; } const descriptor = Object.getOwnPropertyDescriptor(prototype, assertionName); if (!descriptor) { return; } if (typeof descriptor.value === "function") { wrapAssertionMethod(utils, prototype, assertionName, descriptor); return; } if (!descriptor.get || LANGUAGE_CHAINS.has(assertionName) || MODIFIER_CHAINS.has(assertionName)) { return; } wrapAssertionProperty(utils, prototype, assertionName, descriptor); }; const instrumentWithTest = (prototype) => { const descriptor = Object.getOwnPropertyDescriptor(prototype, "withTest"); const original = descriptor?.value; if (!descriptor || typeof original !== "function" || isWrapped(original)) { return; } const wrapped = markWrapped(function (task, ...args) { return original.call(this, getCurrentVitestTask() ?? task, ...args); }); Object.defineProperty(prototype, "withTest", { ...descriptor, value: wrapped, }); }; const instrumentExistingAssertions = (utils, prototype) => { if (prototype[ALLURE_VITEST_EXPECT_PATCHED]) { return; } Object.defineProperty(prototype, ALLURE_VITEST_EXPECT_PATCHED, { value: true, }); for (const assertionName of Object.getOwnPropertyNames(prototype)) { wrapDescriptor(utils, prototype, assertionName); } }; const instrumentExpectExtend = (utils, prototype) => { const expectStatic = chai.expect; if (expectStatic[ALLURE_VITEST_EXPECT_EXTEND_PATCHED]) { return; } Object.defineProperty(expectStatic, ALLURE_VITEST_EXPECT_EXTEND_PATCHED, { value: true, }); const descriptor = Object.getOwnPropertyDescriptor(expectStatic, "extend"); const original = descriptor?.value; if (!descriptor || typeof original !== "function" || isWrapped(original)) { return; } const wrapped = markWrapped(function (...args) { const result = original.apply(this, args); for (const matchers of args) { if (matchers && typeof matchers === "object") { Object.keys(matchers).forEach((assertionName) => wrapDescriptor(utils, prototype, assertionName)); } } return result; }); Object.defineProperty(expectStatic, "extend", { ...descriptor, value: wrapped, }); }; export const registerAllureVitestExpect = () => { const prototype = chai.Assertion.prototype; instrumentWithTest(prototype); instrumentExistingAssertions(chai.util, prototype); instrumentExpectExtend(chai.util, prototype); }; //# sourceMappingURL=matchers.js.map