new getcdpsession
This commit is contained in:
+14
-2
@@ -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,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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user