From 171317f8cb0a60704482bae258cde0be87f13cf9 Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Thu, 15 Jan 2026 13:43:45 +0100 Subject: [PATCH] fix: auto-switch to another page when default page is closed --- playwriter/src/mcp.test.ts | 104 ++++++++++++++++++++++++++++++++----- playwriter/src/mcp.ts | 8 ++- 2 files changed, 97 insertions(+), 15 deletions(-) diff --git a/playwriter/src/mcp.test.ts b/playwriter/src/mcp.test.ts index 36a7296..4c9480c 100644 --- a/playwriter/src/mcp.test.ts +++ b/playwriter/src/mcp.test.ts @@ -737,7 +737,7 @@ describe('MCP Server Tests', () => { // Wait for disconnect to complete await new Promise(resolve => setTimeout(resolve, 100)) - // 3. Verify MCP cannot see the page anymore + // 3. Verify MCP cannot execute code anymore (no pages available) const afterDisconnect = await client.callTool({ name: 'execute', arguments: { @@ -751,7 +751,9 @@ describe('MCP Server Tests', () => { const afterDisconnectOutput = (afterDisconnect as any).content[0].text console.log('After disconnect:', afterDisconnectOutput) - expect(afterDisconnectOutput).toContain('Pages after disconnect: 0') + // After disconnect, there are no pages - so we expect an error + expect((afterDisconnect as any).isError).toBe(true) + expect(afterDisconnectOutput).toContain('No browser tabs are connected') // 4. Re-enable extension on the same page console.log('Re-enabling extension...') @@ -771,21 +773,14 @@ describe('MCP Server Tests', () => { await new Promise(resolve => setTimeout(resolve, 100)) // 5. Reset the MCP client's playwright connection since it was closed by disconnectEverything + // Must use the reset tool since execute requires a valid page console.log('Resetting MCP playwright connection...') const resetResult = await client.callTool({ - name: 'execute', - arguments: { - code: js` - console.log('Resetting playwright connection'); - const result = await resetPlaywright(); - console.log('Reset complete, checking pages'); - const pages = context.pages(); - console.log('Pages after reset:', pages.length); - return { reset: true, pagesCount: pages.length }; - `, - }, + name: 'reset', + arguments: {}, }) console.log('Reset result:', (resetResult as any).content[0].text) + expect((resetResult as any).content[0].text).toContain('Connection reset successfully') // 6. Verify MCP can see the page again console.log('Attempting to access page via MCP...') @@ -2527,6 +2522,89 @@ describe('MCP Server Tests', () => { await page.close() }, 60000) + it('should handle default page being closed and switch to another available page', async () => { + // This test verifies that when the default `page` in MCP scope is closed, + // the MCP automatically switches to another available page instead of failing + // with cryptic "page closed" errors. + + const browserContext = getBrowserContext() + const serviceWorker = await getExtensionServiceWorker(browserContext) + + // 1. Disconnect everything to start fresh + await serviceWorker.evaluate(async () => { + await globalThis.disconnectEverything() + }) + await new Promise(r => setTimeout(r, 100)) + + // 2. Create first page and enable extension + const page1 = await browserContext.newPage() + await page1.goto('https://example.com/first-page') + await page1.bringToFront() + + await serviceWorker.evaluate(async () => { + await globalThis.toggleExtensionForActiveTab() + }) + await new Promise(r => setTimeout(r, 100)) + + // 3. Reset MCP to ensure page1 becomes the default page (only page available) + const resetResult = await client.callTool({ + name: 'reset', + arguments: {}, + }) + expect((resetResult as any).content[0].text).toContain('Connection reset successfully') + + // 4. Verify initial page is accessible via default `page` + const initialResult = await client.callTool({ + name: 'execute', + arguments: { + code: js` + const url = page.url(); + console.log('Initial page URL:', url); + return { url }; + `, + }, + }) + expect((initialResult as any).content[0].text).toContain('first-page') + + // 5. Create second page and enable extension + const page2 = await browserContext.newPage() + await page2.goto('https://example.com/second-page') + await page2.bringToFront() + + await serviceWorker.evaluate(async () => { + await globalThis.toggleExtensionForActiveTab() + }) + await new Promise(r => setTimeout(r, 100)) + + // 6. Close the first page (which is the default `page` in MCP scope) + await page1.close() + await new Promise(r => setTimeout(r, 100)) + + // 7. Execute code via MCP - should NOT fail with "page closed" error + // Instead, it should automatically switch to the second page + const afterCloseResult = await client.callTool({ + name: 'execute', + arguments: { + code: js` + const url = page.url(); + console.log('Page URL after close:', url); + const title = await page.title(); + return { url, title }; + `, + }, + }) + + // Should succeed and return the second page's info + expect((afterCloseResult as any).isError).toBeFalsy() + const output = (afterCloseResult as any).content[0].text + expect(output).toContain('second-page') + expect(output).not.toContain('page closed') + expect(output).not.toContain('Target closed') + + // Cleanup + await page2.close() + }, 60000) + it('should expose CDP discovery endpoints /json/version and /json/list', async () => { const browserContext = getBrowserContext() const serviceWorker = await getExtensionServiceWorker(browserContext) diff --git a/playwriter/src/mcp.ts b/playwriter/src/mcp.ts index ec4c66c..df179da 100644 --- a/playwriter/src/mcp.ts +++ b/playwriter/src/mcp.ts @@ -500,18 +500,22 @@ function setupPageConsoleListener(page: Page) { } async function getCurrentPage(timeout = 5000) { - if (state.page) { + // Check if current page is still valid (not closed) + if (state.page && !state.page.isClosed()) { return state.page } + // Current page is closed or doesn't exist - find another available page if (state.browser) { const contexts = state.browser.contexts() if (contexts.length > 0) { - const pages = contexts[0].pages() + const pages = contexts[0].pages().filter((p) => !p.isClosed()) if (pages.length > 0) { const page = pages[0] await page.waitForLoadState('domcontentloaded', { timeout }).catch(() => {}) + // Update state.page to the new default page + state.page = page return page } }