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) + })