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 # 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 ## 0.0.18
### Patch Changes ### Patch Changes
@@ -11,10 +23,10 @@
### Patch Changes ### 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) - **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 - 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 - 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 - **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 - **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` - **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", "name": "playwriter",
"description": "", "description": "",
"version": "0.0.18", "version": "0.0.19",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "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 { ExtensionState } from 'mcp-extension/src/types.js'
import type { Protocol } from 'devtools-protocol' import type { Protocol } from 'devtools-protocol'
import { imageSize } from 'image-size' import { imageSize } from 'image-size'
import { getCDPSessionForPage } from './cdp-session.js'
const execAsync = promisify(exec) const execAsync = promisify(exec)
@@ -1341,12 +1342,14 @@ describe('MCP Server Tests', () => {
await new Promise(r => setTimeout(r, 500)) await new Promise(r => setTimeout(r, 500))
const layoutMetrics = await serviceWorker.evaluate(async () => { const browser = await chromium.connectOverCDP(getCdpUrl())
const tabs = await chrome.tabs.query({ active: true, currentWindow: true }) const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
const tabId = tabs[0]?.id expect(cdpPage).toBeDefined()
if (!tabId) throw new Error('No active tab')
return await chrome.debugger.sendCommand({ tabId }, 'Page.getLayoutMetrics') const wsUrl = getCdpUrl()
}) as Protocol.Page.GetLayoutMetricsResponse const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
const layoutMetrics = await cdpSession.send<Protocol.Page.GetLayoutMetricsResponse>('Page.getLayoutMetrics')
const normalized = { const normalized = {
cssLayoutViewport: layoutMetrics.cssLayoutViewport, 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) console.log('window.devicePixelRatio:', windowDpr)
expect(windowDpr).toBe(1) expect(windowDpr).toBe(1)
cdpSession.detach()
await browser.close()
await page.close() await page.close()
}, 60000) }, 60000)
it('should support newCDPSession through the relay', async () => { it('should support getCDPSession through the relay', async () => {
const browserContext = getBrowserContext() const browserContext = getBrowserContext()
const serviceWorker = await getExtensionServiceWorker(browserContext) 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')) const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
expect(cdpPage).toBeDefined() 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).toBeDefined()
expect(layoutMetrics.cssVisualViewport.clientWidth).toBeGreaterThan(0) expect(layoutMetrics.cssVisualViewport.clientWidth).toBeGreaterThan(0)
await client.detach() client.detach()
await browser.close() await browser.close()
await page.close() await page.close()
}, 60000) }, 60000)
@@ -1516,19 +1522,32 @@ describe('CDP Session Tests', () => {
it('should enable debugger and pause on debugger statement via CDP session', async () => { it('should enable debugger and pause on debugger statement via CDP session', async () => {
const browserContext = getBrowserContext() const browserContext = getBrowserContext()
const serviceWorker = await getExtensionServiceWorker(browserContext)
const page = await browserContext.newPage() const page = await browserContext.newPage()
await page.goto('https://example.com/') 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') await cdpSession.send('Debugger.enable')
const pausedPromise = new Promise<Protocol.Debugger.PausedEvent>((resolve) => { const pausedPromise = new Promise<Protocol.Debugger.PausedEvent>((resolve) => {
cdpSession.once('Debugger.paused', (params) => { cdpSession.on('Debugger.paused', (params) => {
resolve(params as Protocol.Debugger.PausedEvent) resolve(params as Protocol.Debugger.PausedEvent)
}) })
}) })
page.evaluate(` cdpPage!.evaluate(`
(function testFunction() { (function testFunction() {
const localVar = 'hello'; const localVar = 'hello';
const numberVar = 42; const numberVar = 42;
@@ -1582,10 +1601,10 @@ describe('CDP Session Tests', () => {
const localVars: Record<string, unknown> = {} const localVars: Record<string, unknown> = {}
if (localScope?.object.objectId) { 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, objectId: localScope.object.objectId,
ownProperties: true, ownProperties: true,
}) as { result: Protocol.Runtime.PropertyDescriptor[] } })
for (const prop of result) { for (const prop of result) {
if (prop.value) { 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, callFrameId: topFrame.callFrameId,
expression: 'localVar + " world " + numberVar', expression: 'localVar + " world " + numberVar',
}) as { result: Protocol.Runtime.RemoteObject } })
expect({ expect({
evaluatedExpression: 'localVar + " world " + numberVar', evaluatedExpression: 'localVar + " world " + numberVar',
@@ -1632,20 +1651,34 @@ describe('CDP Session Tests', () => {
await cdpSession.send('Debugger.resume') await cdpSession.send('Debugger.resume')
await cdpSession.send('Debugger.disable') await cdpSession.send('Debugger.disable')
await cdpSession.detach() cdpSession.detach()
await browser.close()
await page.close() await page.close()
}, 30000) }, 60000)
it('should profile JavaScript execution using CDP Profiler', async () => { it('should profile JavaScript execution using CDP Profiler', async () => {
const browserContext = getBrowserContext() const browserContext = getBrowserContext()
const serviceWorker = await getExtensionServiceWorker(browserContext)
const page = await browserContext.newPage() const page = await browserContext.newPage()
await page.goto('https://example.com/') 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.enable')
await cdpSession.send('Profiler.start') await cdpSession.send('Profiler.start')
await page.evaluate(` await cdpPage!.evaluate(`
(() => { (() => {
function fibonacci(n) { function fibonacci(n) {
if (n <= 1) return 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 const functionNames = profile.nodes
.map(n => n.callFrame.functionName) .map(n => n.callFrame.functionName)
@@ -1674,23 +1707,23 @@ describe('CDP Session Tests', () => {
sampleFunctionNames: functionNames, sampleFunctionNames: functionNames,
}).toMatchInlineSnapshot(` }).toMatchInlineSnapshot(`
{ {
"durationMicroseconds": 6956, "durationMicroseconds": 6879,
"hasNodes": true, "hasNodes": true,
"nodeCount": 8, "nodeCount": 5,
"sampleFunctionNames": [ "sampleFunctionNames": [
"(root)", "(root)",
"(program)", "(program)",
"(idle)",
"evaluate", "evaluate",
"parseEvaluationResultValue", "(idle)",
], ],
} }
`) `)
await cdpSession.send('Profiler.disable') await cdpSession.send('Profiler.disable')
await cdpSession.detach() cdpSession.detach()
await browser.close()
await page.close() await page.close()
}, 30000) }, 60000)
it('should click at correct coordinates on high-DPI simulation', async () => { it('should click at correct coordinates on high-DPI simulation', async () => {
const browserContext = getBrowserContext() const browserContext = getBrowserContext()
+10 -11
View File
@@ -13,6 +13,7 @@ import dedent from 'string-dedent'
import { createPatch } from 'diff' import { createPatch } from 'diff'
import { getCdpUrl } from './utils.js' import { getCdpUrl } from './utils.js'
import { waitForPageLoad, WaitForPageLoadOptions, WaitForPageLoadResult } from './wait-for-page-load.js' import { waitForPageLoad, WaitForPageLoadOptions, WaitForPageLoadResult } from './wait-for-page-load.js'
import { getCDPSessionForPage, CDPSession } from './cdp-session.js'
const require = createRequire(import.meta.url) const require = createRequire(import.meta.url)
@@ -62,6 +63,7 @@ interface VMContext {
getLatestLogs: (options?: { page?: Page; count?: number; search?: string | RegExp }) => Promise<string[]> getLatestLogs: (options?: { page?: Page; count?: number; search?: string | RegExp }) => Promise<string[]>
clearAllLogs: () => void clearAllLogs: () => void
waitForPageLoad: (options: WaitForPageLoadOptions) => Promise<WaitForPageLoadResult> waitForPageLoad: (options: WaitForPageLoadOptions) => Promise<WaitForPageLoadResult>
getCDPSession: (options: { page: Page }) => Promise<CDPSession>
require: NodeRequire require: NodeRequire
import: (specifier: string) => Promise<any> import: (specifier: string) => Promise<any>
} }
@@ -220,22 +222,12 @@ async function getPageTargetId(page: Page): Promise<string> {
throw new Error('Page is null or undefined') throw new Error('Page is null or undefined')
} }
// Always use internal _guid for consistency and speed
const guid = (page as any)._guid const guid = (page as any)._guid
if (guid) { if (guid) {
return guid return guid
} }
try { throw new Error('Could not get page identifier: _guid not available')
// 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}`)
}
} }
function setupPageConsoleListener(page: Page) { function setupPageConsoleListener(page: Page) {
@@ -543,6 +535,11 @@ server.tool(
browserLogs.clear() browserLogs.clear()
} }
const getCDPSession = async (options: { page: Page }) => {
const wsUrl = getCdpUrl({ port: RELAY_PORT })
return getCDPSessionForPage({ page: options.page, wsUrl })
}
let vmContextObj: VMContextWithGlobals = { let vmContextObj: VMContextWithGlobals = {
page, page,
context, context,
@@ -553,6 +550,7 @@ server.tool(
getLatestLogs, getLatestLogs,
clearAllLogs, clearAllLogs,
waitForPageLoad, waitForPageLoad,
getCDPSession,
resetPlaywright: async () => { resetPlaywright: async () => {
const { page: newPage, context: newContext } = await resetConnection() const { page: newPage, context: newContext } = await resetConnection()
@@ -566,6 +564,7 @@ server.tool(
getLatestLogs, getLatestLogs,
clearAllLogs, clearAllLogs,
waitForPageLoad, waitForPageLoad,
getCDPSession,
resetPlaywright: vmContextObj.resetPlaywright, resetPlaywright: vmContextObj.resetPlaywright,
require, require,
// TODO --experimental-vm-modules is needed to make import work in vm // 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 - 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 - 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 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 ## 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) - `minWait`: (optional) minimum wait before checking in ms (default: 500)
- Returns: `{ success, readyState, pendingRequests, waitTimeMs, timedOut }` - 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 - 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()` 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=e18]: Search documentation...
- generic [ref=e19]: - generic [ref=e19]:
- generic: ⌘K - generic: ⌘K
- link "102k" [ref=e20] [cursor=pointer]: - link "103k" [ref=e20] [cursor=pointer]:
- /url: https://github.com/shadcn-ui/ui - /url: https://github.com/shadcn-ui/ui
- img - img
- generic [ref=e21]: 102k - generic [ref=e21]: 103k
- button "Toggle theme" [ref=e22]: - button "Toggle theme" [ref=e22]:
- img - img
- generic [ref=e23]: Toggle theme - generic [ref=e23]: Toggle theme