Skip to content

Commit d118b6c

Browse files
authored
fix cypress origin serialization (fix #1306) (#1360)
1 parent 0f1c022 commit d118b6c

File tree

4 files changed

+161
-60
lines changed

4 files changed

+161
-60
lines changed

packages/allure-cypress/src/browser/runtime.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { Status } from "allure-js-commons";
21
import type { AttachmentOptions, Label, Link, ParameterMode, ParameterOptions } from "allure-js-commons";
2+
import { Status } from "allure-js-commons";
33
import { getMessageAndTraceFromError } from "allure-js-commons/sdk";
4-
import { getGlobalTestRuntime, setGlobalTestRuntime } from "allure-js-commons/sdk/runtime";
54
import type { TestRuntime } from "allure-js-commons/sdk/runtime";
5+
import { getGlobalTestRuntime, setGlobalTestRuntime } from "allure-js-commons/sdk/runtime";
66
import type { AllureCypressTaskArgs, CypressMessage } from "../types.js";
77
import { enqueueRuntimeMessage, getRuntimeMessages, setRuntimeMessages } from "./state.js";
88
import { ALLURE_STEP_CMD_SUBJECT, startAllureApiStep, stopCurrentAllureApiStep } from "./steps.js";
@@ -126,7 +126,13 @@ class AllureCypressTestRuntime implements TestRuntime {
126126
});
127127
}
128128

