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/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); }} 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' });