playwriter 0.0.26 → 0.0.29

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.
Files changed (101) hide show
  1. package/bin.js +1 -1
  2. package/dist/bippy.js +966 -0
  3. package/dist/{extension/cdp-relay.d.ts → cdp-relay.d.ts} +3 -2
  4. package/dist/cdp-relay.d.ts.map +1 -0
  5. package/dist/{extension/cdp-relay.js → cdp-relay.js} +101 -3
  6. package/dist/cdp-relay.js.map +1 -0
  7. package/dist/cdp-session.d.ts +1 -1
  8. package/dist/cdp-session.d.ts.map +1 -1
  9. package/dist/cdp-session.js +4 -4
  10. package/dist/cdp-session.js.map +1 -1
  11. package/dist/cli.d.ts +3 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +71 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/debugger-examples-types.d.ts +18 -0
  16. package/dist/debugger-examples-types.d.ts.map +1 -0
  17. package/dist/debugger-examples-types.js +2 -0
  18. package/dist/debugger-examples-types.js.map +1 -0
  19. package/dist/debugger-examples.d.ts +6 -0
  20. package/dist/debugger-examples.d.ts.map +1 -0
  21. package/dist/debugger-examples.js +53 -0
  22. package/dist/debugger-examples.js.map +1 -0
  23. package/dist/debugger-examples.ts +66 -0
  24. package/dist/debugger.d.ts +380 -0
  25. package/dist/debugger.d.ts.map +1 -0
  26. package/dist/debugger.js +631 -0
  27. package/dist/debugger.js.map +1 -0
  28. package/dist/editor-examples.d.ts +11 -0
  29. package/dist/editor-examples.d.ts.map +1 -0
  30. package/dist/editor-examples.js +124 -0
  31. package/dist/editor-examples.js.map +1 -0
  32. package/dist/editor.d.ts +203 -0
  33. package/dist/editor.d.ts.map +1 -0
  34. package/dist/editor.js +335 -0
  35. package/dist/editor.js.map +1 -0
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/mcp-client.d.ts +5 -1
  41. package/dist/mcp-client.d.ts.map +1 -1
  42. package/dist/mcp-client.js +13 -9
  43. package/dist/mcp-client.js.map +1 -1
  44. package/dist/mcp.d.ts +4 -1
  45. package/dist/mcp.d.ts.map +1 -1
  46. package/dist/mcp.js +170 -29
  47. package/dist/mcp.js.map +1 -1
  48. package/dist/mcp.test.d.ts.map +1 -1
  49. package/dist/mcp.test.js +886 -182
  50. package/dist/mcp.test.js.map +1 -1
  51. package/dist/prompt.md +86 -6
  52. package/dist/{extension/protocol.d.ts → protocol.d.ts} +1 -1
  53. package/dist/protocol.d.ts.map +1 -0
  54. package/dist/protocol.js.map +1 -0
  55. package/dist/react-source.d.ts +13 -0
  56. package/dist/react-source.d.ts.map +1 -0
  57. package/dist/react-source.js +66 -0
  58. package/dist/react-source.js.map +1 -0
  59. package/dist/selector-generator.js +7065 -18
  60. package/dist/start-relay-server.d.ts +4 -2
  61. package/dist/start-relay-server.d.ts.map +1 -1
  62. package/dist/start-relay-server.js +3 -3
  63. package/dist/start-relay-server.js.map +1 -1
  64. package/dist/styles.d.ts +27 -0
  65. package/dist/styles.d.ts.map +1 -0
  66. package/dist/styles.js +232 -0
  67. package/dist/styles.js.map +1 -0
  68. package/dist/utils.d.ts +3 -1
  69. package/dist/utils.d.ts.map +1 -1
  70. package/dist/utils.js +7 -3
  71. package/dist/utils.js.map +1 -1
  72. package/dist/wait-for-page-load.d.ts.map +1 -1
  73. package/dist/wait-for-page-load.js +3 -2
  74. package/dist/wait-for-page-load.js.map +1 -1
  75. package/package.json +4 -2
  76. package/src/{extension/cdp-relay.ts → cdp-relay.ts} +109 -5
  77. package/src/cdp-session.ts +4 -4
  78. package/src/cdp-timing.md +128 -0
  79. package/src/cli.ts +85 -0
  80. package/src/debugger-examples-types.ts +10 -0
  81. package/src/debugger-examples.ts +66 -0
  82. package/src/debugger.ts +711 -0
  83. package/src/editor-examples.ts +148 -0
  84. package/src/editor.ts +389 -0
  85. package/src/index.ts +1 -1
  86. package/src/mcp-client.ts +14 -9
  87. package/src/mcp.test.ts +1053 -196
  88. package/src/mcp.ts +195 -30
  89. package/src/prompt.md +86 -6
  90. package/src/{extension/protocol.ts → protocol.ts} +1 -1
  91. package/src/react-source.ts +92 -0
  92. package/src/snapshots/shadcn-ui-accessibility.md +57 -57
  93. package/src/start-relay-server.ts +3 -3
  94. package/src/styles.ts +343 -0
  95. package/src/utils.ts +8 -3
  96. package/src/wait-for-page-load.ts +3 -2
  97. package/dist/extension/cdp-relay.d.ts.map +0 -1
  98. package/dist/extension/cdp-relay.js.map +0 -1
  99. package/dist/extension/protocol.d.ts.map +0 -1
  100. package/dist/extension/protocol.js.map +0 -1
  101. /package/dist/{extension/protocol.js → protocol.js} +0 -0
package/src/mcp.test.ts CHANGED
@@ -11,7 +11,9 @@ import type { ExtensionState } from 'mcp-extension/src/types.js'
11
11
  import type { Protocol } from 'devtools-protocol'
12
12
  import { imageSize } from 'image-size'
13
13
  import { getCDPSessionForPage } from './cdp-session.js'
14
- import { startPlayWriterCDPRelayServer, type RelayServer } from './extension/cdp-relay.js'
14
+ import { Debugger } from './debugger.js'
15
+ import { Editor } from './editor.js'
16
+ import { startPlayWriterCDPRelayServer, type RelayServer } from './cdp-relay.js'
15
17
  import { createFileLogger } from './create-logger.js'
16
18
  import type { CDPCommand } from './cdp-types.js'
17
19
  import { killPortProcess } from 'kill-port-process'
@@ -19,6 +21,7 @@ import { killPortProcess } from 'kill-port-process'
19
21
  declare const window: any
20
22
  declare const document: any
21
23
 
24
+ const TEST_PORT = 19987
22
25
 
23
26
  const execAsync = promisify(exec)
24
27
 
@@ -66,15 +69,15 @@ interface TestContext {
66
69
  }
67
70
 
68
71
  async function setupTestContext({ tempDirPrefix }: { tempDirPrefix: string }): Promise<TestContext> {
69
- await killProcessOnPort(19988)
72
+ await killProcessOnPort(TEST_PORT)
70
73
 
71
74
  console.log('Building extension...')
72
- await execAsync('TESTING=1 pnpm build', { cwd: '../extension' })
75
+ await execAsync(`TESTING=1 PLAYWRITER_PORT=${TEST_PORT} pnpm build`, { cwd: '../extension' })
73
76
  console.log('Extension built')
74
77
 
75
78
  const localLogPath = path.join(process.cwd(), 'relay-server.log')
76
79
  const logger = createFileLogger({ logFilePath: localLogPath })
77
- const relayServer = await startPlayWriterCDPRelayServer({ port: 19988, logger })
80
+ const relayServer = await startPlayWriterCDPRelayServer({ port: TEST_PORT, logger })
78
81
 
79
82
  const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), tempDirPrefix))
80
83
  const extensionPath = path.resolve('../extension/dist')
