update cdp relay state on page navigation
This commit is contained in:
@@ -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}"`)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user