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>
This commit is contained in:
parent
9b08c2b1cb
commit
c9982ad8bf
|
|
@ -164,7 +164,15 @@ it("keeps attached-target ownership after the cleanup response timeout", async (
|
||||||
cleanup: { reason: expect.stringContaining("timed out") },
|
cleanup: { reason: expect.stringContaining("timed out") },
|
||||||
});
|
});
|
||||||
await closeStarted.promise;
|
await closeStarted.promise;
|
||||||
await vi.advanceTimersByTimeAsync(1_000);
|
let responseSettled = false;
|
||||||
|
void timedOutClose
|
||||||
|
.finally(() => {
|
||||||
|
responseSettled = true;
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
await vi.advanceTimersByTimeAsync(29_999);
|
||||||
|
expect(responseSettled).toBe(false);
|
||||||
|
await vi.advanceTimersByTimeAsync(1);
|
||||||
await timeoutExpectation;
|
await timeoutExpectation;
|
||||||
expect(settled).toEqual([]);
|
expect(settled).toEqual([]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ import { BrowserObservationError } from "../domain/browserObservationError.js";
|
||||||
import { withPlaywrightExecutionBoundary } from "./PlaywrightExecutionBoundary.js";
|
import { withPlaywrightExecutionBoundary } from "./PlaywrightExecutionBoundary.js";
|
||||||
|
|
||||||
const OPERATION = "capture_browser_scenario" as const;
|
const OPERATION = "capture_browser_scenario" as const;
|
||||||
|
// Closing a real Chrome persistent context can take several seconds even when
|
||||||
|
// it succeeds. The shorter operation deadline must not misreport cleanup.
|
||||||
|
const CLEANUP_CONFIRMATION_TIMEOUT_MS = 30_000;
|
||||||
|
|
||||||
const throwIfScenarioCancelled = (signal: AbortSignal | undefined): void => {
|
const throwIfScenarioCancelled = (signal: AbortSignal | undefined): void => {
|
||||||
if (signal?.aborted === true)
|
if (signal?.aborted === true)
|
||||||
|
|
@ -71,7 +74,11 @@ export class PlaywrightScenarioBrowserCleanupOwner {
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await withPlaywrightExecutionBoundary(() => cleanup, 1_000, signal);
|
await withPlaywrightExecutionBoundary(
|
||||||
|
() => cleanup,
|
||||||
|
CLEANUP_CONFIRMATION_TIMEOUT_MS,
|
||||||
|
signal,
|
||||||
|
);
|
||||||
} catch (cause: unknown) {
|
} catch (cause: unknown) {
|
||||||
throw new BrowserObservationError(OPERATION, "cleanup_failed", {
|
throw new BrowserObservationError(OPERATION, "cleanup_failed", {
|
||||||
cause,
|
cause,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
@ -54,10 +54,11 @@ export class PlaywrightScenarioEvents {
|
||||||
private readonly enabled: ReadonlySet<EventName>;
|
private readonly enabled: ReadonlySet<EventName>;
|
||||||
private readonly secrets: BrowserScenarioSecrets;
|
private readonly secrets: BrowserScenarioSecrets;
|
||||||
private readonly ownsContext: boolean;
|
private readonly ownsContext: boolean;
|
||||||
private readonly pages = new Set<Page>();
|
private pages = new WeakSet<Page>();
|
||||||
private readonly incompleteFamilies = new Set<EventName>();
|
private readonly incompleteFamilies = new Set<EventName>();
|
||||||
private readonly network: PlaywrightScenarioNetwork | undefined;
|
private readonly network: PlaywrightScenarioNetwork | undefined;
|
||||||
private stopNetwork: (() => void) | undefined;
|
private stopNetwork: (() => void) | undefined;
|
||||||
|
private readonly stopListeners = new Map<object, Set<() => void>>();
|
||||||
private sealed = false;
|
private sealed = false;
|
||||||
|
|
||||||
constructor(options: EventCaptureOptions) {
|
constructor(options: EventCaptureOptions) {
|
||||||
|
|
@ -72,10 +73,12 @@ export class PlaywrightScenarioEvents {
|
||||||
this.secrets = options.secrets;
|
this.secrets = options.secrets;
|
||||||
this.ownsContext = options.ownsContext === true;
|
this.ownsContext = options.ownsContext === true;
|
||||||
this.pages.add(options.page);
|
this.pages.add(options.page);
|
||||||
if (this.ownsContext && this.enabled.size > 0)
|
if (this.ownsContext && this.enabled.size > 0) {
|
||||||
(options.context ?? options.page.context()).on("page", (page) =>
|
const context = options.context ?? options.page.context();
|
||||||
this.popup(page),
|
const onPage = (page: Page) => this.popup(page);
|
||||||
);
|
context.on("page", onPage);
|
||||||
|
this.retainListener(context, () => context.off("page", onPage));
|
||||||
|
}
|
||||||
if (this.enabled.has("network")) {
|
if (this.enabled.has("network")) {
|
||||||
const context = options.context ?? options.page.context();
|
const context = options.context ?? options.page.context();
|
||||||
const collector = new PlaywrightScenarioNetwork({
|
const collector = new PlaywrightScenarioNetwork({
|
||||||
|
|
@ -138,6 +141,8 @@ export class PlaywrightScenarioEvents {
|
||||||
async finish(): Promise<void> {
|
async finish(): Promise<void> {
|
||||||
this.stopNetwork?.();
|
this.stopNetwork?.();
|
||||||
this.stopNetwork = undefined;
|
this.stopNetwork = undefined;
|
||||||
|
for (const owner of this.stopListeners.keys()) this.releaseListeners(owner);
|
||||||
|
this.pages = new WeakSet<Page>();
|
||||||
try {
|
try {
|
||||||
await this.network?.finish();
|
await this.network?.finish();
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -184,34 +189,81 @@ export class PlaywrightScenarioEvents {
|
||||||
return this.sequence;
|
return this.sequence;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private retainListener(owner: object, stop: () => void): void {
|
||||||
|
let stops = this.stopListeners.get(owner);
|
||||||
|
if (stops === undefined) {
|
||||||
|
stops = new Set();
|
||||||
|
this.stopListeners.set(owner, stops);
|
||||||
|
}
|
||||||
|
stops.add(stop);
|
||||||
|
}
|
||||||
|
|
||||||
|
private releaseListeners(owner: object): void {
|
||||||
|
const stops = this.stopListeners.get(owner);
|
||||||
|
this.stopListeners.delete(owner);
|
||||||
|
for (const stop of stops ?? []) stop();
|
||||||
|
}
|
||||||
|
|
||||||
private observePage(page: Page): void {
|
private observePage(page: Page): void {
|
||||||
if (this.enabled.has("console"))
|
if (this.enabled.size > 0) {
|
||||||
page.on("console", (message) => this.console(message));
|
const onClose = () => {
|
||||||
if (this.enabled.has("page-errors"))
|
this.releaseListeners(page);
|
||||||
page.on("pageerror", (error) => {
|
};
|
||||||
|
page.on("close", onClose);
|
||||||
|
this.retainListener(page, () => page.off("close", onClose));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.enabled.has("console")) {
|
||||||
|
const onConsole = (message: ConsoleMessage) => this.console(message);
|
||||||
|
page.on("console", onConsole);
|
||||||
|
this.retainListener(page, () => page.off("console", onConsole));
|
||||||
|
}
|
||||||
|
if (this.enabled.has("page-errors")) {
|
||||||
|
const onError = (error: Error) => {
|
||||||
this.push({
|
this.push({
|
||||||
kind: "page-error",
|
kind: "page-error",
|
||||||
message: this.secrets.redact(error.message),
|
message: this.secrets.redact(error.message),
|
||||||
stack:
|
stack:
|
||||||
error.stack === undefined ? null : this.secrets.redact(error.stack),
|
error.stack === undefined ? null : this.secrets.redact(error.stack),
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
if (this.enabled.has("websockets"))
|
page.on("pageerror", onError);
|
||||||
page.on("websocket", (socket) => this.webSocket(socket));
|
this.retainListener(page, () => page.off("pageerror", onError));
|
||||||
if (this.enabled.has("frames")) {
|
}
|
||||||
page.on("frameattached", (frame) => this.frame("frame-attached", frame));
|
if (this.enabled.has("websockets")) {
|
||||||
page.on("framedetached", (frame) => this.frame("frame-detached", frame));
|
const onWebSocket = (socket: WebSocket) => this.webSocket(socket);
|
||||||
page.on("framenavigated", (frame) =>
|
page.on("websocket", onWebSocket);
|
||||||
this.frame("frame-navigated", frame),
|
this.retainListener(page, () => page.off("websocket", onWebSocket));
|
||||||
);
|
}
|
||||||
|
if (this.enabled.has("frames")) {
|
||||||
|
const onAttached = (frame: Frame) => this.frame("frame-attached", frame);
|
||||||
|
const onDetached = (frame: Frame) => this.frame("frame-detached", frame);
|
||||||
|
const onNavigated = (frame: Frame) =>
|
||||||
|
this.frame("frame-navigated", frame);
|
||||||
|
page.on("frameattached", onAttached);
|
||||||
|
page.on("framedetached", onDetached);
|
||||||
|
page.on("framenavigated", onNavigated);
|
||||||
|
this.retainListener(page, () => page.off("frameattached", onAttached));
|
||||||
|
this.retainListener(page, () => page.off("framedetached", onDetached));
|
||||||
|
this.retainListener(page, () => page.off("framenavigated", onNavigated));
|
||||||
|
}
|
||||||
|
if (this.enabled.has("workers")) {
|
||||||
|
const onWorker = (worker: Worker) => this.worker(worker);
|
||||||
|
page.on("worker", onWorker);
|
||||||
|
this.retainListener(page, () => page.off("worker", onWorker));
|
||||||
}
|
}
|
||||||
if (this.enabled.has("workers"))
|
|
||||||
page.on("worker", (worker) => this.worker(worker));
|
|
||||||
// Any selected family needs descendant discovery in shared contexts.
|
// Any selected family needs descendant discovery in shared contexts.
|
||||||
// Popup lifecycle records remain gated on their own selector below.
|
// Popup lifecycle records remain gated on their own selector below.
|
||||||
if (this.enabled.size > 0) page.on("popup", (popup) => this.popup(popup));
|
if (this.enabled.size > 0) {
|
||||||
if (this.enabled.has("downloads"))
|
const onPopup = (popup: Page) => this.popup(popup);
|
||||||
page.on("download", (download) => this.download(download));
|
page.on("popup", onPopup);
|
||||||
|
this.retainListener(page, () => page.off("popup", onPopup));
|
||||||
|
}
|
||||||
|
if (this.enabled.has("downloads")) {
|
||||||
|
const onDownload = (download: Download) => this.download(download);
|
||||||
|
page.on("download", onDownload);
|
||||||
|
this.retainListener(page, () => page.off("download", onDownload));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private safeUrl(value: string) {
|
private safeUrl(value: string) {
|
||||||
|
|
@ -231,13 +283,20 @@ export class PlaywrightScenarioEvents {
|
||||||
private webSocket(socket: WebSocket): void {
|
private webSocket(socket: WebSocket): void {
|
||||||
const url = this.safeUrl(socket.url());
|
const url = this.safeUrl(socket.url());
|
||||||
this.push({ kind: "websocket-opened", url });
|
this.push({ kind: "websocket-opened", url });
|
||||||
socket.on("framesent", ({ payload }) =>
|
const onSent = ({ payload }: { payload: string | Buffer }) =>
|
||||||
this.webSocketFrame("websocket-frame-sent", url, payload),
|
this.webSocketFrame("websocket-frame-sent", url, payload);
|
||||||
);
|
const onReceived = ({ payload }: { payload: string | Buffer }) =>
|
||||||
socket.on("framereceived", ({ payload }) =>
|
this.webSocketFrame("websocket-frame-received", url, payload);
|
||||||
this.webSocketFrame("websocket-frame-received", url, payload),
|
const onClose = () => {
|
||||||
);
|
this.push({ kind: "websocket-closed", url });
|
||||||
socket.on("close", () => this.push({ kind: "websocket-closed", url }));
|
this.releaseListeners(socket);
|
||||||
|
};
|
||||||
|
socket.on("framesent", onSent);
|
||||||
|
socket.on("framereceived", onReceived);
|
||||||
|
socket.on("close", onClose);
|
||||||
|
this.retainListener(socket, () => socket.off("framesent", onSent));
|
||||||
|
this.retainListener(socket, () => socket.off("framereceived", onReceived));
|
||||||
|
this.retainListener(socket, () => socket.off("close", onClose));
|
||||||
}
|
}
|
||||||
|
|
||||||
private webSocketFrame(
|
private webSocketFrame(
|
||||||
|
|
@ -286,7 +345,12 @@ export class PlaywrightScenarioEvents {
|
||||||
name: null,
|
name: null,
|
||||||
};
|
};
|
||||||
this.push({ kind: "worker-created", ...details });
|
this.push({ kind: "worker-created", ...details });
|
||||||
worker.on("close", () => this.push({ kind: "worker-closed", ...details }));
|
const onClose = () => {
|
||||||
|
this.push({ kind: "worker-closed", ...details });
|
||||||
|
this.releaseListeners(worker);
|
||||||
|
};
|
||||||
|
worker.on("close", onClose);
|
||||||
|
this.retainListener(worker, () => worker.off("close", onClose));
|
||||||
}
|
}
|
||||||
|
|
||||||
private popup(page: Page): void {
|
private popup(page: Page): void {
|
||||||
|
|
@ -305,13 +369,14 @@ export class PlaywrightScenarioEvents {
|
||||||
};
|
};
|
||||||
if (this.enabled.has("popups")) {
|
if (this.enabled.has("popups")) {
|
||||||
this.push({ kind: "popup-opened", ...opened });
|
this.push({ kind: "popup-opened", ...opened });
|
||||||
page.on("close", () =>
|
const onClose = () =>
|
||||||
this.push({
|
this.push({
|
||||||
kind: "popup-closed",
|
kind: "popup-closed",
|
||||||
url: page.url() === "" ? null : this.safeUrl(page.url()),
|
url: page.url() === "" ? null : this.safeUrl(page.url()),
|
||||||
name: null,
|
name: null,
|
||||||
}),
|
});
|
||||||
);
|
page.on("close", onClose);
|
||||||
|
this.retainListener(page, () => page.off("close", onClose));
|
||||||
}
|
}
|
||||||
this.observePage(page);
|
this.observePage(page);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Cli } from "incur";
|
import { Cli } from "incur";
|
||||||
|
import { resolve } from "node:path";
|
||||||
import { expect, it, vi } from "vitest";
|
import { expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import type { BrowserScenarioCapturePort } from "../../src/application/BrowserScenarioCapturePort.js";
|
import type { BrowserScenarioCapturePort } from "../../src/application/BrowserScenarioCapturePort.js";
|
||||||
|
|
@ -32,7 +33,7 @@ vi.mock("../../src/composition/browserScenario.js", () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const scenario = browserScenarioSchema.parse({
|
const scenario = browserScenarioSchema.parse({
|
||||||
browser: { mode: "launch", executable_path: "/fixture/chromium" },
|
browser: { mode: "launch", executable_path: resolve("/fixture/chromium") },
|
||||||
start_url: { url: "https://app.example.test/" },
|
start_url: { url: "https://app.example.test/" },
|
||||||
actions: [{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 }],
|
actions: [{ step_id: "wait", action: "wait_for_timeout", duration_ms: 1 }],
|
||||||
storage: {},
|
storage: {},
|
||||||
|
|
@ -111,6 +112,22 @@ class RetrySession implements BrowserScenarioSessionPort {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it("waits for a slow but successful browser close before removing its profile", async () => {
|
||||||
|
const stages: string[] = [];
|
||||||
|
const cleanup = new PlaywrightScenarioBrowserCleanupOwner({
|
||||||
|
closeBrowser: async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1_250));
|
||||||
|
stages.push("browser-closed");
|
||||||
|
},
|
||||||
|
removeProfile: async () => {
|
||||||
|
stages.push("profile-removed");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await cleanup.close();
|
||||||
|
expect(stages).toEqual(["browser-closed", "profile-removed"]);
|
||||||
|
});
|
||||||
|
|
||||||
const serveScenario = async () => {
|
const serveScenario = async () => {
|
||||||
const cli = Cli.create("browser-cleanup-test", { sync: false });
|
const cli = Cli.create("browser-cleanup-test", { sync: false });
|
||||||
registerBrowserScenarioCommands(cli, silentLogger, {});
|
registerBrowserScenarioCommands(cli, silentLogger, {});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue