Skip to content

Commit

Permalink
fix(jest): better support for jest test errors (fixes #1232, via #1238)
Browse files Browse the repository at this point in the history
  • Loading branch information
baev authored Jan 30, 2025
1 parent 79c2bdf commit 9dfc8b8
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 7 deletions.
49 changes: 46 additions & 3 deletions packages/allure-jest/src/environmentFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import { relative } from "node:path";
import { env } from "node:process";
import * as allure from "allure-js-commons";
import { Stage, Status, type StatusDetails, type TestResult } from "allure-js-commons";
import type { RuntimeMessage } from "allure-js-commons/sdk";
import { type RuntimeMessage, type TestPlanV1, serialize } from "allure-js-commons/sdk";
import { extractMetadataFromString, getMessageAndTraceFromError, getStatusFromError } from "allure-js-commons/sdk";
import type { TestPlanV1 } from "allure-js-commons/sdk";
import {
ReporterRuntime,
createDefaultWriter,
Expand Down Expand Up @@ -371,11 +370,55 @@ const createJestEnvironment = <T extends typeof JestEnvironment>(Base: T): T =>
// jest collects all errors, but we need to report the first one because it's a reason why the test has been failed
const [error] = errors;
const hasMultipleErrors = Array.isArray(error);
const firstError: Error = hasMultipleErrors ? error[0] : error;
const exception: Circus.Exception = hasMultipleErrors ? error[0] : error;

const firstError = this.#convertToError(exception);

// in case user throws non-Error type, the first exception is the user-thrown object,
// while the second one is provided by jest and has correct stack trace
if (hasMultipleErrors && error.length > 1) {
const secondError = this.#convertToError(error[1]);
if (!firstError.message) {
firstError.message = secondError.message;
}
if (!firstError.stack) {
firstError.stack = secondError.stack;
}
}

const details = getMessageAndTraceFromError(firstError);
const status = getStatusFromError(firstError);
return { status, details };
}

#convertToError(exception: Circus.Exception):
| Error
| {
message?: string;
stack?: string;
} {
if (!exception) {
return {};
}
// user may throw an object as well
if (typeof exception !== "object" || !("stack" in exception)) {
return {
message: serialize(exception),
};
}

const prototypeDescriptors = Object.getOwnPropertyDescriptors(Object.getPrototypeOf(exception));
const protoClone = Object.create(null, prototypeDescriptors);
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const clone = Object.create(protoClone, Object.getOwnPropertyDescriptors(exception));

return clone as
| Error
| {
message?: string;
stack?: string;
};
}
};
};

Expand Down
104 changes: 104 additions & 0 deletions packages/allure-jest/test/spec/errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { expect, it } from "vitest";
import { Status } from "allure-js-commons";
import { runJestInlineTest } from "../utils.js";

it("should handle timeout exceptions", async () => {
const { tests } = await runJestInlineTest({
"sample.test.js": `
it("should add two numbers", async () => {
await new Promise(resolve => setTimeout(resolve, 100));
expect(1 + 2).toEqual(3);
}, 50);
`,
});

expect(tests).toHaveLength(1);
const [tr] = tests;
expect(tr).toMatchObject({
status: Status.BROKEN,
statusDetails: {
message: expect.stringContaining("timeout"),
trace: expect.stringContaining("sample.test.js"),
},
});
});

it("should handle string errors", async () => {
const { tests } = await runJestInlineTest({
"sample.test.js": `
it("should add two numbers", async () => {
throw "some string";
});
`,
});

expect(tests).toHaveLength(1);
const [tr] = tests;
expect(tr).toMatchObject({
status: Status.BROKEN,
statusDetails: {
message: expect.stringContaining("some string"),
trace: expect.stringContaining("sample.test.js"),
},
});
});

it("should handle object errors", async () => {
const { tests } = await runJestInlineTest({
"sample.test.js": `
it("should add two numbers", async () => {
throw { val: "some string" };
});
`,
});

expect(tests).toHaveLength(1);
const [tr] = tests;
expect(tr).toMatchObject({
status: Status.BROKEN,
statusDetails: {
message: expect.stringContaining("some string"),
trace: expect.stringContaining("sample.test.js"),
},
});
});

it("should handle expect errors", async () => {
const { tests } = await runJestInlineTest({
"sample.test.js": `
it("should add two numbers", async () => {
expect(1).toEqual(2);
});
`,
});

expect(tests).toHaveLength(1);
const [tr] = tests;
expect(tr).toMatchObject({
status: Status.FAILED,
statusDetails: {
message: expect.stringMatching(/Expected: 2\s+Received: 1/),
trace: expect.stringContaining("sample.test.js"),
},
});
});

it("should set actual and expected values for expects", async () => {
const { tests } = await runJestInlineTest({
"sample.test.js": `
it("should add two numbers", async () => {
expect(1).toEqual(2);
});
`,
});

expect(tests).toHaveLength(1);
const [tr] = tests;
expect(tr).toMatchObject({
status: Status.FAILED,
statusDetails: {
actual: "1",
expected: "2",
},
});
});
26 changes: 22 additions & 4 deletions packages/allure-js-commons/src/sdk/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,27 @@ export const stripAnsi = (str: string): string => {
return str.replace(regex, "");
};

const actualAndExpected = (value: unknown): { actual?: string; expected?: string } => {
if (!value || typeof value !== "object") {
return {};
}

// support for jest asserts
if ("matcherResult" in value && value.matcherResult !== undefined && typeof value.matcherResult === "object") {
return {
actual: serialize((value.matcherResult as any).actual),
expected: serialize((value.matcherResult as any).expected),
};
}

const actual = "actual" in value && value.actual !== undefined ? { actual: serialize(value.actual) } : {};
const expected = "expected" in value && value.expected !== undefined ? { expected: serialize(value.expected) } : {};
return {
...actual,
...expected,
};
};

export const getMessageAndTraceFromError = (
error:
| Error
Expand All @@ -53,13 +74,10 @@ export const getMessageAndTraceFromError = (
},
): StatusDetails => {
const { message, stack } = error;
const actual = "actual" in error && error.actual !== undefined ? { actual: serialize(error.actual) } : {};
const expected = "expected" in error && error.expected !== undefined ? { expected: serialize(error.expected) } : {};
return {
message: message ? stripAnsi(message) : undefined,
trace: stack ? stripAnsi(stack) : undefined,
...actual,
...expected,
...actualAndExpected(error),
};
};

Expand Down

0 comments on commit 9dfc8b8

Please sign in to comment.