diff --git a/packages/playwright-core/src/tools/backend/tab.ts b/packages/playwright-core/src/tools/backend/tab.ts index 441f8a9f75b00..0105a266a891b 100644 --- a/packages/playwright-core/src/tools/backend/tab.ts +++ b/packages/playwright-core/src/tools/backend/tab.ts @@ -283,9 +283,19 @@ export class Tab extends EventEmitter { let title: string | undefined; let consoleCounts = { total: 0, errors: 0, warnings: 0 }; if (!this.crashed) { - await this._raceAgainstModalStates(async () => { - title = await this.page.title(); - }); + // A discarded or unresponsive page (e.g. when attached over CDP) may never + // resolve the title lookup. Cap it so that one bad tab does not block + // rendering of the whole response. + let timeoutId: ReturnType | undefined; + const titleTimeout = new Promise(resolve => timeoutId = setTimeout(resolve, 5000)); + await Promise.race([ + this._raceAgainstModalStates(async () => { + title = await this.page.title(); + }).catch(() => {}), + titleTimeout, + ]); + if (timeoutId) + clearTimeout(timeoutId); consoleCounts = await this.consoleMessageCount(); } const newHeader: TabHeader = { diff --git a/tests/mcp/tabs.spec.ts b/tests/mcp/tabs.spec.ts index 3743dd1f25fea..ff35fd23f9d47 100644 --- a/tests/mcp/tabs.spec.ts +++ b/tests/mcp/tabs.spec.ts @@ -149,3 +149,28 @@ test('reuse first tab when navigating', async ({ startClient, cdpServer, server expect(pages.length).toBe(1); expect(await pages[0].title()).toBe('Title'); }); + +test('unresponsive tab does not block tab listing', async ({ startClient, cdpServer, server }) => { + const browserContext = await cdpServer.start(); + const { client } = await startClient({ args: [`--cdp-endpoint=${cdpServer.endpoint}`] }); + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + + // Second tab whose main thread is blocked, so that title lookup never resolves. + const page = await browserContext.newPage(); + await page.goto(server.HELLO_WORLD); + page.evaluate(() => { while (true) {} }).catch(() => {}); + await new Promise(f => setTimeout(f, 1000)); + + expect(await client.callTool({ + name: 'browser_tabs', + arguments: { + action: 'list', + }, + })).toHaveResponse({ + result: `- 0: (current) [Title](${server.HELLO_WORLD}) +- 1: [](${server.HELLO_WORLD})`, + }); +});