playwriter 0.0.25 → 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 (105) 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/create-logger.d.ts.map +1 -1
  16. package/dist/create-logger.js +2 -1
  17. package/dist/create-logger.js.map +1 -1
  18. package/dist/debugger-examples-types.d.ts +18 -0
  19. package/dist/debugger-examples-types.d.ts.map +1 -0
  20. package/dist/debugger-examples-types.js +2 -0
  21. package/dist/debugger-examples-types.js.map +1 -0
  22. package/dist/debugger-examples.d.ts +6 -0
  23. package/dist/debugger-examples.d.ts.map +1 -0
  24. package/dist/debugger-examples.js +53 -0
  25. package/dist/debugger-examples.js.map +1 -0
  26. package/dist/debugger-examples.ts +66 -0
  27. package/dist/debugger.d.ts +380 -0
  28. package/dist/debugger.d.ts.map +1 -0
  29. package/dist/debugger.js +631 -0
  30. package/dist/debugger.js.map +1 -0
  31. package/dist/editor-examples.d.ts +11 -0
  32. package/dist/editor-examples.d.ts.map +1 -0
  33. package/dist/editor-examples.js +124 -0
  34. package/dist/editor-examples.js.map +1 -0
  35. package/dist/editor.d.ts +203 -0
  36. package/dist/editor.d.ts.map +1 -0
  37. package/dist/editor.js +335 -0
  38. package/dist/editor.js.map +1 -0
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/mcp-client.d.ts +5 -1
  44. package/dist/mcp-client.d.ts.map +1 -1
  45. package/dist/mcp-client.js +13 -9
  46. package/dist/mcp-client.js.map +1 -1
  47. package/dist/mcp.d.ts +4 -1
  48. package/dist/mcp.d.ts.map +1 -1
  49. package/dist/mcp.js +170 -27
  50. package/dist/mcp.js.map +1 -1
  51. package/dist/mcp.test.d.ts.map +1 -1
  52. package/dist/mcp.test.js +886 -182
  53. package/dist/mcp.test.js.map +1 -1
  54. package/dist/prompt.md +86 -6
  55. package/dist/{extension/protocol.d.ts → protocol.d.ts} +1 -1
  56. package/dist/protocol.d.ts.map +1 -0
  57. package/dist/protocol.js.map +1 -0
  58. package/dist/react-source.d.ts +13 -0
  59. package/dist/react-source.d.ts.map +1 -0
  60. package/dist/react-source.js +66 -0
  61. package/dist/react-source.js.map +1 -0
  62. package/dist/selector-generator.js +7065 -18
  63. package/dist/start-relay-server.d.ts +4 -2
  64. package/dist/start-relay-server.d.ts.map +1 -1
  65. package/dist/start-relay-server.js +3 -3
  66. package/dist/start-relay-server.js.map +1 -1
  67. package/dist/styles.d.ts +27 -0
  68. package/dist/styles.d.ts.map +1 -0
  69. package/dist/styles.js +232 -0
  70. package/dist/styles.js.map +1 -0
  71. package/dist/utils.d.ts +3 -1
  72. package/dist/utils.d.ts.map +1 -1
  73. package/dist/utils.js +7 -3
  74. package/dist/utils.js.map +1 -1
  75. package/dist/wait-for-page-load.d.ts.map +1 -1
  76. package/dist/wait-for-page-load.js +3 -2
  77. package/dist/wait-for-page-load.js.map +1 -1
  78. package/package.json +5 -2
  79. package/src/{extension/cdp-relay.ts → cdp-relay.ts} +109 -5
  80. package/src/cdp-session.ts +4 -4
  81. package/src/cdp-timing.md +128 -0
  82. package/src/cli.ts +85 -0
  83. package/src/create-logger.ts +2 -1
  84. package/src/debugger-examples-types.ts +10 -0
  85. package/src/debugger-examples.ts +66 -0
  86. package/src/debugger.ts +711 -0
  87. package/src/editor-examples.ts +148 -0
  88. package/src/editor.ts +389 -0
  89. package/src/index.ts +1 -1
  90. package/src/mcp-client.ts +14 -9
  91. package/src/mcp.test.ts +1053 -196
  92. package/src/mcp.ts +195 -30
  93. package/src/prompt.md +86 -6
  94. package/src/{extension/protocol.ts → protocol.ts} +1 -1
  95. package/src/react-source.ts +92 -0
  96. package/src/snapshots/shadcn-ui-accessibility.md +57 -57
  97. package/src/start-relay-server.ts +3 -3
  98. package/src/styles.ts +343 -0
  99. package/src/utils.ts +8 -3
  100. package/src/wait-for-page-load.ts +3 -2
  101. package/dist/extension/cdp-relay.d.ts.map +0 -1
  102. package/dist/extension/cdp-relay.js.map +0 -1
  103. package/dist/extension/protocol.d.ts.map +0 -1
  104. package/dist/extension/protocol.js.map +0 -1
  105. /package/dist/{extension/protocol.js → protocol.js} +0 -0
package/dist/mcp.test.js CHANGED
@@ -9,9 +9,12 @@ import os from 'node:os';
9
9
  import { getCdpUrl } from './utils.js';
10
10
  import { imageSize } from 'image-size';
11
11
  import { getCDPSessionForPage } from './cdp-session.js';
12
- import { startPlayWriterCDPRelayServer } from './extension/cdp-relay.js';
12
+ import { Debugger } from './debugger.js';
13
+ import { Editor } from './editor.js';
14
+ import { startPlayWriterCDPRelayServer } from './cdp-relay.js';
13
15
  import { createFileLogger } from './create-logger.js';
14
16
  import { killPortProcess } from 'kill-port-process';
17
+ const TEST_PORT = 19987;
15
18
  const execAsync = promisify(exec);
16
19
  async function getExtensionServiceWorker(context) {
17
20
  let serviceWorkers = context.serviceWorkers().filter(sw => sw.url().startsWith('chrome-extension://'));
@@ -45,13 +48,13 @@ async function killProcessOnPort(port) {
45
48
  }
46
49
  }
47
50
  async function setupTestContext({ tempDirPrefix }) {
48
- await killProcessOnPort(19988);
51
+ await killProcessOnPort(TEST_PORT);
49
52
  console.log('Building extension...');
50
- await execAsync('TESTING=1 pnpm build', { cwd: '../extension' });
53
+ await execAsync(`TESTING=1 PLAYWRITER_PORT=${TEST_PORT} pnpm build`, { cwd: '../extension' });
51
54
  console.log('Extension built');
52
55
  const localLogPath = path.join(process.cwd(), 'relay-server.log');
53
56
  const logger = createFileLogger({ logFilePath: localLogPath });
54
- const relayServer = await startPlayWriterCDPRelayServer({ port: 19988, logger });
57
+ const relayServer = await startPlayWriterCDPRelayServer({ port: TEST_PORT, logger });
55
58
  const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), tempDirPrefix));
56
59
  const extensionPath = path.resolve('../extension/dist');
