allure-vitest
Version:
Allure Vitest integration
445 lines • 16.2 kB
JavaScript
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