update cdp relay state on page navigation

This commit is contained in:
Tommy D. Rossi
2025-12-22 18:11:23 +01:00
parent 2decaf6b83
commit 581f283cb8
3 changed files with 235 additions and 11 deletions
+4 -4
View File
@@ -101,7 +101,7 @@ export class CDPSession {
this.eventListeners.get(event)?.delete(callback as (params: unknown) => void)
}
detach() {
close() {
try {
for (const pending of this.pendingRequests.values()) {
pending.reject(new Error('CDPSession detached'))
@@ -128,7 +128,7 @@ export async function getCDPSessionForPage({ page, wsUrl }: { page: Page; wsUrl:
const pages = page.context().pages()
const pageIndex = pages.indexOf(page)
if (pageIndex === -1) {
cdp.detach()
cdp.close()
throw new Error('Page not found in context')
}
@@ -136,13 +136,13 @@ export async function getCDPSessionForPage({ page, wsUrl }: { page: Page; wsUrl:
const pageTargets = targetInfos.filter((t) => t.type === 'page')
if (pageIndex >= pageTargets.length) {
cdp.detach()
cdp.close()
throw new Error(`Page index ${pageIndex} out of bounds (${pageTargets.length} targets)`)
}
const target = pageTargets[pageIndex]
if (target.url !== page.url()) {
cdp.detach()
cdp.close()
throw new Error(`URL mismatch: page has "${page.url()}" but target has "${target.url}"`)
}
+60
View File
@@ -544,6 +544,23 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
} as CDPEventBase,
source: 'extension'
})
} else if (method === 'Target.targetCrashed') {
const crashParams = params as Protocol.Target.TargetCrashedEvent
for (const [sid, target] of connectedTargets.entries()) {
if (target.targetId === crashParams.targetId) {
connectedTargets.delete(sid)
logger?.log(chalk.red('[Server] Target crashed, removing:'), crashParams.targetId)
break
}
}
sendToPlaywright({
message: {
method: 'Target.targetCrashed',
params: crashParams
} as CDPEventBase,
source: 'extension'
})
} else if (method === 'Target.targetInfoChanged') {
const infoParams = params as Protocol.Target.TargetInfoChangedEvent
for (const target of connectedTargets.values()) {
@@ -560,6 +577,49 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
} as CDPEventBase,
source: 'extension'
})
} else if (method === 'Page.frameNavigated') {
const frameParams = params as Protocol.Page.FrameNavigatedEvent
if (!frameParams.frame.parentId && sessionId) {
const target = connectedTargets.get(sessionId)
if (target) {
target.targetInfo = {
...target.targetInfo,
url: frameParams.frame.url,
title: frameParams.frame.name || target.targetInfo.title,
}
logger?.log(chalk.magenta('[Server] Updated target URL from Page.frameNavigated:'), frameParams.frame.url)
}
}
sendToPlaywright({
message: {
sessionId,
method,
params
} as CDPEventBase,
source: 'extension'
})
} else if (method === 'Page.navigatedWithinDocument') {
const navParams = params as Protocol.Page.NavigatedWithinDocumentEvent
if (sessionId) {
const target = connectedTargets.get(sessionId)
if (target) {
target.targetInfo = {
...target.targetInfo,
url: navParams.url,
}
logger?.log(chalk.magenta('[Server] Updated target URL from Page.navigatedWithinDocument:'), navParams.url)
}
}
sendToPlaywright({
message: {
sessionId,
method,
params
} as CDPEventBase,
source: 'extension'
})
} else {
sendToPlaywright({
message: {
+171 -7
View File
@@ -1606,7 +1606,7 @@ describe('MCP Server Tests', () => {
console.log('window.devicePixelRatio:', windowDpr)
expect(windowDpr).toBe(1)
cdpSession.detach()
cdpSession.close()
await browser.close()
await page.close()
}, 60000)
@@ -1636,7 +1636,7 @@ describe('MCP Server Tests', () => {
expect(layoutMetrics.cssVisualViewport).toBeDefined()
expect(layoutMetrics.cssVisualViewport.clientWidth).toBeGreaterThan(0)
client.detach()
client.close()
await browser.close()
await page.close()
}, 60000)
@@ -1764,6 +1764,12 @@ describe('CDP Session Tests', () => {
beforeAll(async () => {
testCtx = await setupTestContext({ tempDirPrefix: 'pw-cdp-test-' })
const serviceWorker = await getExtensionServiceWorker(testCtx.browserContext)
await serviceWorker.evaluate(async () => {
await globalThis.disconnectEverything()
})
await new Promise(r => setTimeout(r, 500))
}, 600000)
afterAll(async () => {
@@ -1907,7 +1913,7 @@ describe('CDP Session Tests', () => {
await cdpSession.send('Debugger.resume')
await cdpSession.send('Debugger.disable')
cdpSession.detach()
cdpSession.close()
await browser.close()
await page.close()
}, 60000)
@@ -1964,21 +1970,179 @@ describe('CDP Session Tests', () => {
sampleFunctionNames: functionNames,
}).toMatchInlineSnapshot(`
{
"durationMicroseconds": 6962,
"durationMicroseconds": 9014,
"hasNodes": true,
"nodeCount": 8,
"nodeCount": 20,
"sampleFunctionNames": [
"(root)",
"(program)",
"(idle)",
"evaluate",
"querySelectorAll",
"fibonacci",
"fibonacci",
"fibonacci",
"fibonacci",
"fibonacci",
"fibonacci",
],
}
`)
await cdpSession.send('Profiler.disable')
cdpSession.detach()
cdpSession.close()
await browser.close()
await page.close()
}, 60000)
it('should update Target.getTargets URL after page navigation', async () => {
const browserContext = getBrowserContext()
const serviceWorker = await getExtensionServiceWorker(browserContext)
const page = await browserContext.newPage()
await page.goto('https://example.com/')
await page.bringToFront()
await serviceWorker.evaluate(async () => {
await globalThis.toggleExtensionForActiveTab()
})
await new Promise(r => setTimeout(r, 500))
const browser = await chromium.connectOverCDP(getCdpUrl())
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
expect(cdpPage).toBeDefined()
const wsUrl = getCdpUrl()
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
const initialTargets = await cdpSession.send('Target.getTargets')
const initialPageTarget = initialTargets.targetInfos.find(t => t.type === 'page' && t.url.includes('example.com'))
expect(initialPageTarget?.url).toBe('https://example.com/')
await cdpPage!.goto('https://news.ycombinator.com/', { waitUntil: 'networkidle' })
await new Promise(r => setTimeout(r, 500))
const afterNavTargets = await cdpSession.send('Target.getTargets')
const allPageTargets = afterNavTargets.targetInfos.filter(t => t.type === 'page')
const aboutBlankTargets = allPageTargets.filter(t => t.url === 'about:blank')
expect(aboutBlankTargets).toHaveLength(0)
const exampleComTargets = allPageTargets.filter(t => t.url.includes('example.com'))
expect(exampleComTargets).toHaveLength(0)
const hnTargets = allPageTargets.filter(t => t.url.includes('news.ycombinator.com'))
expect(hnTargets).toHaveLength(1)
cdpSession.close()
await browser.close()
await page.close()
}, 60000)
it('should return correct targets for multiple pages via Target.getTargets', async () => {
const browserContext = getBrowserContext()
const serviceWorker = await getExtensionServiceWorker(browserContext)
const page1 = await browserContext.newPage()
await page1.goto('https://example.com/')
await page1.bringToFront()
await serviceWorker.evaluate(async () => {
await globalThis.toggleExtensionForActiveTab()
})
const page2 = await browserContext.newPage()
await page2.goto('https://news.ycombinator.com/')
await page2.bringToFront()
await serviceWorker.evaluate(async () => {
await globalThis.toggleExtensionForActiveTab()
})
await new Promise(r => setTimeout(r, 500))
const browser = await chromium.connectOverCDP(getCdpUrl())
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
expect(cdpPage).toBeDefined()
const wsUrl = getCdpUrl()
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
const { targetInfos } = await cdpSession.send('Target.getTargets')
const allPageTargets = targetInfos.filter(t => t.type === 'page')
const aboutBlankTargets = allPageTargets.filter(t => t.url === 'about:blank')
expect(aboutBlankTargets).toHaveLength(0)
const pageTargets = allPageTargets
.map(t => ({ type: t.type, url: t.url }))
.sort((a, b) => a.url.localeCompare(b.url))
expect(pageTargets).toMatchInlineSnapshot(`
[
{
"type": "page",
"url": "https://example.com/",
},
{
"type": "page",
"url": "https://news.ycombinator.com/",
},
]
`)
cdpSession.close()
await browser.close()
await page1.close()
await page2.close()
}, 60000)
it('should maintain CDP session functionality after page URL change', async () => {
const browserContext = getBrowserContext()
const serviceWorker = await getExtensionServiceWorker(browserContext)
const page = await browserContext.newPage()
const initialUrl = 'https://example.com/'
await page.goto(initialUrl)
await page.bringToFront()
await serviceWorker.evaluate(async () => {
await globalThis.toggleExtensionForActiveTab()
})
await new Promise(r => setTimeout(r, 500))
const browser = await chromium.connectOverCDP(getCdpUrl())
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
expect(cdpPage).toBeDefined()
const wsUrl = getCdpUrl()
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
const initialEvalResult = await cdpSession.send('Runtime.evaluate', {
expression: 'document.title',
returnByValue: true,
})
expect(initialEvalResult.result.value).toBe('Example Domain')
const newUrl = 'https://news.ycombinator.com/'
await cdpPage!.goto(newUrl, { waitUntil: 'networkidle' })
expect(cdpPage!.url()).toBe(newUrl)
const layoutMetrics = await cdpSession.send('Page.getLayoutMetrics')
expect(layoutMetrics.cssVisualViewport).toBeDefined()
expect(layoutMetrics.cssVisualViewport.clientWidth).toBeGreaterThan(0)
const afterNavEvalResult = await cdpSession.send('Runtime.evaluate', {
expression: 'document.title',
returnByValue: true,
})
expect(afterNavEvalResult.result.value).toContain('Hacker News')
const locationResult = await cdpSession.send('Runtime.evaluate', {
expression: 'window.location.href',
returnByValue: true,
})
expect(locationResult.result.value).toBe(newUrl)
cdpSession.close()
await browser.close()
await page.close()
}, 60000)