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)
|
this.eventListeners.get(event)?.delete(callback as (params: unknown) => void)
|
||||||
}
|
}
|
||||||
|
|
||||||
detach() {
|
close() {
|
||||||
try {
|
try {
|
||||||
for (const pending of this.pendingRequests.values()) {
|
for (const pending of this.pendingRequests.values()) {
|
||||||
pending.reject(new Error('CDPSession detached'))
|
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 pages = page.context().pages()
|
||||||
const pageIndex = pages.indexOf(page)
|
const pageIndex = pages.indexOf(page)
|
||||||
if (pageIndex === -1) {
|
if (pageIndex === -1) {
|
||||||
cdp.detach()
|
cdp.close()
|
||||||
throw new Error('Page not found in context')
|
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')
|
const pageTargets = targetInfos.filter((t) => t.type === 'page')
|
||||||
|
|
||||||
if (pageIndex >= pageTargets.length) {
|
if (pageIndex >= pageTargets.length) {
|
||||||
cdp.detach()
|
cdp.close()
|
||||||
throw new Error(`Page index ${pageIndex} out of bounds (${pageTargets.length} targets)`)
|
throw new Error(`Page index ${pageIndex} out of bounds (${pageTargets.length} targets)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = pageTargets[pageIndex]
|
const target = pageTargets[pageIndex]
|
||||||
if (target.url !== page.url()) {
|
if (target.url !== page.url()) {
|
||||||
cdp.detach()
|
cdp.close()
|
||||||
throw new Error(`URL mismatch: page has "${page.url()}" but target has "${target.url}"`)
|
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,
|
} as CDPEventBase,
|
||||||
source: 'extension'
|
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') {
|
} else if (method === 'Target.targetInfoChanged') {
|
||||||
const infoParams = params as Protocol.Target.TargetInfoChangedEvent
|
const infoParams = params as Protocol.Target.TargetInfoChangedEvent
|
||||||
for (const target of connectedTargets.values()) {
|
for (const target of connectedTargets.values()) {
|
||||||
@@ -560,6 +577,49 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|||||||
} as CDPEventBase,
|
} as CDPEventBase,
|
||||||
source: 'extension'
|
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 {
|
} else {
|
||||||
sendToPlaywright({
|
sendToPlaywright({
|
||||||
message: {
|
message: {
|
||||||
|
|||||||
+171
-7
@@ -1606,7 +1606,7 @@ describe('MCP Server Tests', () => {
|
|||||||
console.log('window.devicePixelRatio:', windowDpr)
|
console.log('window.devicePixelRatio:', windowDpr)
|
||||||
expect(windowDpr).toBe(1)
|
expect(windowDpr).toBe(1)
|
||||||
|
|
||||||
cdpSession.detach()
|
cdpSession.close()
|
||||||
await browser.close()
|
await browser.close()
|
||||||
await page.close()
|
await page.close()
|
||||||
}, 60000)
|
}, 60000)
|
||||||
@@ -1636,7 +1636,7 @@ describe('MCP Server Tests', () => {
|
|||||||
expect(layoutMetrics.cssVisualViewport).toBeDefined()
|
expect(layoutMetrics.cssVisualViewport).toBeDefined()
|
||||||
expect(layoutMetrics.cssVisualViewport.clientWidth).toBeGreaterThan(0)
|
expect(layoutMetrics.cssVisualViewport.clientWidth).toBeGreaterThan(0)
|
||||||
|
|
||||||
client.detach()
|
client.close()
|
||||||
await browser.close()
|
await browser.close()
|
||||||
await page.close()
|
await page.close()
|
||||||
}, 60000)
|
}, 60000)
|
||||||
@@ -1764,6 +1764,12 @@ describe('CDP Session Tests', () => {
|
|||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
testCtx = await setupTestContext({ tempDirPrefix: 'pw-cdp-test-' })
|
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)
|
}, 600000)
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -1907,7 +1913,7 @@ describe('CDP Session Tests', () => {
|
|||||||
|
|
||||||
await cdpSession.send('Debugger.resume')
|
await cdpSession.send('Debugger.resume')
|
||||||
await cdpSession.send('Debugger.disable')
|
await cdpSession.send('Debugger.disable')
|
||||||
cdpSession.detach()
|
cdpSession.close()
|
||||||
await browser.close()
|
await browser.close()
|
||||||
await page.close()
|
await page.close()
|
||||||
}, 60000)
|
}, 60000)
|
||||||
@@ -1964,21 +1970,179 @@ describe('CDP Session Tests', () => {
|
|||||||
sampleFunctionNames: functionNames,
|
sampleFunctionNames: functionNames,
|
||||||
}).toMatchInlineSnapshot(`
|
}).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"durationMicroseconds": 6962,
|
"durationMicroseconds": 9014,
|
||||||
"hasNodes": true,
|
"hasNodes": true,
|
||||||
"nodeCount": 8,
|
"nodeCount": 20,
|
||||||
"sampleFunctionNames": [
|
"sampleFunctionNames": [
|
||||||
"(root)",
|
"(root)",
|
||||||
"(program)",
|
"(program)",
|
||||||
"(idle)",
|
"(idle)",
|
||||||
"evaluate",
|
"evaluate",
|
||||||
"querySelectorAll",
|
"fibonacci",
|
||||||
|
"fibonacci",
|
||||||
|
"fibonacci",
|
||||||
|
"fibonacci",
|
||||||
|
"fibonacci",
|
||||||
|
"fibonacci",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
await cdpSession.send('Profiler.disable')
|
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 browser.close()
|
||||||
await page.close()
|
await page.close()
|
||||||
}, 60000)
|
}, 60000)
|
||||||
|
|||||||
Reference in New Issue
Block a user