Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions packages/playwright-core/src/tools/backend/tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,9 +283,19 @@ export class Tab extends EventEmitter<TabEventsInterface> {
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<typeof setTimeout> | undefined;
const titleTimeout = new Promise<void>(resolve => timeoutId = setTimeout(resolve, 5000));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to rely on a timeout for this, as it would be unreliable on a slow machine, page hanging on load etc. Also it only addresses title command while any other interaction with the frozen page will lead to an error. We are going to try to find a better fix that can be applied on the browser side to help playwright reliably detect unresponsive/frozen tabs when connecting over CDP and not fail on them.

await Promise.race([
this._raceAgainstModalStates(async () => {
title = await this.page.title();
}).catch(() => {}),
titleTimeout,
]);
if (timeoutId)
clearTimeout(timeoutId);
consoleCounts = await this.consoleMessageCount();
}
const newHeader: TabHeader = {
Expand Down
25 changes: 25 additions & 0 deletions tests/mcp/tabs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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})`,
});
});