@@ -135,7 +138,7 @@ describe('MCP Server Tests', () => {
135
138
  beforeAll(async () => {
136
139
  testCtx = await setupTestContext({ tempDirPrefix: 'pw-test-' })
137
140
 
138
- const result = await createMCPClient()
141
+ const result = await createMCPClient({ port: TEST_PORT })
139
142
  client = result.client
140
143
  cleanup = result.cleanup
141
144
  }, 600000)
@@ -162,9 +165,9 @@ describe('MCP Server Tests', () => {
162
165
  await serviceWorker.evaluate(async () => {
163
166
  await globalThis.toggleExtensionForActiveTab()
164
167
  })
165
- await new Promise(r => setTimeout(r, 500))
168
+ await new Promise(r => setTimeout(r, 100))
166
169
 
167
- const browser = await chromium.connectOverCDP(getCdpUrl())
170
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
168
171
  const cdpPage = browser.contexts()[0].pages().find(p => {
169
172
  return p.url().startsWith('about:')
170
173
  })
@@ -199,7 +202,7 @@ describe('MCP Server Tests', () => {
199
202
  name: 'execute',
200
203
  arguments: {
201
204
  code: js`
202
- await state.page.goto('https://news.ycombinator.com');
205
+ await state.page.goto('https://example.com');
203
206
  const title = await state.page.title();
204
207
  console.log('Page title:', title);
205
208
  return { url: state.page.url(), title };
@@ -210,12 +213,12 @@ describe('MCP Server Tests', () => {
210
213
  [
211
214
  {
212
215
  "text": "Console output:
213
- [log] Page title: Hacker News
216
+ [log] Page title: Example Domain
214
217
 
215
218
  Return value:
216
219
  {
217
- "url": "https://news.ycombinator.com/",
218
- "title": "Hacker News"
220
+ \"url\": \"https://example.com/\",
221
+ \"title\": \"Example Domain\"
219
222
  }",
220
223
  "type": "text",
221
224
  },
@@ -289,7 +292,7 @@ describe('MCP Server Tests', () => {
289
292
  name: 'execute',
290
293
  arguments: {
291
294
  code: js`
292
- await state.page.goto('https://news.ycombinator.com/item?id=1', { waitUntil: 'networkidle' });
295
+ await state.page.goto('https://news.ycombinator.com/item?id=1', { waitUntil: 'domcontentloaded' });
293
296
  const snapshot = await state.page._snapshotForAI();
294
297
  return snapshot;
295
298
  `,
@@ -325,7 +328,7 @@ describe('MCP Server Tests', () => {
325
328
  name: 'execute',
326
329
  arguments: {
327
330
  code: js`
328
- await state.page.goto('https://ui.shadcn.com/', { waitUntil: 'networkidle' });
331
+ await state.page.goto('https://ui.shadcn.com/', { waitUntil: 'domcontentloaded' });
329
332
  const snapshot = await state.page._snapshotForAI();
330
333
  return snapshot;
331
334
  `,
@@ -381,7 +384,7 @@ describe('MCP Server Tests', () => {
381
384
 
382
385
  // 3. Verify we can connect via direct CDP and see the page
383
386
 
384
- let directBrowser = await chromium.connectOverCDP(getCdpUrl())
387
+ let directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
385
388
  let contexts = directBrowser.contexts()
386
389
  let pages = contexts[0].pages()
387
390
 
@@ -407,7 +410,7 @@ describe('MCP Server Tests', () => {
407
410
  // connecting to relay will succeed, but listing pages should NOT show our page
408
411
 
409
412
  // Connect to relay again
410
- directBrowser = await chromium.connectOverCDP(getCdpUrl())
413
+ directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
411
414
  contexts = directBrowser.contexts()
412
415
  pages = contexts[0].pages()
413
416
 
@@ -425,14 +428,14 @@ describe('MCP Server Tests', () => {
425
428
 
426
429
  // 7. Verify page is back
427
430
 
428
- directBrowser = await chromium.connectOverCDP(getCdpUrl())
431
+ directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
429
432
  // Wait a bit for targets to populate
430
- await new Promise(r => setTimeout(r, 500))
433
+ await new Promise(r => setTimeout(r, 100))
431
434
 
432
435
  contexts = directBrowser.contexts()
433
436
  // pages() might need a moment if target attached event comes in
434
437
  if (contexts[0].pages().length === 0) {
435
- await new Promise(r => setTimeout(r, 1000))
438
+ await new Promise(r => setTimeout(r, 100))
436
439
  }
437
440
  pages = contexts[0].pages()
438
441
 
@@ -453,9 +456,9 @@ describe('MCP Server Tests', () => {
453
456
  const serviceWorker = await getExtensionServiceWorker(browserContext)
454
457
 
455
458
  // Connect once
456
- const directBrowser = await chromium.connectOverCDP(getCdpUrl())
459
+ const directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
457
460
  // Wait a bit for connection and initial target discovery
458
- await new Promise(r => setTimeout(r, 500))
461
+ await new Promise(r => setTimeout(r, 100))
459
462
 
460
463
  // 1. Create a new page
461
464
  const page = await browserContext.newPage()
@@ -535,7 +538,7 @@ describe('MCP Server Tests', () => {
535
538
  })
536
539
 
537
540
  // 3. Connect via CDP
538
- const cdpUrl = getCdpUrl()
541
+ const cdpUrl = getCdpUrl({ port: TEST_PORT })
539
542
  const directBrowser = await chromium.connectOverCDP(cdpUrl)
540
543
  const connectedPage = directBrowser.contexts()[0].pages().find(p => p.url() === initialUrl)
541
544
  expect(connectedPage).toBeDefined()
@@ -547,19 +550,19 @@ describe('MCP Server Tests', () => {
547
550
  // We use a loop to check if it's still connected because reload might cause temporary disconnect/reconnect events
548
551
  // that Playwright handles natively if the session ID stays valid.
549
552
  await connectedPage?.reload()
550
- await connectedPage?.waitForLoadState('networkidle')
553
+ await connectedPage?.waitForLoadState('domcontentloaded')
551
554
  expect(await connectedPage?.title()).toBe('Example Domain')
552
555
 
553
556
  // Verify execution after reload
554
557
  expect(await connectedPage?.evaluate(() => 2 + 2)).toBe(4)
555
558
 
556
559
  // 5. Navigate to new URL
557
- const newUrl = 'https://news.ycombinator.com/'
560
+ const newUrl = 'https://example.org/'
558
561
  await connectedPage?.goto(newUrl)
559
- await connectedPage?.waitForLoadState('networkidle')
562
+ await connectedPage?.waitForLoadState('domcontentloaded')
560
563
 
561
564
  expect(connectedPage?.url()).toBe(newUrl)
562
- expect(await connectedPage?.title()).toContain('Hacker News')
565
+ expect(await connectedPage?.title()).toContain('Example Domain')
563
566
 
564
567
  // Verify execution after navigation
565
568
  expect(await connectedPage?.evaluate(() => 3 + 3)).toBe(6)
@@ -571,13 +574,13 @@ describe('MCP Server Tests', () => {
571
574
  it('should support multiple concurrent tabs', async () => {
572
575
  const browserContext = getBrowserContext()
573
576
  const serviceWorker = await getExtensionServiceWorker(browserContext)
574
- await new Promise(resolve => setTimeout(resolve, 500))
577
+ await new Promise(resolve => setTimeout(resolve, 100))
575
578
 
576
579
  // Tab A
577
580
  const pageA = await browserContext.newPage()
578
581
  await pageA.goto('https://example.com/tab-a')
579
582
  await pageA.bringToFront()
580
- await new Promise(resolve => setTimeout(resolve, 500))
583
+ await new Promise(resolve => setTimeout(resolve, 100))
581
584
  await serviceWorker.evaluate(async () => {
582
585
  await globalThis.toggleExtensionForActiveTab()
583
586
  })
@@ -586,7 +589,7 @@ describe('MCP Server Tests', () => {
586
589
  const pageB = await browserContext.newPage()
587
590
  await pageB.goto('https://example.com/tab-b')
588
591
  await pageB.bringToFront()
589
- await new Promise(resolve => setTimeout(resolve, 500))
592
+ await new Promise(resolve => setTimeout(resolve, 100))
590
593
  await serviceWorker.evaluate(async () => {
591
594
  await globalThis.toggleExtensionForActiveTab()
592
595
  })
@@ -616,7 +619,7 @@ describe('MCP Server Tests', () => {
616
619
  expect(targetIds.idA).not.toBe(targetIds.idB)
617
620
 
618
621
  // Verify independent connections
619
- const browser = await chromium.connectOverCDP(getCdpUrl())
622
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
620
623
 
621
624
  const pages = browser.contexts()[0].pages()
622
625
 
@@ -664,7 +667,7 @@ describe('MCP Server Tests', () => {
664
667
  await page.bringToFront()
665
668
 
666
669
  // Wait for load
667
- await page.waitForLoadState('networkidle')
670
+ await page.waitForLoadState('domcontentloaded')
668
671
 
669
672
  // 2. Enable extension for this page
670
673
  await serviceWorker.evaluate(async () => {
@@ -672,9 +675,9 @@ describe('MCP Server Tests', () => {
672
675
  })
673
676
 
674
677
  // 3. Verify via CDP that the correct URL is shown
675
- const browser = await chromium.connectOverCDP(getCdpUrl())
678
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
676
679
  // Wait for sync
677
- await new Promise(r => setTimeout(r, 1000))
680
+ await new Promise(r => setTimeout(r, 100))
678
681
 
679
682
  const cdpPage = browser.contexts()[0].pages().find(p => p.url() === targetUrl)
680
683
 
@@ -694,7 +697,7 @@ describe('MCP Server Tests', () => {
694
697
  const page = pages[0]
695
698
 
696
699
  await page.goto('https://example.com/disconnect-test')
697
- await page.waitForLoadState('networkidle')
700
+ await page.waitForLoadState('domcontentloaded')
698
701
  await page.bringToFront()
699
702
 
700
703
  // Enable extension on this page
@@ -705,7 +708,7 @@ describe('MCP Server Tests', () => {
705
708
  expect(initialEnable.isConnected).toBe(true)
706
709
 
707
710
  // Wait for extension to fully connect
708
- await new Promise(resolve => setTimeout(resolve, 500))
711
+ await new Promise(resolve => setTimeout(resolve, 100))
709
712
 
710
713
  // Verify MCP can see the page
711
714
  const beforeDisconnect = await client.callTool({
@@ -732,7 +735,7 @@ describe('MCP Server Tests', () => {
732
735
  })
733
736
 
734
737
  // Wait for disconnect to complete
735
- await new Promise(resolve => setTimeout(resolve, 500))
738
+ await new Promise(resolve => setTimeout(resolve, 100))
736
739
 
737
740
  // 3. Verify MCP cannot see the page anymore
738
741
  const afterDisconnect = await client.callTool({
@@ -765,7 +768,7 @@ describe('MCP Server Tests', () => {
765
768
 
766
769
  // Wait for extension to fully reconnect and relay server to be ready
767
770
  console.log('Waiting for reconnection to stabilize...')
768
- await new Promise(resolve => setTimeout(resolve, 1000))
771
+ await new Promise(resolve => setTimeout(resolve, 100))
769
772
 
770
773
  // 5. Reset the MCP client's playwright connection since it was closed by disconnectEverything
771
774
  console.log('Resetting MCP playwright connection...')
@@ -1167,23 +1170,23 @@ describe('MCP Server Tests', () => {
1167
1170
  const serviceWorker = await getExtensionServiceWorker(browserContext)
1168
1171
 
1169
1172
  const page = await browserContext.newPage()
1170
- const targetUrl = 'https://x.com'
1171
- await page.goto(targetUrl, { waitUntil: 'domcontentloaded' })
1173
+ const targetUrl = 'https://example.com/sw-test'
1174
+ await page.goto(targetUrl)
1172
1175
  await page.bringToFront()
1173
1176
 
1174
1177
  await serviceWorker.evaluate(async () => {
1175
1178
  await globalThis.toggleExtensionForActiveTab()
1176
1179
  })
1177
1180
 
1178
- await new Promise(r => setTimeout(r, 2000))
1181
+ await new Promise(r => setTimeout(r, 100))
1179
1182
 
1180
- const browser = await chromium.connectOverCDP(getCdpUrl())
1183
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
1181
1184
  const pages = browser.contexts()[0].pages()
1182
- const xPage = pages.find(p => p.url().includes('x.com'))
1185
+ const testPage = pages.find(p => p.url().includes('sw-test'))
1183
1186
 
1184
- expect(xPage).toBeDefined()
1185
- expect(xPage?.url()).toContain('x.com')
1186
- expect(xPage?.url()).not.toContain('sw.js')
1187
+ expect(testPage).toBeDefined()
1188
+ expect(testPage?.url()).toContain('sw-test')
1189
+ expect(testPage?.url()).not.toContain('sw.js')
1187
1190
 
1188
1191
  await browser.close()
1189
1192
  await page.close()
@@ -1203,7 +1206,7 @@ describe('MCP Server Tests', () => {
1203
1206
  })
1204
1207
 
1205
1208
  for (let i = 0; i < 5; i++) {
1206
- const browser = await chromium.connectOverCDP(getCdpUrl())
1209
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
1207
1210
  const pages = browser.contexts()[0].pages()
1208
1211
  const testPage = pages.find(p => p.url().includes('repeated-test'))
1209
1212
 
@@ -1211,7 +1214,7 @@ describe('MCP Server Tests', () => {
1211
1214
  expect(testPage?.url()).toBe(targetUrl)
1212
1215
 
1213
1216
  await browser.close()
1214
- await new Promise(r => setTimeout(r, 200))
1217
+ await new Promise(r => setTimeout(r, 100))
1215
1218
  }
1216
1219
 
1217
1220
  await page.close()
@@ -1230,7 +1233,7 @@ describe('MCP Server Tests', () => {
1230
1233
  await globalThis.toggleExtensionForActiveTab()
1231
1234
  })
1232
1235
 
1233
- await new Promise(r => setTimeout(r, 500))
1236
+ await new Promise(r => setTimeout(r, 400))
1234
1237
 
1235
1238
  const [mcpResult, cdpBrowser] = await Promise.all([
1236
1239
  client.callTool({
@@ -1243,7 +1246,7 @@ describe('MCP Server Tests', () => {
1243
1246
  `,
1244
1247
  },
1245
1248
  }),
1246
- chromium.connectOverCDP(getCdpUrl())
1249
+ chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
1247
1250
  ])
1248
1251
 
1249
1252
  const mcpOutput = (mcpResult as any).content[0].text
@@ -1262,30 +1265,46 @@ describe('MCP Server Tests', () => {
1262
1265
  const serviceWorker = await getExtensionServiceWorker(browserContext)
1263
1266
 
1264
1267
  const page = await browserContext.newPage()
1265
- const targetUrl = 'https://www.youtube.com'
1266
- await page.goto(targetUrl, { waitUntil: 'domcontentloaded' })
1268
+ await page.setContent(`
1269
+ <html>
1270
+ <head><title>Iframe Test Page</title></head>
1271
+ <body>
1272
+ <h1>Iframe Heavy Page</h1>
1273
+ <iframe src="about:blank" id="frame1"></iframe>
1274
+ <iframe src="about:blank" id="frame2"></iframe>
1275
+ <iframe src="about:blank" id="frame3"></iframe>
1276
+ </body>
1277
+ </html>
1278
+ `)
1267
1279
  await page.bringToFront()
1268
1280
 
1269
1281
  await serviceWorker.evaluate(async () => {
1270
1282
  await globalThis.toggleExtensionForActiveTab()
1271
1283
  })
1272
1284
 
1273
- await new Promise(r => setTimeout(r, 3000))
1285
+ await new Promise(r => setTimeout(r, 100))
1274
1286
 
1275
1287
  for (let i = 0; i < 3; i++) {
1276
- const browser = await chromium.connectOverCDP(getCdpUrl())
1288
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
1277
1289
  const pages = browser.contexts()[0].pages()
1278
- const ytPage = pages.find(p => p.url().includes('youtube.com'))
1290
+ let iframePage
1291
+ for (const p of pages) {
1292
+ const html = await p.content()
1293
+ if (html.includes('Iframe Heavy Page')) {
1294
+ iframePage = p
1295
+ break
1296
+ }
1297
+ }
1279
1298
 
1280
- expect(ytPage).toBeDefined()
1281
- expect(ytPage?.url()).toContain('youtube.com')
1299
+ expect(iframePage).toBeDefined()
1300
+ expect(iframePage?.url()).toContain('about:')
1282
1301
 
1283
1302
  await browser.close()
1284
- await new Promise(r => setTimeout(r, 500))
1303
+ await new Promise(r => setTimeout(r, 100))
1285
1304
  }
1286
1305
 
1287
1306
  await page.close()
1288
- }, 60000)
1307
+ }, 30000)
1289
1308
 
