From 0581cd04056a653c2348fa448014f45a2146c0aa Mon Sep 17 00:00:00 2001 From: Ashraf Ali Date: Sat, 29 Aug 2026 19:59:28 +0600 Subject: [PATCH] fix(server): keep pages alive when a reuse-mode connection drops In extension mode, test-run connections engage the reuse-browsers mode, which deliberately keeps pages alive across connections ("Don't close the pages so that user could debug them"). However, the shared browser was not marked as shared, so the connection cleanup treated the contexts created by the connection as isolated and closed them all on disconnect ("Global context cleanup"), killing the page being debugged. Mark the browser as shared in reuse-browsers mode, so contexts with pages survive the connection drop and are picked up by the next connection. Empty contexts are still cleaned up on disconnect as before. This restores the pre-1.54 behavior where stopping a debug session in the VS Code extension keeps the browser open for inspection and recording. Fixes: https://github.com/microsoft/playwright/issues/37822 --- .../src/remote/playwrightServer.ts | 4 +++ tests/library/debug-controller.spec.ts | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/packages/playwright-core/src/remote/playwrightServer.ts b/packages/playwright-core/src/remote/playwrightServer.ts index c1928ce1e6b5e..d0bc9be5a017a 100644 --- a/packages/playwright-core/src/remote/playwrightServer.ts +++ b/packages/playwright-core/src/remote/playwrightServer.ts @@ -222,6 +222,10 @@ export class PlaywrightServer { return { preLaunchedBrowser: browser, denyLaunch: true, + // The browser is shared between sequential connections, so that + // pages created by one connection can be kept alive after it drops + // (e.g. to debug or record them) and picked up by the next one. + sharedBrowser: true, dispose: async () => { // Don't close the pages so that user could debug them, // but close all the empty contexts to clean up. diff --git a/tests/library/debug-controller.spec.ts b/tests/library/debug-controller.spec.ts index 784b5e00253c1..9a63778ca28d0 100644 --- a/tests/library/debug-controller.spec.ts +++ b/tests/library/debug-controller.spec.ts @@ -296,6 +296,38 @@ test('should reset routes before reuse', async ({ server, connectedBrowserFactor await browser2.close(); }); +test('should keep pages alive when the test connection drops', async ({ backend, connectedBrowserFactory }, testInfo) => { + testInfo.annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/37822' }); + + const pageCounts: number[] = []; + backend.on('stateChanged', params => pageCounts.push(params.pageCount)); + await backend.setReportStateChanged({ enabled: true }, undefined); + + // Emulates "Debug Test": the test process creates its own context and page. + const browser1 = await connectedBrowserFactory(); + const context1 = await browser1.newContext(); + const page1 = await context1.newPage(); + await page1.setContent(''); + const contextEmpty = await browser1.newContext(); + expect(contextEmpty.pages()).toHaveLength(0); + await expect.poll(() => pageCounts[pageCounts.length - 1]).toBe(1); + + // Emulates the user hitting "Stop" during debugging: the test process is + // killed and its connection drops without a graceful close. + await browser1.close(); + + // The next connection (e.g. "Record at cursor") still sees the page alive: + // contexts with pages are kept around for debugging, empty ones are cleaned up. + const browser2 = await connectedBrowserFactory(); + const contexts = browser2.contexts(); + expect(contexts).toHaveLength(1); + const page = contexts[0].pages()[0]; + await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible(); + + // The backend never reported the debugged page as closed. + expect(pageCounts).not.toContain(0); +}); + test('should highlight inside iframe', async ({ backend, connectedBrowser }, testInfo) => { testInfo.annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33146' });