From 9de439570b948ecdfd3c4c88ee010b6499443529 Mon Sep 17 00:00:00 2001 From: Lukas Dziadek <45572841+o070256@users.noreply.github.com> Date: Sun, 26 Apr 2026 10:33:45 +0200 Subject: [PATCH 1/2] fix(mcp): handle page crash event in Tab constructor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Tab constructor listens for the 'close' event to clean up state, but not for 'crash'. A renderer crash leaves the tab as a zombie in _tabs — technically open but non-functional. Any subsequent tool call against it will either hang or fail with confusing errors. Add a 'crash' listener that calls the same _onClose() handler, ensuring consistent cleanup regardless of how the page terminates. --- packages/playwright-core/src/tools/backend/tab.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/playwright-core/src/tools/backend/tab.ts b/packages/playwright-core/src/tools/backend/tab.ts index e409fa344d07c..03e99783ea1ff 100644 --- a/packages/playwright-core/src/tools/backend/tab.ts +++ b/packages/playwright-core/src/tools/backend/tab.ts @@ -115,6 +115,7 @@ export class Tab extends EventEmitter { eventsHelper.addEventListener(p, 'response', response => this._handleResponse(response)), eventsHelper.addEventListener(p, 'requestfailed', request => this._handleRequestFailed(request)), eventsHelper.addEventListener(p, 'close', () => this._onClose()), + eventsHelper.addEventListener(p, 'crash', () => this._onClose()), eventsHelper.addEventListener(p, 'filechooser', chooser => { this.setModalState({ type: 'fileChooser', From 65f188dfe216de3cf7b0abfea432b13d186841ef Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 28 Apr 2026 17:30:22 +0200 Subject: [PATCH 2/2] test(mcp): add regression test for crashed tab cleanup Verifies that a renderer crash removes the tab from the MCP server's internal list and that subsequent tool calls succeed without hanging. Co-Authored-By: Claude Sonnet 4.6 --- tests/mcp/tabs.spec.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/mcp/tabs.spec.ts b/tests/mcp/tabs.spec.ts index 3743dd1f25fea..c7f6a04dfc681 100644 --- a/tests/mcp/tabs.spec.ts +++ b/tests/mcp/tabs.spec.ts @@ -149,3 +149,35 @@ test('reuse first tab when navigating', async ({ startClient, cdpServer, server expect(pages.length).toBe(1); expect(await pages[0].title()).toBe('Title'); }); + +test('crashed tab is removed from tabs list and server recovers', async ({ client, mcpBrowser }) => { + test.skip(mcpBrowser !== 'chromium', 'Crash via chrome://crash is chromium-only'); + + // Open a second tab alongside the initial one + await client.callTool({ + name: 'browser_tabs', + arguments: { action: 'new' }, + }); + + // Crash the second tab; navigation to chrome://crash throws, that's expected + await client.callTool({ + name: 'browser_navigate', + arguments: { url: 'chrome://crash' }, + }).catch(() => {}); + + // Crashed tab must be gone — only the initial tab (index 0) should remain + expect(await client.callTool({ + name: 'browser_tabs', + arguments: { action: 'list' }, + })).toHaveResponse({ + result: `- 0: (current) [](about:blank)`, + }); + + // Server must recover: subsequent navigation must work without hanging + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: 'data:text/html,recovered' }, + })).toHaveResponse({ + snapshot: expect.stringContaining('recovered'), + }); +});