From 0581cd04056a653c2348fa448014f45a2146c0aa Mon Sep 17 00:00:00 2001 From: Ashraf Ali Date: Sat, 29 Aug 2026 19:59:28 +0600 Subject: [PATCH 1/2] 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' }); From c968e27261c4c01b974cd036d300736cfe0c41a6 Mon Sep 17 00:00:00 2001 From: Ashraf Ali Date: Sun, 30 Aug 2026 11:48:39 +0600 Subject: [PATCH 2/2] fix(trace-viewer): label filter inputs and make error source link keyboard-accessible Filter inputs in the network tab and UI mode relied on placeholder alone, which is not a reliable accessible name. The error source location in the Errors tab was a span with a click handler, so it could not be focused or activated from the keyboard - now a real button styled identically. Fixes: https://github.com/microsoft/playwright/issues/42463 --- packages/trace-viewer/src/ui/actionList.css | 7 ++++++- packages/trace-viewer/src/ui/errorsTab.tsx | 2 +- packages/trace-viewer/src/ui/networkFilters.tsx | 1 + packages/trace-viewer/src/ui/uiModeFiltersView.tsx | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/trace-viewer/src/ui/actionList.css b/packages/trace-viewer/src/ui/actionList.css index 7161559c62af6..eccb93b24b8de 100644 --- a/packages/trace-viewer/src/ui/actionList.css +++ b/packages/trace-viewer/src/ui/actionList.css @@ -49,10 +49,15 @@ color: var(--vscode-foreground); } -.action-location > span { +.action-location > button { margin: 0 4px; cursor: pointer; text-decoration: underline; + background: none; + border: none; + padding: 0; + color: inherit; + font: inherit; } .action-duration { diff --git a/packages/trace-viewer/src/ui/errorsTab.tsx b/packages/trace-viewer/src/ui/errorsTab.tsx index 36d65c4b7cc97..b8615bb583120 100644 --- a/packages/trace-viewer/src/ui/errorsTab.tsx +++ b/packages/trace-viewer/src/ui/errorsTab.tsx @@ -73,7 +73,7 @@ function ErrorView({ message, error, sdkLanguage, revealInSource }: { message: s }}> {error.action && renderAction(error.action, { sdkLanguage })} {location &&
- @ revealInSource(error)}>{location} + @
} diff --git a/packages/trace-viewer/src/ui/networkFilters.tsx b/packages/trace-viewer/src/ui/networkFilters.tsx index dedb250dae895..a08f9a99cf815 100644 --- a/packages/trace-viewer/src/ui/networkFilters.tsx +++ b/packages/trace-viewer/src/ui/networkFilters.tsx @@ -35,6 +35,7 @@ export const NetworkFilters = ({ filterState, onFilterStateChange }: { onFilterStateChange({ ...filterState, searchValue: e.target.value })} diff --git a/packages/trace-viewer/src/ui/uiModeFiltersView.tsx b/packages/trace-viewer/src/ui/uiModeFiltersView.tsx index 0579a741e1d89..6bf4fdcdaef1f 100644 --- a/packages/trace-viewer/src/ui/uiModeFiltersView.tsx +++ b/packages/trace-viewer/src/ui/uiModeFiltersView.tsx @@ -46,7 +46,7 @@ export const FiltersView: React.FC<{ { setFilterText(e.target.value); }}