playwriter 0.0.15 → 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdp-session.d.ts +21 -0
- package/dist/cdp-session.d.ts.map +1 -0
- package/dist/cdp-session.js +114 -0
- package/dist/cdp-session.js.map +1 -0
- package/dist/cdp-types.d.ts +15 -0
- package/dist/cdp-types.d.ts.map +1 -1
- package/dist/cdp-types.js.map +1 -1
- package/dist/create-logger.d.ts +9 -0
- package/dist/create-logger.d.ts.map +1 -0
- package/dist/create-logger.js +43 -0
- package/dist/create-logger.js.map +1 -0
- package/dist/extension/cdp-relay.d.ts +7 -3
- package/dist/extension/cdp-relay.d.ts.map +1 -1
- package/dist/extension/cdp-relay.js +46 -13
- package/dist/extension/cdp-relay.js.map +1 -1
- package/dist/mcp.js +52 -27
- package/dist/mcp.js.map +1 -1
- package/dist/mcp.test.d.ts.map +1 -1
- package/dist/mcp.test.js +625 -185
- package/dist/mcp.test.js.map +1 -1
- package/dist/prompt.md +36 -8
- package/dist/selector-generator.js +331 -0
- package/dist/start-relay-server.d.ts +1 -3
- package/dist/start-relay-server.d.ts.map +1 -1
- package/dist/start-relay-server.js +3 -16
- package/dist/start-relay-server.js.map +1 -1
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +34 -0
- package/dist/utils.js.map +1 -1
- package/dist/wait-for-page-load.d.ts +16 -0
- package/dist/wait-for-page-load.d.ts.map +1 -0
- package/dist/wait-for-page-load.js +114 -0
- package/dist/wait-for-page-load.js.map +1 -0
- package/package.json +4 -2
- package/src/cdp-session.ts +142 -0
- package/src/cdp-types.ts +6 -0
- package/src/create-logger.ts +56 -0
- package/src/debugger.md +453 -0
- package/src/extension/cdp-relay.ts +57 -15
- package/src/mcp.test.ts +743 -191
- package/src/mcp.ts +63 -29
- package/src/prompt.md +36 -8
- package/src/snapshots/shadcn-ui-accessibility.md +94 -91
- package/src/start-relay-server.ts +3 -20
- package/src/utils.ts +43 -0
- package/src/wait-for-page-load.ts +162 -0
package/src/mcp.test.ts
CHANGED
|
@@ -8,17 +8,21 @@ import fs from 'node:fs'
|
|
|
8
8
|
import os from 'node:os'
|
|
9
9
|
import { getCdpUrl } from './utils.js'
|
|
10
10
|
import type { ExtensionState } from 'mcp-extension/src/types.js'
|
|
11
|
+
import type { Protocol } from 'devtools-protocol'
|
|
12
|
+
import { imageSize } from 'image-size'
|
|
13
|
+
import { getCDPSessionForPage } from './cdp-session.js'
|
|
14
|
+
import { startPlayWriterCDPRelayServer, type RelayServer } from './extension/cdp-relay.js'
|
|
15
|
+
import { createFileLogger } from './create-logger.js'
|
|
16
|
+
import type { CDPCommand } from './cdp-types.js'
|
|
11
17
|
|
|
12
|
-
|
|
18
|
+
declare const window: any
|
|
19
|
+
declare const document: any
|
|
13
20
|
|
|
14
21
|
|
|
15
22
|
const execAsync = promisify(exec)
|
|
16
23
|
|
|
17
24
|
async function getExtensionServiceWorker(context: BrowserContext) {
|
|
18
|
-
|
|
19
25
|
let serviceWorkers = context.serviceWorkers().filter(sw => sw.url().startsWith('chrome-extension://'))
|
|
20
|
-
|
|
21
|
-
|
|
22
26
|
let serviceWorker = serviceWorkers[0]
|
|
23
27
|
if (!serviceWorker) {
|
|
24
28
|
serviceWorker = await context.waitForEvent('serviceworker', {
|
|
@@ -26,6 +30,14 @@ async function getExtensionServiceWorker(context: BrowserContext) {
|
|
|
26
30
|
})
|
|
27
31
|
}
|
|
28
32
|
|
|
33
|
+
for (let i = 0; i < 50; i++) {
|
|
34
|
+
const isReady = await serviceWorker.evaluate(() => {
|
|
35
|
+
// @ts-ignore
|
|
36
|
+
return typeof globalThis.toggleExtensionForActiveTab === 'function'
|
|
37
|
+
})
|
|
38
|
+
if (isReady) break
|
|
39
|
+
await new Promise(r => setTimeout(r, 100))
|
|
40
|
+
}
|
|
29
41
|
|
|
30
42
|
return serviceWorker
|
|
31
43
|
}
|
|
@@ -51,6 +63,67 @@ async function killProcessOnPort(port: number): Promise<void> {
|
|
|
51
63
|
}
|
|
52
64
|
}
|
|
53
65
|
|
|
66
|
+
interface TestContext {
|
|
67
|
+
browserContext: Awaited<ReturnType<typeof chromium.launchPersistentContext>>
|
|
68
|
+
userDataDir: string
|
|
69
|
+
relayServer: RelayServer
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function setupTestContext({ tempDirPrefix }: { tempDirPrefix: string }): Promise<TestContext> {
|
|
73
|
+
await killProcessOnPort(19988)
|
|
74
|
+
|
|
75
|
+
console.log('Building extension...')
|
|
76
|
+
await execAsync('TESTING=1 pnpm build', { cwd: '../extension' })
|
|
77
|
+
console.log('Extension built')
|
|
78
|
+
|
|
79
|
+
const localLogPath = path.join(process.cwd(), 'relay-server.log')
|
|
80
|
+
const logger = createFileLogger({ logFilePath: localLogPath })
|
|
81
|
+
const relayServer = await startPlayWriterCDPRelayServer({ port: 19988, logger })
|
|
82
|
+
|
|
83
|
+
const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), tempDirPrefix))
|
|
84
|
+
const extensionPath = path.resolve('../extension/dist')
|
|
85
|
+
|
|
86
|
+
const browserContext = await chromium.launchPersistentContext(userDataDir, {
|
|
87
|
+
channel: 'chromium',
|
|
88
|
+
headless: !process.env.HEADFUL,
|
|
89
|
+
args: [
|
|
90
|
+
`--disable-extensions-except=${extensionPath}`,
|
|
91
|
+
`--load-extension=${extensionPath}`,
|
|
92
|
+
],
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
96
|
+
|
|
97
|
+
const page = await browserContext.newPage()
|
|
98
|
+
await page.goto('about:blank')
|
|
99
|
+
|
|
100
|
+
await serviceWorker.evaluate(async () => {
|
|
101
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
return { browserContext, userDataDir, relayServer }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function cleanupTestContext(ctx: TestContext | null, cleanup?: (() => Promise<void>) | null): Promise<void> {
|
|
108
|
+
if (ctx?.browserContext) {
|
|
109
|
+
await ctx.browserContext.close()
|
|
110
|
+
}
|
|
111
|
+
if (ctx?.relayServer) {
|
|
112
|
+
ctx.relayServer.close()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (ctx?.userDataDir) {
|
|
116
|
+
try {
|
|
117
|
+
fs.rmSync(ctx.userDataDir, { recursive: true, force: true })
|
|
118
|
+
} catch (e) {
|
|
119
|
+
console.error('Failed to cleanup user data dir:', e)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (cleanup) {
|
|
123
|
+
await cleanup()
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
54
127
|
declare global {
|
|
55
128
|
var toggleExtensionForActiveTab: () => Promise<{ isConnected: boolean; state: ExtensionState }>;
|
|
56
129
|
var getExtensionState: () => ExtensionState;
|
|
@@ -60,106 +133,57 @@ declare global {
|
|
|
60
133
|
describe('MCP Server Tests', () => {
|
|
61
134
|
let client: Awaited<ReturnType<typeof createMCPClient>>['client']
|
|
62
135
|
let cleanup: (() => Promise<void>) | null = null
|
|
63
|
-
let
|
|
64
|
-
let userDataDir: string
|
|
65
|
-
let relayServerProcess: any
|
|
136
|
+
let testCtx: TestContext | null = null
|
|
66
137
|
|
|
67
138
|
beforeAll(async () => {
|
|
68
|
-
await
|
|
69
|
-
|
|
70
|
-
// Build extension
|
|
71
|
-
console.log('Building extension...')
|
|
72
|
-
await execAsync('TESTING=1 pnpm build', { cwd: '../extension' })
|
|
73
|
-
console.log('Extension built')
|
|
74
|
-
|
|
75
|
-
// Start Relay Server manually
|
|
76
|
-
relayServerProcess = spawn('pnpm', ['tsx', 'src/start-relay-server.ts'], {
|
|
77
|
-
cwd: process.cwd(),
|
|
78
|
-
stdio: 'inherit'
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
// Wait for port 19988 to be ready
|
|
82
|
-
await new Promise<void>((resolve, reject) => {
|
|
83
|
-
let retries = 0
|
|
84
|
-
const interval = setInterval(async () => {
|
|
85
|
-
try {
|
|
86
|
-
const { stdout } = await execAsync('lsof -ti:19988')
|
|
87
|
-
if (stdout.trim()) {
|
|
88
|
-
clearInterval(interval)
|
|
89
|
-
resolve()
|
|
90
|
-
}
|
|
91
|
-
} catch {
|
|
92
|
-
// ignore
|
|
93
|
-
}
|
|
94
|
-
retries++
|
|
95
|
-
if (retries > 30) {
|
|
96
|
-
clearInterval(interval)
|
|
97
|
-
reject(new Error('Relay server failed to start'))
|
|
98
|
-
}
|
|
99
|
-
}, 1000)
|
|
100
|
-
})
|
|
139
|
+
testCtx = await setupTestContext({ tempDirPrefix: 'pw-test-' })
|
|
101
140
|
|
|
102
141
|
const result = await createMCPClient()
|
|
103
142
|
client = result.client
|
|
104
143
|
cleanup = result.cleanup
|
|
144
|
+
}, 600000)
|
|
105
145
|
|
|
106
|
-
|
|
107
|
-
|
|
146
|
+
afterAll(async () => {
|
|
147
|
+
await cleanupTestContext(testCtx, cleanup)
|
|
148
|
+
cleanup = null
|
|
149
|
+
testCtx = null
|
|
150
|
+
})
|
|
108
151
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
`--disable-extensions-except=${extensionPath}`,
|
|
114
|
-
`--load-extension=${extensionPath}`,
|
|
115
|
-
],
|
|
116
|
-
})
|
|
152
|
+
const getBrowserContext = () => {
|
|
153
|
+
if (!testCtx?.browserContext) throw new Error('Browser not initialized')
|
|
154
|
+
return testCtx.browserContext
|
|
155
|
+
}
|
|
117
156
|
|
|
118
|
-
|
|
157
|
+
it('should inject script via addScriptTag through CDP relay', async () => {
|
|
158
|
+
const browserContext = getBrowserContext()
|
|
119
159
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
120
160
|
|
|
121
|
-
// Wait for extension to initialize global functions
|
|
122
|
-
for (let i = 0; i < 50; i++) {
|
|
123
|
-
const isReady = await serviceWorker.evaluate(() => {
|
|
124
|
-
// @ts-ignore
|
|
125
|
-
return typeof globalThis.toggleExtensionForActiveTab === 'function'
|
|
126
|
-
})
|
|
127
|
-
if (isReady) break
|
|
128
|
-
await new Promise(r => setTimeout(r, 100))
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Create a page to attach to
|
|
132
161
|
const page = await browserContext.newPage()
|
|
133
|
-
await page.
|
|
162
|
+
await page.setContent('<html><body><button id="btn">Click</button></body></html>')
|
|
163
|
+
await page.bringToFront()
|
|
134
164
|
|
|
135
|
-
// Connect the tab
|
|
136
165
|
await serviceWorker.evaluate(async () => {
|
|
137
|
-
|
|
166
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
138
167
|
})
|
|
168
|
+
await new Promise(r => setTimeout(r, 500))
|
|
139
169
|
|
|
140
|
-
|
|
170
|
+
const browser = await chromium.connectOverCDP(getCdpUrl())
|
|
171
|
+
const cdpPage = browser.contexts()[0].pages().find(p => {
|
|
172
|
+
return p.url().startsWith('about:')
|
|
173
|
+
})
|
|
174
|
+
expect(cdpPage).toBeDefined()
|
|
141
175
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
await browserContext.close()
|
|
145
|
-
}
|
|
146
|
-
if (relayServerProcess) {
|
|
147
|
-
relayServerProcess.kill()
|
|
148
|
-
}
|
|
149
|
-
await killProcessOnPort(19988)
|
|
176
|
+
const hasGlobalBefore = await cdpPage!.evaluate(() => !!(globalThis as any).__testGlobal)
|
|
177
|
+
expect(hasGlobalBefore).toBe(false)
|
|
150
178
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
await cleanup()
|
|
160
|
-
cleanup = null
|
|
161
|
-
}
|
|
162
|
-
})
|
|
179
|
+
await cdpPage!.addScriptTag({ content: 'globalThis.__testGlobal = { foo: "bar" };' })
|
|
180
|
+
|
|
181
|
+
const hasGlobalAfter = await cdpPage!.evaluate(() => (globalThis as any).__testGlobal)
|
|
182
|
+
expect(hasGlobalAfter).toEqual({ foo: 'bar' })
|
|
183
|
+
|
|
184
|
+
await browser.close()
|
|
185
|
+
await page.close()
|
|
186
|
+
}, 60000)
|
|
163
187
|
|
|
164
188
|
it('should execute code and capture console output', async () => {
|
|
165
189
|
await client.callTool({
|
|
@@ -204,7 +228,7 @@ describe('MCP Server Tests', () => {
|
|
|
204
228
|
}, 30000)
|
|
205
229
|
|
|
206
230
|
it('should show extension as connected for pages created via newPage()', async () => {
|
|
207
|
-
|
|
231
|
+
const browserContext = getBrowserContext()
|
|
208
232
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
209
233
|
|
|
210
234
|
// Create a page via MCP (which uses context.newPage())
|
|
@@ -341,9 +365,7 @@ describe('MCP Server Tests', () => {
|
|
|
341
365
|
})
|
|
342
366
|
|
|
343
367
|
it('should handle new pages and toggling with new connections', async () => {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
// Find the correct service worker by URL
|
|
368
|
+
const browserContext = getBrowserContext()
|
|
347
369
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
348
370
|
|
|
349
371
|
// 1. Create a new page
|
|
@@ -430,8 +452,7 @@ describe('MCP Server Tests', () => {
|
|
|
430
452
|
})
|
|
431
453
|
|
|
432
454
|
it('should handle new pages and toggling with persistent connection', async () => {
|
|
433
|
-
|
|
434
|
-
|
|
455
|
+
const browserContext = getBrowserContext()
|
|
435
456
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
436
457
|
|
|
437
458
|
// Connect once
|
|
@@ -502,7 +523,7 @@ describe('MCP Server Tests', () => {
|
|
|
502
523
|
await directBrowser.close()
|
|
503
524
|
})
|
|
504
525
|
it('should maintain connection across reloads and navigation', async () => {
|
|
505
|
-
|
|
526
|
+
const browserContext = getBrowserContext()
|
|
506
527
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
507
528
|
|
|
508
529
|
// 1. Setup page
|
|
@@ -551,7 +572,7 @@ describe('MCP Server Tests', () => {
|
|
|
551
572
|
})
|
|
552
573
|
|
|
553
574
|
it('should support multiple concurrent tabs', async () => {
|
|
554
|
-
|
|
575
|
+
const browserContext = getBrowserContext()
|
|
555
576
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
556
577
|
await new Promise(resolve => setTimeout(resolve, 500))
|
|
557
578
|
|
|
@@ -610,94 +631,8 @@ describe('MCP Server Tests', () => {
|
|
|
610
631
|
expect(results).toMatchInlineSnapshot(`
|
|
611
632
|
[
|
|
612
633
|
{
|
|
613
|
-
"title": "
|
|
614
|
-
"url": "
|
|
615
|
-
},
|
|
616
|
-
{
|
|
617
|
-
"title": "Example Domain",
|
|
618
|
-
"url": "https://example.com/tab-a",
|
|
619
|
-
},
|
|
620
|
-
{
|
|
621
|
-
"title": "Example Domain",
|
|
622
|
-
"url": "https://example.com/tab-b",
|
|
623
|
-
},
|
|
624
|
-
]
|
|
625
|
-
`)
|
|
626
|
-
|
|
627
|
-
// Verify execution on both pages
|
|
628
|
-
const pageA_CDP = pages.find(p => p.url().includes('tab-a'))
|
|
629
|
-
const pageB_CDP = pages.find(p => p.url().includes('tab-b'))
|
|
630
|
-
|
|
631
|
-
expect(await pageA_CDP?.evaluate(() => 10 + 10)).toBe(20)
|
|
632
|
-
expect(await pageB_CDP?.evaluate(() => 20 + 20)).toBe(40)
|
|
633
|
-
|
|
634
|
-
await browser.close()
|
|
635
|
-
await pageA.close()
|
|
636
|
-
await pageB.close()
|
|
637
|
-
})
|
|
638
|
-
|
|
639
|
-
it('should support multiple concurrent tabs', async () => {
|
|
640
|
-
if (!browserContext) throw new Error('Browser not initialized')
|
|
641
|
-
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
642
|
-
await new Promise(resolve => setTimeout(resolve, 500))
|
|
643
|
-
|
|
644
|
-
// Tab A
|
|
645
|
-
const pageA = await browserContext.newPage()
|
|
646
|
-
await pageA.goto('https://example.com/tab-a')
|
|
647
|
-
await pageA.bringToFront()
|
|
648
|
-
await new Promise(resolve => setTimeout(resolve, 500))
|
|
649
|
-
await serviceWorker.evaluate(async () => {
|
|
650
|
-
await globalThis.toggleExtensionForActiveTab()
|
|
651
|
-
})
|
|
652
|
-
|
|
653
|
-
// Tab B
|
|
654
|
-
const pageB = await browserContext.newPage()
|
|
655
|
-
await pageB.goto('https://example.com/tab-b')
|
|
656
|
-
await pageB.bringToFront()
|
|
657
|
-
await new Promise(resolve => setTimeout(resolve, 500))
|
|
658
|
-
await serviceWorker.evaluate(async () => {
|
|
659
|
-
await globalThis.toggleExtensionForActiveTab()
|
|
660
|
-
})
|
|
661
|
-
|
|
662
|
-
// Get target IDs for both
|
|
663
|
-
const targetIds = await serviceWorker.evaluate(async () => {
|
|
664
|
-
const state = globalThis.getExtensionState()
|
|
665
|
-
const chrome = globalThis.chrome
|
|
666
|
-
const tabs = await chrome.tabs.query({})
|
|
667
|
-
const tabA = tabs.find((t: any) => t.url?.includes('tab-a'))
|
|
668
|
-
const tabB = tabs.find((t: any) => t.url?.includes('tab-b'))
|
|
669
|
-
return {
|
|
670
|
-
idA: state.tabs.get(tabA?.id ?? -1)?.targetId,
|
|
671
|
-
idB: state.tabs.get(tabB?.id ?? -1)?.targetId
|
|
672
|
-
}
|
|
673
|
-
})
|
|
674
|
-
|
|
675
|
-
expect(targetIds).toMatchInlineSnapshot({
|
|
676
|
-
idA: expect.any(String),
|
|
677
|
-
idB: expect.any(String)
|
|
678
|
-
}, `
|
|
679
|
-
{
|
|
680
|
-
"idA": Any<String>,
|
|
681
|
-
"idB": Any<String>,
|
|
682
|
-
}
|
|
683
|
-
`)
|
|
684
|
-
expect(targetIds.idA).not.toBe(targetIds.idB)
|
|
685
|
-
|
|
686
|
-
// Verify independent connections
|
|
687
|
-
const browser = await chromium.connectOverCDP(getCdpUrl())
|
|
688
|
-
|
|
689
|
-
const pages = browser.contexts()[0].pages()
|
|
690
|
-
|
|
691
|
-
const results = await Promise.all(pages.map(async (p) => ({
|
|
692
|
-
url: p.url(),
|
|
693
|
-
title: await p.title()
|
|
694
|
-
})))
|
|
695
|
-
|
|
696
|
-
expect(results).toMatchInlineSnapshot(`
|
|
697
|
-
[
|
|
698
|
-
{
|
|
699
|
-
"title": "🎄 Twelve Days of Shell 🎄",
|
|
700
|
-
"url": "https://12days.cmdchallenge.com/",
|
|
634
|
+
"title": "",
|
|
635
|
+
"url": "about:blank",
|
|
701
636
|
},
|
|
702
637
|
{
|
|
703
638
|
"title": "Example Domain",
|
|
@@ -723,10 +658,9 @@ describe('MCP Server Tests', () => {
|
|
|
723
658
|
})
|
|
724
659
|
|
|
725
660
|
it('should show correct url when enabling extension after navigation', async () => {
|
|
726
|
-
|
|
661
|
+
const browserContext = getBrowserContext()
|
|
727
662
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
728
663
|
|
|
729
|
-
// 1. Open a new page (extension not yet enabled for it)
|
|
730
664
|
const page = await browserContext.newPage()
|
|
731
665
|
const targetUrl = 'https://example.com/late-enable'
|
|
732
666
|
await page.goto(targetUrl)
|
|
@@ -755,10 +689,9 @@ describe('MCP Server Tests', () => {
|
|
|
755
689
|
})
|
|
756
690
|
|
|
757
691
|
it('should be able to reconnect after disconnecting everything', async () => {
|
|
758
|
-
|
|
692
|
+
const browserContext = getBrowserContext()
|
|
759
693
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
760
694
|
|
|
761
|
-
// 1. Use the existing about:blank page from beforeAll
|
|
762
695
|
const pages = await browserContext.pages()
|
|
763
696
|
expect(pages.length).toBeGreaterThan(0)
|
|
764
697
|
const page = pages[0]
|
|
@@ -1233,7 +1166,7 @@ describe('MCP Server Tests', () => {
|
|
|
1233
1166
|
}, 30000)
|
|
1234
1167
|
|
|
1235
1168
|
it('should maintain correct page.url() with service worker pages', async () => {
|
|
1236
|
-
|
|
1169
|
+
const browserContext = getBrowserContext()
|
|
1237
1170
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1238
1171
|
|
|
1239
1172
|
const page = await browserContext.newPage()
|
|
@@ -1260,7 +1193,7 @@ describe('MCP Server Tests', () => {
|
|
|
1260
1193
|
}, 30000)
|
|
1261
1194
|
|
|
1262
1195
|
it('should maintain correct page.url() after repeated connections', async () => {
|
|
1263
|
-
|
|
1196
|
+
const browserContext = getBrowserContext()
|
|
1264
1197
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1265
1198
|
|
|
1266
1199
|
const page = await browserContext.newPage()
|
|
@@ -1288,7 +1221,7 @@ describe('MCP Server Tests', () => {
|
|
|
1288
1221
|
}, 30000)
|
|
1289
1222
|
|
|
1290
1223
|
it('should maintain correct page.url() with concurrent MCP and CDP connections', async () => {
|
|
1291
|
-
|
|
1224
|
+
const browserContext = getBrowserContext()
|
|
1292
1225
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1293
1226
|
|
|
1294
1227
|
const page = await browserContext.newPage()
|
|
@@ -1328,7 +1261,7 @@ describe('MCP Server Tests', () => {
|
|
|
1328
1261
|
}, 30000)
|
|
1329
1262
|
|
|
1330
1263
|
it('should maintain correct page.url() with iframe-heavy pages', async () => {
|
|
1331
|
-
|
|
1264
|
+
const browserContext = getBrowserContext()
|
|
1332
1265
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1333
1266
|
|
|
1334
1267
|
const page = await browserContext.newPage()
|
|
@@ -1357,8 +1290,362 @@ describe('MCP Server Tests', () => {
|
|
|
1357
1290
|
await page.close()
|
|
1358
1291
|
}, 60000)
|
|
1359
1292
|
|
|
1293
|
+
it('should capture screenshot correctly', async () => {
|
|
1294
|
+
const browserContext = getBrowserContext()
|
|
1295
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1296
|
+
|
|
1297
|
+
const page = await browserContext.newPage()
|
|
1298
|
+
await page.goto('https://example.com/')
|
|
1299
|
+
await page.bringToFront()
|
|
1300
|
+
|
|
1301
|
+
await serviceWorker.evaluate(async () => {
|
|
1302
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1303
|
+
})
|
|
1304
|
+
|
|
1305
|
+
await new Promise(r => setTimeout(r, 500))
|
|
1306
|
+
|
|
1307
|
+
const capturedCommands: CDPCommand[] = []
|
|
1308
|
+
const commandHandler = ({ command }: { clientId: string; command: CDPCommand }) => {
|
|
1309
|
+
if (command.method === 'Page.captureScreenshot') {
|
|
1310
|
+
capturedCommands.push(command)
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
testCtx!.relayServer.on('cdp:command', commandHandler)
|
|
1314
|
+
|
|
1315
|
+
const browser = await chromium.connectOverCDP(getCdpUrl())
|
|
1316
|
+
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
|
|
1317
|
+
|
|
1318
|
+
expect(cdpPage).toBeDefined()
|
|
1319
|
+
|
|
1320
|
+
const viewportSize = cdpPage!.viewportSize()
|
|
1321
|
+
console.log('Viewport size:', viewportSize)
|
|
1322
|
+
|
|
1323
|
+
const viewportScreenshot = await cdpPage!.screenshot()
|
|
1324
|
+
expect(viewportScreenshot).toBeDefined()
|
|
1325
|
+
|
|
1326
|
+
const viewportDimensions = imageSize(viewportScreenshot)
|
|
1327
|
+
console.log('Viewport screenshot dimensions:', viewportDimensions)
|
|
1328
|
+
expect(viewportDimensions.width).toBeGreaterThan(0)
|
|
1329
|
+
expect(viewportDimensions.height).toBeGreaterThan(0)
|
|
1330
|
+
if (viewportSize) {
|
|
1331
|
+
expect(viewportDimensions.width).toBe(viewportSize.width)
|
|
1332
|
+
expect(viewportDimensions.height).toBe(viewportSize.height)
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
const fullPageScreenshot = await cdpPage!.screenshot({ fullPage: true })
|
|
1336
|
+
expect(fullPageScreenshot).toBeDefined()
|
|
1337
|
+
|
|
1338
|
+
const fullPageDimensions = imageSize(fullPageScreenshot)
|
|
1339
|
+
console.log('Full page screenshot dimensions:', fullPageDimensions)
|
|
1340
|
+
expect(fullPageDimensions.width).toBeGreaterThan(0)
|
|
1341
|
+
expect(fullPageDimensions.height).toBeGreaterThan(0)
|
|
1342
|
+
expect(fullPageDimensions.width).toBeGreaterThanOrEqual(viewportDimensions.width!)
|
|
1343
|
+
|
|
1344
|
+
testCtx!.relayServer.off('cdp:command', commandHandler)
|
|
1345
|
+
|
|
1346
|
+
expect(capturedCommands.length).toBe(2)
|
|
1347
|
+
expect(capturedCommands.map(c => ({
|
|
1348
|
+
method: c.method,
|
|
1349
|
+
params: c.params
|
|
1350
|
+
}))).toMatchInlineSnapshot(`
|
|
1351
|
+
[
|
|
1352
|
+
{
|
|
1353
|
+
"method": "Page.captureScreenshot",
|
|
1354
|
+
"params": {
|
|
1355
|
+
"captureBeyondViewport": false,
|
|
1356
|
+
"clip": {
|
|
1357
|
+
"height": 720,
|
|
1358
|
+
"scale": 1,
|
|
1359
|
+
"width": 1280,
|
|
1360
|
+
"x": 0,
|
|
1361
|
+
"y": 0,
|
|
1362
|
+
},
|
|
1363
|
+
"format": "png",
|
|
1364
|
+
},
|
|
1365
|
+
},
|
|
1366
|
+
{
|
|
1367
|
+
"method": "Page.captureScreenshot",
|
|
1368
|
+
"params": {
|
|
1369
|
+
"captureBeyondViewport": false,
|
|
1370
|
+
"clip": {
|
|
1371
|
+
"height": 528,
|
|
1372
|
+
"scale": 1,
|
|
1373
|
+
"width": 1280,
|
|
1374
|
+
"x": 0,
|
|
1375
|
+
"y": 0,
|
|
1376
|
+
},
|
|
1377
|
+
"format": "png",
|
|
1378
|
+
},
|
|
1379
|
+
},
|
|
1380
|
+
]
|
|
1381
|
+
`)
|
|
1382
|
+
|
|
1383
|
+
const screenshotPath = path.join(os.tmpdir(), 'playwriter-test-screenshot.png')
|
|
1384
|
+
fs.writeFileSync(screenshotPath, viewportScreenshot)
|
|
1385
|
+
console.log('Screenshot saved to:', screenshotPath)
|
|
1386
|
+
|
|
1387
|
+
await browser.close()
|
|
1388
|
+
await page.close()
|
|
1389
|
+
}, 60000)
|
|
1390
|
+
|
|
1391
|
+
it('should capture element screenshot with correct coordinates', async () => {
|
|
1392
|
+
const browserContext = getBrowserContext()
|
|
1393
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1394
|
+
|
|
1395
|
+
const target = { x: 200, y: 150, width: 300, height: 100 }
|
|
1396
|
+
const scrolledTarget = { x: 100, y: 1500, width: 200, height: 80 }
|
|
1397
|
+
|
|
1398
|
+
const page = await browserContext.newPage()
|
|
1399
|
+
await page.setContent(`
|
|
1400
|
+
<html>
|
|
1401
|
+
<head>
|
|
1402
|
+
<style>
|
|
1403
|
+
body { margin: 0; padding: 0; height: 2000px; }
|
|
1404
|
+
#target {
|
|
1405
|
+
position: absolute;
|
|
1406
|
+
top: ${target.y}px;
|
|
1407
|
+
left: ${target.x}px;
|
|
1408
|
+
width: ${target.width}px;
|
|
1409
|
+
height: ${target.height}px;
|
|
1410
|
+
background: red;
|
|
1411
|
+
}
|
|
1412
|
+
#scrolled-target {
|
|
1413
|
+
position: absolute;
|
|
1414
|
+
top: ${scrolledTarget.y}px;
|
|
1415
|
+
left: ${scrolledTarget.x}px;
|
|
1416
|
+
width: ${scrolledTarget.width}px;
|
|
1417
|
+
height: ${scrolledTarget.height}px;
|
|
1418
|
+
background: blue;
|
|
1419
|
+
}
|
|
1420
|
+
</style>
|
|
1421
|
+
</head>
|
|
1422
|
+
<body>
|
|
1423
|
+
<div id="target">Target Element</div>
|
|
1424
|
+
<div id="scrolled-target">Scrolled Target</div>
|
|
1425
|
+
</body>
|
|
1426
|
+
</html>
|
|
1427
|
+
`)
|
|
1428
|
+
await page.bringToFront()
|
|
1429
|
+
|
|
1430
|
+
await serviceWorker.evaluate(async () => {
|
|
1431
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1432
|
+
})
|
|
1433
|
+
|
|
1434
|
+
await new Promise(r => setTimeout(r, 500))
|
|
1435
|
+
|
|
1436
|
+
const capturedCommands: CDPCommand[] = []
|
|
1437
|
+
const commandHandler = ({ command }: { clientId: string; command: CDPCommand }) => {
|
|
1438
|
+
if (command.method === 'Page.captureScreenshot') {
|
|
1439
|
+
capturedCommands.push(command)
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
testCtx!.relayServer.on('cdp:command', commandHandler)
|
|
1443
|
+
|
|
1444
|
+
const browser = await chromium.connectOverCDP(getCdpUrl())
|
|
1445
|
+
let cdpPage
|
|
1446
|
+
for (const p of browser.contexts()[0].pages()) {
|
|
1447
|
+
const html = await p.content()
|
|
1448
|
+
if (html.includes('scrolled-target')) {
|
|
1449
|
+
cdpPage = p
|
|
1450
|
+
break
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
expect(cdpPage).toBeDefined()
|
|
1454
|
+
|
|
1455
|
+
await cdpPage!.locator('#target').screenshot()
|
|
1456
|
+
|
|
1457
|
+
await cdpPage!.locator('#scrolled-target').screenshot()
|
|
1458
|
+
|
|
1459
|
+
testCtx!.relayServer.off('cdp:command', commandHandler)
|
|
1460
|
+
|
|
1461
|
+
expect(capturedCommands.length).toBe(2)
|
|
1462
|
+
|
|
1463
|
+
const targetCmd = capturedCommands[0]
|
|
1464
|
+
expect(targetCmd.method).toBe('Page.captureScreenshot')
|
|
1465
|
+
const targetClip = (targetCmd.params as any).clip
|
|
1466
|
+
expect(targetClip.x).toBe(target.x)
|
|
1467
|
+
expect(targetClip.y).toBe(target.y)
|
|
1468
|
+
expect(targetClip.width).toBe(target.width)
|
|
1469
|
+
expect(targetClip.height).toBe(target.height)
|
|
1470
|
+
|
|
1471
|
+
const scrolledCmd = capturedCommands[1]
|
|
1472
|
+
expect(scrolledCmd.method).toBe('Page.captureScreenshot')
|
|
1473
|
+
const scrolledClip = (scrolledCmd.params as any).clip
|
|
1474
|
+
expect(scrolledClip.x).toBe(scrolledTarget.x)
|
|
1475
|
+
expect(scrolledClip.y).toBe(scrolledTarget.y)
|
|
1476
|
+
expect(scrolledClip.width).toBe(scrolledTarget.width)
|
|
1477
|
+
expect(scrolledClip.height).toBe(scrolledTarget.height)
|
|
1478
|
+
|
|
1479
|
+
await browser.close()
|
|
1480
|
+
await page.close()
|
|
1481
|
+
}, 60000)
|
|
1482
|
+
|
|
1483
|
+
it('should get locator string for element using getLocatorStringForElement', async () => {
|
|
1484
|
+
const browserContext = getBrowserContext()
|
|
1485
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1486
|
+
|
|
1487
|
+
const page = await browserContext.newPage()
|
|
1488
|
+
await page.setContent(`
|
|
1489
|
+
<html>
|
|
1490
|
+
<body>
|
|
1491
|
+
<button id="test-btn">Click Me</button>
|
|
1492
|
+
<input type="text" placeholder="Enter name" />
|
|
1493
|
+
</body>
|
|
1494
|
+
</html>
|
|
1495
|
+
`)
|
|
1496
|
+
await page.bringToFront()
|
|
1497
|
+
|
|
1498
|
+
await serviceWorker.evaluate(async () => {
|
|
1499
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1500
|
+
})
|
|
1501
|
+
|
|
1502
|
+
await new Promise(r => setTimeout(r, 500))
|
|
1503
|
+
|
|
1504
|
+
const result = await client.callTool({
|
|
1505
|
+
name: 'execute',
|
|
1506
|
+
arguments: {
|
|
1507
|
+
code: js`
|
|
1508
|
+
let testPage;
|
|
1509
|
+
for (const p of context.pages()) {
|
|
1510
|
+
const html = await p.content();
|
|
1511
|
+
if (html.includes('test-btn')) { testPage = p; break; }
|
|
1512
|
+
}
|
|
1513
|
+
if (!testPage) throw new Error('Test page not found');
|
|
1514
|
+
const btn = testPage.locator('#test-btn');
|
|
1515
|
+
const locatorString = await getLocatorStringForElement(btn);
|
|
1516
|
+
console.log('Locator string:', locatorString);
|
|
1517
|
+
const locatorFromString = eval('testPage.' + locatorString);
|
|
1518
|
+
const count = await locatorFromString.count();
|
|
1519
|
+
console.log('Locator count:', count);
|
|
1520
|
+
const text = await locatorFromString.textContent();
|
|
1521
|
+
console.log('Locator text:', text);
|
|
1522
|
+
`,
|
|
1523
|
+
timeout: 30000,
|
|
1524
|
+
},
|
|
1525
|
+
})
|
|
1526
|
+
|
|
1527
|
+
expect(result.isError).toBeFalsy()
|
|
1528
|
+
const text = (result.content as any)[0]?.text || ''
|
|
1529
|
+
expect(text).toContain('Locator string:')
|
|
1530
|
+
expect(text).toContain("getByRole('button', { name: 'Click Me' })")
|
|
1531
|
+
expect(text).toContain('Locator count: 1')
|
|
1532
|
+
expect(text).toContain('Locator text: Click Me')
|
|
1533
|
+
|
|
1534
|
+
await page.close()
|
|
1535
|
+
}, 60000)
|
|
1536
|
+
|
|
1537
|
+
it('should return correct layout metrics via CDP', async () => {
|
|
1538
|
+
const browserContext = getBrowserContext()
|
|
1539
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1540
|
+
|
|
1541
|
+
const page = await browserContext.newPage()
|
|
1542
|
+
await page.goto('https://example.com/')
|
|
1543
|
+
await page.bringToFront()
|
|
1544
|
+
|
|
1545
|
+
await serviceWorker.evaluate(async () => {
|
|
1546
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1547
|
+
})
|
|
1548
|
+
|
|
1549
|
+
await new Promise(r => setTimeout(r, 500))
|
|
1550
|
+
|
|
1551
|
+
const browser = await chromium.connectOverCDP(getCdpUrl())
|
|
1552
|
+
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
|
|
1553
|
+
expect(cdpPage).toBeDefined()
|
|
1554
|
+
|
|
1555
|
+
const wsUrl = getCdpUrl()
|
|
1556
|
+
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
|
1557
|
+
|
|
1558
|
+
const layoutMetrics = await cdpSession.send('Page.getLayoutMetrics')
|
|
1559
|
+
|
|
1560
|
+
const normalized = {
|
|
1561
|
+
cssLayoutViewport: layoutMetrics.cssLayoutViewport,
|
|
1562
|
+
cssVisualViewport: layoutMetrics.cssVisualViewport,
|
|
1563
|
+
layoutViewport: layoutMetrics.layoutViewport,
|
|
1564
|
+
visualViewport: layoutMetrics.visualViewport,
|
|
1565
|
+
devicePixelRatio: layoutMetrics.cssVisualViewport.clientWidth > 0
|
|
1566
|
+
? layoutMetrics.visualViewport.clientWidth / layoutMetrics.cssVisualViewport.clientWidth
|
|
1567
|
+
: 1,
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
expect(normalized).toMatchInlineSnapshot(`
|
|
1571
|
+
{
|
|
1572
|
+
"cssLayoutViewport": {
|
|
1573
|
+
"clientHeight": 720,
|
|
1574
|
+
"clientWidth": 1280,
|
|
1575
|
+
"pageX": 0,
|
|
1576
|
+
"pageY": 0,
|
|
1577
|
+
},
|
|
1578
|
+
"cssVisualViewport": {
|
|
1579
|
+
"clientHeight": 720,
|
|
1580
|
+
"clientWidth": 1280,
|
|
1581
|
+
"offsetX": 0,
|
|
1582
|
+
"offsetY": 0,
|
|
1583
|
+
"pageX": 0,
|
|
1584
|
+
"pageY": 0,
|
|
1585
|
+
"scale": 1,
|
|
1586
|
+
"zoom": 1,
|
|
1587
|
+
},
|
|
1588
|
+
"devicePixelRatio": 1,
|
|
1589
|
+
"layoutViewport": {
|
|
1590
|
+
"clientHeight": 720,
|
|
1591
|
+
"clientWidth": 1280,
|
|
1592
|
+
"pageX": 0,
|
|
1593
|
+
"pageY": 0,
|
|
1594
|
+
},
|
|
1595
|
+
"visualViewport": {
|
|
1596
|
+
"clientHeight": 720,
|
|
1597
|
+
"clientWidth": 1280,
|
|
1598
|
+
"offsetX": 0,
|
|
1599
|
+
"offsetY": 0,
|
|
1600
|
+
"pageX": 0,
|
|
1601
|
+
"pageY": 0,
|
|
1602
|
+
"scale": 1,
|
|
1603
|
+
"zoom": 1,
|
|
1604
|
+
},
|
|
1605
|
+
}
|
|
1606
|
+
`)
|
|
1607
|
+
|
|
1608
|
+
const windowDpr = await cdpPage!.evaluate(() => (globalThis as any).devicePixelRatio)
|
|
1609
|
+
console.log('window.devicePixelRatio:', windowDpr)
|
|
1610
|
+
expect(windowDpr).toBe(1)
|
|
1611
|
+
|
|
1612
|
+
cdpSession.detach()
|
|
1613
|
+
await browser.close()
|
|
1614
|
+
await page.close()
|
|
1615
|
+
}, 60000)
|
|
1616
|
+
|
|
1617
|
+
it('should support getCDPSession through the relay', async () => {
|
|
1618
|
+
const browserContext = getBrowserContext()
|
|
1619
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1620
|
+
|
|
1621
|
+
const page = await browserContext.newPage()
|
|
1622
|
+
await page.goto('https://example.com/')
|
|
1623
|
+
await page.bringToFront()
|
|
1624
|
+
|
|
1625
|
+
await serviceWorker.evaluate(async () => {
|
|
1626
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1627
|
+
})
|
|
1628
|
+
|
|
1629
|
+
await new Promise(r => setTimeout(r, 500))
|
|
1630
|
+
|
|
1631
|
+
const browser = await chromium.connectOverCDP(getCdpUrl())
|
|
1632
|
+
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
|
|
1633
|
+
expect(cdpPage).toBeDefined()
|
|
1634
|
+
|
|
1635
|
+
const wsUrl = getCdpUrl()
|
|
1636
|
+
const client = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
|
1637
|
+
|
|
1638
|
+
const layoutMetrics = await client.send('Page.getLayoutMetrics')
|
|
1639
|
+
expect(layoutMetrics.cssVisualViewport).toBeDefined()
|
|
1640
|
+
expect(layoutMetrics.cssVisualViewport.clientWidth).toBeGreaterThan(0)
|
|
1641
|
+
|
|
1642
|
+
client.detach()
|
|
1643
|
+
await browser.close()
|
|
1644
|
+
await page.close()
|
|
1645
|
+
}, 60000)
|
|
1646
|
+
|
|
1360
1647
|
it('should work with stagehand', async () => {
|
|
1361
|
-
|
|
1648
|
+
const browserContext = getBrowserContext()
|
|
1362
1649
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1363
1650
|
|
|
1364
1651
|
await serviceWorker.evaluate(async () => {
|
|
@@ -1421,3 +1708,268 @@ function tryJsonParse(str: string) {
|
|
|
1421
1708
|
return str
|
|
1422
1709
|
}
|
|
1423
1710
|
}
|
|
1711
|
+
|
|
1712
|
+
describe('CDP Session Tests', () => {
|
|
1713
|
+
let testCtx: TestContext | null = null
|
|
1714
|
+
|
|
1715
|
+
beforeAll(async () => {
|
|
1716
|
+
testCtx = await setupTestContext({ tempDirPrefix: 'pw-cdp-test-' })
|
|
1717
|
+
}, 600000)
|
|
1718
|
+
|
|
1719
|
+
afterAll(async () => {
|
|
1720
|
+
await cleanupTestContext(testCtx)
|
|
1721
|
+
testCtx = null
|
|
1722
|
+
})
|
|
1723
|
+
|
|
1724
|
+
const getBrowserContext = () => {
|
|
1725
|
+
if (!testCtx?.browserContext) throw new Error('Browser not initialized')
|
|
1726
|
+
return testCtx.browserContext
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
it('should enable debugger and pause on debugger statement via CDP session', async () => {
|
|
1730
|
+
const browserContext = getBrowserContext()
|
|
1731
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1732
|
+
|
|
1733
|
+
const page = await browserContext.newPage()
|
|
1734
|
+
await page.goto('https://example.com/')
|
|
1735
|
+
await page.bringToFront()
|
|
1736
|
+
|
|
1737
|
+
await serviceWorker.evaluate(async () => {
|
|
1738
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1739
|
+
})
|
|
1740
|
+
await new Promise(r => setTimeout(r, 500))
|
|
1741
|
+
|
|
1742
|
+
const browser = await chromium.connectOverCDP(getCdpUrl())
|
|
1743
|
+
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
|
|
1744
|
+
expect(cdpPage).toBeDefined()
|
|
1745
|
+
|
|
1746
|
+
const wsUrl = getCdpUrl()
|
|
1747
|
+
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
|
1748
|
+
await cdpSession.send('Debugger.enable')
|
|
1749
|
+
|
|
1750
|
+
const pausedPromise = new Promise<Protocol.Debugger.PausedEvent>((resolve) => {
|
|
1751
|
+
cdpSession.on('Debugger.paused', (params) => {
|
|
1752
|
+
resolve(params as Protocol.Debugger.PausedEvent)
|
|
1753
|
+
})
|
|
1754
|
+
})
|
|
1755
|
+
|
|
1756
|
+
cdpPage!.evaluate(`
|
|
1757
|
+
(function testFunction() {
|
|
1758
|
+
const localVar = 'hello';
|
|
1759
|
+
const numberVar = 42;
|
|
1760
|
+
const objVar = { key: 'value', nested: { a: 1 } };
|
|
1761
|
+
debugger;
|
|
1762
|
+
return localVar + numberVar;
|
|
1763
|
+
})()
|
|
1764
|
+
`)
|
|
1765
|
+
|
|
1766
|
+
const pausedEvent = await Promise.race([
|
|
1767
|
+
pausedPromise,
|
|
1768
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Debugger.paused timeout')), 5000))
|
|
1769
|
+
])
|
|
1770
|
+
|
|
1771
|
+
const stackTrace = pausedEvent.callFrames.map(frame => ({
|
|
1772
|
+
functionName: frame.functionName || '(anonymous)',
|
|
1773
|
+
lineNumber: frame.location.lineNumber,
|
|
1774
|
+
columnNumber: frame.location.columnNumber,
|
|
1775
|
+
}))
|
|
1776
|
+
|
|
1777
|
+
expect({
|
|
1778
|
+
reason: pausedEvent.reason,
|
|
1779
|
+
stackTrace: stackTrace.slice(0, 3),
|
|
1780
|
+
}).toMatchInlineSnapshot(`
|
|
1781
|
+
{
|
|
1782
|
+
"reason": "other",
|
|
1783
|
+
"stackTrace": [
|
|
1784
|
+
{
|
|
1785
|
+
"columnNumber": 16,
|
|
1786
|
+
"functionName": "testFunction",
|
|
1787
|
+
"lineNumber": 4,
|
|
1788
|
+
},
|
|
1789
|
+
{
|
|
1790
|
+
"columnNumber": 14,
|
|
1791
|
+
"functionName": "(anonymous)",
|
|
1792
|
+
"lineNumber": 6,
|
|
1793
|
+
},
|
|
1794
|
+
{
|
|
1795
|
+
"columnNumber": 29,
|
|
1796
|
+
"functionName": "evaluate",
|
|
1797
|
+
"lineNumber": 289,
|
|
1798
|
+
},
|
|
1799
|
+
],
|
|
1800
|
+
}
|
|
1801
|
+
`)
|
|
1802
|
+
|
|
1803
|
+
const topFrame = pausedEvent.callFrames[0]
|
|
1804
|
+
const scopeChain = topFrame.scopeChain
|
|
1805
|
+
|
|
1806
|
+
const localScope = scopeChain.find(s => s.type === 'local')
|
|
1807
|
+
const localVars: Record<string, unknown> = {}
|
|
1808
|
+
|
|
1809
|
+
if (localScope?.object.objectId) {
|
|
1810
|
+
const propsResult = await cdpSession.send('Runtime.getProperties', {
|
|
1811
|
+
objectId: localScope.object.objectId,
|
|
1812
|
+
ownProperties: true,
|
|
1813
|
+
})
|
|
1814
|
+
|
|
1815
|
+
for (const prop of propsResult.result) {
|
|
1816
|
+
if (prop.value) {
|
|
1817
|
+
localVars[prop.name] = prop.value.type === 'object'
|
|
1818
|
+
? `[object ${prop.value.className || prop.value.subtype || 'Object'}]`
|
|
1819
|
+
: prop.value.value
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
expect({
|
|
1825
|
+
scopeTypes: scopeChain.map(s => s.type),
|
|
1826
|
+
localVariables: localVars,
|
|
1827
|
+
}).toMatchInlineSnapshot(`
|
|
1828
|
+
{
|
|
1829
|
+
"localVariables": {
|
|
1830
|
+
"localVar": "hello",
|
|
1831
|
+
"numberVar": 42,
|
|
1832
|
+
"objVar": "[object Object]",
|
|
1833
|
+
},
|
|
1834
|
+
"scopeTypes": [
|
|
1835
|
+
"local",
|
|
1836
|
+
"global",
|
|
1837
|
+
],
|
|
1838
|
+
}
|
|
1839
|
+
`)
|
|
1840
|
+
|
|
1841
|
+
const evalResult = await cdpSession.send('Debugger.evaluateOnCallFrame', {
|
|
1842
|
+
callFrameId: topFrame.callFrameId,
|
|
1843
|
+
expression: 'localVar + " world " + numberVar',
|
|
1844
|
+
})
|
|
1845
|
+
|
|
1846
|
+
expect({
|
|
1847
|
+
evaluatedExpression: 'localVar + " world " + numberVar',
|
|
1848
|
+
result: evalResult.result.value,
|
|
1849
|
+
type: evalResult.result.type,
|
|
1850
|
+
}).toMatchInlineSnapshot(`
|
|
1851
|
+
{
|
|
1852
|
+
"evaluatedExpression": "localVar + " world " + numberVar",
|
|
1853
|
+
"result": "hello world 42",
|
|
1854
|
+
"type": "string",
|
|
1855
|
+
}
|
|
1856
|
+
`)
|
|
1857
|
+
|
|
1858
|
+
await cdpSession.send('Debugger.resume')
|
|
1859
|
+
await cdpSession.send('Debugger.disable')
|
|
1860
|
+
cdpSession.detach()
|
|
1861
|
+
await browser.close()
|
|
1862
|
+
await page.close()
|
|
1863
|
+
}, 60000)
|
|
1864
|
+
|
|
1865
|
+
it('should profile JavaScript execution using CDP Profiler', async () => {
|
|
1866
|
+
const browserContext = getBrowserContext()
|
|
1867
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1868
|
+
|
|
1869
|
+
const page = await browserContext.newPage()
|
|
1870
|
+
await page.goto('https://example.com/')
|
|
1871
|
+
await page.bringToFront()
|
|
1872
|
+
|
|
1873
|
+
await serviceWorker.evaluate(async () => {
|
|
1874
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1875
|
+
})
|
|
1876
|
+
await new Promise(r => setTimeout(r, 500))
|
|
1877
|
+
|
|
1878
|
+
const browser = await chromium.connectOverCDP(getCdpUrl())
|
|
1879
|
+
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
|
|
1880
|
+
expect(cdpPage).toBeDefined()
|
|
1881
|
+
|
|
1882
|
+
const wsUrl = getCdpUrl()
|
|
1883
|
+
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
|
1884
|
+
await cdpSession.send('Profiler.enable')
|
|
1885
|
+
await cdpSession.send('Profiler.start')
|
|
1886
|
+
|
|
1887
|
+
await cdpPage!.evaluate(`
|
|
1888
|
+
(() => {
|
|
1889
|
+
function fibonacci(n) {
|
|
1890
|
+
if (n <= 1) return n
|
|
1891
|
+
return fibonacci(n - 1) + fibonacci(n - 2)
|
|
1892
|
+
}
|
|
1893
|
+
for (let i = 0; i < 5; i++) {
|
|
1894
|
+
fibonacci(20)
|
|
1895
|
+
}
|
|
1896
|
+
for (let i = 0; i < 1000; i++) {
|
|
1897
|
+
document.querySelectorAll('*')
|
|
1898
|
+
}
|
|
1899
|
+
})()
|
|
1900
|
+
`)
|
|
1901
|
+
|
|
1902
|
+
const stopResult = await cdpSession.send('Profiler.stop')
|
|
1903
|
+
const profile = stopResult.profile
|
|
1904
|
+
|
|
1905
|
+
const functionNames = profile.nodes
|
|
1906
|
+
.map(n => n.callFrame.functionName)
|
|
1907
|
+
.filter(name => name && name.length > 0)
|
|
1908
|
+
.slice(0, 10)
|
|
1909
|
+
|
|
1910
|
+
expect({
|
|
1911
|
+
hasNodes: profile.nodes.length > 0,
|
|
1912
|
+
nodeCount: profile.nodes.length,
|
|
1913
|
+
durationMicroseconds: profile.endTime - profile.startTime,
|
|
1914
|
+
sampleFunctionNames: functionNames,
|
|
1915
|
+
}).toMatchInlineSnapshot(`
|
|
1916
|
+
{
|
|
1917
|
+
"durationMicroseconds": 11057,
|
|
1918
|
+
"hasNodes": true,
|
|
1919
|
+
"nodeCount": 4,
|
|
1920
|
+
"sampleFunctionNames": [
|
|
1921
|
+
"(root)",
|
|
1922
|
+
"(program)",
|
|
1923
|
+
"(idle)",
|
|
1924
|
+
"fibonacci",
|
|
1925
|
+
],
|
|
1926
|
+
}
|
|
1927
|
+
`)
|
|
1928
|
+
|
|
1929
|
+
await cdpSession.send('Profiler.disable')
|
|
1930
|
+
cdpSession.detach()
|
|
1931
|
+
await browser.close()
|
|
1932
|
+
await page.close()
|
|
1933
|
+
}, 60000)
|
|
1934
|
+
|
|
1935
|
+
it('should click at correct coordinates on high-DPI simulation', async () => {
|
|
1936
|
+
const browserContext = getBrowserContext()
|
|
1937
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1938
|
+
|
|
1939
|
+
const page = await browserContext.newPage()
|
|
1940
|
+
await page.goto('https://example.com/')
|
|
1941
|
+
await page.bringToFront()
|
|
1942
|
+
|
|
1943
|
+
await serviceWorker.evaluate(async () => {
|
|
1944
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1945
|
+
})
|
|
1946
|
+
await new Promise(r => setTimeout(r, 500))
|
|
1947
|
+
|
|
1948
|
+
const browser = await chromium.connectOverCDP(getCdpUrl())
|
|
1949
|
+
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
|
|
1950
|
+
expect(cdpPage).toBeDefined()
|
|
1951
|
+
|
|
1952
|
+
const h1Bounds = await cdpPage!.locator('h1').boundingBox()
|
|
1953
|
+
expect(h1Bounds).toBeDefined()
|
|
1954
|
+
console.log('H1 bounding box:', h1Bounds)
|
|
1955
|
+
|
|
1956
|
+
await cdpPage!.evaluate(() => {
|
|
1957
|
+
(window as any).clickedAt = null;
|
|
1958
|
+
document.addEventListener('click', (e) => {
|
|
1959
|
+
(window as any).clickedAt = { x: e.clientX, y: e.clientY };
|
|
1960
|
+
});
|
|
1961
|
+
})
|
|
1962
|
+
|
|
1963
|
+
await cdpPage!.locator('h1').click()
|
|
1964
|
+
|
|
1965
|
+
const clickedAt = await cdpPage!.evaluate(() => (window as any).clickedAt)
|
|
1966
|
+
console.log('Clicked at:', clickedAt)
|
|
1967
|
+
|
|
1968
|
+
expect(clickedAt).toBeDefined()
|
|
1969
|
+
expect(clickedAt.x).toBeGreaterThan(0)
|
|
1970
|
+
expect(clickedAt.y).toBeGreaterThan(0)
|
|
1971
|
+
|
|
1972
|
+
await browser.close()
|
|
1973
|
+
await page.close()
|
|
1974
|
+
}, 60000)
|
|
1975
|
+
})
|