From 27fc9147bb15ea9d5c20f5a70eb77d418f3a08d3 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 7 Oct 2026 13:31:06 -0700 Subject: [PATCH] fix(desktop): resume paused frames so sign-in and captchas work in browser tabs When the server drives a desktop tab through the CDP relay, Playwright auto-attaches with waitForDebuggerOnStart, so Chromium pauses each new cross-site iframe and worker until it receives Runtime.runIfWaitingForDebugger. The relay answered that command itself for every session, so those frames never ran. Google sign-in, captchas and Turnstile run in such frames and failed ("JavaScript is turned off"). The relay now answers it only for the page, which is already running, and forwards it to child sessions. Co-Authored-By: Claude Opus 5.5 (1M context) --- apps/desktop/src/preview/CdpRelay.test.ts | 22 ++++++++++++++++++++++ apps/desktop/src/preview/CdpRelay.ts | 9 +++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/preview/CdpRelay.test.ts b/apps/desktop/src/preview/CdpRelay.test.ts index 4ec88a7cc516..a87d8af85ea0 100644 --- a/apps/desktop/src/preview/CdpRelay.test.ts +++ b/apps/desktop/src/preview/CdpRelay.test.ts @@ -75,6 +75,28 @@ describe("CDP relay", () => { }); }); + it("resumes paused child frames, so cross-site iframes run", async () => { + const send = vi.fn(async () => ({})); + const relay = createCdpRelayConnection(makeTarget(send), () => {}); + // The page's own session is already running; only its children wait. + relay.receive( + JSON.stringify({ + id: 1, + method: "Runtime.runIfWaitingForDebugger", + sessionId: "t3-preview-page", + }), + ); + relay.receive( + JSON.stringify({ + id: 2, + method: "Runtime.runIfWaitingForDebugger", + sessionId: "iframe-session", + }), + ); + await settle(); + expect(send.mock.calls).toEqual([["Runtime.runIfWaitingForDebugger", {}, "iframe-session"]]); + }); + it("routes tab events to the page session once the page is announced", async () => { const written: Array> = []; const relay = createCdpRelayConnection(makeTarget(vi.fn()), (raw) => diff --git a/apps/desktop/src/preview/CdpRelay.ts b/apps/desktop/src/preview/CdpRelay.ts index 19000aa50c63..62c2410c19b8 100644 --- a/apps/desktop/src/preview/CdpRelay.ts +++ b/apps/desktop/src/preview/CdpRelay.ts @@ -158,9 +158,14 @@ export function createCdpRelayConnection( }; const pageCommand = (command: CdpCommand): Promise => { - // The page is already running; nothing waits for a debugger. - if (command.method === "Runtime.runIfWaitingForDebugger") return Promise.resolve({}); const child = sessions.get(command.sessionId!) === "page" ? undefined : command.sessionId; + // The page itself is already running, but its child frames are not: + // Playwright auto-attaches with waitForDebuggerOnStart, so Chromium pauses + // every new cross-site iframe and worker until this reaches it. Swallowing + // it left sign-in checks and captchas, which run in such frames, frozen. + if (command.method === "Runtime.runIfWaitingForDebugger" && child === undefined) { + return Promise.resolve({}); + } return target.send(command.method, command.params ?? {}, child); };