Files
rea/src/browser/PlaywrightScenarioEvents.sharedContext.test.ts
T
c9982ad8bf fix(browser): release scenario listeners and allow bounded Chrome close (#2441)
* fix(browser): release scenario event listeners on finish

* fix(browser): allow Chrome cleanup to complete within bounded deadline

* style(browser): format cleanup deadline regression

* test(browser): expect popup cleanup listener

---------

Co-authored-by: LcokHcak <268763621+LcokHcak@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-13 22:16:25 +08:00

131 lines
5.3 KiB
TypeScript

import { EventEmitter } from "node:events";
import type { Page } from "playwright-core";
import { expect, it } from "vitest";
import { browserScenarioSchema } from "../domain/browserScenario.js";
import { BrowserScenarioSecrets } from "./BrowserScenarioSecrets.js";
import { PlaywrightScenarioEvents } from "./PlaywrightScenarioEvents.js";
it("omits a request without a frame in a shared context", () => {
const context = new EventEmitter();
const page = new EventEmitter() as unknown as Page;
const scenario = browserScenarioSchema.parse({
browser: { mode: "launch", executable_path: "/opt/chromium" },
start_url: { url: "https://app.example.test/" },
actions: [{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 }],
capture: { events: ["network"] },
});
const secrets = BrowserScenarioSecrets.resolve(scenario, {});
if (secrets === undefined) throw new Error("Expected resolved secrets");
const events = new PlaywrightScenarioEvents({
page,
context: context as never,
ownsContext: false,
enabled: new Set(["network"]),
secrets,
});
const unknownRequest = {
frame: () => {
throw new Error("Frame for this navigation request is unavailable");
},
url: () => "https://unrelated.example.test/private",
};
context.emit("request", unknownRequest);
expect(events.result().items).toEqual([]);
expect(events.limitations()).toContain(
"Shared-context network capture excludes requests without a known root or descendant Page frame.",
);
});
it("detaches root and popup listeners when a scenario finishes", async () => {
const context = new EventEmitter();
const page = new EventEmitter() as unknown as Page;
const popup = Object.assign(new EventEmitter(), {
url: () => "https://app.example.test/popup",
});
const scenario = browserScenarioSchema.parse({
browser: { mode: "launch", executable_path: "/opt/chromium" },
start_url: { url: "https://app.example.test/" },
actions: [{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 }],
capture: { events: ["console", "popups"] },
});
const secrets = BrowserScenarioSecrets.resolve(scenario, {});
if (secrets === undefined) throw new Error("Expected resolved secrets");
const events = new PlaywrightScenarioEvents({
page,
context: context as never,
ownsContext: false,
enabled: new Set(["console", "popups"]),
secrets,
});
(page as unknown as EventEmitter).emit("popup", popup);
expect((page as unknown as EventEmitter).listenerCount("console")).toBe(1);
expect(popup.listenerCount("console")).toBe(1);
// Popup closure is recorded, then its page-owned listeners are released.
expect(popup.listenerCount("close")).toBe(2);
await events.finish();
expect((page as unknown as EventEmitter).listenerCount("console")).toBe(0);
expect((page as unknown as EventEmitter).listenerCount("popup")).toBe(0);
expect(popup.listenerCount("console")).toBe(0);
expect(popup.listenerCount("popup")).toBe(0);
expect(popup.listenerCount("close")).toBe(0);
});
it("detaches worker and websocket listeners when a scenario finishes", async () => {
const context = new EventEmitter();
const page = new EventEmitter() as unknown as Page;
const socket = Object.assign(new EventEmitter(), {
url: () => "wss://app.example.test/socket",
});
const worker = Object.assign(new EventEmitter(), {
url: () => "https://app.example.test/worker.js",
});
const scenario = browserScenarioSchema.parse({
browser: { mode: "launch", executable_path: "/opt/chromium" },
start_url: { url: "https://app.example.test/" },
actions: [{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 }],
capture: { events: ["websockets", "workers"] },
});
const secrets = BrowserScenarioSecrets.resolve(scenario, {});
if (secrets === undefined) throw new Error("Expected resolved secrets");
const events = new PlaywrightScenarioEvents({
page,
context: context as never,
ownsContext: false,
enabled: new Set(["websockets", "workers"]),
secrets,
});
(page as unknown as EventEmitter).emit("websocket", socket);
(page as unknown as EventEmitter).emit("worker", worker);
expect(socket.listenerCount("framesent")).toBe(1);
expect(worker.listenerCount("close")).toBe(1);
await events.finish();
expect(socket.listenerCount("framesent")).toBe(0);
expect(socket.listenerCount("framereceived")).toBe(0);
expect(socket.listenerCount("close")).toBe(0);
expect(worker.listenerCount("close")).toBe(0);
});
it("detaches owned-context popup discovery when a scenario finishes", async () => {
const context = new EventEmitter();
const page = new EventEmitter() as unknown as Page;
const scenario = browserScenarioSchema.parse({
browser: { mode: "launch", executable_path: "/opt/chromium" },
start_url: { url: "https://app.example.test/" },
actions: [{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 }],
capture: { events: ["console"] },
});
const secrets = BrowserScenarioSecrets.resolve(scenario, {});
if (secrets === undefined) throw new Error("Expected resolved secrets");
const events = new PlaywrightScenarioEvents({
page,
context: context as never,
ownsContext: true,
enabled: new Set(["console"]),
secrets,
});
expect(context.listenerCount("page")).toBe(1);
await events.finish();
expect(context.listenerCount("page")).toBe(0);
});