From d593e20a3c106d273e66767ac59b7111a1338248 Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Tue, 16 Dec 2025 19:40:58 +0100 Subject: [PATCH] new getcdpsession --- playwriter/CHANGELOG.md | 16 +++- playwriter/package.json | 2 +- playwriter/src/mcp.test.ts | 91 +++++++++++++------ playwriter/src/mcp.ts | 21 ++--- playwriter/src/prompt.md | 5 + .../src/snapshots/shadcn-ui-accessibility.md | 4 +- 6 files changed, 94 insertions(+), 45 deletions(-) diff --git a/playwriter/CHANGELOG.md b/playwriter/CHANGELOG.md index 9534eb0..12c79fd 100644 --- a/playwriter/CHANGELOG.md +++ b/playwriter/CHANGELOG.md @@ -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` diff --git a/playwriter/package.json b/playwriter/package.json index 831fb63..c2bd6fd 100644 --- a/playwriter/package.json +++ b/playwriter/package.json @@ -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", diff --git a/playwriter/src/mcp.test.ts b/playwriter/src/mcp.test.ts index 5a6510e..e5dad9f 100644 --- a/playwriter/src/mcp.test.ts +++ b/playwriter/src/mcp.test.ts @@ -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('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('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((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 = {} 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() diff --git a/playwriter/src/mcp.ts b/playwriter/src/mcp.ts index 78c2603..8754e40 100644 --- a/playwriter/src/mcp.ts +++ b/playwriter/src/mcp.ts @@ -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 clearAllLogs: () => void waitForPageLoad: (options: WaitForPageLoadOptions) => Promise + getCDPSession: (options: { page: Page }) => Promise require: NodeRequire import: (specifier: string) => Promise } @@ -220,22 +222,12 @@ async function getPageTargetId(page: Page): Promise { 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 diff --git a/playwriter/src/prompt.md b/playwriter/src/prompt.md index 66475f1..9af2988 100644 --- a/playwriter/src/prompt.md +++ b/playwriter/src/prompt.md @@ -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()` diff --git a/playwriter/src/snapshots/shadcn-ui-accessibility.md b/playwriter/src/snapshots/shadcn-ui-accessibility.md index 4af73d3..f0c9c50 100644 --- a/playwriter/src/snapshots/shadcn-ui-accessibility.md +++ b/playwriter/src/snapshots/shadcn-ui-accessibility.md @@ -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