diff --git a/apps/server/src/preview/ServerBrowser.test.ts b/apps/server/src/preview/ServerBrowser.test.ts index 60ed5045d2639..b71da48ca8f79 100644 --- a/apps/server/src/preview/ServerBrowser.test.ts +++ b/apps/server/src/preview/ServerBrowser.test.ts @@ -666,6 +666,40 @@ it.live("reports a pending dialog without evaluating the page and resolves it ex ).pipe(Effect.provide(layer)), ); +it.live("a dialog the desktop closes without Playwright stops blocking the tab", () => + Effect.scoped( + Effect.gen(function* () { + const browser = yield* ServerBrowser.ServerBrowser; + const broker = yield* Broker.PreviewAutomationBroker; + const manager = yield* Manager.PreviewManager; + yield* Effect.yieldNow; + desktopRendersNext = true; + const opened = yield* manager.open({ threadId: scope.thread.threadId, runtime: "server" }); + const tabId = PreviewTabId.make(opened.tabId); + yield* browser.attachViewer(viewerInput(tabId, false)); + const { page, sessions } = desktopConnections[0]!.context; + expect(sessions[0]!.send).toHaveBeenCalledWith("Page.enable"); + // Electron cancels a beforeunload as soon as it opens; only CDP reports the close. + page.emit("dialog", { + type: () => "beforeunload", + message: () => "", + defaultValue: () => "", + }); + sessions[0]!.emit("Page.javascriptDialogClosed"); + const status = yield* broker.invoke({ + scope, + tabId, + operation: "status", + input: {}, + }); + expect(status.dialog).toBeNull(); + expect( + yield* broker.invoke({ scope, tabId, operation: "evaluate", input: { expression: "1" } }), + ).toBe("evaluated"); + }), + ).pipe(Effect.provide(layer)), +); + it.live("a tab opened to a file the browser cannot show reports the file to download", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/preview/ServerBrowser.ts b/apps/server/src/preview/ServerBrowser.ts index 5b386e5f0797a..72132e78e11bb 100644 --- a/apps/server/src/preview/ServerBrowser.ts +++ b/apps/server/src/preview/ServerBrowser.ts @@ -741,7 +741,9 @@ const make = Effect.gen(function* () { // The desktop's page is already a normal browser. A popup's first request // went out before it became a tab; everything after presents as Chrome. if (!desktop) await presentAsChrome(cdp, host); + // Dialog close events need this session's Page domain, which recordCopies turns on. if (!desktop) await recordCopies(cdp); + else await cdp.send("Page.enable"); page.setDefaultTimeout(NAVIGATION_TIMEOUT_MS); page.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT_MS); const control = new SessionControl(snapshot.automationOwner ?? null, () => @@ -874,6 +876,14 @@ const make = Effect.gen(function* () { tab.dialog = dialog; broadcastControl(tab); }); + // Playwright misses dialogs that close without it. Electron cancels + // beforeunload itself, and a person can answer one in the desktop view. + cdp.on("Page.javascriptDialogClosed", () => { + if (!tab.dialog) return; + tab.dialog = null; + ServerBrowserPage.invalidateRefs(page); + broadcastControl(tab); + }); page.on("download", (download) => void saveDownload(tab, download)); updateFileChooserInterception(tab); // Popups become tabs and keep `window.opener`, so sign-in popups can report back.