57
60
  const browserContext = await chromium.launchPersistentContext(userDataDir, {
@@ -96,7 +99,7 @@ describe('MCP Server Tests', () => {
96
99
  let testCtx = null;
97
100
  beforeAll(async () => {
98
101
  testCtx = await setupTestContext({ tempDirPrefix: 'pw-test-' });
99
- const result = await createMCPClient();
102
+ const result = await createMCPClient({ port: TEST_PORT });
100
103
  client = result.client;
101
104
  cleanup = result.cleanup;
102
105
  }, 600000);
@@ -119,8 +122,8 @@ describe('MCP Server Tests', () => {
119
122
  await serviceWorker.evaluate(async () => {
120
123
  await globalThis.toggleExtensionForActiveTab();
121
124
  });
122
- await new Promise(r => setTimeout(r, 500));
123
- const browser = await chromium.connectOverCDP(getCdpUrl());
125
+ await new Promise(r => setTimeout(r, 100));
126
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
124
127
  const cdpPage = browser.contexts()[0].pages().find(p => {
125
128
  return p.url().startsWith('about:');
126
129
  });
@@ -149,7 +152,7 @@ describe('MCP Server Tests', () => {
149
152
  name: 'execute',
150
153
  arguments: {
151
154
  code: js `
152
- await state.page.goto('https://news.ycombinator.com');
155
+ await state.page.goto('https://example.com');
153
156
  const title = await state.page.title();
154
157
  console.log('Page title:', title);
155
158
  return { url: state.page.url(), title };
@@ -160,12 +163,12 @@ describe('MCP Server Tests', () => {
160
163
  [
161
164
  {
162
165
  "text": "Console output:
163
- [log] Page title: Hacker News
166
+ [log] Page title: Example Domain
164
167
 
165
168
  Return value:
166
169
  {
167
- "url": "https://news.ycombinator.com/",
168
- "title": "Hacker News"
170
+ \"url\": \"https://example.com/\",
171
+ \"title\": \"Example Domain\"
169
172
  }",
170
173
  "type": "text",
171
174
  },
@@ -232,7 +235,7 @@ describe('MCP Server Tests', () => {
232
235
  name: 'execute',
233
236
  arguments: {
234
237
  code: js `
235
- await state.page.goto('https://news.ycombinator.com/item?id=1', { waitUntil: 'networkidle' });
238
+ await state.page.goto('https://news.ycombinator.com/item?id=1', { waitUntil: 'domcontentloaded' });
236
239
  const snapshot = await state.page._snapshotForAI();
237
240
  return snapshot;
238
241
  `,
@@ -262,7 +265,7 @@ describe('MCP Server Tests', () => {
262
265
  name: 'execute',
263
266
  arguments: {
264
267
  code: js `
265
- await state.page.goto('https://ui.shadcn.com/', { waitUntil: 'networkidle' });
268
+ await state.page.goto('https://ui.shadcn.com/', { waitUntil: 'domcontentloaded' });
266
269
  const snapshot = await state.page._snapshotForAI();
267
270
  return snapshot;
268
271
  `,
@@ -308,7 +311,7 @@ describe('MCP Server Tests', () => {
308
311
  });
309
312
  expect(result.isConnected).toBe(true);
310
313
  // 3. Verify we can connect via direct CDP and see the page
311
- let directBrowser = await chromium.connectOverCDP(getCdpUrl());
314
+ let directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
312
315
  let contexts = directBrowser.contexts();
313
316
  let pages = contexts[0].pages();
314
317
  // Find our page
@@ -327,7 +330,7 @@ describe('MCP Server Tests', () => {
327
330
  // 5. Try to connect/use the page.
328
331
  // connecting to relay will succeed, but listing pages should NOT show our page
329
332
  // Connect to relay again
330
- directBrowser = await chromium.connectOverCDP(getCdpUrl());
333
+ directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
331
334
  contexts = directBrowser.contexts();
332
335
  pages = contexts[0].pages();
333
336
  foundPage = pages.find(p => p.url() === testUrl);
@@ -339,13 +342,13 @@ describe('MCP Server Tests', () => {
339
342
  });
340
343
  expect(resultEnabled.isConnected).toBe(true);
341
344
  // 7. Verify page is back
342
- directBrowser = await chromium.connectOverCDP(getCdpUrl());
345
+ directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
343
346
  // Wait a bit for targets to populate
344
- await new Promise(r => setTimeout(r, 500));
347
+ await new Promise(r => setTimeout(r, 100));
345
348
  contexts = directBrowser.contexts();
346
349
  // pages() might need a moment if target attached event comes in
347
350
  if (contexts[0].pages().length === 0) {
348
- await new Promise(r => setTimeout(r, 1000));
351
+ await new Promise(r => setTimeout(r, 100));
349
352
  }
350
353
  pages = contexts[0].pages();
351
354
  foundPage = pages.find(p => p.url() === testUrl);
@@ -361,9 +364,9 @@ describe('MCP Server Tests', () => {
361
364
  const browserContext = getBrowserContext();
362
365
  const serviceWorker = await getExtensionServiceWorker(browserContext);
363
366
  // Connect once
364
- const directBrowser = await chromium.connectOverCDP(getCdpUrl());
367
+ const directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
365
368
  // Wait a bit for connection and initial target discovery
366
- await new Promise(r => setTimeout(r, 500));
369
+ await new Promise(r => setTimeout(r, 100));
367
370
  // 1. Create a new page
368
371
  const page = await browserContext.newPage();
369
372
  const testUrl = 'https://example.com/persistent';
@@ -433,7 +436,7 @@ describe('MCP Server Tests', () => {
433
436
  await globalThis.toggleExtensionForActiveTab();
434
437
  });
435
438
  // 3. Connect via CDP
436
- const cdpUrl = getCdpUrl();
439
+ const cdpUrl = getCdpUrl({ port: TEST_PORT });
437
440
  const directBrowser = await chromium.connectOverCDP(cdpUrl);
438
441
  const connectedPage = directBrowser.contexts()[0].pages().find(p => p.url() === initialUrl);
439
442
  expect(connectedPage).toBeDefined();
@@ -443,16 +446,16 @@ describe('MCP Server Tests', () => {
443
446
  // We use a loop to check if it's still connected because reload might cause temporary disconnect/reconnect events
444
447
  // that Playwright handles natively if the session ID stays valid.
445
448
  await connectedPage?.reload();
446
- await connectedPage?.waitForLoadState('networkidle');
449
+ await connectedPage?.waitForLoadState('domcontentloaded');
447
450
  expect(await connectedPage?.title()).toBe('Example Domain');
448
451
  // Verify execution after reload
449
452
  expect(await connectedPage?.evaluate(() => 2 + 2)).toBe(4);
450
453
  // 5. Navigate to new URL
451
- const newUrl = 'https://news.ycombinator.com/';
454
+ const newUrl = 'https://example.org/';
452
455
  await connectedPage?.goto(newUrl);
453
- await connectedPage?.waitForLoadState('networkidle');
456
+ await connectedPage?.waitForLoadState('domcontentloaded');
454
457
  expect(connectedPage?.url()).toBe(newUrl);
455
- expect(await connectedPage?.title()).toContain('Hacker News');
458
+ expect(await connectedPage?.title()).toContain('Example Domain');
456
459
  // Verify execution after navigation
457
460
  expect(await connectedPage?.evaluate(() => 3 + 3)).toBe(6);
458
461
  await directBrowser.close();
@@ -461,12 +464,12 @@ describe('MCP Server Tests', () => {
461
464
  it('should support multiple concurrent tabs', async () => {
462
465
  const browserContext = getBrowserContext();
463
466
  const serviceWorker = await getExtensionServiceWorker(browserContext);
464
- await new Promise(resolve => setTimeout(resolve, 500));
467
+ await new Promise(resolve => setTimeout(resolve, 100));
465
468
  // Tab A
466
469
  const pageA = await browserContext.newPage();
467
470
  await pageA.goto('https://example.com/tab-a');
468
471
  await pageA.bringToFront();
469
- await new Promise(resolve => setTimeout(resolve, 500));
472
+ await new Promise(resolve => setTimeout(resolve, 100));
470
473
  await serviceWorker.evaluate(async () => {
471
474
  await globalThis.toggleExtensionForActiveTab();
472
475
  });
@@ -474,7 +477,7 @@ describe('MCP Server Tests', () => {
474
477
  const pageB = await browserContext.newPage();
475
478
  await pageB.goto('https://example.com/tab-b');
476
479
  await pageB.bringToFront();
477
- await new Promise(resolve => setTimeout(resolve, 500));
480
+ await new Promise(resolve => setTimeout(resolve, 100));
478
481
  await serviceWorker.evaluate(async () => {
479
482
  await globalThis.toggleExtensionForActiveTab();
480
483
  });
@@ -501,7 +504,7 @@ describe('MCP Server Tests', () => {
501
504
  `);
502
505
  expect(targetIds.idA).not.toBe(targetIds.idB);
503
506
  // Verify independent connections
504
- const browser = await chromium.connectOverCDP(getCdpUrl());
507
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
505
508
  const pages = browser.contexts()[0].pages();
506
509
  const results = await Promise.all(pages.map(async (p) => ({
507
510
  url: p.url(),
@@ -540,15 +543,15 @@ describe('MCP Server Tests', () => {
540
543
  await page.goto(targetUrl);
541
544
  await page.bringToFront();
542
545
  // Wait for load
543
- await page.waitForLoadState('networkidle');
546
+ await page.waitForLoadState('domcontentloaded');
544
547
  // 2. Enable extension for this page
545
548
  await serviceWorker.evaluate(async () => {
546
549
  await globalThis.toggleExtensionForActiveTab();
547
550
  });
548
551
  // 3. Verify via CDP that the correct URL is shown
549
- const browser = await chromium.connectOverCDP(getCdpUrl());
552
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
550
553
  // Wait for sync
551
- await new Promise(r => setTimeout(r, 1000));
554
+ await new Promise(r => setTimeout(r, 100));
552
555
  const cdpPage = browser.contexts()[0].pages().find(p => p.url() === targetUrl);
553
556
  expect(cdpPage).toBeDefined();
554
557
  expect(cdpPage?.url()).toBe(targetUrl);
@@ -562,7 +565,7 @@ describe('MCP Server Tests', () => {
562
565
  expect(pages.length).toBeGreaterThan(0);
563
566
  const page = pages[0];
564
567
  await page.goto('https://example.com/disconnect-test');
565
- await page.waitForLoadState('networkidle');
568
+ await page.waitForLoadState('domcontentloaded');
566
569
  await page.bringToFront();
567
570
  // Enable extension on this page
568
571
  const initialEnable = await serviceWorker.evaluate(async () => {
@@ -571,7 +574,7 @@ describe('MCP Server Tests', () => {
571
574
  console.log('Initial enable result:', initialEnable);
572
575
  expect(initialEnable.isConnected).toBe(true);
573
576
  // Wait for extension to fully connect
574
- await new Promise(resolve => setTimeout(resolve, 500));
577
+ await new Promise(resolve => setTimeout(resolve, 100));
575
578
  // Verify MCP can see the page
576
579
  const beforeDisconnect = await client.callTool({
577
580
  name: 'execute',
@@ -594,7 +597,7 @@ describe('MCP Server Tests', () => {
594
597
  await globalThis.disconnectEverything();
595
598
  });
596
599
  // Wait for disconnect to complete
597
- await new Promise(resolve => setTimeout(resolve, 500));
600
+ await new Promise(resolve => setTimeout(resolve, 100));
598
601
  // 3. Verify MCP cannot see the page anymore
599
602
  const afterDisconnect = await client.callTool({
600
603
  name: 'execute',
@@ -622,7 +625,7 @@ describe('MCP Server Tests', () => {
622
625
  expect(reconnectResult.isConnected).toBe(true);
623
626
  // Wait for extension to fully reconnect and relay server to be ready
624
627
  console.log('Waiting for reconnection to stabilize...');
625
- await new Promise(resolve => setTimeout(resolve, 1000));
628
+ await new Promise(resolve => setTimeout(resolve, 100));
626
629
  // 5. Reset the MCP client's playwright connection since it was closed by disconnectEverything
627
630
  console.log('Resetting MCP playwright connection...');
628
631
  const resetResult = await client.callTool({
@@ -985,19 +988,19 @@ describe('MCP Server Tests', () => {
985
988
  const browserContext = getBrowserContext();
986
989
  const serviceWorker = await getExtensionServiceWorker(browserContext);
987
990
  const page = await browserContext.newPage();
988
- const targetUrl = 'https://x.com';
989
- await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
991
+ const targetUrl = 'https://example.com/sw-test';
992
+ await page.goto(targetUrl);
990
993
  await page.bringToFront();
991
994
  await serviceWorker.evaluate(async () => {
992
995
  await globalThis.toggleExtensionForActiveTab();
993
996
  });
994
- await new Promise(r => setTimeout(r, 2000));
995
- const browser = await chromium.connectOverCDP(getCdpUrl());
997
+ await new Promise(r => setTimeout(r, 100));
998
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
996
999
  const pages = browser.contexts()[0].pages();
997
- const xPage = pages.find(p => p.url().includes('x.com'));
998
- expect(xPage).toBeDefined();
999
- expect(xPage?.url()).toContain('x.com');
1000
- expect(xPage?.url()).not.toContain('sw.js');
1000
+ const testPage = pages.find(p => p.url().includes('sw-test'));
1001
+ expect(testPage).toBeDefined();
1002
+ expect(testPage?.url()).toContain('sw-test');
1003
+ expect(testPage?.url()).not.toContain('sw.js');
1001
1004
  await browser.close();
1002
1005
  await page.close();
1003
1006
  }, 30000);
@@ -1012,13 +1015,13 @@ describe('MCP Server Tests', () => {
1012
1015
  await globalThis.toggleExtensionForActiveTab();
1013
1016
  });
1014
1017
  for (let i = 0; i < 5; i++) {
1015
- const browser = await chromium.connectOverCDP(getCdpUrl());
1018
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1016
1019
  const pages = browser.contexts()[0].pages();
1017
1020
  const testPage = pages.find(p => p.url().includes('repeated-test'));
1018
1021
  expect(testPage).toBeDefined();
1019
1022
  expect(testPage?.url()).toBe(targetUrl);
1020
1023
  await browser.close();
1021
- await new Promise(r => setTimeout(r, 200));
1024
+ await new Promise(r => setTimeout(r, 100));
1022
1025
  }
1023
1026
  await page.close();
1024
1027
  }, 30000);
@@ -1032,7 +1035,7 @@ describe('MCP Server Tests', () => {
1032
1035
  await serviceWorker.evaluate(async () => {
1033
1036
  await globalThis.toggleExtensionForActiveTab();
1034
1037
  });
1035
- await new Promise(r => setTimeout(r, 500));
1038
+ await new Promise(r => setTimeout(r, 400));
1036
1039
  const [mcpResult, cdpBrowser] = await Promise.all([
1037
1040
  client.callTool({
1038
1041
  name: 'execute',
@@ -1044,7 +1047,7 @@ describe('MCP Server Tests', () => {
1044
1047
  `,
1045
1048
  },
1046
1049
  }),
1047
- chromium.connectOverCDP(getCdpUrl())
1050
+ chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
1048
1051
  ]);
1049
1052
  const mcpOutput = mcpResult.content[0].text;
1050
1053
  expect(mcpOutput).toContain(targetUrl);
@@ -1058,24 +1061,40 @@ describe('MCP Server Tests', () => {
1058
1061
  const browserContext = getBrowserContext();
1059
1062
  const serviceWorker = await getExtensionServiceWorker(browserContext);
1060
1063
  const page = await browserContext.newPage();
1061
- const targetUrl = 'https://www.youtube.com';
1062
- await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
1064
+ await page.setContent(`
1065
+ <html>
1066
+ <head><title>Iframe Test Page</title></head>
1067
+ <body>
1068
+ <h1>Iframe Heavy Page</h1>
1069
+ <iframe src="about:blank" id="frame1"></iframe>
1070
+ <iframe src="about:blank" id="frame2"></iframe>
1071
+ <iframe src="about:blank" id="frame3"></iframe>
1072
+ </body>
1073
+ </html>
1074
+ `);
1063
1075
  await page.bringToFront();
1064
1076
  await serviceWorker.evaluate(async () => {
1065
1077
  await globalThis.toggleExtensionForActiveTab();
1066
1078
  });
1067
- await new Promise(r => setTimeout(r, 3000));
1079
+ await new Promise(r => setTimeout(r, 100));
1068
1080
  for (let i = 0; i < 3; i++) {
1069
- const browser = await chromium.connectOverCDP(getCdpUrl());
1081
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1070
1082
  const pages = browser.contexts()[0].pages();
1071
- const ytPage = pages.find(p => p.url().includes('youtube.com'));
1072
- expect(ytPage).toBeDefined();
1073
- expect(ytPage?.url()).toContain('youtube.com');
1083
+ let iframePage;
1084
+ for (const p of pages) {
1085
+ const html = await p.content();
1086
+ if (html.includes('Iframe Heavy Page')) {
1087
+ iframePage = p;
1088
+ break;
1089
+ }
1090
+ }
1091
+ expect(iframePage).toBeDefined();
1092
+ expect(iframePage?.url()).toContain('about:');
1074
1093
  await browser.close();
1075
- await new Promise(r => setTimeout(r, 500));
1094
+ await new Promise(r => setTimeout(r, 100));
1076
1095
  }
1077
1096
  await page.close();
1078
- }, 60000);
1097
+ }, 30000);
1079
1098
  it('should capture screenshot correctly', async () => {
1080
1099
  const browserContext = getBrowserContext();
1081
1100
  const serviceWorker = await getExtensionServiceWorker(browserContext);
@@ -1085,7 +1104,7 @@ describe('MCP Server Tests', () => {
1085
1104
  await serviceWorker.evaluate(async () => {
1086
1105
  await globalThis.toggleExtensionForActiveTab();
1087
1106
  });
1088
- await new Promise(r => setTimeout(r, 500));
1107
+ await new Promise(r => setTimeout(r, 100));
1089
1108
  const capturedCommands = [];
1090
1109
  const commandHandler = ({ command }) => {
1091
1110
  if (command.method === 'Page.captureScreenshot') {
@@ -1093,7 +1112,7 @@ describe('MCP Server Tests', () => {
1093
1112
  }
1094
1113
  };
1095
1114
  testCtx.relayServer.on('cdp:command', commandHandler);
1096
- const browser = await chromium.connectOverCDP(getCdpUrl());
1115
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1097
1116
  const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'));
1098
1117
  expect(cdpPage).toBeDefined();
1099
1118
  const viewportSize = cdpPage.viewportSize();
@@ -1197,7 +1216,7 @@ describe('MCP Server Tests', () => {
1197
1216
  await serviceWorker.evaluate(async () => {
1198
1217
  await globalThis.toggleExtensionForActiveTab();
1199
1218
  });
1200
- await new Promise(r => setTimeout(r, 500));
1219
+ await new Promise(r => setTimeout(r, 100));
1201
1220
  const capturedCommands = [];
1202
1221
  const commandHandler = ({ command }) => {
1203
1222
  if (command.method === 'Page.captureScreenshot') {
@@ -1205,7 +1224,7 @@ describe('MCP Server Tests', () => {
1205
1224
  }
1206
1225
  };
1207
1226
  testCtx.relayServer.on('cdp:command', commandHandler);
1208
- const browser = await chromium.connectOverCDP(getCdpUrl());
1227
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1209
1228
  let cdpPage;
1210
1229
  for (const p of browser.contexts()[0].pages()) {
1211
1230
  const html = await p.content();
@@ -1252,7 +1271,7 @@ describe('MCP Server Tests', () => {
1252
1271
  await serviceWorker.evaluate(async () => {
1253
1272
  await globalThis.toggleExtensionForActiveTab();
1254
1273
  });
1255
- await new Promise(r => setTimeout(r, 500));
1274
+ await new Promise(r => setTimeout(r, 400));
1256
1275
  const result = await client.callTool({
1257
1276
  name: 'execute',
1258
1277
  arguments: {
@@ -1283,6 +1302,184 @@ describe('MCP Server Tests', () => {
1283
1302
  expect(text).toContain('Locator text: Click Me');
1284
1303
  await page.close();
1285
1304
  }, 60000);
1305
+ it('should get styles for element using getStylesForLocator', async () => {
1306
+ const browserContext = getBrowserContext();
1307
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1308
+ const page = await browserContext.newPage();
1309
+ await page.setContent(`
1310
+ <html>
1311
+ <head>
1312
+ <style>
1313
+ body { font-family: Arial, sans-serif; color: #333; }
1314
+ .container { padding: 20px; margin: 10px; }
1315
+ #main-btn { background-color: blue; color: white; border-radius: 4px; }
1316
+ .btn { padding: 8px 16px; }
1317
+ </style>
1318
+ </head>
1319
+ <body>
1320
+ <div class="container">
1321
+ <button id="main-btn" class="btn" style="font-weight: bold;">Click Me</button>
1322
+ </div>
1323
+ </body>
1324
+ </html>
1325
+ `);
1326
+ await page.bringToFront();
1327
+ await serviceWorker.evaluate(async () => {
1328
+ await globalThis.toggleExtensionForActiveTab();
1329
+ });
1330
+ await new Promise(r => setTimeout(r, 400));
1331
+ const stylesResult = await client.callTool({
1332
+ name: 'execute',
1333
+ arguments: {
1334
+ code: js `
1335
+ let testPage;
1336
+ for (const p of context.pages()) {
1337
+ const html = await p.content();
1338
+ if (html.includes('main-btn')) { testPage = p; break; }
1339
+ }
1340
+ if (!testPage) throw new Error('Test page not found');
1341
+ const btn = testPage.locator('#main-btn');
1342
+ const styles = await getStylesForLocator({ locator: btn });
1343
+ return styles;
1344
+ `,
1345
+ timeout: 30000,
1346
+ },
1347
+ });
1348
+ expect(stylesResult.isError).toBeFalsy();
1349
+ const stylesText = stylesResult.content[0]?.text || '';
1350
+ expect(stylesText).toMatchInlineSnapshot(`
1351
+ "Return value:
1352
+ {
1353
+ "element": "button#main-btn.btn",
1354
+ "inlineStyle": {
1355
+ "font-weight": "bold"
1356
+ },
1357
+ "rules": [
1358
+ {
1359
+ "selector": ".btn",
1360
+ "source": null,
1361
+ "origin": "regular",
1362
+ "declarations": {
1363
+ "padding": "8px 16px",
1364
+ "padding-top": "8px",
1365
+ "padding-right": "16px",
1366
+ "padding-bottom": "8px",
1367
+ "padding-left": "16px"
1368
+ },
1369
+ "inheritedFrom": null
1370
+ },
1371
+ {
1372
+ "selector": "#main-btn",
1373
+ "source": null,
1374
+ "origin": "regular",
1375
+ "declarations": {
1376
+ "background-color": "blue",
1377
+ "color": "white",
1378
+ "border-radius": "4px",
1379
+ "border-top-left-radius": "4px",
1380
+ "border-top-right-radius": "4px",
1381
+ "border-bottom-right-radius": "4px",
1382
+ "border-bottom-left-radius": "4px"
1383
+ },
1384
+ "inheritedFrom": null
1385
+ },
1386
+ {
1387
+ "selector": ".container",
1388
+ "source": null,
1389
+ "origin": "regular",
1390
+ "declarations": {
1391
+ "padding": "20px",
1392
+ "margin": "10px",
1393
+ "padding-top": "20px",
1394
+ "padding-right": "20px",
1395
+ "padding-bottom": "20px",
1396
+ "padding-left": "20px",
1397
+ "margin-top": "10px",
1398
+ "margin-right": "10px",
1399
+ "margin-bottom": "10px",
1400
+ "margin-left": "10px"
1401
+ },
1402
+ "inheritedFrom": "ancestor[1]"
1403
+ },
1404
+ {
1405
+ "selector": "body",
1406
+ "source": null,
1407
+ "origin": "regular",
1408
+ "declarations": {
1409
+ "font-family": "Arial, sans-serif",
1410
+ "color": "rgb(51, 51, 51)"
1411
+ },
1412
+ "inheritedFrom": "ancestor[2]"
1413
+ }
1414
+ ]
1415
+ }"
1416
+ `);
1417
+ const formattedResult = await client.callTool({
1418
+ name: 'execute',
1419
+ arguments: {
1420
+ code: js `
1421
+ let testPage;
1422
+ for (const p of context.pages()) {
1423
+ const html = await p.content();
1424
+ if (html.includes('main-btn')) { testPage = p; break; }
1425
+ }
1426
+ if (!testPage) throw new Error('Test page not found');
1427
+ const btn = testPage.locator('#main-btn');
1428
+ const styles = await getStylesForLocator({ locator: btn });
1429
+ return formatStylesAsText(styles);
1430
+ `,
1431
+ timeout: 30000,
1432
+ },
1433
+ });
1434
+ expect(formattedResult.isError).toBeFalsy();
1435
+ const formattedText = formattedResult.content[0]?.text || '';
1436
+ expect(formattedText).toMatchInlineSnapshot(`
1437
+ "Return value:
1438
+ Element: button#main-btn.btn
1439
+
1440
+ Inline styles:
1441
+ font-weight: bold
1442
+
1443
+ Matched rules:
1444
+ .btn {
1445
+ padding: 8px 16px;
1446
+ padding-top: 8px;
1447
+ padding-right: 16px;
1448
+ padding-bottom: 8px;
1449
+ padding-left: 16px;
1450
+ }
1451
+ #main-btn {
1452
+ background-color: blue;
1453
+ color: white;
1454
+ border-radius: 4px;
1455
+ border-top-left-radius: 4px;
1456
+ border-top-right-radius: 4px;
1457
+ border-bottom-right-radius: 4px;
1458
+ border-bottom-left-radius: 4px;
1459
+ }
1460
+
1461
+ Inherited from ancestor[1]:
1462
+ .container {
1463
+ padding: 20px;
1464
+ margin: 10px;
1465
+ padding-top: 20px;
1466
+ padding-right: 20px;
1467
+ padding-bottom: 20px;
1468
+ padding-left: 20px;
1469
+ margin-top: 10px;
1470
+ margin-right: 10px;
1471
+ margin-bottom: 10px;
1472
+ margin-left: 10px;
1473
+ }
1474
+
1475
+ Inherited from ancestor[2]:
1476
+ body {
1477
+ font-family: Arial, sans-serif;
1478
+ color: rgb(51, 51, 51);
1479
+ }"
1480
+ `);
1481
+ await page.close();
1482
+ }, 60000);
1286
1483
  it('should return correct layout metrics via CDP', async () => {
1287
1484
  const browserContext = getBrowserContext();
1288
1485
  const serviceWorker = await getExtensionServiceWorker(browserContext);
@@ -1292,11 +1489,11 @@ describe('MCP Server Tests', () => {
1292
1489
  await serviceWorker.evaluate(async () => {
1293
1490
  await globalThis.toggleExtensionForActiveTab();
1294
1491
  });
1295
- await new Promise(r => setTimeout(r, 500));
1296
- const browser = await chromium.connectOverCDP(getCdpUrl());
1492
+ await new Promise(r => setTimeout(r, 100));
1493
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1297
1494
  const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'));
1298
1495
  expect(cdpPage).toBeDefined();
1299
- const wsUrl = getCdpUrl();
1496
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
1300
1497
  const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
1301
1498
  const layoutMetrics = await cdpSession.send('Page.getLayoutMetrics');
1302
1499
  const normalized = {
@@ -1348,7 +1545,7 @@ describe('MCP Server Tests', () => {
1348
1545
  const windowDpr = await cdpPage.evaluate(() => globalThis.devicePixelRatio);
1349
1546
  console.log('window.devicePixelRatio:', windowDpr);
1350
1547
  expect(windowDpr).toBe(1);
1351
- cdpSession.detach();
1548
+ cdpSession.close();
1352
1549
  await browser.close();
1353
1550
  await page.close();
1354
1551
  }, 60000);
@@ -1361,16 +1558,16 @@ describe('MCP Server Tests', () => {
1361
1558
  await serviceWorker.evaluate(async () => {
1362
1559
  await globalThis.toggleExtensionForActiveTab();
1363
1560
  });
1364
- await new Promise(r => setTimeout(r, 500));
1365
- const browser = await chromium.connectOverCDP(getCdpUrl());
1561
+ await new Promise(r => setTimeout(r, 100));
1562
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1366
1563
  const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'));
1367
1564
  expect(cdpPage).toBeDefined();
1368
- const wsUrl = getCdpUrl();
1565
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
1369
1566
  const client = await getCDPSessionForPage({ page: cdpPage, wsUrl });
1370
1567
  const layoutMetrics = await client.send('Page.getLayoutMetrics');
1371
1568
  expect(layoutMetrics.cssVisualViewport).toBeDefined();
1372
1569
  expect(layoutMetrics.cssVisualViewport.clientWidth).toBeGreaterThan(0);
1373
- client.detach();
1570
+ client.close();
1374
1571
  await browser.close();
1375
1572
  await page.close();
1376
1573
  }, 60000);
@@ -1380,30 +1577,30 @@ describe('MCP Server Tests', () => {
1380
1577
  await serviceWorker.evaluate(async () => {
1381
1578
  await globalThis.disconnectEverything();
1382
1579
  });
1383
- await new Promise(r => setTimeout(r, 500));
1580
+ await new Promise(r => setTimeout(r, 100));
1384
1581
  const targetUrl = 'https://example.com/';
1385
1582
  const enableResult = await serviceWorker.evaluate(async (url) => {
1386
1583
  const tab = await chrome.tabs.create({ url, active: true });
1387
- await new Promise(r => setTimeout(r, 1000));
1584
+ await new Promise(r => setTimeout(r, 100));
1388
1585
  return await globalThis.toggleExtensionForActiveTab();
1389
1586
  }, targetUrl);
1390
1587
  console.log('Extension enabled:', enableResult);
1391
1588
  expect(enableResult.isConnected).toBe(true);
1392
- await new Promise(r => setTimeout(r, 1000));
1589
+ await new Promise(r => setTimeout(r, 100));
1393
1590
  const { Stagehand } = await import('@browserbasehq/stagehand');
1394
1591
  const stagehand = new Stagehand({
1395
1592
  env: 'LOCAL',
1396
1593
  verbose: 1,
1397
1594
  disablePino: true,
1398
1595
  localBrowserLaunchOptions: {
1399
- cdpUrl: getCdpUrl(),
1596
+ cdpUrl: getCdpUrl({ port: TEST_PORT }),
1400
1597
  },
1401
1598
  });
1402
1599
  console.log('Initializing Stagehand...');
1403
1600
  await stagehand.init();
1404
1601
  console.log('Stagehand initialized');
1405
1602
  const context = stagehand.context;
1406
- console.log('Stagehand context:', context);
1603
+ // console.log('Stagehand context:', context)
1407
1604
  expect(context).toBeDefined();
1408
1605
  const pages = context.pages();
1409
1606
  console.log('Stagehand pages:', pages.length, pages.map(p => p.url()));
@@ -1427,7 +1624,7 @@ describe('MCP Server Tests', () => {
1427
1624
  await serviceWorker.evaluate(async () => {
1428
1625
  await globalThis.toggleExtensionForActiveTab();
1429
1626
  });
1430
- await new Promise(r => setTimeout(r, 500));
1627
+ await new Promise(r => setTimeout(r, 100));
1431
1628
  const result = await client.callTool({
1432
1629
  name: 'execute',
1433
1630
  arguments: {
@@ -1472,6 +1669,11 @@ describe('CDP Session Tests', () => {
1472
1669
  let testCtx = null;
1473
1670
  beforeAll(async () => {
1474
1671
  testCtx = await setupTestContext({ tempDirPrefix: 'pw-cdp-test-' });
1672
+ const serviceWorker = await getExtensionServiceWorker(testCtx.browserContext);
1673
+ await serviceWorker.evaluate(async () => {
1674
+ await globalThis.disconnectEverything();
1675
+ });
1676
+ await new Promise(r => setTimeout(r, 100));
1475
1677
  }, 600000);
1476
1678
  afterAll(async () => {
1477
1679
  await cleanupTestContext(testCtx);
@@ -1482,7 +1684,7 @@ describe('CDP Session Tests', () => {
1482
1684
  throw new Error('Browser not initialized');
1483
1685
  return testCtx.browserContext;
1484
1686
  };
1485
- it('should enable debugger and pause on debugger statement via CDP session', async () => {
1687
+ it('should use Debugger class to set breakpoints and inspect variables', async () => {
1486
1688
  const browserContext = getBrowserContext();
1487
1689
  const serviceWorker = await getExtensionServiceWorker(browserContext);
1488
1690
  const page = await browserContext.newPage();
@@ -1491,112 +1693,192 @@ describe('CDP Session Tests', () => {
1491
1693
  await serviceWorker.evaluate(async () => {
1492
1694
  await globalThis.toggleExtensionForActiveTab();
1493
1695
  });
1494
- await new Promise(r => setTimeout(r, 500));
1495
- const browser = await chromium.connectOverCDP(getCdpUrl());
1696
+ await new Promise(r => setTimeout(r, 100));
1697
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1496
1698
  const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'));
1497
1699
  expect(cdpPage).toBeDefined();
1498
- const wsUrl = getCdpUrl();
1700
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
1499
1701
  const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
1500
- await cdpSession.send('Debugger.enable');
1702
+ const dbg = new Debugger({ cdp: cdpSession });
1703
+ await dbg.enable();
1704
+ expect(dbg.isPaused()).toBe(false);
1501
1705
  const pausedPromise = new Promise((resolve) => {
1502
- cdpSession.on('Debugger.paused', (params) => {
1503
- resolve(params);
1706
+ cdpSession.on('Debugger.paused', () => {
1707
+ resolve();
1504
1708
  });
1505
1709
  });
1506
1710
  cdpPage.evaluate(`
1507
1711
  (function testFunction() {
1508
1712
  const localVar = 'hello';
1509
1713
  const numberVar = 42;
1510
- const objVar = { key: 'value', nested: { a: 1 } };
1511
1714
  debugger;
1512
1715
  return localVar + numberVar;
1513
1716
  })()
1514
1717
  `);
1515
- const pausedEvent = await Promise.race([
1718
+ await Promise.race([
1516
1719
  pausedPromise,
1517
1720
  new Promise((_, reject) => setTimeout(() => reject(new Error('Debugger.paused timeout')), 5000))
1518
1721
  ]);
1519
- const stackTrace = pausedEvent.callFrames.map(frame => ({
1520
- functionName: frame.functionName || '(anonymous)',
1521
- lineNumber: frame.location.lineNumber,
1522
- columnNumber: frame.location.columnNumber,
1523
- }));
1524
- expect({
1525
- reason: pausedEvent.reason,
1526
- stackTrace: stackTrace.slice(0, 3),
1527
- }).toMatchInlineSnapshot(`
1722
+ expect(dbg.isPaused()).toBe(true);
1723
+ const location = await dbg.getLocation();
1724
+ expect(location.callstack[0].functionName).toBe('testFunction');
1725
+ expect(location.sourceContext).toContain('debugger');
1726
+ const vars = await dbg.inspectLocalVariables();
1727
+ expect(vars).toMatchInlineSnapshot(`
1528
1728
  {
1529
- "reason": "other",
1530
- "stackTrace": [
1531
- {
1532
- "columnNumber": 16,
1533
- "functionName": "testFunction",
1534
- "lineNumber": 4,
1535
- },
1536
- {
1537
- "columnNumber": 14,
1538
- "functionName": "(anonymous)",
1539
- "lineNumber": 6,
1540
- },
1541
- {
1542
- "columnNumber": 29,
1543
- "functionName": "evaluate",
1544
- "lineNumber": 289,
1545
- },
1546
- ],
1729
+ "localVar": "hello",
1730
+ "numberVar": 42,
1547
1731
  }
1548
1732
  `);
1549
- const topFrame = pausedEvent.callFrames[0];
1550
- const scopeChain = topFrame.scopeChain;
1551
- const localScope = scopeChain.find(s => s.type === 'local');
1552
- const localVars = {};
1553
- if (localScope?.object.objectId) {
1554
- const propsResult = await cdpSession.send('Runtime.getProperties', {
1555
- objectId: localScope.object.objectId,
1556
- ownProperties: true,
1557
- });
1558
- for (const prop of propsResult.result) {
1559
- if (prop.value) {
1560
- localVars[prop.name] = prop.value.type === 'object'
1561
- ? `[object ${prop.value.className || prop.value.subtype || 'Object'}]`
1562
- : prop.value.value;
1563
- }
1733
+ const evalResult = await dbg.evaluate({ expression: 'localVar + " world"' });
1734
+ expect(evalResult.value).toBe('hello world');
1735
+ await dbg.resume();
1736
+ await new Promise(r => setTimeout(r, 100));
1737
+ expect(dbg.isPaused()).toBe(false);
1738
+ cdpSession.close();
1739
+ await browser.close();
1740
+ await page.close();
1741
+ }, 60000);
1742
+ it('should list scripts with Debugger class', async () => {
1743
+ const browserContext = getBrowserContext();
1744
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1745
+ const page = await browserContext.newPage();
1746
+ await page.setContent(`
1747
+ <html>
1748
+ <head>
1749
+ <script src="data:text/javascript,function testScript() { return 42; }"></script>
1750
+ </head>
1751
+ <body><h1>Script Test</h1></body>
1752
+ </html>
1753
+ `);
1754
+ await page.bringToFront();
1755
+ await serviceWorker.evaluate(async () => {
1756
+ await globalThis.toggleExtensionForActiveTab();
1757
+ });
1758
+ await new Promise(r => setTimeout(r, 100));
1759
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1760
+ let cdpPage;
1761
+ for (const p of browser.contexts()[0].pages()) {
1762
+ const html = await p.content();
1763
+ if (html.includes('Script Test')) {
1764
+ cdpPage = p;
1765
+ break;
1564
1766
  }
1565
1767
  }
1566
- expect({
1567
- scopeTypes: scopeChain.map(s => s.type),
1568
- localVariables: localVars,
1569
- }).toMatchInlineSnapshot(`
1570
- {
1571
- "localVariables": {
1572
- "localVar": "hello",
1573
- "numberVar": 42,
1574
- "objVar": "[object Object]",
1575
- },
1576
- "scopeTypes": [
1577
- "local",
1578
- "global",
1579
- ],
1580
- }
1768
+ expect(cdpPage).toBeDefined();
1769
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
1770
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
1771
+ const dbg = new Debugger({ cdp: cdpSession });
1772
+ const scripts = await dbg.listScripts();
1773
+ expect(scripts.length).toBeGreaterThan(0);
1774
+ expect(scripts[0]).toHaveProperty('scriptId');
1775
+ expect(scripts[0]).toHaveProperty('url');
1776
+ const dataScripts = await dbg.listScripts({ search: 'data:' });
1777
+ expect(dataScripts.length).toBeGreaterThan(0);
1778
+ cdpSession.close();
1779
+ await browser.close();
1780
+ await page.close();
1781
+ }, 60000);
1782
+ it('should manage breakpoints with Debugger class', async () => {
1783
+ const browserContext = getBrowserContext();
1784
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1785
+ const page = await browserContext.newPage();
1786
+ await page.setContent(`
1787
+ <html>
1788
+ <head>
1789
+ <script src="data:text/javascript,function testFunc() { return 42; }"></script>
1790
+ </head>
1791
+ <body></body>
1792
+ </html>
1581
1793
  `);
1582
- const evalResult = await cdpSession.send('Debugger.evaluateOnCallFrame', {
1583
- callFrameId: topFrame.callFrameId,
1584
- expression: 'localVar + " world " + numberVar',
1585
- });
1586
- expect({
1587
- evaluatedExpression: 'localVar + " world " + numberVar',
1588
- result: evalResult.result.value,
1589
- type: evalResult.result.type,
1590
- }).toMatchInlineSnapshot(`
1591
- {
1592
- "evaluatedExpression": "localVar + " world " + numberVar",
1593
- "result": "hello world 42",
1594
- "type": "string",
1595
- }
1794
+ await page.bringToFront();
1795
+ await serviceWorker.evaluate(async () => {
1796
+ await globalThis.toggleExtensionForActiveTab();
1797
+ });
1798
+ await new Promise(r => setTimeout(r, 100));
1799
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1800
+ let cdpPage;
1801
+ for (const p of browser.contexts()[0].pages()) {
1802
+ const html = await p.content();
1803
+ if (html.includes('testFunc')) {
1804
+ cdpPage = p;
1805
+ break;
1806
+ }
1807
+ }
1808
+ expect(cdpPage).toBeDefined();
1809
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
1810
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
1811
+ const dbg = new Debugger({ cdp: cdpSession });
1812
+ await dbg.enable();
1813
+ expect(dbg.listBreakpoints()).toHaveLength(0);
1814
+ const bpId = await dbg.setBreakpoint({ file: 'https://example.com/test.js', line: 1 });
1815
+ expect(typeof bpId).toBe('string');
1816
+ expect(dbg.listBreakpoints()).toHaveLength(1);
1817
+ expect(dbg.listBreakpoints()[0]).toMatchObject({
1818
+ id: bpId,
1819
+ file: 'https://example.com/test.js',
1820
+ line: 1,
1821
+ });
1822
+ await dbg.deleteBreakpoint({ breakpointId: bpId });
1823
+ expect(dbg.listBreakpoints()).toHaveLength(0);
1824
+ cdpSession.close();
1825
+ await browser.close();
1826
+ await page.close();
1827
+ }, 60000);
1828
+ it('should step through code with Debugger class', async () => {
1829
+ const browserContext = getBrowserContext();
1830
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1831
+ const page = await browserContext.newPage();
1832
+ await page.goto('https://example.com/');
1833
+ await page.bringToFront();
1834
+ await serviceWorker.evaluate(async () => {
1835
+ await globalThis.toggleExtensionForActiveTab();
1836
+ });
1837
+ await new Promise(r => setTimeout(r, 100));
1838
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1839
+ const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'));
1840
+ expect(cdpPage).toBeDefined();
1841
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
1842
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
1843
+ const dbg = new Debugger({ cdp: cdpSession });
1844
+ await dbg.enable();
1845
+ const pausedPromise = new Promise((resolve) => {
1846
+ cdpSession.on('Debugger.paused', () => resolve());
1847
+ });
1848
+ cdpPage.evaluate(`
1849
+ (function outer() {
1850
+ function inner() {
1851
+ const x = 1;
1852
+ debugger;
1853
+ const y = 2;
1854
+ return x + y;
1855
+ }
1856
+ const result = inner();
1857
+ return result;
1858
+ })()
1596
1859
  `);
1597
- await cdpSession.send('Debugger.resume');
1598
- await cdpSession.send('Debugger.disable');
1599
- cdpSession.detach();
1860
+ await pausedPromise;
1861
+ expect(dbg.isPaused()).toBe(true);
1862
+ const location1 = await dbg.getLocation();
1863
+ expect(location1.callstack.length).toBeGreaterThanOrEqual(2);
1864
+ expect(location1.callstack[0].functionName).toBe('inner');
1865
+ expect(location1.callstack[1].functionName).toBe('outer');
1866
+ const stepOverPromise = new Promise((resolve) => {
1867
+ cdpSession.on('Debugger.paused', () => resolve());
1868
+ });
1869
+ await dbg.stepOver();
1870
+ await stepOverPromise;
1871
+ const location2 = await dbg.getLocation();
1872
+ expect(location2.lineNumber).toBeGreaterThan(location1.lineNumber);
1873
+ const stepOutPromise = new Promise((resolve) => {
1874
+ cdpSession.on('Debugger.paused', () => resolve());
1875
+ });
1876
+ await dbg.stepOut();
1877
+ await stepOutPromise;
1878
+ const location3 = await dbg.getLocation();
1879
+ expect(location3.callstack[0].functionName).toBe('outer');
1880
+ await dbg.resume();
1881
+ cdpSession.close();
1600
1882
  await browser.close();
1601
1883
  await page.close();
1602
1884
  }, 60000);
@@ -1609,11 +1891,11 @@ describe('CDP Session Tests', () => {
1609
1891
  await serviceWorker.evaluate(async () => {
1610
1892
  await globalThis.toggleExtensionForActiveTab();
1611
1893
  });
1612
- await new Promise(r => setTimeout(r, 500));
1613
- const browser = await chromium.connectOverCDP(getCdpUrl());
1894
+ await new Promise(r => setTimeout(r, 100));
1895
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1614
1896
  const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'));
1615
1897
  expect(cdpPage).toBeDefined();
1616
- const wsUrl = getCdpUrl();
1898
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
1617
1899
  const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
1618
1900
  await cdpSession.send('Profiler.enable');
1619
1901
  await cdpSession.send('Profiler.start');
@@ -1637,27 +1919,268 @@ describe('CDP Session Tests', () => {
1637
1919
  .map(n => n.callFrame.functionName)
1638
1920
  .filter(name => name && name.length > 0)
1639
1921
  .slice(0, 10);
1640
- expect({
1641
- hasNodes: profile.nodes.length > 0,
1642
- nodeCount: profile.nodes.length,
1643
- durationMicroseconds: profile.endTime - profile.startTime,
1644
- sampleFunctionNames: functionNames,
1645
- }).toMatchInlineSnapshot(`
1922
+ expect(profile.nodes.length).toBeGreaterThan(0);
1923
+ expect(profile.endTime - profile.startTime).toBeGreaterThan(0);
1924
+ expect(functionNames.every((name) => typeof name === 'string')).toBe(true);
1925
+ await cdpSession.send('Profiler.disable');
1926
+ cdpSession.close();
1927
+ await browser.close();
1928
+ await page.close();
1929
+ }, 60000);
1930
+ it('should update Target.getTargets URL after page navigation', async () => {
1931
+ const browserContext = getBrowserContext();
1932
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1933
+ const page = await browserContext.newPage();
1934
+ await page.goto('https://example.com/');
1935
+ await page.bringToFront();
1936
+ await serviceWorker.evaluate(async () => {
1937
+ await globalThis.toggleExtensionForActiveTab();
1938
+ });
1939
+ await new Promise(r => setTimeout(r, 100));
1940
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1941
+ const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'));
1942
+ expect(cdpPage).toBeDefined();
1943
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
1944
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
1945
+ const initialTargets = await cdpSession.send('Target.getTargets');
1946
+ const initialPageTarget = initialTargets.targetInfos.find(t => t.type === 'page' && t.url.includes('example.com'));
1947
+ expect(initialPageTarget?.url).toBe('https://example.com/');
1948
+ await cdpPage.goto('https://example.org/', { waitUntil: 'domcontentloaded' });
1949
+ await new Promise(r => setTimeout(r, 100));
1950
+ const afterNavTargets = await cdpSession.send('Target.getTargets');
1951
+ const allPageTargets = afterNavTargets.targetInfos.filter(t => t.type === 'page');
1952
+ const aboutBlankTargets = allPageTargets.filter(t => t.url === 'about:blank');
1953
+ expect(aboutBlankTargets).toHaveLength(0);
1954
+ const exampleComTargets = allPageTargets.filter(t => t.url.includes('example.com'));
1955
+ expect(exampleComTargets).toHaveLength(0);
1956
+ const exampleOrgTargets = allPageTargets.filter(t => t.url.includes('example.org'));
1957
+ expect(exampleOrgTargets).toHaveLength(1);
1958
+ cdpSession.close();
1959
+ await browser.close();
1960
+ await page.close();
1961
+ }, 60000);
1962
+ it('should return correct targets for multiple pages via Target.getTargets', async () => {
1963
+ const browserContext = getBrowserContext();
1964
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1965
+ const page1 = await browserContext.newPage();
1966
+ await page1.goto('https://example.com/');
1967
+ await page1.bringToFront();
1968
+ await serviceWorker.evaluate(async () => {
1969
+ await globalThis.toggleExtensionForActiveTab();
1970
+ });
1971
+ const page2 = await browserContext.newPage();
1972
+ await page2.goto('https://example.org/');
1973
+ await page2.bringToFront();
1974
+ await serviceWorker.evaluate(async () => {
1975
+ await globalThis.toggleExtensionForActiveTab();
1976
+ });
1977
+ await new Promise(r => setTimeout(r, 100));
1978
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1979
+ const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'));
1980
+ expect(cdpPage).toBeDefined();
1981
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
1982
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
1983
+ const { targetInfos } = await cdpSession.send('Target.getTargets');
1984
+ const allPageTargets = targetInfos.filter(t => t.type === 'page');
1985
+ const aboutBlankTargets = allPageTargets.filter(t => t.url === 'about:blank');
1986
+ expect(aboutBlankTargets).toHaveLength(0);
1987
+ const pageTargets = allPageTargets
1988
+ .map(t => ({ type: t.type, url: t.url }))
1989
+ .sort((a, b) => a.url.localeCompare(b.url));
1990
+ expect(pageTargets).toMatchInlineSnapshot(`
1991
+ [
1992
+ {
1993
+ "type": "page",
1994
+ "url": "https://example.com/",
1995
+ },
1996
+ {
1997
+ "type": "page",
1998
+ "url": "https://example.org/",
1999
+ },
2000
+ ]
2001
+ `);
2002
+ cdpSession.close();
2003
+ await browser.close();
2004
+ await page1.close();
2005
+ await page2.close();
2006
+ }, 60000);
2007
+ it('should create CDP session for page after navigation', async () => {
2008
+ const browserContext = getBrowserContext();
2009
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2010
+ const page = await browserContext.newPage();
2011
+ await page.goto('https://example.com/');
2012
+ await page.bringToFront();
2013
+ await serviceWorker.evaluate(async () => {
2014
+ await globalThis.toggleExtensionForActiveTab();
2015
+ });
2016
+ await new Promise(r => setTimeout(r, 100));
2017
+ await page.goto('https://example.org/', { waitUntil: 'domcontentloaded' });
2018
+ await new Promise(r => setTimeout(r, 100));
2019
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2020
+ const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.org'));
2021
+ expect(cdpPage).toBeDefined();
2022
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
2023
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
2024
+ const evalResult = await cdpSession.send('Runtime.evaluate', {
2025
+ expression: 'document.title',
2026
+ returnByValue: true,
2027
+ });
2028
+ expect(evalResult.result.value).toContain('Example Domain');
2029
+ cdpSession.close();
2030
+ await browser.close();
2031
+ await page.close();
2032
+ }, 60000);
2033
+ it('should maintain CDP session functionality after page URL change', async () => {
2034
+ const browserContext = getBrowserContext();
2035
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2036
+ const page = await browserContext.newPage();
2037
+ const initialUrl = 'https://example.com/';
2038
+ await page.goto(initialUrl);
2039
+ await page.bringToFront();
2040
+ await serviceWorker.evaluate(async () => {
2041
+ await globalThis.toggleExtensionForActiveTab();
2042
+ });
2043
+ await new Promise(r => setTimeout(r, 100));
2044
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2045
+ const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'));
2046
+ expect(cdpPage).toBeDefined();
2047
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
2048
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
2049
+ const initialEvalResult = await cdpSession.send('Runtime.evaluate', {
2050
+ expression: 'document.title',
2051
+ returnByValue: true,
2052
+ });
2053
+ expect(initialEvalResult.result.value).toBe('Example Domain');
2054
+ const newUrl = 'https://example.org/';
2055
+ await cdpPage.goto(newUrl, { waitUntil: 'domcontentloaded' });
2056
+ expect(cdpPage.url()).toBe(newUrl);
2057
+ const layoutMetrics = await cdpSession.send('Page.getLayoutMetrics');
2058
+ expect(layoutMetrics.cssVisualViewport).toBeDefined();
2059
+ expect(layoutMetrics.cssVisualViewport.clientWidth).toBeGreaterThan(0);
2060
+ const afterNavEvalResult = await cdpSession.send('Runtime.evaluate', {
2061
+ expression: 'document.title',
2062
+ returnByValue: true,
2063
+ });
2064
+ expect(afterNavEvalResult.result.value).toContain('Example Domain');
2065
+ const locationResult = await cdpSession.send('Runtime.evaluate', {
2066
+ expression: 'window.location.href',
2067
+ returnByValue: true,
2068
+ });
2069
+ expect(locationResult.result.value).toBe(newUrl);
2070
+ cdpSession.close();
2071
+ await browser.close();
2072
+ await page.close();
2073
+ }, 60000);
2074
+ it('should pause on all exceptions with setPauseOnExceptions', async () => {
2075
+ const browserContext = getBrowserContext();
2076
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2077
+ const page = await browserContext.newPage();
2078
+ await page.goto('https://example.com/');
2079
+ await page.bringToFront();
2080
+ await serviceWorker.evaluate(async () => {
2081
+ await globalThis.toggleExtensionForActiveTab();
2082
+ });
2083
+ await new Promise(r => setTimeout(r, 100));
2084
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2085
+ const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'));
2086
+ expect(cdpPage).toBeDefined();
2087
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
2088
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
2089
+ const dbg = new Debugger({ cdp: cdpSession });
2090
+ await dbg.enable();
2091
+ await dbg.setPauseOnExceptions({ state: 'all' });
2092
+ const pausedPromise = new Promise((resolve) => {
2093
+ cdpSession.on('Debugger.paused', () => resolve());
2094
+ });
2095
+ cdpPage.evaluate(`
2096
+ (function() {
2097
+ try {
2098
+ throw new Error('Caught test error');
2099
+ } catch (e) {
2100
+ // caught but should still pause with state 'all'
2101
+ }
2102
+ })()
2103
+ `).catch(() => { });
2104
+ await Promise.race([
2105
+ pausedPromise,
2106
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Debugger.paused timeout')), 5000))
2107
+ ]);
2108
+ expect(dbg.isPaused()).toBe(true);
2109
+ const location = await dbg.getLocation();
2110
+ expect(location.sourceContext).toContain('throw');
2111
+ await dbg.resume();
2112
+ await dbg.setPauseOnExceptions({ state: 'none' });
2113
+ cdpSession.close();
2114
+ await browser.close();
2115
+ await page.close();
2116
+ }, 60000);
2117
+ it('should inspect local and global variables with inline snapshots', async () => {
2118
+ const browserContext = getBrowserContext();
2119
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2120
+ const page = await browserContext.newPage();
2121
+ await page.setContent(`
2122
+ <html>
2123
+ <head>
2124
+ <script>
2125
+ const GLOBAL_CONFIG = 'production';
2126
+ function runTest() {
2127
+ const userName = 'Alice';
2128
+ const userAge = 25;
2129
+ const settings = { theme: 'dark', lang: 'en' };
2130
+ const scores = [10, 20, 30];
2131
+ debugger;
2132
+ return userName;
2133
+ }
2134
+ </script>
2135
+ </head>
2136
+ <body>
2137
+ <button onclick="runTest()">Run</button>
2138
+ </body>
2139
+ </html>
2140
+ `);
2141
+ await page.bringToFront();
2142
+ await serviceWorker.evaluate(async () => {
2143
+ await globalThis.toggleExtensionForActiveTab();
2144
+ });
2145
+ await new Promise(r => setTimeout(r, 100));
2146
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2147
+ let cdpPage;
2148
+ for (const p of browser.contexts()[0].pages()) {
2149
+ const html = await p.content();
2150
+ if (html.includes('runTest')) {
2151
+ cdpPage = p;
2152
+ break;
2153
+ }
2154
+ }
2155
+ expect(cdpPage).toBeDefined();
2156
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
2157
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
2158
+ const dbg = new Debugger({ cdp: cdpSession });
2159
+ await dbg.enable();
2160
+ const globalVars = await dbg.inspectGlobalVariables();
2161
+ expect(globalVars).toMatchInlineSnapshot(`
2162
+ [
2163
+ "GLOBAL_CONFIG",
2164
+ ]
2165
+ `);
2166
+ const pausedPromise = new Promise((resolve) => {
2167
+ cdpSession.on('Debugger.paused', () => resolve());
2168
+ });
2169
+ cdpPage.evaluate('runTest()');
2170
+ await pausedPromise;
2171
+ expect(dbg.isPaused()).toBe(true);
2172
+ const localVars = await dbg.inspectLocalVariables();
2173
+ expect(localVars).toMatchInlineSnapshot(`
1646
2174
  {
1647
- "durationMicroseconds": 6962,
1648
- "hasNodes": true,
1649
- "nodeCount": 8,
1650
- "sampleFunctionNames": [
1651
- "(root)",
1652
- "(program)",
1653
- "(idle)",
1654
- "evaluate",
1655
- "querySelectorAll",
1656
- ],
2175
+ "GLOBAL_CONFIG": "production",
2176
+ "scores": "[array]",
2177
+ "settings": "[object]",
2178
+ "userAge": 25,
2179
+ "userName": "Alice",
1657
2180
  }
1658
2181
  `);
1659
- await cdpSession.send('Profiler.disable');
1660
- cdpSession.detach();
2182
+ await dbg.resume();
2183
+ cdpSession.close();
1661
2184
  await browser.close();
1662
2185
  await page.close();
1663
2186
  }, 60000);
@@ -1670,8 +2193,8 @@ describe('CDP Session Tests', () => {
1670
2193
  await serviceWorker.evaluate(async () => {
1671
2194
  await globalThis.toggleExtensionForActiveTab();
1672
2195
  });
1673
- await new Promise(r => setTimeout(r, 500));
1674
- const browser = await chromium.connectOverCDP(getCdpUrl());
2196
+ await new Promise(r => setTimeout(r, 100));
2197
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1675
2198
  const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'));
1676
2199
  expect(cdpPage).toBeDefined();
1677
2200
  const h1Bounds = await cdpPage.locator('h1').boundingBox();
@@ -1692,5 +2215,186 @@ describe('CDP Session Tests', () => {
1692
2215
  await browser.close();
1693
2216
  await page.close();
1694
2217
  }, 60000);
2218
+ it('should use Editor class to list, read, and edit scripts', async () => {
2219
+ const browserContext = getBrowserContext();
2220
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2221
+ const page = await browserContext.newPage();
2222
+ await page.goto('https://example.com/');
2223
+ await page.bringToFront();
2224
+ await serviceWorker.evaluate(async () => {
2225
+ await globalThis.toggleExtensionForActiveTab();
2226
+ });
2227
+ await new Promise(r => setTimeout(r, 100));
2228
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2229
+ const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'));
2230
+ expect(cdpPage).toBeDefined();
2231
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
2232
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
2233
+ const editor = new Editor({ cdp: cdpSession });
2234
+ await editor.enable();
2235
+ await cdpPage.addScriptTag({
2236
+ content: `
2237
+ function greetUser(name) {
2238
+ console.log('Hello, ' + name);
2239
+ return 'Hello, ' + name;
2240
+ }
2241
+ `,
2242
+ });
2243
+ await new Promise(r => setTimeout(r, 100));
2244
+ const scripts = await editor.list();
2245
+ expect(scripts.length).toBeGreaterThan(0);
2246
+ const matches = await editor.grep({ regex: /greetUser/ });
2247
+ expect(matches.length).toBeGreaterThan(0);
2248
+ const match = matches[0];
2249
+ const { content, totalLines } = await editor.read({ url: match.url });
2250
+ expect(content).toContain('greetUser');
2251
+ expect(totalLines).toBeGreaterThan(0);
2252
+ await editor.edit({
2253
+ url: match.url,
2254
+ oldString: "console.log('Hello, ' + name);",
2255
+ newString: "console.log('Hello, ' + name); console.log('EDITOR_TEST_MARKER');",
2256
+ });
2257
+ const consoleLogs = [];
2258
+ cdpPage.on('console', msg => {
2259
+ consoleLogs.push(msg.text());
2260
+ });
2261
+ await cdpPage.evaluate(() => {
2262
+ window.greetUser('World');
2263
+ });
2264
+ await new Promise(r => setTimeout(r, 100));
2265
+ expect(consoleLogs).toContain('Hello, World');
2266
+ expect(consoleLogs).toContain('EDITOR_TEST_MARKER');
2267
+ cdpSession.close();
2268
+ await browser.close();
2269
+ await page.close();
2270
+ }, 60000);
2271
+ it('editor can list, read, and edit CSS stylesheets', async () => {
2272
+ const browserContext = getBrowserContext();
2273
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2274
+ const page = await browserContext.newPage();
2275
+ await page.goto('https://example.com/');
2276
+ await page.bringToFront();
2277
+ await serviceWorker.evaluate(async () => {
2278
+ await globalThis.toggleExtensionForActiveTab();
2279
+ });
2280
+ await new Promise(r => setTimeout(r, 100));
2281
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2282
+ const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'));
2283
+ expect(cdpPage).toBeDefined();
2284
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
2285
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
2286
+ const editor = new Editor({ cdp: cdpSession });
2287
+ await editor.enable();
2288
+ await cdpPage.addStyleTag({
2289
+ content: `
2290
+ .editor-test-element {
2291
+ color: rgb(255, 0, 0);
2292
+ background-color: rgb(0, 0, 255);
2293
+ }
2294
+ `,
2295
+ });
2296
+ await new Promise(r => setTimeout(r, 100));
2297
+ const stylesheets = await editor.list({ pattern: /inline-css:/ });
2298
+ expect(stylesheets.length).toBeGreaterThan(0);
2299
+ const cssMatches = await editor.grep({ regex: /editor-test-element/, pattern: /inline-css:/ });
2300
+ expect(cssMatches.length).toBeGreaterThan(0);
2301
+ const cssMatch = cssMatches[0];
2302
+ const { content, totalLines } = await editor.read({ url: cssMatch.url });
2303
+ expect(content).toContain('editor-test-element');
2304
+ expect(content).toContain('rgb(255, 0, 0)');
2305
+ expect(totalLines).toBeGreaterThan(0);
2306
+ await cdpPage.evaluate(() => {
2307
+ const el = document.createElement('div');
2308
+ el.className = 'editor-test-element';
2309
+ el.id = 'test-div';
2310
+ el.textContent = 'Test';
2311
+ document.body.appendChild(el);
2312
+ });
2313
+ const colorBefore = await cdpPage.evaluate(() => {
2314
+ const el = document.getElementById('test-div');
2315
+ return window.getComputedStyle(el).color;
2316
+ });
2317
+ expect(colorBefore).toBe('rgb(255, 0, 0)');
2318
+ await editor.edit({
2319
+ url: cssMatch.url,
2320
+ oldString: 'color: rgb(255, 0, 0);',
2321
+ newString: 'color: rgb(0, 255, 0);',
2322
+ });
2323
+ const colorAfter = await cdpPage.evaluate(() => {
2324
+ const el = document.getElementById('test-div');
2325
+ return window.getComputedStyle(el).color;
2326
+ });
2327
+ expect(colorAfter).toBe('rgb(0, 255, 0)');
2328
+ cdpSession.close();
2329
+ await browser.close();
2330
+ await page.close();
2331
+ }, 60000);
2332
+ it('should inject bippy and find React fiber with getReactSource', async () => {
2333
+ const browserContext = getBrowserContext();
2334
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2335
+ const page = await browserContext.newPage();
2336
+ await page.setContent(`
2337
+ <!DOCTYPE html>
2338
+ <html>
2339
+ <head>
2340
+ <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
2341
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
2342
+ </head>
2343
+ <body>
2344
+ <div id="root"></div>
2345
+ <script>
2346
+ function MyComponent() {
2347
+ return React.createElement('button', { id: 'react-btn' }, 'Click me');
2348
+ }
2349
+ const root = ReactDOM.createRoot(document.getElementById('root'));
2350
+ root.render(React.createElement(MyComponent));
2351
+ </script>
2352
+ </body>
2353
+ </html>
2354
+ `);
2355
+ await page.bringToFront();
2356
+ await serviceWorker.evaluate(async () => {
2357
+ await globalThis.toggleExtensionForActiveTab();
2358
+ });
2359
+ await new Promise(r => setTimeout(r, 500));
2360
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2361
+ const pages = browser.contexts()[0].pages();
2362
+ const cdpPage = pages.find(p => p.url().startsWith('about:'));
2363
+ expect(cdpPage).toBeDefined();
2364
+ const btn = cdpPage.locator('#react-btn');
2365
+ const btnCount = await btn.count();
2366
+ expect(btnCount).toBe(1);
2367
+ const hasBippyBefore = await cdpPage.evaluate(() => !!globalThis.__bippy);
2368
+ expect(hasBippyBefore).toBe(false);
2369
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
2370
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
2371
+ const { getReactSource } = await import('./react-source.js');
2372
+ const source = await getReactSource({ locator: btn, cdp: cdpSession });
2373
+ const hasBippyAfter = await cdpPage.evaluate(() => !!globalThis.__bippy);
2374
+ expect(hasBippyAfter).toBe(true);
2375
+ const hasFiber = await btn.evaluate((el) => {
2376
+ const bippy = globalThis.__bippy;
2377
+ const fiber = bippy.getFiberFromHostInstance(el);
2378
+ return !!fiber;
2379
+ });
2380
+ expect(hasFiber).toBe(true);
2381
+ const componentName = await btn.evaluate((el) => {
2382
+ const bippy = globalThis.__bippy;
2383
+ const fiber = bippy.getFiberFromHostInstance(el);
2384
+ let current = fiber;
2385
+ while (current) {
2386
+ if (bippy.isCompositeFiber(current)) {
2387
+ return bippy.getDisplayName(current.type);
2388
+ }
2389
+ current = current.return;
2390
+ }
2391
+ return null;
2392
+ });
2393
+ expect(componentName).toBe('MyComponent');
2394
+ console.log('Component name from fiber:', componentName);
2395
+ console.log('Source location (null for UMD React, works on local dev servers with JSX transform):', source);
2396
+ await browser.close();
2397
+ await page.close();
2398
+ }, 60000);
1695
2399
  });
1696
2400
  //# sourceMappingURL=mcp.test.js.map