Skip to content

Commit 3c23fa9

Browse files
todticlaude
andcommitted
fix(allure-cypress): don't reset spec context on duplicate cypress_run_start
When a spec navigates to a cross-origin URL via cy.visit, Cypress can re-initialise the browser-side support code in the secondary origin context. If the Allure env is not yet present there, initializeAllure() re-registers Mocha event listeners and the Mocha runner fires another "start" event, which enqueues a second cypress_run_start message for the same spec path. Previously #startRun always called #initializeSpecContext, throwing away the live context and all accumulated test state. This left in-progress tests without a corresponding stop message, resulting in duplicate or pending/skipped result files in the Allure report even though the tests actually passed in Cypress. Fix: skip re-initialisation when the context already exists. In cypress open the spec context is deleted by endSpec before the user can trigger a re-run, so the guard does not affect that mode. Fixes: #1329 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b52d149 commit 3c23fa9

2 files changed

Lines changed: 106 additions & 6 deletions

File tree

packages/allure-cypress/src/reporter.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -219,12 +219,19 @@ export class AllureCypress {
219219
};
220220

221221
#startRun = (absolutePath: string) => {
222-
// This function is executed once on `cypress run`, but it can be executed
223-
// multiple times during an interactive session (`cypress open`). Ideally,
224-
// in that case, we should remove previous result objects that haven't been
225-
// written yet, but it would've required support in ReporterRuntime.
226-
// Currently, we're discarding the entire spec context.
227-
this.#initializeSpecContext(absolutePath);
222+
// In `cypress run` this fires exactly once per spec. In `cypress open`
223+
// it may fire again when the user re-runs a spec; by then `endSpec` has
224+
// already deleted the context, so the `has` check is false and we start
225+
// fresh as expected.
226+
// If a spec triggers a secondary browser context (e.g. cross-origin
227+
// cy.visit), Cypress may re-fire the Mocha "start" event mid-run, sending
228+
// a second cypress_run_start for the same spec path while the first
229+
// context is still live. Reinitialising in that case would discard all
230+
// in-progress state and produce duplicate / pending result files, so we
231+
// skip the initialisation when the context already exists.
232+
if (!this.specContextByAbsolutePath.has(absolutePath)) {
233+
this.#initializeSpecContext(absolutePath);
234+
}
228235
};
229236

230237
#startSuite = (context: SpecContext, { data: { id, name, root } }: CypressSuiteStartMessage) => {
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { Stage, Status } from "allure-js-commons";
2+
import { InMemoryWriter, ReporterRuntime } from "allure-js-commons/sdk/reporter";
3+
import { expect, it } from "vitest";
4+
5+
import { AllureCypress } from "../../src/reporter.js";
6+
import type { AllureCypressTaskArgs, CypressMessage } from "../../src/types.js";
7+
8+
const SPEC_PATH = "/cypress/e2e/sample.cy.js";
9+
10+
const makeReporter = () => {
11+
const writer = new InMemoryWriter();
12+
const reporter = new AllureCypress({});
13+
reporter.allureRuntime = new ReporterRuntime({ writer });
14+
return { reporter, writer };
15+
};
16+
17+
const captureTaskHandlers = (reporter: AllureCypress) => {
18+
const tasks: Record<string, (args: AllureCypressTaskArgs) => null> = {};
19+
reporter.attachToCypress(((event: string, handlers: Record<string, (args: AllureCypressTaskArgs) => null>) => {
20+
if (event === "task") {
21+
Object.assign(tasks, handlers);
22+
}
23+
}) as any);
24+
return tasks;
25+
};
26+
27+
const sendMessages = (
28+
tasks: Record<string, (args: AllureCypressTaskArgs) => null>,
29+
messages: CypressMessage[],
30+
{ isFinal = false } = {},
31+
) => {
32+
const args: AllureCypressTaskArgs = { absolutePath: SPEC_PATH, messages, isInteractive: false };
33+
if (isFinal) {
34+
tasks.reportFinalAllureCypressSpecMessages(args);
35+
} else {
36+
tasks.reportAllureCypressSpecMessages(args);
37+
}
38+
};
39+
40+
const makeRunMessages = (): CypressMessage[] => [
41+
{ type: "cypress_run_start", data: {} },
42+
{ type: "cypress_suite_start", data: { id: "root", name: "", root: true, start: 0 } },
43+
{ type: "cypress_suite_start", data: { id: "s1", name: "Suite", root: false, start: 0 } },
44+
];
45+
46+
const makeTestMessages = (name: string): CypressMessage[] => [
47+
{ type: "cypress_test_start", data: { name, fullNameSuffix: name, start: 0, labels: [] } },
48+
{ type: "cypress_test_pass", data: {} },
49+
{ type: "cypress_test_end", data: { duration: 1, retries: 0 } },
50+
];
51+
52+
const makeEndMessages = (): CypressMessage[] => [
53+
{ type: "cypress_suite_end", data: { root: false, stop: 1 } },
54+
{ type: "cypress_suite_end", data: { root: true, stop: 1 } },
55+
];
56+
57+
it("preserves spec context when cypress_run_start is received a second time", () => {
58+
const { reporter } = makeReporter();
59+
const tasks = captureTaskHandlers(reporter);
60+
61+
sendMessages(tasks, makeRunMessages());
62+
63+
const contextAfterFirstStart = reporter.specContextByAbsolutePath.get(SPEC_PATH);
64+
expect(contextAfterFirstStart).toBeDefined();
65+
66+
// Simulate a second cypress_run_start for the same spec (e.g. from a cross-origin cy.visit
67+
// causing the browser-side support code to re-initialise mid-run).
68+
sendMessages(tasks, [{ type: "cypress_run_start", data: {} }]);
69+
70+
const contextAfterSecondStart = reporter.specContextByAbsolutePath.get(SPEC_PATH);
71+
expect(contextAfterSecondStart).toBe(contextAfterFirstStart);
72+
});
73+
74+
it("produces exactly the expected test results when cypress_run_start is duplicated mid-run", () => {
75+
const { reporter, writer } = makeReporter();
76+
const tasks = captureTaskHandlers(reporter);
77+
78+
// Batch 1: run starts, first test runs and completes.
79+
sendMessages(tasks, [...makeRunMessages(), ...makeTestMessages("test one")]);
80+
81+
// Batch 2: spurious second cypress_run_start (cross-origin scenario), then second test.
82+
sendMessages(tasks, [{ type: "cypress_run_start", data: {} }, ...makeTestMessages("test two"), ...makeEndMessages()]);
83+
84+
// Finalise (non-interactive: endSpec is called via after:spec, simulate it directly).
85+
reporter.endSpec(SPEC_PATH);
86+
87+
expect(writer.tests).toHaveLength(2);
88+
for (const test of writer.tests) {
89+
expect(test.status).toBe(Status.PASSED);
90+
expect(test.stage).toBe(Stage.FINISHED);
91+
}
92+
expect(writer.tests.map((t) => t.name)).toEqual(expect.arrayContaining(["test one", "test two"]));
93+
});

0 commit comments

Comments
 (0)