mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
fix(browser): preserve external defaults during CDP attachment (#7188)
Attach to external browsers without implicit default-context overrides, and own temporary focus only on the selected page. Preserve unrelated page media and exact page inventory through CLI/MCP. Remove bootstrap-page retries; retain late/failed cleanup ownership and verified SDK notices. Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing) Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
@@ -173,6 +173,12 @@ distinction: it closes and deletes provider-owned profiles, but only disconnects
|
||||
from an external CDP browser. Real-browser verification checks both outcomes and
|
||||
confirms that an attached external browser remains alive.
|
||||
|
||||
CDP connections leave Playwright's default-context overrides disabled. The
|
||||
selected page receives the declared scenario environment and temporary focus
|
||||
emulation, so actions can complete while that page is in the background.
|
||||
Its environment owner clears these overrides before disconnecting; other pages
|
||||
keep their media settings. Connecting does not create a bootstrap page.
|
||||
|
||||
That cleanup requires REA to remain running. Abrupt termination, including
|
||||
Windows process-tree termination or POSIX `SIGKILL`, can leave the launch
|
||||
profile and Playwright artifacts behind. A profile can retain cookies, history,
|
||||
|
||||
@@ -3710,11 +3710,11 @@ pkce-challenge@5.0.1 | runtime | MIT
|
||||
Package: https://www.npmjs.com/package/pkce-challenge/v/5.0.1
|
||||
Archive: https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz
|
||||
Integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==
|
||||
playwright-core@1.58.2 | runtime | Apache-2.0
|
||||
playwright-core@1.64.1 | runtime | Apache-2.0
|
||||
Lockfile path: node_modules/playwright-core
|
||||
Package: https://www.npmjs.com/package/playwright-core/v/1.58.2
|
||||
Archive: https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz
|
||||
Integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==
|
||||
Package: https://www.npmjs.com/package/playwright-core/v/1.64.1
|
||||
Archive: https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz
|
||||
Integrity: sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==
|
||||
plist@5.0.0 | runtime | MIT
|
||||
Lockfile path: node_modules/plist
|
||||
Package: https://www.npmjs.com/package/plist/v/5.0.0
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Generated
+5
-5
@@ -39,7 +39,7 @@
|
||||
"openpgp": "6.3.2",
|
||||
"parse5": "8.0.1",
|
||||
"pino": "10.4.0",
|
||||
"playwright-core": "1.58.2",
|
||||
"playwright-core": "1.64.1",
|
||||
"plist": "5.0.0",
|
||||
"saxes": "6.0.0",
|
||||
"semver": "7.8.5",
|
||||
@@ -10529,15 +10529,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||
"version": "1.64.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz",
|
||||
"integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/plist": {
|
||||
|
||||
+1
-1
@@ -368,7 +368,7 @@
|
||||
"openpgp": "6.3.2",
|
||||
"parse5": "8.0.1",
|
||||
"pino": "10.4.0",
|
||||
"playwright-core": "1.58.2",
|
||||
"playwright-core": "1.64.1",
|
||||
"plist": "5.0.0",
|
||||
"saxes": "6.0.0",
|
||||
"semver": "7.8.5",
|
||||
|
||||
@@ -140,9 +140,54 @@ export async function verifyScenarioEnvironment(endpoint, targetId, origin) {
|
||||
return JSON.parse(value.result.value);
|
||||
};
|
||||
const provider = createBrowserScenarioProvider(process.env);
|
||||
let unselected;
|
||||
let unselectedTarget;
|
||||
return withVerifierCleanup(async () => {
|
||||
// The fixture owns this extra page. A scenario selecting targetId must not
|
||||
// initialize Playwright defaults on another caller-owned page.
|
||||
const created = await connection.send("Target.createTarget", {
|
||||
url: "about:blank",
|
||||
});
|
||||
unselectedTarget = created.targetId;
|
||||
const pages = await (await fetch(`${endpoint}/json/list`)).json();
|
||||
const other = pages.find(({ id }) => id === unselectedTarget);
|
||||
assert.ok(other, "Owned unselected fixture page is missing");
|
||||
unselected = await CdpConnection.connect(
|
||||
other.webSocketDebuggerUrl,
|
||||
"capture_browser_scenario",
|
||||
);
|
||||
const pageIds = pages
|
||||
.filter(({ type }) => type === "page")
|
||||
.map(({ id }) => id)
|
||||
.sort();
|
||||
await unselected.send("Emulation.setEmulatedMedia", {
|
||||
features: [{ name: "prefers-color-scheme", value: "dark" }],
|
||||
});
|
||||
const assertUnselectedState = async () => {
|
||||
const observed = await unselected.send("Runtime.evaluate", {
|
||||
expression: "matchMedia('(prefers-color-scheme: dark)').matches",
|
||||
returnByValue: true,
|
||||
});
|
||||
assert.equal(
|
||||
observed.result.value,
|
||||
true,
|
||||
"Scenario attachment changed media on an unselected page",
|
||||
);
|
||||
};
|
||||
await assertUnselectedState();
|
||||
// Settle the selected tab's actual layout before taking the restoration
|
||||
// baseline, then leave it in the background for the scenario's actions.
|
||||
await connection.send("Target.activateTarget", { targetId });
|
||||
await connection.send("Runtime.evaluate", {
|
||||
expression:
|
||||
"new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))",
|
||||
awaitPromise: true,
|
||||
});
|
||||
const baseline = await readEnvironment();
|
||||
const callerLanguage = await readEnvironment(languageExpression);
|
||||
await connection.send("Target.activateTarget", {
|
||||
targetId: unselectedTarget,
|
||||
});
|
||||
const scenario = environmentScenario({
|
||||
endpoint,
|
||||
targetId,
|
||||
@@ -157,6 +202,14 @@ export async function verifyScenarioEnvironment(endpoint, targetId, origin) {
|
||||
);
|
||||
const remaining = await (await fetch(`${endpoint}/json/list`)).json();
|
||||
assert.ok(remaining.some(({ id }) => id === targetId));
|
||||
assert.deepEqual(
|
||||
remaining
|
||||
.filter(({ type }) => type === "page")
|
||||
.map(({ id }) => id)
|
||||
.sort(),
|
||||
pageIds,
|
||||
"Scenario attachment changed the caller's page inventory",
|
||||
);
|
||||
};
|
||||
const cliCapture = (await runScenarioCli(scenario)).normalized_result;
|
||||
assertCapture(cliCapture);
|
||||
@@ -195,6 +248,7 @@ export async function verifyScenarioEnvironment(endpoint, targetId, origin) {
|
||||
targetId,
|
||||
origin,
|
||||
assertRestored,
|
||||
assertUnselectedState,
|
||||
});
|
||||
}, [() => client.close(), () => transport.close()]);
|
||||
await assertRestored();
|
||||
@@ -235,8 +289,25 @@ export async function verifyScenarioEnvironment(endpoint, targetId, origin) {
|
||||
action_failure_cleanup: true,
|
||||
cancellation_cleanup: true,
|
||||
external_target_preserved: true,
|
||||
unselected_page_media_preserved: true,
|
||||
};
|
||||
}, [() => provider.close(), () => connection.close()]);
|
||||
}, [
|
||||
() => provider.close(),
|
||||
() => unselected?.close(),
|
||||
async () => {
|
||||
if (unselectedTarget !== undefined)
|
||||
assert.equal(
|
||||
(
|
||||
await connection.send("Target.closeTarget", {
|
||||
targetId: unselectedTarget,
|
||||
})
|
||||
).success,
|
||||
true,
|
||||
"Owned unselected fixture page was not released",
|
||||
);
|
||||
},
|
||||
() => connection.close(),
|
||||
]);
|
||||
}
|
||||
|
||||
const verifyConcurrentMcpCapture = async ({
|
||||
@@ -247,6 +318,7 @@ const verifyConcurrentMcpCapture = async ({
|
||||
targetId,
|
||||
origin,
|
||||
assertRestored,
|
||||
assertUnselectedState,
|
||||
}) => {
|
||||
const firstScenario = environmentScenario({
|
||||
endpoint,
|
||||
@@ -279,6 +351,7 @@ const verifyConcurrentMcpCapture = async ({
|
||||
try {
|
||||
const first = callTool(firstScenario, firstCancellation.signal);
|
||||
await waitForEnvironment(connection, expected, "A");
|
||||
await assertUnselectedState();
|
||||
|
||||
const second = callTool(secondScenario, secondCancellation.signal);
|
||||
await client.ping();
|
||||
|
||||
@@ -387,6 +387,11 @@ it("restores its environment overrides before releasing an attached browser", as
|
||||
const cleanupOrder: string[] = [];
|
||||
const fixture = attachedBrowserFixture({
|
||||
send: async (method, parameters) => {
|
||||
if (
|
||||
method === "Emulation.setFocusEmulationEnabled" &&
|
||||
(parameters as { enabled?: boolean }).enabled === false
|
||||
)
|
||||
cleanupOrder.push("focus");
|
||||
if (
|
||||
method === "Emulation.setTimezoneOverride" &&
|
||||
(parameters as { timezoneId?: string }).timezoneId === ""
|
||||
@@ -435,6 +440,7 @@ it("restores its environment overrides before releasing an attached browser", as
|
||||
expect(fixture.environmentSession.detach).toHaveBeenCalledOnce();
|
||||
expect(fixture.browser.close).toHaveBeenCalledOnce();
|
||||
expect(cleanupOrder).toEqual([
|
||||
"focus",
|
||||
"timezone",
|
||||
"locale",
|
||||
"device-metrics",
|
||||
@@ -488,33 +494,40 @@ it("checks a late stop after attached restoration and before transport disconnec
|
||||
]);
|
||||
});
|
||||
|
||||
it("retains attached environment ownership until a failed reset can be retried", async () => {
|
||||
let localeResetAttempts = 0;
|
||||
const fixture = attachedBrowserFixture({
|
||||
send: async (method, parameters) => {
|
||||
if (
|
||||
method === "Emulation.setLocaleOverride" &&
|
||||
(parameters as { locale?: string }).locale === ""
|
||||
) {
|
||||
localeResetAttempts += 1;
|
||||
if (localeResetAttempts === 1) throw new Error("reset interrupted");
|
||||
}
|
||||
},
|
||||
});
|
||||
const opened = await fixture.open();
|
||||
it.each(["locale", "focus"] as const)(
|
||||
"retains a failed %s reset for cleanup retry",
|
||||
async (mutation) => {
|
||||
let resetAttempts = 0;
|
||||
const fixture = attachedBrowserFixture({
|
||||
send: async (method, parameters) => {
|
||||
if (
|
||||
(mutation === "locale" &&
|
||||
method === "Emulation.setLocaleOverride" &&
|
||||
(parameters as { locale?: string }).locale === "") ||
|
||||
(mutation === "focus" &&
|
||||
method === "Emulation.setFocusEmulationEnabled" &&
|
||||
(parameters as { enabled?: boolean }).enabled === false)
|
||||
) {
|
||||
resetAttempts += 1;
|
||||
if (resetAttempts === 1) throw new Error("reset interrupted");
|
||||
}
|
||||
},
|
||||
});
|
||||
const opened = await fixture.open();
|
||||
|
||||
await expect(opened.cleanup.close()).rejects.toMatchObject({
|
||||
reason: "cleanup_failed",
|
||||
cleanup: { reason: "reset interrupted" },
|
||||
});
|
||||
expect(fixture.browser.close).not.toHaveBeenCalled();
|
||||
expect(fixture.environmentSession.detach).not.toHaveBeenCalled();
|
||||
await expect(opened.cleanup.close()).rejects.toMatchObject({
|
||||
reason: "cleanup_failed",
|
||||
cleanup: { reason: "reset interrupted" },
|
||||
});
|
||||
expect(fixture.browser.close).not.toHaveBeenCalled();
|
||||
expect(fixture.environmentSession.detach).not.toHaveBeenCalled();
|
||||
|
||||
await opened.cleanup.close();
|
||||
expect(localeResetAttempts).toBe(2);
|
||||
expect(fixture.environmentSession.detach).toHaveBeenCalledOnce();
|
||||
expect(fixture.browser.close).toHaveBeenCalledOnce();
|
||||
});
|
||||
await opened.cleanup.close();
|
||||
expect(resetAttempts).toBe(2);
|
||||
expect(fixture.environmentSession.detach).toHaveBeenCalledOnce();
|
||||
expect(fixture.browser.close).toHaveBeenCalledOnce();
|
||||
},
|
||||
);
|
||||
|
||||
it("does not clear an attached environment override it failed to acquire", async () => {
|
||||
const fixture = attachedBrowserFixture({
|
||||
@@ -1260,75 +1273,96 @@ it("closes a browser that connects after its deadline without waiting to report
|
||||
expect(closed).toBe(true);
|
||||
});
|
||||
|
||||
it("restores an environment mutation that completes after its deadline", async () => {
|
||||
let restored = false;
|
||||
const scenario = browserScenarioSchema.parse({
|
||||
browser: {
|
||||
mode: "connect",
|
||||
cdp_endpoint: "http://127.0.0.1:9222",
|
||||
target_id: "late-environment-target",
|
||||
},
|
||||
start_url: { url: "https://example.test" },
|
||||
environment: { service_workers: "allow" },
|
||||
command_timeout_ms: 40,
|
||||
actions: scenarioActions,
|
||||
});
|
||||
const discoverySession = {
|
||||
send: vi.fn(async () => ({
|
||||
targetInfo: { targetId: "late-environment-target" },
|
||||
})),
|
||||
detach: vi.fn(async () => undefined),
|
||||
} as unknown as CDPSession;
|
||||
const environmentSession = {
|
||||
send: vi.fn(async () => undefined),
|
||||
detach: vi.fn(async () => undefined),
|
||||
} as unknown as CDPSession;
|
||||
const page = {
|
||||
emulateMedia: vi.fn(async (options: { colorScheme?: string | null }) => {
|
||||
if (options.colorScheme === null) {
|
||||
restored = true;
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 80));
|
||||
}),
|
||||
isClosed: vi.fn(() => false),
|
||||
} as unknown as Page;
|
||||
const context = {
|
||||
pages: () => [page],
|
||||
newCDPSession: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(discoverySession)
|
||||
.mockResolvedValueOnce(environmentSession),
|
||||
} as unknown as BrowserContext;
|
||||
const browser = {
|
||||
close: vi.fn(async () => undefined),
|
||||
contexts: () => [context],
|
||||
isConnected: vi.fn(() => true),
|
||||
version: () => "late-environment",
|
||||
};
|
||||
await expect(
|
||||
openPlaywrightScenarioBrowser(
|
||||
scenario,
|
||||
{},
|
||||
{
|
||||
launcher: {
|
||||
connectOverCDP: async () => browser,
|
||||
launchPersistentContext: async () => {
|
||||
throw new Error("Unexpected launch");
|
||||
it.each(["media", "focus"] as const)(
|
||||
"restores a late %s mutation after its deadline",
|
||||
async (mutation) => {
|
||||
let restored = false;
|
||||
const scenario = browserScenarioSchema.parse({
|
||||
browser: {
|
||||
mode: "connect",
|
||||
cdp_endpoint: "http://127.0.0.1:9222",
|
||||
target_id: "late-environment-target",
|
||||
},
|
||||
start_url: { url: "https://example.test" },
|
||||
environment: { service_workers: "allow" },
|
||||
command_timeout_ms: 40,
|
||||
actions: scenarioActions,
|
||||
});
|
||||
const discoverySession = {
|
||||
send: vi.fn(async () => ({
|
||||
targetInfo: { targetId: "late-environment-target" },
|
||||
})),
|
||||
detach: vi.fn(async () => undefined),
|
||||
} as unknown as CDPSession;
|
||||
const environmentSession = {
|
||||
send: vi.fn(
|
||||
async (method: string, parameters?: { enabled?: boolean }) => {
|
||||
if (method !== "Emulation.setFocusEmulationEnabled") return;
|
||||
if (parameters?.enabled === false) {
|
||||
if (mutation === "focus") restored = true;
|
||||
} else if (mutation === "focus")
|
||||
await new Promise((resolve) => setTimeout(resolve, 80));
|
||||
},
|
||||
),
|
||||
detach: vi.fn(async () => undefined),
|
||||
} as unknown as CDPSession;
|
||||
const page = {
|
||||
emulateMedia: vi.fn(async (options: { colorScheme?: string | null }) => {
|
||||
if (options.colorScheme === null) {
|
||||
if (mutation === "media") restored = true;
|
||||
return;
|
||||
}
|
||||
if (mutation === "media")
|
||||
await new Promise((resolve) => setTimeout(resolve, 80));
|
||||
}),
|
||||
isClosed: vi.fn(() => false),
|
||||
} as unknown as Page;
|
||||
const context = {
|
||||
pages: () => [page],
|
||||
newCDPSession: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(discoverySession)
|
||||
.mockResolvedValueOnce(environmentSession),
|
||||
} as unknown as BrowserContext;
|
||||
const browser = {
|
||||
close: vi.fn(async () => undefined),
|
||||
contexts: () => [context],
|
||||
isConnected: vi.fn(() => true),
|
||||
version: () => "late-environment",
|
||||
};
|
||||
await expect(
|
||||
openPlaywrightScenarioBrowser(
|
||||
scenario,
|
||||
{},
|
||||
{
|
||||
launcher: {
|
||||
connectOverCDP: async () => browser,
|
||||
launchPersistentContext: async () => {
|
||||
throw new Error("Unexpected launch");
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
).rejects.toMatchObject({
|
||||
reason: "timeout",
|
||||
userMessage: expect.stringContaining("Emulation.setEmulatedMedia"),
|
||||
});
|
||||
expect(restored).toBe(true);
|
||||
expect(environmentSession.send).not.toHaveBeenCalledWith(
|
||||
"Emulation.clearDeviceMetricsOverride",
|
||||
);
|
||||
expect(browser.close).toHaveBeenCalledOnce();
|
||||
});
|
||||
),
|
||||
).rejects.toMatchObject({
|
||||
reason: "timeout",
|
||||
userMessage: expect.stringContaining(
|
||||
mutation === "media"
|
||||
? "Emulation.setEmulatedMedia"
|
||||
: "Emulation.setFocusEmulationEnabled",
|
||||
),
|
||||
});
|
||||
expect(restored).toBe(true);
|
||||
if (mutation === "media")
|
||||
expect(environmentSession.send).not.toHaveBeenCalledWith(
|
||||
"Emulation.clearDeviceMetricsOverride",
|
||||
);
|
||||
else
|
||||
expect(environmentSession.send).toHaveBeenCalledWith(
|
||||
"Emulation.clearDeviceMetricsOverride",
|
||||
);
|
||||
expect(browser.close).toHaveBeenCalledOnce();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
new Error("Timeout unrelated launch diagnostic"),
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
Browser,
|
||||
BrowserContext,
|
||||
CDPSession,
|
||||
ConnectOverCDPOptions,
|
||||
Page,
|
||||
chromium,
|
||||
} from "playwright-core";
|
||||
@@ -435,6 +436,7 @@ const findConnectedPage = async (
|
||||
|
||||
/** Owns the target state changed while one scenario uses an attached page. */
|
||||
class AttachedScenarioEnvironmentOwner {
|
||||
#focusOverride = false;
|
||||
#mediaOverride = false;
|
||||
#deviceMetricsOverride = false;
|
||||
#localeOverride = false;
|
||||
@@ -475,6 +477,7 @@ class AttachedScenarioEnvironmentOwner {
|
||||
|
||||
get restored(): boolean {
|
||||
return (
|
||||
!this.#focusOverride &&
|
||||
!this.#mediaOverride &&
|
||||
!this.#deviceMetricsOverride &&
|
||||
!this.#localeOverride &&
|
||||
@@ -563,6 +566,18 @@ class AttachedScenarioEnvironmentOwner {
|
||||
"Emulation.setTimezoneOverride",
|
||||
);
|
||||
throwIfScenarioCancelled(signal);
|
||||
|
||||
// Keep scenario actions usable in the selected background page without
|
||||
// Playwright's connection-wide focus override on unrelated pages.
|
||||
await this.#command(
|
||||
this.session
|
||||
.send("Emulation.setFocusEmulationEnabled", { enabled: true })
|
||||
.then(() => {
|
||||
this.#focusOverride = true;
|
||||
}),
|
||||
"Emulation.setFocusEmulationEnabled",
|
||||
);
|
||||
throwIfScenarioCancelled(signal);
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
@@ -577,6 +592,16 @@ class AttachedScenarioEnvironmentOwner {
|
||||
await settleScenarioCommand(cause);
|
||||
}
|
||||
const failures: unknown[] = [];
|
||||
if (this.#focusOverride) {
|
||||
try {
|
||||
await this.session.send("Emulation.setFocusEmulationEnabled", {
|
||||
enabled: false,
|
||||
});
|
||||
this.#focusOverride = false;
|
||||
} catch (cause: unknown) {
|
||||
failures.push(cause);
|
||||
}
|
||||
}
|
||||
if (this.#timezoneOverride) {
|
||||
try {
|
||||
await this.session.send("Emulation.setTimezoneOverride", {
|
||||
@@ -669,7 +694,7 @@ type ScenarioBrowserLauncher = Pick<
|
||||
> & {
|
||||
readonly connectOverCDP: (
|
||||
endpoint: string,
|
||||
options?: { readonly timeout?: number },
|
||||
options?: Pick<ConnectOverCDPOptions, "timeout" | "noDefaults">,
|
||||
) => Promise<Pick<Browser, "contexts" | "close" | "isConnected" | "version">>;
|
||||
};
|
||||
|
||||
@@ -698,6 +723,10 @@ const openAttachedScenarioBrowser = async (
|
||||
() =>
|
||||
launcher.connectOverCDP(browserRequest.cdp_endpoint, {
|
||||
timeout: commandTimeoutMs,
|
||||
// Only the selected target's environment owner may apply overrides.
|
||||
// Playwright defaults also mutate unrelated external pages and can
|
||||
// require context management on an otherwise usable CDP endpoint.
|
||||
noDefaults: true,
|
||||
}),
|
||||
commandTimeoutMs,
|
||||
"handshake",
|
||||
|
||||
@@ -341,6 +341,10 @@ describe("request dispatch boundary", () => {
|
||||
let gate: ((route: Route) => Promise<void>) | undefined;
|
||||
vi.spyOn(page, "route").mockImplementation(async (_match, handler) => {
|
||||
gate = handler as typeof gate;
|
||||
return {
|
||||
dispose: async () => undefined,
|
||||
[Symbol.asyncDispose]: async () => undefined,
|
||||
};
|
||||
});
|
||||
const unroute = vi.spyOn(page, "unroute");
|
||||
const sent = vi.fn();
|
||||
|
||||
Vendored
+4
-1
@@ -101,7 +101,10 @@ export const completedModuleResolverLauncher =
|
||||
return {
|
||||
version: () => "controlled-browser",
|
||||
newContext: async () => ({
|
||||
route: async () => {},
|
||||
route: async () => ({
|
||||
dispose: async () => undefined,
|
||||
[Symbol.asyncDispose]: async () => undefined,
|
||||
}),
|
||||
newPage: async () => ({
|
||||
on: () => {},
|
||||
goto: async () => null,
|
||||
|
||||
Reference in New Issue
Block a user