129-
logStep(name: string, status: Status = Status.PASSED, error?: Error) {
129+
logStep(name: string, status: Status = Status.PASSED, error?: Error): PromiseLike<void> {
130+
if (this.#isInOriginContext()) {
131+
startAllureApiStep(name);
132+
stopCurrentAllureApiStep(status, error ? getMessageAndTraceFromError(error) : undefined);
133+
return Cypress.Promise.resolve();
134+
}
135+
130136
return cy
131137
.wrap(ALLURE_STEP_CMD_SUBJECT, { log: false })
132138
.then(() => {
@@ -203,4 +209,44 @@ class AllureCypressTestRuntime implements TestRuntime {
203209
this.#resetMessages();
204210
return messages;
205211
}
212+
213+
#isInOriginContext(): boolean {
214+
try {
215+
const hasOriginContext = !!(window as any).cypressOriginContext;
216+
const hasOriginWindow = !!(window as any).cypressOriginWindow;
217+
218+
if (hasOriginContext || hasOriginWindow) {
219+
return true;
220+
}
221+
222+
const baseUrl = Cypress.config("baseUrl");
223+
const currentOrigin = window.location.origin;
224+
225+
if (baseUrl && currentOrigin !== baseUrl) {
226+
return true;
227+
}
228+
229+
const cypressInstance = (window as any).Cypress;
230+
231+
if (cypressInstance && cypressInstance.state && cypressInstance.state("origin")) {
232+
return true;
233+
}
234+
235+
try {
236+
const cyExists = typeof cy !== "undefined";
237+
const cyTaskExists = typeof cy.task !== "undefined";
238+
239+
// In cy.origin context, cy.task may not be available or may throw
240+
if (!cyExists || !cyTaskExists) {
241+
return true;
242+
}
243+
} catch (error) {
244+
return true;
245+
}
246+
247+
return false;
248+
} catch (error) {
249+
return true;
250+
}
251+
}
206252
}

packages/allure-cypress/src/browser/serialize.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,43 @@
11
import { serialize } from "allure-js-commons/sdk";
22
import { getConfig } from "./state.js";
33

4-
export default (value: unknown) =>
5-
isDomObject(value) ? stringifyAsDom(value) : serialize(value, getSerializeOptions());
4+
export default (value: unknown) => {
5+
return isDomObject(value) ? stringifyAsDom(value) : serializeAsObject(value);
6+
};
7+
8+
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
9+
typeof value === "object" && value !== null && !Array.isArray(value);
10+
11+
const stripAncestorRefs = (value: unknown, ancestors: object[] = []): unknown => {
12+
if (!isPlainObject(value)) {
13+
return value;
14+
}
15+
16+
const result: Record<string, unknown> = {};
17+
ancestors.push(value);
18+
19+
for (const [key, prop] of Object.entries(value)) {
20+
if (typeof prop === "object" && prop !== null) {
21+
if (ancestors.includes(prop)) {
22+
continue;
23+
}
24+
result[key] = stripAncestorRefs(prop, ancestors);
25+
} else {
26+
result[key] = prop;
27+
}
28+
}
29+
30+
ancestors.pop();
31+
return result;
32+
};
33+
34+
export const serializeAsObject = (value: unknown) => {
35+
if (isPlainObject(value)) {
36+
const cleaned = stripAncestorRefs(value);
37+
return serialize({ ...(cleaned as object) }, getSerializeOptions());
38+
}
39+
return serialize(value, getSerializeOptions());
40+
};
641

742
const getSerializeOptions = () => {
843
const {

packages/allure-cypress/test/spec/commands.test.ts

Lines changed: 75 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -211,60 +211,46 @@ describe("parameter serialization", () => {
211211
`,
212212
});
213213

214-
expect(tests).toEqual([
215-
expect.objectContaining({
216-
steps: [
217-
expect.objectContaining({
218-
parameters: [
219-
{
220-
name: "Yielded",
221-
value: JSON.stringify({}),
222-
},
223-
],
224-
}),
225-
expect.objectContaining({
226-
parameters: [
227-
{
228-
name: "Yielded",
229-
value: JSON.stringify({ ref: { foo: {}, bar: {} } }),
230-
},
231-
],
232-
}),
233-
expect.objectContaining({
234-
parameters: [
235-
{
236-
name: "Yielded",
237-
value: JSON.stringify({ ref: {} }),
238-
},
239-
],
240-
}),
241-
expect.objectContaining({
242-
parameters: [
243-
{
244-
name: "Yielded",
245-
value: `${"A".repeat(128)}...`,
246-
},
247-
],
248-
}),
249-
expect.objectContaining({
250-
parameters: [
251-
{
252-
name: "Yielded",
253-
value: `[${String.raw`"A",`.repeat(31)}"A"...`,
254-
},
255-
],
256-
}),
257-
expect.objectContaining({
258-
parameters: [
259-
{
260-
name: "Yielded",
261-
value: JSON.stringify({ foo: { bar: { qux: "qut" } } }),
262-
},
263-
],
264-
}),
265-
],
266-
}),
267-
]);
214+
expect(tests).toHaveLength(1);
215+
const steps = tests[0].steps;
216+
expect(steps).toHaveLength(6);
217+
218+
const [step1, step2, step3, step4, step5, step6] = steps;
219+
220+
expect(step1).toMatchObject({
221+
parameters: [{ name: "Yielded", value: JSON.stringify({}) }],
222+
});
223+
expect(step1.parameters).toHaveLength(1);
224+
225+
expect(step2).toMatchObject({
226+
parameters: [{ name: "Yielded", value: JSON.stringify({ ref: { foo: {}, bar: {} } }) }],
227+
});
228+
expect(step2.parameters).toHaveLength(1);
229+
230+
expect(step3).toMatchObject({
231+
parameters: [{ name: "Yielded", value: JSON.stringify({ ref: {} }) }],
232+
});
233+
expect(step3.parameters).toHaveLength(1);
234+
235+
expect(step4).toMatchObject({
236+
parameters: [{ name: "Yielded", value: `${"A".repeat(128)}...` }],
237+
});
238+
expect(step4.parameters).toHaveLength(1);
239+
240+
expect(step5).toMatchObject({
241+
parameters: [
242+
{
243+
name: "Yielded",
244+
value: `[${`"A",`.repeat(31)}"A"...`,
245+
},
246+
],
247+
});
248+
expect(step5.parameters).toHaveLength(1);
249+
250+
expect(step6).toMatchObject({
251+
parameters: [{ name: "Yielded", value: JSON.stringify({ foo: { bar: { qux: "qut" } } }) }],
252+
});
253+
expect(step6.parameters).toHaveLength(1);
268254
});
269255

270256
it("should take the limits from the config", async () => {
@@ -1021,7 +1007,7 @@ it("should support commands in hooks", async () => {
10211007
]);
10221008
});
10231009

1024-
it("should log cy.origin commands correctly in cross-origin testing", async () => {
1010+
it("should log cy.origin commands correctly in cross-origin testing without hanging", async () => {
10251011
issue("1280");
10261012
const { tests } = await runCypressInlineTest({
10271013
"cypress/e2e/sample.cy.js": () => `
@@ -1061,3 +1047,37 @@ it("should log cy.origin commands correctly in cross-origin testing", async () =
10611047
}),
10621048
]);
10631049
});
1050+
1051+
it("should not hang when using cy.origin with allure steps", async () => {
1052+
const { tests } = await runCypressInlineTest({
1053+
"cypress/e2e/origin-hang-test.cy.js": () => `
1054+
it("should complete cy.origin without hanging", () => {
1055+
cy.origin("https://example.com", () => {
1056+
cy.log("inside origin");
1057+
cy.wrap("test").should("eq", "test");
1058+
});
1059+
cy.log("outside origin");
1060+
});
1061+
`,
1062+
});
1063+
1064+
expect(tests).toEqual([
1065+
expect.objectContaining({
1066+
name: "should complete cy.origin without hanging",
1067+
status: Status.PASSED,
1068+
stage: Stage.FINISHED,
1069+
steps: [
1070+
expect.objectContaining({
1071+
name: "origin https://example.com",
1072+
status: Status.PASSED,
1073+
stage: Stage.FINISHED,
1074+
}),
1075+
expect.objectContaining({
1076+
name: "log outside origin",
1077+
status: Status.PASSED,
1078+
stage: Stage.FINISHED,
1079+
}),
1080+
],
1081+
}),
1082+
]);
1083+
});

0 commit comments

Comments
 (0)