new getcdpsession

This commit is contained in:
Tommy D. Rossi
2025-12-16 19:40:58 +01:00
parent 31cd9b2a3f
commit d593e20a3c
6 changed files with 94 additions and 45 deletions
+14 -2
View File
@@ -1,5 +1,17 @@
# Changelog
## 0.0.19
### Patch Changes
- **Added `getCDPSession` utility**: New function to send raw CDP commands through the relay
- Works with `getCDPSession({ page })` in MCP execute context
- Returns `{ send, on, off, detach }` interface for CDP commands and events
- Uses page index matching with URL verification for reliable target identification
- **Converted CDP tests to use relay**: All CDP Session tests now go through the relay instead of direct playwright CDP
- Debugger, Profiler, and layout metrics tests all use `getCDPSessionForPage`
- **Added warning about `newCDPSession`**: Documented in prompt.md that `page.context().newCDPSession()` does not work through the relay
## 0.0.18
### Patch Changes
@@ -11,10 +23,10 @@
### Patch Changes
- **Improved error debugging**: Log file path now included in error messages and tool description. Log file writes to OS temp directory by default (`PLAYWRITER_LOG_PATH` env var to override)
- **Added CDP Session tests**: New test suite for direct CDP usage via `page.context().newCDPSession(page)`
- **Added CDP Session tests**: New test suite for CDP commands through the relay
- Debugger test: pauses on `debugger` statement, captures stack trace, local variables, and evaluates expressions
- Profiler test: profiles JavaScript execution with inline snapshot of function names
- Performance metrics test: captures metrics like Documents, Nodes, JSHeapUsedSize
- Layout metrics test: captures viewport dimensions via CDP
- **Refactored test setup**: Extracted `setupTestContext()` and `cleanupTestContext()` to deduplicate beforeAll/afterAll code
- **Improved `getExtensionServiceWorker`**: Now waits for extension global functions to be ready before returning
- **Better TypeScript types**: Uses `Protocol.Debugger.PausedEvent`, `Protocol.Profiler.Profile`, `Protocol.Performance.Metric` instead of `any`
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "playwriter",
"description": "",
"version": "0.0.18",
"version": "0.0.19",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
+62 -29
View File
@@ -10,6 +10,7 @@ import { getCdpUrl } from './utils.js'
import type { ExtensionState } from 'mcp-extension/src/types.js'
import type { Protocol } from 'devtools-protocol'
import { imageSize } from 'image-size'
import { getCDPSessionForPage } from './cdp-session.js'
const execAsync = promisify(exec)
@@ -1341,12 +1342,14 @@ describe('MCP Server Tests', () => {
await new Promise(r => setTimeout(r, 500))
const layoutMetrics = await serviceWorker.evaluate(async () => {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true })
const tabId = tabs[0]?.id
if (!tabId) throw new Error('No active tab')
return await chrome.debugger.sendCommand({ tabId }, 'Page.getLayoutMetrics')
}) as Protocol.Page.GetLayoutMetricsResponse
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 layoutMetrics = await cdpSession.send<Protocol.Page.GetLayoutMetricsResponse>('Page.getLayoutMetrics')
const normalized = {
cssLayoutViewport: layoutMetrics.cssLayoutViewport,
@@ -1396,14 +1399,16 @@ describe('MCP Server Tests', () => {
}
`)
const windowDpr = await page.evaluate(() => (globalThis as any).devicePixelRatio)
const windowDpr = await cdpPage!.evaluate(() => (globalThis as any).devicePixelRatio)
console.log('window.devicePixelRatio:', windowDpr)
expect(windowDpr).toBe(1)
cdpSession.detach()
await browser.close()
await page.close()
}, 60000)
it('should support newCDPSession through the relay', async () => {
it('should support getCDPSession through the relay', async () => {
const browserContext = getBrowserContext()
const serviceWorker = await getExtensionServiceWorker(browserContext)
@@ -1421,13 +1426,14 @@ describe('MCP Server Tests', () => {
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
expect(cdpPage).toBeDefined()
const client = await cdpPage!.context().newCDPSession(cdpPage!)
const wsUrl = getCdpUrl()
const client = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
const layoutMetrics = await client.send('Page.getLayoutMetrics') as Protocol.Page.GetLayoutMetricsResponse
const layoutMetrics = await client.send<Protocol.Page.GetLayoutMetricsResponse>('Page.getLayoutMetrics')
expect(layoutMetrics.cssVisualViewport).toBeDefined()
expect(layoutMetrics.cssVisualViewport.clientWidth).toBeGreaterThan(0)
await client.detach()
client.detach()
await browser.close()
await page.close()
}, 60000)
@@ -1516,19 +1522,32 @@ describe('CDP Session Tests', () => {
it('should enable debugger and pause on debugger statement via CDP session', async () => {
const browserContext = getBrowserContext()
const serviceWorker = await getExtensionServiceWorker(browserContext)
const page = await browserContext.newPage()
await page.goto('https://example.com/')
await page.bringToFront()
const cdpSession = await page.context().newCDPSession(page)
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 })
await cdpSession.send('Debugger.enable')
const pausedPromise = new Promise<Protocol.Debugger.PausedEvent>((resolve) => {
cdpSession.once('Debugger.paused', (params) => {
cdpSession.on('Debugger.paused', (params) => {
resolve(params as Protocol.Debugger.PausedEvent)
})
})
page.evaluate(`
cdpPage!.evaluate(`
(function testFunction() {
const localVar = 'hello';
const numberVar = 42;
@@ -1582,10 +1601,10 @@ describe('CDP Session Tests', () => {
const localVars: Record<string, unknown> = {}
if (localScope?.object.objectId) {
const { result } = await cdpSession.send('Runtime.getProperties', {
const { result } = await cdpSession.send<{ result: Protocol.Runtime.PropertyDescriptor[] }>('Runtime.getProperties', {
objectId: localScope.object.objectId,
ownProperties: true,
}) as { result: Protocol.Runtime.PropertyDescriptor[] }
})
for (const prop of result) {
if (prop.value) {
@@ -1613,10 +1632,10 @@ describe('CDP Session Tests', () => {
}
`)
const evalResult = await cdpSession.send('Debugger.evaluateOnCallFrame', {
const evalResult = await cdpSession.send<{ result: Protocol.Runtime.RemoteObject }>('Debugger.evaluateOnCallFrame', {
callFrameId: topFrame.callFrameId,
expression: 'localVar + " world " + numberVar',
}) as { result: Protocol.Runtime.RemoteObject }
})
expect({
evaluatedExpression: 'localVar + " world " + numberVar',
@@ -1632,20 +1651,34 @@ describe('CDP Session Tests', () => {
await cdpSession.send('Debugger.resume')
await cdpSession.send('Debugger.disable')
await cdpSession.detach()
cdpSession.detach()
await browser.close()
await page.close()
}, 30000)
}, 60000)
it('should profile JavaScript execution using CDP Profiler', async () => {
const browserContext = getBrowserContext()
const serviceWorker = await getExtensionServiceWorker(browserContext)
const page = await browserContext.newPage()
await page.goto('https://example.com/')
await page.bringToFront()
const cdpSession = await page.context().newCDPSession(page)
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 })
await cdpSession.send('Profiler.enable')
await cdpSession.send('Profiler.start')
await page.evaluate(`
await cdpPage!.evaluate(`
(() => {
function fibonacci(n) {
if (n <= 1) return n
@@ -1660,7 +1693,7 @@ describe('CDP Session Tests', () => {
})()
`)
const { profile } = await cdpSession.send('Profiler.stop') as { profile: Protocol.Profiler.Profile }
const { profile } = await cdpSession.send<{ profile: Protocol.Profiler.Profile }>('Profiler.stop')
const functionNames = profile.nodes
.map(n => n.callFrame.functionName)
@@ -1674,23 +1707,23 @@ describe('CDP Session Tests', () => {
sampleFunctionNames: functionNames,
}).toMatchInlineSnapshot(`
{
"durationMicroseconds": 6956,
"durationMicroseconds": 6879,
"hasNodes": true,
"nodeCount": 8,
"nodeCount": 5,
"sampleFunctionNames": [
"(root)",
"(program)",
"(idle)",
"evaluate",
"parseEvaluationResultValue",
"(idle)",
],
}
`)
await cdpSession.send('Profiler.disable')
await cdpSession.detach()
cdpSession.detach()
await browser.close()
await page.close()
}, 30000)
}, 60000)
it('should click at correct coordinates on high-DPI simulation', async () => {
const browserContext = getBrowserContext()
+10 -11
View File
@@ -13,6 +13,7 @@ import dedent from 'string-dedent'
import { createPatch } from 'diff'
import { getCdpUrl } from './utils.js'
import { waitForPageLoad, WaitForPageLoadOptions, WaitForPageLoadResult } from './wait-for-page-load.js'
import { getCDPSessionForPage, CDPSession } from './cdp-session.js'
const require = createRequire(import.meta.url)
@@ -62,6 +63,7 @@ interface VMContext {
getLatestLogs: (options?: { page?: Page; count?: number; search?: string | RegExp }) => Promise<string[]>
clearAllLogs: () => void
waitForPageLoad: (options: WaitForPageLoadOptions) => Promise<WaitForPageLoadResult>
getCDPSession: (options: { page: Page }) => Promise<CDPSession>
require: NodeRequire
import: (specifier: string) => Promise<any>
}
@@ -220,22 +222,12 @@ async function getPageTargetId(page: Page): Promise<string> {
throw new Error('Page is null or undefined')
}
// Always use internal _guid for consistency and speed
const guid = (page as any)._guid
if (guid) {
return guid
}
try {
// Fallback to CDP if _guid is not available
const client = await page.context().newCDPSession(page)
const { targetInfo } = await client.send('Target.getTargetInfo')
await client.detach()
return targetInfo.targetId
} catch (e) {
throw new Error(`Could not get page identifier: ${e}`)
}
throw new Error('Could not get page identifier: _guid not available')
}
function setupPageConsoleListener(page: Page) {
@@ -543,6 +535,11 @@ server.tool(
browserLogs.clear()
}
const getCDPSession = async (options: { page: Page }) => {
const wsUrl = getCdpUrl({ port: RELAY_PORT })
return getCDPSessionForPage({ page: options.page, wsUrl })
}
let vmContextObj: VMContextWithGlobals = {
page,
context,
@@ -553,6 +550,7 @@ server.tool(
getLatestLogs,
clearAllLogs,
waitForPageLoad,
getCDPSession,
resetPlaywright: async () => {
const { page: newPage, context: newContext } = await resetConnection()
@@ -566,6 +564,7 @@ server.tool(
getLatestLogs,
clearAllLogs,
waitForPageLoad,
getCDPSession,
resetPlaywright: vmContextObj.resetPlaywright,
require,
// TODO --experimental-vm-modules is needed to make import work in vm
+5
View File
@@ -52,6 +52,7 @@ IMPORTANT! never call bringToFront unless specifically asked by the user. It is
- only call `page.close()` if the user asks you so or if you previously created this page yourself with `newPage`. do not close user created pages unless asked
- try to never sleep or run `page.waitForTimeout` unless you have to. there are better ways to wait for an element
- never close browser or context. NEVER call `browser.close()`
- NEVER use `page.context().newCDPSession()` or `browser.newCDPSession()` - these do not work through the playwriter relay. If you need to send raw CDP commands, use the `getCDPSession` utility function instead.
## always check the current page state after an action
@@ -88,6 +89,10 @@ you have access to some functions in addition to playwright methods:
- `minWait`: (optional) minimum wait before checking in ms (default: 500)
- Returns: `{ success, readyState, pendingRequests, waitTimeMs, timedOut }`
- Filters out: ad networks (doubleclick, googlesyndication), analytics (google-analytics, mixpanel, segment), social (facebook.net, twitter), support widgets (intercom, zendesk), and slow fonts/images
- `getCDPSession({ page })`: creates a CDP session to send raw Chrome DevTools Protocol commands. Use this instead of `page.context().newCDPSession()` which does not work through the playwriter relay.
- `page`: the page object to create the session for
- Returns: `{ send(method, params?), on(event, callback), off(event, callback), detach() }`
- Example: `const cdp = await getCDPSession({ page }); const metrics = await cdp.send('Page.getLayoutMetrics'); cdp.detach();`
To bring a tab to front and focus it, use the standard Playwright method `await page.bringToFront()`
@@ -25,10 +25,10 @@ Return value:
- generic [ref=e18]: Search documentation...
- generic [ref=e19]:
- generic: ⌘K
- link "102k" [ref=e20] [cursor=pointer]:
- link "103k" [ref=e20] [cursor=pointer]:
- /url: https://github.com/shadcn-ui/ui
- img
- generic [ref=e21]: 102k
- generic [ref=e21]: 103k
- button "Toggle theme" [ref=e22]:
- img
- generic [ref=e23]: Toggle theme