1290
1309
  it('should capture screenshot correctly', async () => {
1291
1310
  const browserContext = getBrowserContext()
@@ -1299,7 +1318,7 @@ describe('MCP Server Tests', () => {
1299
1318
  await globalThis.toggleExtensionForActiveTab()
1300
1319
  })
1301
1320
 
1302
- await new Promise(r => setTimeout(r, 500))
1321
+ await new Promise(r => setTimeout(r, 100))
1303
1322
 
1304
1323
  const capturedCommands: CDPCommand[] = []
1305
1324
  const commandHandler = ({ command }: { clientId: string; command: CDPCommand }) => {
@@ -1309,7 +1328,7 @@ describe('MCP Server Tests', () => {
1309
1328
  }
1310
1329
  testCtx!.relayServer.on('cdp:command', commandHandler)
1311
1330
 
1312
- const browser = await chromium.connectOverCDP(getCdpUrl())
1331
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
1313
1332
  const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
1314
1333
 
1315
1334
  expect(cdpPage).toBeDefined()
@@ -1428,7 +1447,7 @@ describe('MCP Server Tests', () => {
1428
1447
  await globalThis.toggleExtensionForActiveTab()
1429
1448
  })
1430
1449
 
1431
- await new Promise(r => setTimeout(r, 500))
1450
+ await new Promise(r => setTimeout(r, 100))
1432
1451
 
1433
1452
  const capturedCommands: CDPCommand[] = []
1434
1453
  const commandHandler = ({ command }: { clientId: string; command: CDPCommand }) => {
@@ -1438,7 +1457,7 @@ describe('MCP Server Tests', () => {
1438
1457
  }
1439
1458
  testCtx!.relayServer.on('cdp:command', commandHandler)
1440
1459
 
1441
- const browser = await chromium.connectOverCDP(getCdpUrl())
1460
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
1442
1461
  let cdpPage
1443
1462
  for (const p of browser.contexts()[0].pages()) {
1444
1463
  const html = await p.content()
@@ -1496,7 +1515,7 @@ describe('MCP Server Tests', () => {
1496
1515
  await globalThis.toggleExtensionForActiveTab()
1497
1516
  })
1498
1517
 
1499
- await new Promise(r => setTimeout(r, 500))
1518
+ await new Promise(r => setTimeout(r, 400))
1500
1519
 
1501
1520
  const result = await client.callTool({
1502
1521
  name: 'execute',
@@ -1531,6 +1550,193 @@ describe('MCP Server Tests', () => {
1531
1550
  await page.close()
1532
1551
  }, 60000)
1533
1552
 
1553
+ it('should get styles for element using getStylesForLocator', async () => {
1554
+ const browserContext = getBrowserContext()
1555
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
1556
+
1557
+ const page = await browserContext.newPage()
1558
+ await page.setContent(`
1559
+ <html>
1560
+ <head>
1561
+ <style>
1562
+ body { font-family: Arial, sans-serif; color: #333; }
1563
+ .container { padding: 20px; margin: 10px; }
1564
+ #main-btn { background-color: blue; color: white; border-radius: 4px; }
1565
+ .btn { padding: 8px 16px; }
1566
+ </style>
1567
+ </head>
1568
+ <body>
1569
+ <div class="container">
1570
+ <button id="main-btn" class="btn" style="font-weight: bold;">Click Me</button>
1571
+ </div>
1572
+ </body>
1573
+ </html>
1574
+ `)
1575
+ await page.bringToFront()
1576
+
1577
+ await serviceWorker.evaluate(async () => {
1578
+ await globalThis.toggleExtensionForActiveTab()
1579
+ })
1580
+
1581
+ await new Promise(r => setTimeout(r, 400))
1582
+
1583
+ const stylesResult = await client.callTool({
1584
+ name: 'execute',
1585
+ arguments: {
1586
+ code: js`
1587
+ let testPage;
1588
+ for (const p of context.pages()) {
1589
+ const html = await p.content();
1590
+ if (html.includes('main-btn')) { testPage = p; break; }
1591
+ }
1592
+ if (!testPage) throw new Error('Test page not found');
1593
+ const btn = testPage.locator('#main-btn');
1594
+ const styles = await getStylesForLocator({ locator: btn });
1595
+ return styles;
1596
+ `,
1597
+ timeout: 30000,
1598
+ },
1599
+ })
1600
+
1601
+ expect(stylesResult.isError).toBeFalsy()
1602
+ const stylesText = (stylesResult.content as any)[0]?.text || ''
1603
+ expect(stylesText).toMatchInlineSnapshot(`
1604
+ "Return value:
1605
+ {
1606
+ "element": "button#main-btn.btn",
1607
+ "inlineStyle": {
1608
+ "font-weight": "bold"
1609
+ },
1610
+ "rules": [
1611
+ {
1612
+ "selector": ".btn",
1613
+ "source": null,
1614
+ "origin": "regular",
1615
+ "declarations": {
1616
+ "padding": "8px 16px",
1617
+ "padding-top": "8px",
1618
+ "padding-right": "16px",
1619
+ "padding-bottom": "8px",
1620
+ "padding-left": "16px"
1621
+ },
1622
+ "inheritedFrom": null
1623
+ },
1624
+ {
1625
+ "selector": "#main-btn",
1626
+ "source": null,
1627
+ "origin": "regular",
1628
+ "declarations": {
1629
+ "background-color": "blue",
1630
+ "color": "white",
1631
+ "border-radius": "4px",
1632
+ "border-top-left-radius": "4px",
1633
+ "border-top-right-radius": "4px",
1634
+ "border-bottom-right-radius": "4px",
1635
+ "border-bottom-left-radius": "4px"
1636
+ },
1637
+ "inheritedFrom": null
1638
+ },
1639
+ {
1640
+ "selector": ".container",
1641
+ "source": null,
1642
+ "origin": "regular",
1643
+ "declarations": {
1644
+ "padding": "20px",
1645
+ "margin": "10px",
1646
+ "padding-top": "20px",
1647
+ "padding-right": "20px",
1648
+ "padding-bottom": "20px",
1649
+ "padding-left": "20px",
1650
+ "margin-top": "10px",
1651
+ "margin-right": "10px",
1652
+ "margin-bottom": "10px",
1653
+ "margin-left": "10px"
1654
+ },
1655
+ "inheritedFrom": "ancestor[1]"
1656
+ },
1657
+ {
1658
+ "selector": "body",
1659
+ "source": null,
1660
+ "origin": "regular",
1661
+ "declarations": {
1662
+ "font-family": "Arial, sans-serif",
1663
+ "color": "rgb(51, 51, 51)"
1664
+ },
1665
+ "inheritedFrom": "ancestor[2]"
1666
+ }
1667
+ ]
1668
+ }"
1669
+ `)
1670
+
1671
+ const formattedResult = await client.callTool({
1672
+ name: 'execute',
1673
+ arguments: {
1674
+ code: js`
1675
+ let testPage;
1676
+ for (const p of context.pages()) {
1677
+ const html = await p.content();
1678
+ if (html.includes('main-btn')) { testPage = p; break; }
1679
+ }
1680
+ if (!testPage) throw new Error('Test page not found');
1681
+ const btn = testPage.locator('#main-btn');
1682
+ const styles = await getStylesForLocator({ locator: btn });
1683
+ return formatStylesAsText(styles);
1684
+ `,
1685
+ timeout: 30000,
1686
+ },
1687
+ })
1688
+
1689
+ expect(formattedResult.isError).toBeFalsy()
1690
+ const formattedText = (formattedResult.content as any)[0]?.text || ''
1691
+ expect(formattedText).toMatchInlineSnapshot(`
1692
+ "Return value:
1693
+ Element: button#main-btn.btn
1694
+
1695
+ Inline styles:
1696
+ font-weight: bold
1697
+
1698
+ Matched rules:
1699
+ .btn {
1700
+ padding: 8px 16px;
1701
+ padding-top: 8px;
1702
+ padding-right: 16px;
1703
+ padding-bottom: 8px;
1704
+ padding-left: 16px;
1705
+ }
1706
+ #main-btn {
1707
+ background-color: blue;
1708
+ color: white;
1709
+ border-radius: 4px;
1710
+ border-top-left-radius: 4px;
1711
+ border-top-right-radius: 4px;
1712
+ border-bottom-right-radius: 4px;
1713
+ border-bottom-left-radius: 4px;
1714
+ }
1715
+
1716
+ Inherited from ancestor[1]:
1717
+ .container {
1718
+ padding: 20px;
1719
+ margin: 10px;
1720
+ padding-top: 20px;
1721
+ padding-right: 20px;
1722
+ padding-bottom: 20px;
1723
+ padding-left: 20px;
1724
+ margin-top: 10px;
1725
+ margin-right: 10px;
1726
+ margin-bottom: 10px;
1727
+ margin-left: 10px;
1728
+ }
1729
+
1730
+ Inherited from ancestor[2]:
1731
+ body {
1732
+ font-family: Arial, sans-serif;
1733
+ color: rgb(51, 51, 51);
1734
+ }"
1735
+ `)
1736
+
1737
+ await page.close()
1738
+ }, 60000)
1739
+
1534
1740
  it('should return correct layout metrics via CDP', async () => {
1535
1741
  const browserContext = getBrowserContext()
1536
1742
  const serviceWorker = await getExtensionServiceWorker(browserContext)
@@ -1543,13 +1749,13 @@ describe('MCP Server Tests', () => {
1543
1749
  await globalThis.toggleExtensionForActiveTab()
1544
1750
  })
1545
1751
 
1546
- await new Promise(r => setTimeout(r, 500))
1752
+ await new Promise(r => setTimeout(r, 100))
1547
1753
 
1548
- const browser = await chromium.connectOverCDP(getCdpUrl())
1754
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
1549
1755
  const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
1550
1756
  expect(cdpPage).toBeDefined()
1551
1757
 
1552
- const wsUrl = getCdpUrl()
1758
+ const wsUrl = getCdpUrl({ port: TEST_PORT })
1553
1759
  const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
1554
1760
 
1555
1761
  const layoutMetrics = await cdpSession.send('Page.getLayoutMetrics')
@@ -1606,7 +1812,7 @@ describe('MCP Server Tests', () => {
1606
1812
  console.log('window.devicePixelRatio:', windowDpr)
1607
1813
  expect(windowDpr).toBe(1)
1608
1814
 
1609
- cdpSession.detach()
1815
+ cdpSession.close()
1610
1816
  await browser.close()
1611
1817
  await page.close()
1612
1818
  }, 60000)
@@ -1623,20 +1829,20 @@ describe('MCP Server Tests', () => {
1623
1829
  await globalThis.toggleExtensionForActiveTab()
1624
1830
  })
1625
1831
 
1626
- await new Promise(r => setTimeout(r, 500))
1832
+ await new Promise(r => setTimeout(r, 100))
1627
1833
 
1628
- const browser = await chromium.connectOverCDP(getCdpUrl())
1834
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
1629
1835
  const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
1630
1836
  expect(cdpPage).toBeDefined()
1631
1837
 
1632
- const wsUrl = getCdpUrl()
1838
+ const wsUrl = getCdpUrl({ port: TEST_PORT })
1633
1839
  const client = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
1634
1840
 
1635
1841
  const layoutMetrics = await client.send('Page.getLayoutMetrics')
1636
1842
  expect(layoutMetrics.cssVisualViewport).toBeDefined()
1637
1843
  expect(layoutMetrics.cssVisualViewport.clientWidth).toBeGreaterThan(0)
1638
1844
 
1639
- client.detach()
1845
+ client.close()
1640
1846
  await browser.close()
1641
1847
  await page.close()
1642
1848
  }, 60000)
@@ -1648,20 +1854,20 @@ describe('MCP Server Tests', () => {
1648
1854
  await serviceWorker.evaluate(async () => {
1649
1855
  await globalThis.disconnectEverything()
1650
1856
  })
1651
- await new Promise(r => setTimeout(r, 500))
1857
+ await new Promise(r => setTimeout(r, 100))
1652
1858
 
1653
1859
  const targetUrl = 'https://example.com/'
1654
1860
 
1655
1861
  const enableResult = await serviceWorker.evaluate(async (url) => {
1656
1862
  const tab = await chrome.tabs.create({ url, active: true })
1657
- await new Promise(r => setTimeout(r, 1000))
1863
+ await new Promise(r => setTimeout(r, 100))
1658
1864
  return await globalThis.toggleExtensionForActiveTab()
1659
1865
  }, targetUrl)
1660
1866
 
1661
1867
  console.log('Extension enabled:', enableResult)
1662
1868
  expect(enableResult.isConnected).toBe(true)
1663
1869
 
1664
- await new Promise(r => setTimeout(r, 1000))
1870
+ await new Promise(r => setTimeout(r, 100))
1665
1871
 
1666
1872
  const { Stagehand } = await import('@browserbasehq/stagehand')
1667
1873
 
@@ -1670,7 +1876,7 @@ describe('MCP Server Tests', () => {
1670
1876
  verbose: 1,
1671
1877
  disablePino: true,
1672
1878
  localBrowserLaunchOptions: {
1673
- cdpUrl: getCdpUrl(),
1879
+ cdpUrl: getCdpUrl({ port: TEST_PORT }),
1674
1880
  },
1675
1881
  })
1676
1882
 
@@ -1679,7 +1885,7 @@ describe('MCP Server Tests', () => {
1679
1885
  console.log('Stagehand initialized')
1680
1886
 
1681
1887
  const context = stagehand.context
1682
- console.log('Stagehand context:', context)
1888
+ // console.log('Stagehand context:', context)
1683
1889
  expect(context).toBeDefined()
1684
1890
 
1685
1891
  const pages = context.pages()
@@ -1711,7 +1917,7 @@ describe('MCP Server Tests', () => {
1711
1917
  await serviceWorker.evaluate(async () => {
1712
1918
  await globalThis.toggleExtensionForActiveTab()
1713
1919
  })
1714
- await new Promise(r => setTimeout(r, 500))
1920
+ await new Promise(r => setTimeout(r, 100))
1715
1921
 
1716
1922
  const result = await client.callTool({
1717
1923
  name: 'execute',
@@ -1764,6 +1970,12 @@ describe('CDP Session Tests', () => {
1764
1970
 
1765
1971
  beforeAll(async () => {
1766
1972
  testCtx = await setupTestContext({ tempDirPrefix: 'pw-cdp-test-' })
1973
+
1974
+ const serviceWorker = await getExtensionServiceWorker(testCtx.browserContext)
1975
+ await serviceWorker.evaluate(async () => {
1976
+ await globalThis.disconnectEverything()
1977
+ })
1978
+ await new Promise(r => setTimeout(r, 100))
1767
1979
  }, 600000)
1768
1980
 
1769
1981
  afterAll(async () => {
@@ -1776,7 +1988,7 @@ describe('CDP Session Tests', () => {
1776
1988
  return testCtx.browserContext
1777
1989
  }
1778
1990
 
1779
- it('should enable debugger and pause on debugger statement via CDP session', async () => {
1991
+ it('should use Debugger class to set breakpoints and inspect variables', async () => {
1780
1992
  const browserContext = getBrowserContext()
1781
1993
  const serviceWorker = await getExtensionServiceWorker(browserContext)
1782
1994
 
@@ -1787,19 +1999,23 @@ describe('CDP Session Tests', () => {
1787
1999
  await serviceWorker.evaluate(async () => {
1788
2000
  await globalThis.toggleExtensionForActiveTab()
1789
2001
  })
1790
- await new Promise(r => setTimeout(r, 500))
2002
+ await new Promise(r => setTimeout(r, 100))
1791
2003
 
1792
- const browser = await chromium.connectOverCDP(getCdpUrl())
2004
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
1793
2005
  const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
1794
2006
  expect(cdpPage).toBeDefined()
1795
2007
 
1796
- const wsUrl = getCdpUrl()
2008
+ const wsUrl = getCdpUrl({ port: TEST_PORT })
1797
2009
  const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
1798
- await cdpSession.send('Debugger.enable')
2010
+ const dbg = new Debugger({ cdp: cdpSession })
2011
+
2012
+ await dbg.enable()
1799
2013
 
1800
- const pausedPromise = new Promise<Protocol.Debugger.PausedEvent>((resolve) => {
1801
- cdpSession.on('Debugger.paused', (params) => {
1802
- resolve(params as Protocol.Debugger.PausedEvent)
2014
+ expect(dbg.isPaused()).toBe(false)
2015
+
2016
+ const pausedPromise = new Promise<void>((resolve) => {
2017
+ cdpSession.on('Debugger.paused', () => {
2018
+ resolve()
1803
2019
  })
1804
2020
  })
1805
2021
 
@@ -1807,107 +2023,215 @@ describe('CDP Session Tests', () => {
1807
2023
  (function testFunction() {
1808
2024
  const localVar = 'hello';
1809
2025
  const numberVar = 42;
1810
- const objVar = { key: 'value', nested: { a: 1 } };
1811
2026
  debugger;
1812
2027
  return localVar + numberVar;
1813
2028
  })()
1814
2029
  `)
1815
2030
 
1816
- const pausedEvent = await Promise.race([
2031
+ await Promise.race([
1817
2032
  pausedPromise,
1818
2033
  new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Debugger.paused timeout')), 5000))
1819
2034
  ])
1820
2035
 
1821
- const stackTrace = pausedEvent.callFrames.map(frame => ({
1822
- functionName: frame.functionName || '(anonymous)',
1823
- lineNumber: frame.location.lineNumber,
1824
- columnNumber: frame.location.columnNumber,
1825
- }))
2036
+ expect(dbg.isPaused()).toBe(true)
2037
+
2038
+ const location = await dbg.getLocation()
2039
+ expect(location.callstack[0].functionName).toBe('testFunction')
2040
+ expect(location.sourceContext).toContain('debugger')
1826
2041
 
1827
- expect({
1828
- reason: pausedEvent.reason,
1829
- stackTrace: stackTrace.slice(0, 3),
1830
- }).toMatchInlineSnapshot(`
2042
+ const vars = await dbg.inspectLocalVariables()
2043
+ expect(vars).toMatchInlineSnapshot(`
1831
2044
  {
1832
- "reason": "other",
1833
- "stackTrace": [
1834
- {
1835
- "columnNumber": 16,
1836
- "functionName": "testFunction",
1837
- "lineNumber": 4,
1838
- },
1839
- {
1840
- "columnNumber": 14,
1841
- "functionName": "(anonymous)",
1842
- "lineNumber": 6,
1843
- },
1844
- {
1845
- "columnNumber": 29,
1846
- "functionName": "evaluate",
1847
- "lineNumber": 289,
1848
- },
1849
- ],
2045
+ "localVar": "hello",
2046
+ "numberVar": 42,
1850
2047
  }
1851
2048
  `)
1852
2049
 
1853
- const topFrame = pausedEvent.callFrames[0]
1854
- const scopeChain = topFrame.scopeChain
2050
+ const evalResult = await dbg.evaluate({ expression: 'localVar + " world"' })
2051
+ expect(evalResult.value).toBe('hello world')
1855
2052
 
1856
- const localScope = scopeChain.find(s => s.type === 'local')
1857
- const localVars: Record<string, unknown> = {}
2053
+ await dbg.resume()
2054
+ await new Promise(r => setTimeout(r, 100))
2055
+ expect(dbg.isPaused()).toBe(false)
1858
2056
 
1859
- if (localScope?.object.objectId) {
1860
- const propsResult = await cdpSession.send('Runtime.getProperties', {
1861
- objectId: localScope.object.objectId,
1862
- ownProperties: true,
1863
- })
2057
+ cdpSession.close()
2058
+ await browser.close()
2059
+ await page.close()
2060
+ }, 60000)
1864
2061
 
1865
- for (const prop of propsResult.result) {
1866
- if (prop.value) {
1867
- localVars[prop.name] = prop.value.type === 'object'
1868
- ? `[object ${prop.value.className || prop.value.subtype || 'Object'}]`
1869
- : prop.value.value
1870
- }
2062
+ it('should list scripts with Debugger class', async () => {
2063
+ const browserContext = getBrowserContext()
2064
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
2065
+
2066
+ const page = await browserContext.newPage()
2067
+ await page.setContent(`
2068
+ <html>
2069
+ <head>
2070
+ <script src="data:text/javascript,function testScript() { return 42; }"></script>
2071
+ </head>
2072
+ <body><h1>Script Test</h1></body>
2073
+ </html>
2074
+ `)
2075
+ await page.bringToFront()
2076
+
2077
+ await serviceWorker.evaluate(async () => {
2078
+ await globalThis.toggleExtensionForActiveTab()
2079
+ })
2080
+ await new Promise(r => setTimeout(r, 100))
2081
+
2082
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
2083
+ let cdpPage
2084
+ for (const p of browser.contexts()[0].pages()) {
2085
+ const html = await p.content()
2086
+ if (html.includes('Script Test')) {
2087
+ cdpPage = p
2088
+ break
1871
2089
  }
1872
2090
  }
2091
+ expect(cdpPage).toBeDefined()
1873
2092
 
1874
- expect({
1875
- scopeTypes: scopeChain.map(s => s.type),
1876
- localVariables: localVars,
1877
- }).toMatchInlineSnapshot(`
1878
- {
1879
- "localVariables": {
1880
- "localVar": "hello",
1881
- "numberVar": 42,
1882
- "objVar": "[object Object]",
1883
- },
1884
- "scopeTypes": [
1885
- "local",
1886
- "global",
1887
- ],
1888
- }
2093
+ const wsUrl = getCdpUrl({ port: TEST_PORT })
2094
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
2095
+ const dbg = new Debugger({ cdp: cdpSession })
2096
+
2097
+ const scripts = await dbg.listScripts()
2098
+ expect(scripts.length).toBeGreaterThan(0)
2099
+ expect(scripts[0]).toHaveProperty('scriptId')
2100
+ expect(scripts[0]).toHaveProperty('url')
2101
+
2102
+ const dataScripts = await dbg.listScripts({ search: 'data:' })
2103
+ expect(dataScripts.length).toBeGreaterThan(0)
2104
+
2105
+ cdpSession.close()
2106
+ await browser.close()
2107
+ await page.close()
2108
+ }, 60000)
2109
+
2110
+ it('should manage breakpoints with Debugger class', async () => {
2111
+ const browserContext = getBrowserContext()
2112
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
2113
+
2114
+ const page = await browserContext.newPage()
2115
+ await page.setContent(`
2116
+ <html>
2117
+ <head>
2118
+ <script src="data:text/javascript,function testFunc() { return 42; }"></script>
2119
+ </head>
2120
+ <body></body>
2121
+ </html>
1889
2122
  `)
2123
+ await page.bringToFront()
1890
2124
 
1891
- const evalResult = await cdpSession.send('Debugger.evaluateOnCallFrame', {
1892
- callFrameId: topFrame.callFrameId,
1893
- expression: 'localVar + " world " + numberVar',
2125
+ await serviceWorker.evaluate(async () => {
2126
+ await globalThis.toggleExtensionForActiveTab()
1894
2127
  })
2128
+ await new Promise(r => setTimeout(r, 100))
1895
2129
 
1896
- expect({
1897
- evaluatedExpression: 'localVar + " world " + numberVar',
1898
- result: evalResult.result.value,
1899
- type: evalResult.result.type,
1900
- }).toMatchInlineSnapshot(`
1901
- {
1902
- "evaluatedExpression": "localVar + " world " + numberVar",
1903
- "result": "hello world 42",
1904
- "type": "string",
1905
- }
2130
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
2131
+ let cdpPage
2132
+ for (const p of browser.contexts()[0].pages()) {
2133
+ const html = await p.content()
2134
+ if (html.includes('testFunc')) {
2135
+ cdpPage = p
2136
+ break
2137
+ }
2138
+ }
2139
+ expect(cdpPage).toBeDefined()
2140
+
2141
+ const wsUrl = getCdpUrl({ port: TEST_PORT })
2142
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
2143
+ const dbg = new Debugger({ cdp: cdpSession })
2144
+
2145
+ await dbg.enable()
2146
+
2147
+ expect(dbg.listBreakpoints()).toHaveLength(0)
2148
+
2149
+ const bpId = await dbg.setBreakpoint({ file: 'https://example.com/test.js', line: 1 })
2150
+ expect(typeof bpId).toBe('string')
2151
+ expect(dbg.listBreakpoints()).toHaveLength(1)
2152
+ expect(dbg.listBreakpoints()[0]).toMatchObject({
2153
+ id: bpId,
2154
+ file: 'https://example.com/test.js',
2155
+ line: 1,
2156
+ })
2157
+
2158
+ await dbg.deleteBreakpoint({ breakpointId: bpId })
2159
+ expect(dbg.listBreakpoints()).toHaveLength(0)
2160
+
2161
+ cdpSession.close()
2162
+ await browser.close()
2163
+ await page.close()
2164
+ }, 60000)
2165
+
2166
+ it('should step through code with Debugger class', async () => {
2167
+ const browserContext = getBrowserContext()
2168
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
2169
+
2170
+ const page = await browserContext.newPage()
2171
+ await page.goto('https://example.com/')
2172
+ await page.bringToFront()
2173
+
2174
+ await serviceWorker.evaluate(async () => {
2175
+ await globalThis.toggleExtensionForActiveTab()
2176
+ })
2177
+ await new Promise(r => setTimeout(r, 100))
2178
+
2179
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
2180
+ const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
2181
+ expect(cdpPage).toBeDefined()
2182
+
2183
+ const wsUrl = getCdpUrl({ port: TEST_PORT })
2184
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
2185
+ const dbg = new Debugger({ cdp: cdpSession })
2186
+
2187
+ await dbg.enable()
2188
+
2189
+ const pausedPromise = new Promise<void>((resolve) => {
2190
+ cdpSession.on('Debugger.paused', () => resolve())
2191
+ })
2192
+
2193
+ cdpPage!.evaluate(`
2194
+ (function outer() {
2195
+ function inner() {
2196
+ const x = 1;
2197
+ debugger;
2198
+ const y = 2;
2199
+ return x + y;
2200
+ }
2201
+ const result = inner();
2202
+ return result;
2203
+ })()
1906
2204
  `)
1907
2205
 
1908
- await cdpSession.send('Debugger.resume')
1909
- await cdpSession.send('Debugger.disable')
1910
- cdpSession.detach()
2206
+ await pausedPromise
2207
+ expect(dbg.isPaused()).toBe(true)
2208
+
2209
+ const location1 = await dbg.getLocation()
2210
+ expect(location1.callstack.length).toBeGreaterThanOrEqual(2)
2211
+ expect(location1.callstack[0].functionName).toBe('inner')
2212
+ expect(location1.callstack[1].functionName).toBe('outer')
2213
+
2214
+ const stepOverPromise = new Promise<void>((resolve) => {
2215
+ cdpSession.on('Debugger.paused', () => resolve())
2216
+ })
2217
+ await dbg.stepOver()
2218
+ await stepOverPromise
2219
+
2220
+ const location2 = await dbg.getLocation()
2221
+ expect(location2.lineNumber).toBeGreaterThan(location1.lineNumber)
2222
+
2223
+ const stepOutPromise = new Promise<void>((resolve) => {
2224
+ cdpSession.on('Debugger.paused', () => resolve())
2225
+ })
2226
+ await dbg.stepOut()
2227
+ await stepOutPromise
2228
+
2229
+ const location3 = await dbg.getLocation()
2230
+ expect(location3.callstack[0].functionName).toBe('outer')
2231
+
2232
+ await dbg.resume()
2233
+
2234
+ cdpSession.close()
1911
2235
  await browser.close()
1912
2236
  await page.close()
1913
2237
  }, 60000)
@@ -1923,13 +2247,13 @@ describe('CDP Session Tests', () => {
1923
2247
  await serviceWorker.evaluate(async () => {
1924
2248
  await globalThis.toggleExtensionForActiveTab()
1925
2249
  })
1926
- await new Promise(r => setTimeout(r, 500))
2250
+ await new Promise(r => setTimeout(r, 100))
1927
2251
 
1928
- const browser = await chromium.connectOverCDP(getCdpUrl())
2252
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
1929
2253
  const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
1930
2254
  expect(cdpPage).toBeDefined()
1931
2255
 
1932
- const wsUrl = getCdpUrl()
2256
+ const wsUrl = getCdpUrl({ port: TEST_PORT })
1933
2257
  const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
1934
2258
  await cdpSession.send('Profiler.enable')
1935
2259
  await cdpSession.send('Profiler.start')
@@ -1957,33 +2281,17 @@ describe('CDP Session Tests', () => {
1957
2281
  .filter(name => name && name.length > 0)
1958
2282
  .slice(0, 10)
1959
2283
 
1960
- expect({
1961
- hasNodes: profile.nodes.length > 0,
1962
- nodeCount: profile.nodes.length,
1963
- durationMicroseconds: profile.endTime - profile.startTime,
1964
- sampleFunctionNames: functionNames,
1965
- }).toMatchInlineSnapshot(`
1966
- {
1967
- "durationMicroseconds": 6962,
1968
- "hasNodes": true,
1969
- "nodeCount": 8,
1970
- "sampleFunctionNames": [
1971
- "(root)",
1972
- "(program)",
1973
- "(idle)",
1974
- "evaluate",
1975
- "querySelectorAll",
1976
- ],
1977
- }
1978
- `)
2284
+ expect(profile.nodes.length).toBeGreaterThan(0)
2285
+ expect(profile.endTime - profile.startTime).toBeGreaterThan(0)
2286
+ expect(functionNames.every((name) => typeof name === 'string')).toBe(true)
1979
2287
 
1980
2288
  await cdpSession.send('Profiler.disable')
1981
- cdpSession.detach()
2289
+ cdpSession.close()
1982
2290
  await browser.close()
1983
2291
  await page.close()
1984
2292
  }, 60000)
1985
2293
 
1986
- it('should click at correct coordinates on high-DPI simulation', async () => {
2294
+ it('should update Target.getTargets URL after page navigation', async () => {
1987
2295
  const browserContext = getBrowserContext()
1988
2296
  const serviceWorker = await getExtensionServiceWorker(browserContext)
1989
2297
 
@@ -1994,26 +2302,353 @@ describe('CDP Session Tests', () => {
1994
2302
  await serviceWorker.evaluate(async () => {
1995
2303
  await globalThis.toggleExtensionForActiveTab()
1996
2304
  })
1997
- await new Promise(r => setTimeout(r, 500))
2305
+ await new Promise(r => setTimeout(r, 100))
1998
2306
 
1999
- const browser = await chromium.connectOverCDP(getCdpUrl())
2307
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
2000
2308
  const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
2001
2309
  expect(cdpPage).toBeDefined()
2002
2310
 
2003
- const h1Bounds = await cdpPage!.locator('h1').boundingBox()
2004
- expect(h1Bounds).toBeDefined()
2005
- console.log('H1 bounding box:', h1Bounds)
2311
+ const wsUrl = getCdpUrl({ port: TEST_PORT })
2312
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
2006
2313
 
2007
- await cdpPage!.evaluate(() => {
2008
- (window as any).clickedAt = null;
2009
- document.addEventListener('click', (e) => {
2010
- (window as any).clickedAt = { x: e.clientX, y: e.clientY };
2011
- });
2012
- })
2314
+ const initialTargets = await cdpSession.send('Target.getTargets')
2315
+ const initialPageTarget = initialTargets.targetInfos.find(t => t.type === 'page' && t.url.includes('example.com'))
2316
+ expect(initialPageTarget?.url).toBe('https://example.com/')
2013
2317
 
2014
- await cdpPage!.locator('h1').click()
2318
+ await cdpPage!.goto('https://example.org/', { waitUntil: 'domcontentloaded' })
2319
+ await new Promise(r => setTimeout(r, 100))
2015
2320
 
2016
- const clickedAt = await cdpPage!.evaluate(() => (window as any).clickedAt)
2321
+ const afterNavTargets = await cdpSession.send('Target.getTargets')
2322
+ const allPageTargets = afterNavTargets.targetInfos.filter(t => t.type === 'page')
2323
+
2324
+ const aboutBlankTargets = allPageTargets.filter(t => t.url === 'about:blank')
2325
+ expect(aboutBlankTargets).toHaveLength(0)
2326
+
2327
+ const exampleComTargets = allPageTargets.filter(t => t.url.includes('example.com'))
2328
+ expect(exampleComTargets).toHaveLength(0)
2329
+
2330
+ const exampleOrgTargets = allPageTargets.filter(t => t.url.includes('example.org'))
2331
+ expect(exampleOrgTargets).toHaveLength(1)
2332
+
2333
+ cdpSession.close()
2334
+ await browser.close()
2335
+ await page.close()
2336
+ }, 60000)
2337
+
2338
+ it('should return correct targets for multiple pages via Target.getTargets', async () => {
2339
+ const browserContext = getBrowserContext()
2340
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
2341
+
2342
+ const page1 = await browserContext.newPage()
2343
+ await page1.goto('https://example.com/')
2344
+ await page1.bringToFront()
2345
+ await serviceWorker.evaluate(async () => {
2346
+ await globalThis.toggleExtensionForActiveTab()
2347
+ })
2348
+
2349
+ const page2 = await browserContext.newPage()
2350
+ await page2.goto('https://example.org/')
2351
+ await page2.bringToFront()
2352
+ await serviceWorker.evaluate(async () => {
2353
+ await globalThis.toggleExtensionForActiveTab()
2354
+ })
2355
+
2356
+ await new Promise(r => setTimeout(r, 100))
2357
+
2358
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
2359
+ const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
2360
+ expect(cdpPage).toBeDefined()
2361
+
2362
+ const wsUrl = getCdpUrl({ port: TEST_PORT })
2363
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
2364
+
2365
+ const { targetInfos } = await cdpSession.send('Target.getTargets')
2366
+ const allPageTargets = targetInfos.filter(t => t.type === 'page')
2367
+
2368
+ const aboutBlankTargets = allPageTargets.filter(t => t.url === 'about:blank')
2369
+ expect(aboutBlankTargets).toHaveLength(0)
2370
+
2371
+ const pageTargets = allPageTargets
2372
+ .map(t => ({ type: t.type, url: t.url }))
2373
+ .sort((a, b) => a.url.localeCompare(b.url))
2374
+
2375
+ expect(pageTargets).toMatchInlineSnapshot(`
2376
+ [
2377
+ {
2378
+ "type": "page",
2379
+ "url": "https://example.com/",
2380
+ },
2381
+ {
2382
+ "type": "page",
2383
+ "url": "https://example.org/",
2384
+ },
2385
+ ]
2386
+ `)
2387
+
2388
+ cdpSession.close()
2389
+ await browser.close()
2390
+ await page1.close()
2391
+ await page2.close()
2392
+ }, 60000)
2393
+
2394
+ it('should create CDP session for page after navigation', async () => {
2395
+ const browserContext = getBrowserContext()
2396
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
2397
+
2398
+ const page = await browserContext.newPage()
2399
+ await page.goto('https://example.com/')
2400
+ await page.bringToFront()
2401
+
2402
+ await serviceWorker.evaluate(async () => {
2403
+ await globalThis.toggleExtensionForActiveTab()
2404
+ })
2405
+ await new Promise(r => setTimeout(r, 100))
2406
+
2407
+ await page.goto('https://example.org/', { waitUntil: 'domcontentloaded' })
2408
+ await new Promise(r => setTimeout(r, 100))
2409
+
2410
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
2411
+ const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.org'))
2412
+ expect(cdpPage).toBeDefined()
2413
+
2414
+ const wsUrl = getCdpUrl({ port: TEST_PORT })
2415
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
2416
+
2417
+ const evalResult = await cdpSession.send('Runtime.evaluate', {
2418
+ expression: 'document.title',
2419
+ returnByValue: true,
2420
+ })
2421
+ expect(evalResult.result.value).toContain('Example Domain')
2422
+
2423
+ cdpSession.close()
2424
+ await browser.close()
2425
+ await page.close()
2426
+ }, 60000)
2427
+
2428
+ it('should maintain CDP session functionality after page URL change', async () => {
2429
+ const browserContext = getBrowserContext()
2430
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
2431
+
2432
+ const page = await browserContext.newPage()
2433
+ const initialUrl = 'https://example.com/'
2434
+ await page.goto(initialUrl)
2435
+ await page.bringToFront()
2436
+
2437
+ await serviceWorker.evaluate(async () => {
2438
+ await globalThis.toggleExtensionForActiveTab()
2439
+ })
2440
+ await new Promise(r => setTimeout(r, 100))
2441
+
2442
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
2443
+ const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
2444
+ expect(cdpPage).toBeDefined()
2445
+
2446
+ const wsUrl = getCdpUrl({ port: TEST_PORT })
2447
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
2448
+
2449
+ const initialEvalResult = await cdpSession.send('Runtime.evaluate', {
2450
+ expression: 'document.title',
2451
+ returnByValue: true,
2452
+ })
2453
+ expect(initialEvalResult.result.value).toBe('Example Domain')
2454
+
2455
+ const newUrl = 'https://example.org/'
2456
+ await cdpPage!.goto(newUrl, { waitUntil: 'domcontentloaded' })
2457
+
2458
+ expect(cdpPage!.url()).toBe(newUrl)
2459
+
2460
+ const layoutMetrics = await cdpSession.send('Page.getLayoutMetrics')
2461
+ expect(layoutMetrics.cssVisualViewport).toBeDefined()
2462
+ expect(layoutMetrics.cssVisualViewport.clientWidth).toBeGreaterThan(0)
2463
+
2464
+ const afterNavEvalResult = await cdpSession.send('Runtime.evaluate', {
2465
+ expression: 'document.title',
2466
+ returnByValue: true,
2467
+ })
2468
+ expect(afterNavEvalResult.result.value).toContain('Example Domain')
2469
+
2470
+ const locationResult = await cdpSession.send('Runtime.evaluate', {
2471
+ expression: 'window.location.href',
2472
+ returnByValue: true,
2473
+ })
2474
+ expect(locationResult.result.value).toBe(newUrl)
2475
+
2476
+ cdpSession.close()
2477
+ await browser.close()
2478
+ await page.close()
2479
+ }, 60000)
2480
+
2481
+ it('should pause on all exceptions with setPauseOnExceptions', async () => {
2482
+ const browserContext = getBrowserContext()
2483
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
2484
+
2485
+ const page = await browserContext.newPage()
2486
+ await page.goto('https://example.com/')
2487
+ await page.bringToFront()
2488
+
2489
+ await serviceWorker.evaluate(async () => {
2490
+ await globalThis.toggleExtensionForActiveTab()
2491
+ })
2492
+ await new Promise(r => setTimeout(r, 100))
2493
+
2494
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
2495
+ const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
2496
+ expect(cdpPage).toBeDefined()
2497
+
2498
+ const wsUrl = getCdpUrl({ port: TEST_PORT })
2499
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
2500
+ const dbg = new Debugger({ cdp: cdpSession })
2501
+
2502
+ await dbg.enable()
2503
+ await dbg.setPauseOnExceptions({ state: 'all' })
2504
+
2505
+ const pausedPromise = new Promise<void>((resolve) => {
2506
+ cdpSession.on('Debugger.paused', () => resolve())
2507
+ })
2508
+
2509
+ cdpPage!.evaluate(`
2510
+ (function() {
2511
+ try {
2512
+ throw new Error('Caught test error');
2513
+ } catch (e) {
2514
+ // caught but should still pause with state 'all'
2515
+ }
2516
+ })()
2517
+ `).catch(() => {})
2518
+
2519
+ await Promise.race([
2520
+ pausedPromise,
2521
+ new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Debugger.paused timeout')), 5000))
2522
+ ])
2523
+
2524
+ expect(dbg.isPaused()).toBe(true)
2525
+
2526
+ const location = await dbg.getLocation()
2527
+ expect(location.sourceContext).toContain('throw')
2528
+
2529
+ await dbg.resume()
2530
+
2531
+ await dbg.setPauseOnExceptions({ state: 'none' })
2532
+
2533
+ cdpSession.close()
2534
+ await browser.close()
2535
+ await page.close()
2536
+ }, 60000)
2537
+
2538
+ it('should inspect local and global variables with inline snapshots', async () => {
2539
+ const browserContext = getBrowserContext()
2540
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
2541
+
2542
+ const page = await browserContext.newPage()
2543
+ await page.setContent(`
2544
+ <html>
2545
+ <head>
2546
+ <script>
2547
+ const GLOBAL_CONFIG = 'production';
2548
+ function runTest() {
2549
+ const userName = 'Alice';
2550
+ const userAge = 25;
2551
+ const settings = { theme: 'dark', lang: 'en' };
2552
+ const scores = [10, 20, 30];
2553
+ debugger;
2554
+ return userName;
2555
+ }
2556
+ </script>
2557
+ </head>
2558
+ <body>
2559
+ <button onclick="runTest()">Run</button>
2560
+ </body>
2561
+ </html>
2562
+ `)
2563
+ await page.bringToFront()
2564
+
2565
+ await serviceWorker.evaluate(async () => {
2566
+ await globalThis.toggleExtensionForActiveTab()
2567
+ })
2568
+ await new Promise(r => setTimeout(r, 100))
2569
+
2570
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
2571
+ let cdpPage
2572
+ for (const p of browser.contexts()[0].pages()) {
2573
+ const html = await p.content()
2574
+ if (html.includes('runTest')) {
2575
+ cdpPage = p
2576
+ break
2577
+ }
2578
+ }
2579
+ expect(cdpPage).toBeDefined()
2580
+
2581
+ const wsUrl = getCdpUrl({ port: TEST_PORT })
2582
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
2583
+ const dbg = new Debugger({ cdp: cdpSession })
2584
+
2585
+ await dbg.enable()
2586
+
2587
+ const globalVars = await dbg.inspectGlobalVariables()
2588
+ expect(globalVars).toMatchInlineSnapshot(`
2589
+ [
2590
+ "GLOBAL_CONFIG",
2591
+ ]
2592
+ `)
2593
+
2594
+ const pausedPromise = new Promise<void>((resolve) => {
2595
+ cdpSession.on('Debugger.paused', () => resolve())
2596
+ })
2597
+
2598
+ cdpPage!.evaluate('runTest()')
2599
+
2600
+ await pausedPromise
2601
+ expect(dbg.isPaused()).toBe(true)
2602
+
2603
+ const localVars = await dbg.inspectLocalVariables()
2604
+ expect(localVars).toMatchInlineSnapshot(`
2605
+ {
2606
+ "GLOBAL_CONFIG": "production",
2607
+ "scores": "[array]",
2608
+ "settings": "[object]",
2609
+ "userAge": 25,
2610
+ "userName": "Alice",
2611
+ }
2612
+ `)
2613
+
2614
+ await dbg.resume()
2615
+
2616
+ cdpSession.close()
2617
+ await browser.close()
2618
+ await page.close()
2619
+ }, 60000)
2620
+
2621
+ it('should click at correct coordinates on high-DPI simulation', async () => {
2622
+ const browserContext = getBrowserContext()
2623
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
2624
+
2625
+ const page = await browserContext.newPage()
2626
+ await page.goto('https://example.com/')
2627
+ await page.bringToFront()
2628
+
2629
+ await serviceWorker.evaluate(async () => {
2630
+ await globalThis.toggleExtensionForActiveTab()
2631
+ })
2632
+ await new Promise(r => setTimeout(r, 100))
2633
+
2634
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
2635
+ const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
2636
+ expect(cdpPage).toBeDefined()
2637
+
2638
+ const h1Bounds = await cdpPage!.locator('h1').boundingBox()
2639
+ expect(h1Bounds).toBeDefined()
2640
+ console.log('H1 bounding box:', h1Bounds)
2641
+
2642
+ await cdpPage!.evaluate(() => {
2643
+ (window as any).clickedAt = null;
2644
+ document.addEventListener('click', (e) => {
2645
+ (window as any).clickedAt = { x: e.clientX, y: e.clientY };
2646
+ });
2647
+ })
2648
+
2649
+ await cdpPage!.locator('h1').click()
2650
+
2651
+ const clickedAt = await cdpPage!.evaluate(() => (window as any).clickedAt)
2017
2652
  console.log('Clicked at:', clickedAt)
2018
2653
 
2019
2654
  expect(clickedAt).toBeDefined()
@@ -2023,4 +2658,226 @@ describe('CDP Session Tests', () => {
2023
2658
  await browser.close()
2024
2659
  await page.close()
2025
2660
  }, 60000)
2661
+
2662
+ it('should use Editor class to list, read, and edit scripts', async () => {
2663
+ const browserContext = getBrowserContext()
2664
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
2665
+
2666
+ const page = await browserContext.newPage()
2667
+ await page.goto('https://example.com/')
2668
+ await page.bringToFront()
2669
+
2670
+ await serviceWorker.evaluate(async () => {
2671
+ await globalThis.toggleExtensionForActiveTab()
2672
+ })
2673
+ await new Promise(r => setTimeout(r, 100))
2674
+
2675
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
2676
+ const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
2677
+ expect(cdpPage).toBeDefined()
2678
+
2679
+ const wsUrl = getCdpUrl({ port: TEST_PORT })
2680
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
2681
+ const editor = new Editor({ cdp: cdpSession })
2682
+
2683
+ await editor.enable()
2684
+
2685
+ await cdpPage!.addScriptTag({
2686
+ content: `
2687
+ function greetUser(name) {
2688
+ console.log('Hello, ' + name);
2689
+ return 'Hello, ' + name;
2690
+ }
2691
+ `,
2692
+ })
2693
+ await new Promise(r => setTimeout(r, 100))
2694
+ const scripts = await editor.list()
2695
+ expect(scripts.length).toBeGreaterThan(0)
2696
+
2697
+ const matches = await editor.grep({ regex: /greetUser/ })
2698
+ expect(matches.length).toBeGreaterThan(0)
2699
+
2700
+ const match = matches[0]
2701
+ const { content, totalLines } = await editor.read({ url: match.url })
2702
+ expect(content).toContain('greetUser')
2703
+ expect(totalLines).toBeGreaterThan(0)
2704
+
2705
+ await editor.edit({
2706
+ url: match.url,
2707
+ oldString: "console.log('Hello, ' + name);",
2708
+ newString: "console.log('Hello, ' + name); console.log('EDITOR_TEST_MARKER');",
2709
+ })
2710
+
2711
+ const consoleLogs: string[] = []
2712
+ cdpPage!.on('console', msg => {
2713
+ consoleLogs.push(msg.text())
2714
+ })
2715
+
2716
+ await cdpPage!.evaluate(() => {
2717
+ (window as any).greetUser('World')
2718
+ })
2719
+ await new Promise(r => setTimeout(r, 100))
2720
+
2721
+ expect(consoleLogs).toContain('Hello, World')
2722
+ expect(consoleLogs).toContain('EDITOR_TEST_MARKER')
2723
+
2724
+ cdpSession.close()
2725
+ await browser.close()
2726
+ await page.close()
2727
+ }, 60000)
2728
+
2729
+ it('editor can list, read, and edit CSS stylesheets', async () => {
2730
+ const browserContext = getBrowserContext()
2731
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
2732
+
2733
+ const page = await browserContext.newPage()
2734
+ await page.goto('https://example.com/')
2735
+ await page.bringToFront()
2736
+
2737
+ await serviceWorker.evaluate(async () => {
2738
+ await globalThis.toggleExtensionForActiveTab()
2739
+ })
2740
+ await new Promise(r => setTimeout(r, 100))
2741
+
2742
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
2743
+ const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
2744
+ expect(cdpPage).toBeDefined()
2745
+
2746
+ const wsUrl = getCdpUrl({ port: TEST_PORT })
2747
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
2748
+ const editor = new Editor({ cdp: cdpSession })
2749
+
2750
+ await editor.enable()
2751
+
2752
+ await cdpPage!.addStyleTag({
2753
+ content: `
2754
+ .editor-test-element {
2755
+ color: rgb(255, 0, 0);
2756
+ background-color: rgb(0, 0, 255);
2757
+ }
2758
+ `,
2759
+ })
2760
+ await new Promise(r => setTimeout(r, 100))
2761
+ const stylesheets = await editor.list({ pattern: /inline-css:/ })
2762
+ expect(stylesheets.length).toBeGreaterThan(0)
2763
+
2764
+ const cssMatches = await editor.grep({ regex: /editor-test-element/, pattern: /inline-css:/ })
2765
+ expect(cssMatches.length).toBeGreaterThan(0)
2766
+
2767
+ const cssMatch = cssMatches[0]
2768
+ const { content, totalLines } = await editor.read({ url: cssMatch.url })
2769
+ expect(content).toContain('editor-test-element')
2770
+ expect(content).toContain('rgb(255, 0, 0)')
2771
+ expect(totalLines).toBeGreaterThan(0)
2772
+
2773
+ await cdpPage!.evaluate(() => {
2774
+ const el = document.createElement('div')
2775
+ el.className = 'editor-test-element'
2776
+ el.id = 'test-div'
2777
+ el.textContent = 'Test'
2778
+ document.body.appendChild(el)
2779
+ })
2780
+
2781
+ const colorBefore = await cdpPage!.evaluate(() => {
2782
+ const el = document.getElementById('test-div')!
2783
+ return window.getComputedStyle(el).color
2784
+ })
2785
+ expect(colorBefore).toBe('rgb(255, 0, 0)')
2786
+
2787
+ await editor.edit({
2788
+ url: cssMatch.url,
2789
+ oldString: 'color: rgb(255, 0, 0);',
2790
+ newString: 'color: rgb(0, 255, 0);',
2791
+ })
2792
+
2793
+ const colorAfter = await cdpPage!.evaluate(() => {
2794
+ const el = document.getElementById('test-div')!
2795
+ return window.getComputedStyle(el).color
2796
+ })
2797
+ expect(colorAfter).toBe('rgb(0, 255, 0)')
2798
+
2799
+ cdpSession.close()
2800
+ await browser.close()
2801
+ await page.close()
2802
+ }, 60000)
2803
+
2804
+ it('should inject bippy and find React fiber with getReactSource', async () => {
2805
+ const browserContext = getBrowserContext()
2806
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
2807
+
2808
+ const page = await browserContext.newPage()
2809
+ await page.setContent(`
2810
+ <!DOCTYPE html>
2811
+ <html>
2812
+ <head>
2813
+ <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
2814
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
2815
+ </head>
2816
+ <body>
2817
+ <div id="root"></div>
2818
+ <script>
2819
+ function MyComponent() {
2820
+ return React.createElement('button', { id: 'react-btn' }, 'Click me');
2821
+ }
2822
+ const root = ReactDOM.createRoot(document.getElementById('root'));
2823
+ root.render(React.createElement(MyComponent));
2824
+ </script>
2825
+ </body>
2826
+ </html>
2827
+ `)
2828
+ await page.bringToFront()
2829
+
2830
+ await serviceWorker.evaluate(async () => {
2831
+ await globalThis.toggleExtensionForActiveTab()
2832
+ })
2833
+ await new Promise(r => setTimeout(r, 500))
2834
+
2835
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
2836
+ const pages = browser.contexts()[0].pages()
2837
+ const cdpPage = pages.find(p => p.url().startsWith('about:'))
2838
+ expect(cdpPage).toBeDefined()
2839
+
2840
+ const btn = cdpPage!.locator('#react-btn')
2841
+ const btnCount = await btn.count()
2842
+ expect(btnCount).toBe(1)
2843
+
2844
+ const hasBippyBefore = await cdpPage!.evaluate(() => !!(globalThis as any).__bippy)
2845
+ expect(hasBippyBefore).toBe(false)
2846
+
2847
+ const wsUrl = getCdpUrl({ port: TEST_PORT })
2848
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
2849
+
2850
+ const { getReactSource } = await import('./react-source.js')
2851
+ const source = await getReactSource({ locator: btn, cdp: cdpSession })
2852
+
2853
+ const hasBippyAfter = await cdpPage!.evaluate(() => !!(globalThis as any).__bippy)
2854
+ expect(hasBippyAfter).toBe(true)
2855
+
2856
+ const hasFiber = await btn.evaluate((el) => {
2857
+ const bippy = (globalThis as any).__bippy
2858
+ const fiber = bippy.getFiberFromHostInstance(el)
2859
+ return !!fiber
2860
+ })
2861
+ expect(hasFiber).toBe(true)
2862
+
2863
+ const componentName = await btn.evaluate((el) => {
2864
+ const bippy = (globalThis as any).__bippy
2865
+ const fiber = bippy.getFiberFromHostInstance(el)
2866
+ let current = fiber
2867
+ while (current) {
2868
+ if (bippy.isCompositeFiber(current)) {
2869
+ return bippy.getDisplayName(current.type)
2870
+ }
2871
+ current = current.return
2872
+ }
2873
+ return null
2874
+ })
2875
+ expect(componentName).toBe('MyComponent')
2876
+
2877
+ console.log('Component name from fiber:', componentName)
2878
+ console.log('Source location (null for UMD React, works on local dev servers with JSX transform):', source)
2879
+
2880
+ await browser.close()
2881
+ await page.close()
2882
+ }, 60000)
2026
2883
  })