From 7588e2a54cc1cc4d3d740d10c04db18002f5a756 Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Tue, 25 Nov 2025 12:49:26 +0100 Subject: [PATCH] fix: page.url() returning empty after extension runs for a while Target.targetInfoChanged handler was updating parent page's cached targetInfo with child target info (service workers, iframes) because it looked up targets by sessionId instead of targetId. Now correctly iterates connectedTargets to find matching targetId. --- playwriter/CHANGELOG.md | 6 ++ playwriter/package.json | 2 +- playwriter/src/extension/cdp-relay.ts | 10 +- playwriter/src/mcp.test.ts | 149 ++++++++++++++++++++++++++ 4 files changed, 160 insertions(+), 7 deletions(-) diff --git a/playwriter/CHANGELOG.md b/playwriter/CHANGELOG.md index 801ae35..001f025 100644 --- a/playwriter/CHANGELOG.md +++ b/playwriter/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.0.11 + +### Patch Changes + +- **Fixed `page.url()` returning empty after extension runs for a while**: The `Target.targetInfoChanged` handler was incorrectly updating the parent page's cached `targetInfo` with child target info (service workers, iframes). Now correctly looks up targets by `targetId` instead of `sessionId`. + ## 0.0.10 ### Patch Changes diff --git a/playwriter/package.json b/playwriter/package.json index 1664afd..a59f992 100644 --- a/playwriter/package.json +++ b/playwriter/package.json @@ -1,7 +1,7 @@ { "name": "playwriter", "description": "", - "version": "0.0.9", + "version": "0.0.11", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/playwriter/src/extension/cdp-relay.ts b/playwriter/src/extension/cdp-relay.ts index 1854fdd..569d772 100644 --- a/playwriter/src/extension/cdp-relay.ts +++ b/playwriter/src/extension/cdp-relay.ts @@ -466,12 +466,10 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127. }) } else if (method === 'Target.targetInfoChanged') { const infoParams = params as Protocol.Target.TargetInfoChangedEvent - const target = connectedTargets.get(sessionId ||'') - if (target) { - target.targetInfo = infoParams.targetInfo - // Update targetId mapping if needed, though usually targetId doesn't change - if (target.targetId !== infoParams.targetInfo.targetId) { - target.targetId = infoParams.targetInfo.targetId + for (const target of connectedTargets.values()) { + if (target.targetId === infoParams.targetInfo.targetId) { + target.targetInfo = infoParams.targetInfo + break } } diff --git a/playwriter/src/mcp.test.ts b/playwriter/src/mcp.test.ts index 8184fd5..b6d1a91 100644 --- a/playwriter/src/mcp.test.ts +++ b/playwriter/src/mcp.test.ts @@ -613,6 +613,18 @@ describe('MCP Server Tests', () => { "title": "", "url": "about:blank", }, + { + "title": "Example Domain", + "url": "https://example.com/mcp-test", + }, + { + "title": "Example Domain", + "url": "https://example.com/", + }, + { + "title": "Example Domain", + "url": "https://example.com/", + }, { "title": "Example Domain", "url": "https://example.com/tab-a", @@ -699,6 +711,18 @@ describe('MCP Server Tests', () => { "title": "", "url": "about:blank", }, + { + "title": "Example Domain", + "url": "https://example.com/mcp-test", + }, + { + "title": "Example Domain", + "url": "https://example.com/", + }, + { + "title": "Example Domain", + "url": "https://example.com/", + }, { "title": "Example Domain", "url": "https://example.com/tab-a", @@ -1232,6 +1256,131 @@ describe('MCP Server Tests', () => { }) }, 30000) + it('should maintain correct page.url() with service worker pages', async () => { + if (!browserContext) throw new Error('Browser not initialized') + const serviceWorker = await getExtensionServiceWorker(browserContext) + + const page = await browserContext.newPage() + const targetUrl = 'https://x.com' + await page.goto(targetUrl, { waitUntil: 'domcontentloaded' }) + await page.bringToFront() + + await serviceWorker.evaluate(async () => { + await globalThis.toggleExtensionForActiveTab() + }) + + await new Promise(r => setTimeout(r, 2000)) + + const browser = await chromium.connectOverCDP(getCdpUrl()) + const pages = browser.contexts()[0].pages() + const xPage = pages.find(p => p.url().includes('x.com')) + + expect(xPage).toBeDefined() + expect(xPage?.url()).toContain('x.com') + expect(xPage?.url()).not.toContain('sw.js') + + await browser.close() + await page.close() + }, 30000) + + it('should maintain correct page.url() after repeated connections', async () => { + if (!browserContext) throw new Error('Browser not initialized') + const serviceWorker = await getExtensionServiceWorker(browserContext) + + const page = await browserContext.newPage() + const targetUrl = 'https://example.com/repeated-test' + await page.goto(targetUrl) + await page.bringToFront() + + await serviceWorker.evaluate(async () => { + await globalThis.toggleExtensionForActiveTab() + }) + + for (let i = 0; i < 5; i++) { + const browser = await chromium.connectOverCDP(getCdpUrl()) + const pages = browser.contexts()[0].pages() + const testPage = pages.find(p => p.url().includes('repeated-test')) + + expect(testPage).toBeDefined() + expect(testPage?.url()).toBe(targetUrl) + + await browser.close() + await new Promise(r => setTimeout(r, 200)) + } + + await page.close() + }, 30000) + + it('should maintain correct page.url() with concurrent MCP and CDP connections', async () => { + if (!browserContext) throw new Error('Browser not initialized') + const serviceWorker = await getExtensionServiceWorker(browserContext) + + const page = await browserContext.newPage() + const targetUrl = 'https://example.com/concurrent-test' + await page.goto(targetUrl) + await page.bringToFront() + + await serviceWorker.evaluate(async () => { + await globalThis.toggleExtensionForActiveTab() + }) + + await new Promise(r => setTimeout(r, 500)) + + const [mcpResult, cdpBrowser] = await Promise.all([ + client.callTool({ + name: 'execute', + arguments: { + code: js` + const pages = context.pages(); + const testPage = pages.find(p => p.url().includes('concurrent-test')); + return { url: testPage?.url(), found: !!testPage }; + `, + }, + }), + chromium.connectOverCDP(getCdpUrl()) + ]) + + const mcpOutput = (mcpResult as any).content[0].text + expect(mcpOutput).toContain(targetUrl) + + const cdpPages = cdpBrowser.contexts()[0].pages() + const cdpPage = cdpPages.find(p => p.url().includes('concurrent-test')) + expect(cdpPage?.url()).toBe(targetUrl) + + await cdpBrowser.close() + await page.close() + }, 30000) + + it('should maintain correct page.url() with iframe-heavy pages', async () => { + if (!browserContext) throw new Error('Browser not initialized') + const serviceWorker = await getExtensionServiceWorker(browserContext) + + const page = await browserContext.newPage() + const targetUrl = 'https://www.youtube.com' + await page.goto(targetUrl, { waitUntil: 'domcontentloaded' }) + await page.bringToFront() + + await serviceWorker.evaluate(async () => { + await globalThis.toggleExtensionForActiveTab() + }) + + await new Promise(r => setTimeout(r, 3000)) + + for (let i = 0; i < 3; i++) { + const browser = await chromium.connectOverCDP(getCdpUrl()) + const pages = browser.contexts()[0].pages() + const ytPage = pages.find(p => p.url().includes('youtube.com')) + + expect(ytPage).toBeDefined() + expect(ytPage?.url()).toContain('youtube.com') + + await browser.close() + await new Promise(r => setTimeout(r, 500)) + } + + await page.close() + }, 60000) + })