e2e-pilot 0.0.69

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 (152) hide show
  1. package/bin.js +3 -0
  2. package/dist/aria-snapshot.d.ts +95 -0
  3. package/dist/aria-snapshot.d.ts.map +1 -0
  4. package/dist/aria-snapshot.js +490 -0
  5. package/dist/aria-snapshot.js.map +1 -0
  6. package/dist/bippy.js +971 -0
  7. package/dist/cdp-relay.d.ts +16 -0
  8. package/dist/cdp-relay.d.ts.map +1 -0
  9. package/dist/cdp-relay.js +715 -0
  10. package/dist/cdp-relay.js.map +1 -0
  11. package/dist/cdp-session.d.ts +42 -0
  12. package/dist/cdp-session.d.ts.map +1 -0
  13. package/dist/cdp-session.js +154 -0
  14. package/dist/cdp-session.js.map +1 -0
  15. package/dist/cdp-types.d.ts +63 -0
  16. package/dist/cdp-types.d.ts.map +1 -0
  17. package/dist/cdp-types.js +91 -0
  18. package/dist/cdp-types.js.map +1 -0
  19. package/dist/cli.d.ts +3 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +213 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/create-logger.d.ts +9 -0
  24. package/dist/create-logger.d.ts.map +1 -0
  25. package/dist/create-logger.js +25 -0
  26. package/dist/create-logger.js.map +1 -0
  27. package/dist/debugger-api.md +458 -0
  28. package/dist/debugger-examples-types.d.ts +24 -0
  29. package/dist/debugger-examples-types.d.ts.map +1 -0
  30. package/dist/debugger-examples-types.js +2 -0
  31. package/dist/debugger-examples-types.js.map +1 -0
  32. package/dist/debugger-examples.d.ts +6 -0
  33. package/dist/debugger-examples.d.ts.map +1 -0
  34. package/dist/debugger-examples.js +53 -0
  35. package/dist/debugger-examples.js.map +1 -0
  36. package/dist/debugger.d.ts +381 -0
  37. package/dist/debugger.d.ts.map +1 -0
  38. package/dist/debugger.js +633 -0
  39. package/dist/debugger.js.map +1 -0
  40. package/dist/editor-api.md +364 -0
  41. package/dist/editor-examples.d.ts +11 -0
  42. package/dist/editor-examples.d.ts.map +1 -0
  43. package/dist/editor-examples.js +124 -0
  44. package/dist/editor-examples.js.map +1 -0
  45. package/dist/editor.d.ts +203 -0
  46. package/dist/editor.d.ts.map +1 -0
  47. package/dist/editor.js +336 -0
  48. package/dist/editor.js.map +1 -0
  49. package/dist/execute.d.ts +50 -0
  50. package/dist/execute.d.ts.map +1 -0
  51. package/dist/execute.js +576 -0
  52. package/dist/execute.js.map +1 -0
  53. package/dist/index.d.ts +11 -0
  54. package/dist/index.d.ts.map +1 -0
  55. package/dist/index.js +7 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/mcp-client.d.ts +20 -0
  58. package/dist/mcp-client.d.ts.map +1 -0
  59. package/dist/mcp-client.js +56 -0
  60. package/dist/mcp-client.js.map +1 -0
  61. package/dist/mcp.d.ts +5 -0
  62. package/dist/mcp.d.ts.map +1 -0
  63. package/dist/mcp.js +720 -0
  64. package/dist/mcp.js.map +1 -0
  65. package/dist/mcp.test.d.ts +10 -0
  66. package/dist/mcp.test.d.ts.map +1 -0
  67. package/dist/mcp.test.js +2999 -0
  68. package/dist/mcp.test.js.map +1 -0
  69. package/dist/network-capture.d.ts +23 -0
  70. package/dist/network-capture.d.ts.map +1 -0
  71. package/dist/network-capture.js +98 -0
  72. package/dist/network-capture.js.map +1 -0
  73. package/dist/protocol.d.ts +54 -0
  74. package/dist/protocol.d.ts.map +1 -0
  75. package/dist/protocol.js +2 -0
  76. package/dist/protocol.js.map +1 -0
  77. package/dist/react-source.d.ts +13 -0
  78. package/dist/react-source.d.ts.map +1 -0
  79. package/dist/react-source.js +68 -0
  80. package/dist/react-source.js.map +1 -0
  81. package/dist/scoped-fs.d.ts +94 -0
  82. package/dist/scoped-fs.d.ts.map +1 -0
  83. package/dist/scoped-fs.js +356 -0
  84. package/dist/scoped-fs.js.map +1 -0
  85. package/dist/selector-generator.js +8126 -0
  86. package/dist/start-relay-server.d.ts +6 -0
  87. package/dist/start-relay-server.d.ts.map +1 -0
  88. package/dist/start-relay-server.js +33 -0
  89. package/dist/start-relay-server.js.map +1 -0
  90. package/dist/styles-api.md +117 -0
  91. package/dist/styles-examples.d.ts +8 -0
  92. package/dist/styles-examples.d.ts.map +1 -0
  93. package/dist/styles-examples.js +64 -0
  94. package/dist/styles-examples.js.map +1 -0
  95. package/dist/styles.d.ts +27 -0
  96. package/dist/styles.d.ts.map +1 -0
  97. package/dist/styles.js +234 -0
  98. package/dist/styles.js.map +1 -0
  99. package/dist/trace-utils.d.ts +14 -0
  100. package/dist/trace-utils.d.ts.map +1 -0
  101. package/dist/trace-utils.js +21 -0
  102. package/dist/trace-utils.js.map +1 -0
  103. package/dist/utils.d.ts +20 -0
  104. package/dist/utils.d.ts.map +1 -0
  105. package/dist/utils.js +75 -0
  106. package/dist/utils.js.map +1 -0
  107. package/dist/wait-for-page-load.d.ts +16 -0
  108. package/dist/wait-for-page-load.d.ts.map +1 -0
  109. package/dist/wait-for-page-load.js +127 -0
  110. package/dist/wait-for-page-load.js.map +1 -0
  111. package/package.json +67 -0
  112. package/src/aria-snapshot.ts +610 -0
  113. package/src/assets/aria-labels-github-snapshot.txt +605 -0
  114. package/src/assets/aria-labels-github.png +0 -0
  115. package/src/assets/aria-labels-google-snapshot.txt +49 -0
  116. package/src/assets/aria-labels-google.png +0 -0
  117. package/src/assets/aria-labels-hacker-news-snapshot.txt +1023 -0
  118. package/src/assets/aria-labels-hacker-news.png +0 -0
  119. package/src/cdp-relay.ts +925 -0
  120. package/src/cdp-session.ts +203 -0
  121. package/src/cdp-timing.md +128 -0
  122. package/src/cdp-types.ts +155 -0
  123. package/src/cli.ts +250 -0
  124. package/src/create-logger.ts +36 -0
  125. package/src/debugger-examples-types.ts +13 -0
  126. package/src/debugger-examples.ts +66 -0
  127. package/src/debugger.md +453 -0
  128. package/src/debugger.ts +713 -0
  129. package/src/editor-examples.ts +148 -0
  130. package/src/editor.ts +390 -0
  131. package/src/execute.ts +763 -0
  132. package/src/index.ts +10 -0
  133. package/src/mcp-client.ts +78 -0
  134. package/src/mcp.test.ts +3596 -0
  135. package/src/mcp.ts +876 -0
  136. package/src/network-capture.ts +140 -0
  137. package/src/prompt.bak.md +323 -0
  138. package/src/prompt.md +7 -0
  139. package/src/protocol.ts +63 -0
  140. package/src/react-source.ts +94 -0
  141. package/src/resource.md +436 -0
  142. package/src/scoped-fs.ts +411 -0
  143. package/src/snapshots/hacker-news-focused-accessibility.md +202 -0
  144. package/src/snapshots/hacker-news-initial-accessibility.md +11 -0
  145. package/src/snapshots/hacker-news-tabbed-accessibility.md +202 -0
  146. package/src/snapshots/shadcn-ui-accessibility.md +11 -0
  147. package/src/start-relay-server.ts +43 -0
  148. package/src/styles-examples.ts +77 -0
  149. package/src/styles.ts +345 -0
  150. package/src/trace-utils.ts +43 -0
  151. package/src/utils.ts +91 -0
  152. package/src/wait-for-page-load.ts +174 -0
@@ -0,0 +1,2999 @@
1
+ import { createMCPClient } from './mcp-client.js';
2
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
3
+ import { exec } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ import { chromium } from 'playwright-core';
6
+ import path from 'node:path';
7
+ import fs from 'node:fs';
8
+ import os from 'node:os';
9
+ import { getCdpUrl } from './utils.js';
10
+ import { imageSize } from 'image-size';
11
+ import { getCDPSessionForPage } from './cdp-session.js';
12
+ import { Debugger } from './debugger.js';
13
+ import { Editor } from './editor.js';
14
+ import { startE2EPilotCDPRelayServer } from './cdp-relay.js';
15
+ import { createFileLogger } from './create-logger.js';
16
+ import { killPortProcess } from 'kill-port-process';
17
+ const TEST_PORT = 19987;
18
+ const TEST_EXTENSION_OUT_DIR = 'dist-test';
19
+ const execAsync = promisify(exec);
20
+ async function getExtensionServiceWorker(context) {
21
+ let serviceWorkers = context.serviceWorkers().filter((sw) => sw.url().startsWith('chrome-extension://'));
22
+ let serviceWorker = serviceWorkers[0];
23
+ if (!serviceWorker) {
24
+ serviceWorker = await context.waitForEvent('serviceworker', {
25
+ predicate: (sw) => sw.url().startsWith('chrome-extension://'),
26
+ });
27
+ }
28
+ for (let i = 0; i < 50; i++) {
29
+ const isReady = await serviceWorker.evaluate(() => {
30
+ // @ts-ignore
31
+ return typeof globalThis.toggleExtensionForActiveTab === 'function';
32
+ });
33
+ if (isReady)
34
+ break;
35
+ await new Promise((r) => setTimeout(r, 100));
36
+ }
37
+ return serviceWorker;
38
+ }
39
+ function js(strings, ...values) {
40
+ return strings.reduce((result, str, i) => result + str + (values[i] || ''), '');
41
+ }
42
+ async function killProcessOnPort(port) {
43
+ try {
44
+ await killPortProcess(port);
45
+ console.log(`Killed processes on port ${port}`);
46
+ }
47
+ catch (err) {
48
+ console.error('Error killing process on port:', err);
49
+ }
50
+ }
51
+ async function setupTestContext({ tempDirPrefix }) {
52
+ await killProcessOnPort(TEST_PORT);
53
+ console.log('Building extension...');
54
+ await execAsync(`TESTING=1 E2E_PILOT_PORT=${TEST_PORT} E2E_PILOT_EXTENSION_OUT_DIR=${TEST_EXTENSION_OUT_DIR} pnpm build`, { cwd: '../extension' });
55
+ console.log('Extension built');
56
+ const localLogPath = path.join(process.cwd(), 'relay-server.log');
57
+ const logger = createFileLogger({ logFilePath: localLogPath });
58
+ const relayServer = await startE2EPilotCDPRelayServer({ port: TEST_PORT, logger });
59
+ const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), tempDirPrefix));
60
+ const extensionPath = path.resolve('../extension', TEST_EXTENSION_OUT_DIR);
61
+ const browserContext = await chromium.launchPersistentContext(userDataDir, {
62
+ channel: 'chromium',
63
+ headless: !process.env.HEADFUL,
64
+ colorScheme: 'dark',
65
+ args: [`--disable-extensions-except=${extensionPath}`, `--load-extension=${extensionPath}`],
66
+ });
67
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
68
+ const page = await browserContext.newPage();
69
+ await page.goto('about:blank');
70
+ await serviceWorker.evaluate(async () => {
71
+ await globalThis.toggleExtensionForActiveTab();
72
+ });
73
+ return { browserContext, userDataDir, relayServer };
74
+ }
75
+ async function cleanupTestContext(ctx, cleanup) {
76
+ if (ctx?.browserContext) {
77
+ await ctx.browserContext.close();
78
+ }
79
+ if (ctx?.relayServer) {
80
+ ctx.relayServer.close();
81
+ }
82
+ if (ctx?.userDataDir) {
83
+ try {
84
+ fs.rmSync(ctx.userDataDir, { recursive: true, force: true });
85
+ }
86
+ catch (e) {
87
+ console.error('Failed to cleanup user data dir:', e);
88
+ }
89
+ }
90
+ if (cleanup) {
91
+ await cleanup();
92
+ }
93
+ }
94
+ describe('MCP Server Tests', () => {
95
+ let client;
96
+ let cleanup = null;
97
+ let testCtx = null;
98
+ beforeAll(async () => {
99
+ testCtx = await setupTestContext({ tempDirPrefix: 'pw-test-' });
100
+ const result = await createMCPClient({ port: TEST_PORT });
101
+ client = result.client;
102
+ cleanup = result.cleanup;
103
+ }, 600000);
104
+ afterAll(async () => {
105
+ await cleanupTestContext(testCtx, cleanup);
106
+ cleanup = null;
107
+ testCtx = null;
108
+ });
109
+ const getBrowserContext = () => {
110
+ if (!testCtx?.browserContext)
111
+ throw new Error('Browser not initialized');
112
+ return testCtx.browserContext;
113
+ };
114
+ it('should inject script via addScriptTag through CDP relay', async () => {
115
+ const browserContext = getBrowserContext();
116
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
117
+ const page = await browserContext.newPage();
118
+ await page.setContent('<html><body><button id="btn">Click</button></body></html>');
119
+ await page.bringToFront();
120
+ await serviceWorker.evaluate(async () => {
121
+ await globalThis.toggleExtensionForActiveTab();
122
+ });
123
+ await new Promise((r) => setTimeout(r, 100));
124
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
125
+ const cdpPage = browser
126
+ .contexts()[0]
127
+ .pages()
128
+ .find((p) => {
129
+ return p.url().startsWith('about:');
130
+ });
131
+ expect(cdpPage).toBeDefined();
132
+ const hasGlobalBefore = await cdpPage.evaluate(() => !!globalThis.__testGlobal);
133
+ expect(hasGlobalBefore).toBe(false);
134
+ await cdpPage.addScriptTag({ content: 'globalThis.__testGlobal = { foo: "bar" };' });
135
+ const hasGlobalAfter = await cdpPage.evaluate(() => globalThis.__testGlobal);
136
+ expect(hasGlobalAfter).toEqual({ foo: 'bar' });
137
+ await browser.close();
138
+ await page.close();
139
+ }, 60000);
140
+ it('should execute code and capture console output', async () => {
141
+ await client.callTool({
142
+ name: 'execute',
143
+ arguments: {
144
+ code: js `
145
+ const newPage = await context.newPage();
146
+ state.page = newPage;
147
+ if (!state.pages) state.pages = [];
148
+ state.pages.push(newPage);
149
+ `,
150
+ },
151
+ });
152
+ const result = await client.callTool({
153
+ name: 'execute',
154
+ arguments: {
155
+ code: js `
156
+ await state.page.goto('https://example.com');
157
+ const title = await state.page.title();
158
+ console.log('Page title:', title);
159
+ return { url: state.page.url(), title };
160
+ `,
161
+ },
162
+ });
163
+ expect(result.content).toMatchInlineSnapshot(`
164
+ [
165
+ {
166
+ "text": "MCP error -32602: Input validation error: Invalid arguments for tool execute: [
167
+ {
168
+ "code": "invalid_type",
169
+ "expected": "string",
170
+ "received": "undefined",
171
+ "path": [
172
+ "intend"
173
+ ],
174
+ "message": "Required"
175
+ }
176
+ ]",
177
+ "type": "text",
178
+ },
179
+ ]
180
+ `);
181
+ expect(result.content).toBeDefined();
182
+ }, 30000);
183
+ it('should show extension as connected for pages created via newPage()', async () => {
184
+ const browserContext = getBrowserContext();
185
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
186
+ // Create a page via MCP (which uses context.newPage())
187
+ await client.callTool({
188
+ name: 'execute',
189
+ arguments: {
190
+ code: js `
191
+ const newPage = await context.newPage();
192
+ state.testPage = newPage;
193
+ await newPage.goto('https://example.com/mcp-test');
194
+ return newPage.url();
195
+ `,
196
+ },
197
+ });
198
+ // Get extension state to verify the page is marked as connected
199
+ const extensionState = await serviceWorker.evaluate(async () => {
200
+ const state = globalThis.getExtensionState();
201
+ const tabs = await chrome.tabs.query({});
202
+ const testTab = tabs.find((t) => t.url?.includes('mcp-test'));
203
+ return {
204
+ connected: !!testTab && !!testTab.id && state.tabs.has(testTab.id),
205
+ tabId: testTab?.id,
206
+ tabInfo: testTab?.id ? state.tabs.get(testTab.id) : null,
207
+ connectionState: state.connectionState,
208
+ };
209
+ });
210
+ expect(extensionState.connected).toBe(true);
211
+ expect(extensionState.tabInfo?.state).toBe('connected');
212
+ expect(extensionState.connectionState).toBe('connected');
213
+ // Clean up
214
+ await client.callTool({
215
+ name: 'execute',
216
+ arguments: {
217
+ code: js `
218
+ if (state.testPage) {
219
+ await state.testPage.close();
220
+ delete state.testPage;
221
+ }
222
+ `,
223
+ },
224
+ });
225
+ }, 30000);
226
+ it('should get accessibility snapshot of hacker news', async () => {
227
+ await client.callTool({
228
+ name: 'execute',
229
+ arguments: {
230
+ code: js `
231
+ const newPage = await context.newPage();
232
+ state.page = newPage;
233
+ if (!state.pages) state.pages = [];
234
+ state.pages.push(newPage);
235
+ `,
236
+ },
237
+ });
238
+ const result = await client.callTool({
239
+ name: 'execute',
240
+ arguments: {
241
+ code: js `
242
+ await state.page.goto('https://news.ycombinator.com/item?id=1', { waitUntil: 'domcontentloaded' });
243
+ const snapshot = await state.page._snapshotForAI();
244
+ return snapshot;
245
+ `,
246
+ },
247
+ });
248
+ const initialData = typeof result === 'object' && result.content?.[0]?.text ? tryJsonParse(result.content[0].text) : result;
249
+ await expect(initialData).toMatchFileSnapshot('snapshots/hacker-news-initial-accessibility.md');
250
+ expect(result.content).toBeDefined();
251
+ expect(initialData).toContain('table');
252
+ expect(initialData).toContain('Hacker News');
253
+ }, 30000);
254
+ it('should get accessibility snapshot of shadcn UI', async () => {
255
+ await client.callTool({
256
+ name: 'execute',
257
+ arguments: {
258
+ code: js `
259
+ const newPage = await context.newPage();
260
+ state.page = newPage;
261
+ if (!state.pages) state.pages = [];
262
+ state.pages.push(newPage);
263
+ `,
264
+ },
265
+ });
266
+ const snapshot = await client.callTool({
267
+ name: 'execute',
268
+ arguments: {
269
+ code: js `
270
+ await state.page.goto('https://ui.shadcn.com/', { waitUntil: 'domcontentloaded' });
271
+ const snapshot = await state.page._snapshotForAI();
272
+ return snapshot;
273
+ `,
274
+ },
275
+ });
276
+ const data = typeof snapshot === 'object' && snapshot.content?.[0]?.text ? tryJsonParse(snapshot.content[0].text) : snapshot;
277
+ await expect(data).toMatchFileSnapshot('snapshots/shadcn-ui-accessibility.md');
278
+ expect(snapshot.content).toBeDefined();
279
+ expect(data).toContain('shadcn');
280
+ }, 30000);
281
+ it('should close all created pages', async () => {
282
+ const result = await client.callTool({
283
+ name: 'execute',
284
+ arguments: {
285
+ code: js `
286
+ if (state.pages && state.pages.length > 0) {
287
+ for (const page of state.pages) {
288
+ await page.close();
289
+ }
290
+ const closedCount = state.pages.length;
291
+ state.pages = [];
292
+ return { closedCount };
293
+ }
294
+ return { closedCount: 0 };
295
+ `,
296
+ },
297
+ });
298
+ });
299
+ it('should handle new pages and toggling with new connections', async () => {
300
+ const browserContext = getBrowserContext();
301
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
302
+ // 1. Create a new page
303
+ const page = await browserContext.newPage();
304
+ const testUrl = 'https://example.com/';
305
+ await page.goto(testUrl);
306
+ await page.bringToFront();
307
+ // 2. Enable extension on this new tab
308
+ // Since it's a new page, extension is not connected yet
309
+ const result = await serviceWorker.evaluate(async () => {
310
+ return await globalThis.toggleExtensionForActiveTab();
311
+ });
312
+ expect(result.isConnected).toBe(true);
313
+ // 3. Verify we can connect via direct CDP and see the page
314
+ let directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
315
+ let contexts = directBrowser.contexts();
316
+ let pages = contexts[0].pages();
317
+ // Find our page
318
+ let foundPage = pages.find((p) => p.url() === testUrl);
319
+ expect(foundPage).toBeDefined();
320
+ expect(foundPage?.url()).toBe(testUrl);
321
+ // Verify execution works
322
+ const sum1 = await foundPage?.evaluate(() => 1 + 1);
323
+ expect(sum1).toBe(2);
324
+ await directBrowser.close();
325
+ // 4. Disable extension on this tab
326
+ const resultDisabled = await serviceWorker.evaluate(async () => {
327
+ return await globalThis.toggleExtensionForActiveTab();
328
+ });
329
+ expect(resultDisabled.isConnected).toBe(false);
330
+ // 5. Try to connect/use the page.
331
+ // connecting to relay will succeed, but listing pages should NOT show our page
332
+ // Connect to relay again
333
+ directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
334
+ contexts = directBrowser.contexts();
335
+ pages = contexts[0].pages();
336
+ foundPage = pages.find((p) => p.url() === testUrl);
337
+ expect(foundPage).toBeUndefined();
338
+ await directBrowser.close();
339
+ // 6. Re-enable extension
340
+ const resultEnabled = await serviceWorker.evaluate(async () => {
341
+ return await globalThis.toggleExtensionForActiveTab();
342
+ });
343
+ expect(resultEnabled.isConnected).toBe(true);
344
+ // 7. Verify page is back
345
+ directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
346
+ // Wait a bit for targets to populate
347
+ await new Promise((r) => setTimeout(r, 100));
348
+ contexts = directBrowser.contexts();
349
+ // pages() might need a moment if target attached event comes in
350
+ if (contexts[0].pages().length === 0) {
351
+ await new Promise((r) => setTimeout(r, 100));
352
+ }
353
+ pages = contexts[0].pages();
354
+ foundPage = pages.find((p) => p.url() === testUrl);
355
+ expect(foundPage).toBeDefined();
356
+ expect(foundPage?.url()).toBe(testUrl);
357
+ // Verify execution works again
358
+ const sum2 = await foundPage?.evaluate(() => 2 + 2);
359
+ expect(sum2).toBe(4);
360
+ await directBrowser.close();
361
+ await page.close();
362
+ });
363
+ it('should handle new pages and toggling with persistent connection', async () => {
364
+ const browserContext = getBrowserContext();
365
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
366
+ // Connect once
367
+ const directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
368
+ // Wait a bit for connection and initial target discovery
369
+ await new Promise((r) => setTimeout(r, 100));
370
+ // 1. Create a new page
371
+ const page = await browserContext.newPage();
372
+ const testUrl = 'https://example.com/persistent';
373
+ await page.goto(testUrl);
374
+ await page.bringToFront();
375
+ // 2. Enable extension
376
+ await serviceWorker.evaluate(async () => {
377
+ await globalThis.toggleExtensionForActiveTab();
378
+ });
379
+ // 3. Verify page appears (polling)
380
+ let foundPage;
381
+ for (let i = 0; i < 50; i++) {
382
+ const pages = directBrowser.contexts()[0].pages();
383
+ foundPage = pages.find((p) => p.url() === testUrl);
384
+ if (foundPage)
385
+ break;
386
+ await new Promise((r) => setTimeout(r, 100));
387
+ }
388
+ expect(foundPage).toBeDefined();
389
+ expect(foundPage?.url()).toBe(testUrl);
390
+ // Verify execution works
391
+ const sum1 = await foundPage?.evaluate(() => 10 + 20);
392
+ expect(sum1).toBe(30);
393
+ // 4. Disable extension
394
+ await serviceWorker.evaluate(async () => {
395
+ await globalThis.toggleExtensionForActiveTab();
396
+ });
397
+ // 5. Verify page disappears (polling)
398
+ for (let i = 0; i < 50; i++) {
399
+ const pages = directBrowser.contexts()[0].pages();
400
+ foundPage = pages.find((p) => p.url() === testUrl);
401
+ if (!foundPage)
402
+ break;
403
+ await new Promise((r) => setTimeout(r, 100));
404
+ }
405
+ expect(foundPage).toBeUndefined();
406
+ // 6. Re-enable extension
407
+ await serviceWorker.evaluate(async () => {
408
+ await globalThis.toggleExtensionForActiveTab();
409
+ });
410
+ // 7. Verify page reappears (polling)
411
+ for (let i = 0; i < 50; i++) {
412
+ const pages = directBrowser.contexts()[0].pages();
413
+ foundPage = pages.find((p) => p.url() === testUrl);
414
+ if (foundPage)
415
+ break;
416
+ await new Promise((r) => setTimeout(r, 100));
417
+ }
418
+ expect(foundPage).toBeDefined();
419
+ expect(foundPage?.url()).toBe(testUrl);
420
+ // Verify execution works again
421
+ const sum2 = await foundPage?.evaluate(() => 30 + 40);
422
+ expect(sum2).toBe(70);
423
+ await page.close();
424
+ await directBrowser.close();
425
+ });
426
+ it('should maintain connection across reloads and navigation', async () => {
427
+ const browserContext = getBrowserContext();
428
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
429
+ // 1. Setup page
430
+ const page = await browserContext.newPage();
431
+ const initialUrl = 'https://example.com/';
432
+ await page.goto(initialUrl);
433
+ await page.bringToFront();
434
+ // 2. Enable extension
435
+ await serviceWorker.evaluate(async () => {
436
+ await globalThis.toggleExtensionForActiveTab();
437
+ });
438
+ // 3. Connect via CDP
439
+ const cdpUrl = getCdpUrl({ port: TEST_PORT });
440
+ const directBrowser = await chromium.connectOverCDP(cdpUrl);
441
+ const connectedPage = directBrowser
442
+ .contexts()[0]
443
+ .pages()
444
+ .find((p) => p.url() === initialUrl);
445
+ expect(connectedPage).toBeDefined();
446
+ // Verify execution
447
+ expect(await connectedPage?.evaluate(() => 1 + 1)).toBe(2);
448
+ // 4. Reload
449
+ // We use a loop to check if it's still connected because reload might cause temporary disconnect/reconnect events
450
+ // that Playwright handles natively if the session ID stays valid.
451
+ await connectedPage?.reload();
452
+ await connectedPage?.waitForLoadState('domcontentloaded');
453
+ expect(await connectedPage?.title()).toBe('Example Domain');
454
+ // Verify execution after reload
455
+ expect(await connectedPage?.evaluate(() => 2 + 2)).toBe(4);
456
+ // 5. Navigate to new URL
457
+ const newUrl = 'https://example.org/';
458
+ await connectedPage?.goto(newUrl);
459
+ await connectedPage?.waitForLoadState('domcontentloaded');
460
+ expect(connectedPage?.url()).toBe(newUrl);
461
+ expect(await connectedPage?.title()).toContain('Example Domain');
462
+ // Verify execution after navigation
463
+ expect(await connectedPage?.evaluate(() => 3 + 3)).toBe(6);
464
+ await directBrowser.close();
465
+ await page.close();
466
+ });
467
+ it('should support multiple concurrent tabs', async () => {
468
+ const browserContext = getBrowserContext();
469
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
470
+ await new Promise((resolve) => setTimeout(resolve, 100));
471
+ // Tab A
472
+ const pageA = await browserContext.newPage();
473
+ await pageA.goto('https://example.com/tab-a');
474
+ await pageA.bringToFront();
475
+ await new Promise((resolve) => setTimeout(resolve, 100));
476
+ await serviceWorker.evaluate(async () => {
477
+ await globalThis.toggleExtensionForActiveTab();
478
+ });
479
+ // Tab B
480
+ const pageB = await browserContext.newPage();
481
+ await pageB.goto('https://example.com/tab-b');
482
+ await pageB.bringToFront();
483
+ await new Promise((resolve) => setTimeout(resolve, 100));
484
+ await serviceWorker.evaluate(async () => {
485
+ await globalThis.toggleExtensionForActiveTab();
486
+ });
487
+ // Get target IDs for both
488
+ const targetIds = await serviceWorker.evaluate(async () => {
489
+ const state = globalThis.getExtensionState();
490
+ const chrome = globalThis.chrome;
491
+ const tabs = await chrome.tabs.query({});
492
+ const tabA = tabs.find((t) => t.url?.includes('tab-a'));
493
+ const tabB = tabs.find((t) => t.url?.includes('tab-b'));
494
+ return {
495
+ idA: state.tabs.get(tabA?.id ?? -1)?.targetId,
496
+ idB: state.tabs.get(tabB?.id ?? -1)?.targetId,
497
+ };
498
+ });
499
+ expect(targetIds).toMatchInlineSnapshot({
500
+ idA: expect.any(String),
501
+ idB: expect.any(String),
502
+ }, `
503
+ {
504
+ "idA": Any<String>,
505
+ "idB": Any<String>,
506
+ }
507
+ `);
508
+ expect(targetIds.idA).not.toBe(targetIds.idB);
509
+ // Verify independent connections
510
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
511
+ const pages = browser.contexts()[0].pages();
512
+ const results = await Promise.all(pages.map(async (p) => ({
513
+ url: p.url(),
514
+ title: await p.title(),
515
+ })));
516
+ expect(results).toMatchInlineSnapshot(`
517
+ [
518
+ {
519
+ "title": "",
520
+ "url": "about:blank",
521
+ },
522
+ {
523
+ "title": "Example Domain",
524
+ "url": "https://example.com/tab-a",
525
+ },
526
+ {
527
+ "title": "Example Domain",
528
+ "url": "https://example.com/tab-b",
529
+ },
530
+ ]
531
+ `);
532
+ // Verify execution on both pages
533
+ const pageA_CDP = pages.find((p) => p.url().includes('tab-a'));
534
+ const pageB_CDP = pages.find((p) => p.url().includes('tab-b'));
535
+ expect(await pageA_CDP?.evaluate(() => 10 + 10)).toBe(20);
536
+ expect(await pageB_CDP?.evaluate(() => 20 + 20)).toBe(40);
537
+ await browser.close();
538
+ await pageA.close();
539
+ await pageB.close();
540
+ });
541
+ it('should show correct url when enabling extension after navigation', async () => {
542
+ const browserContext = getBrowserContext();
543
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
544
+ const page = await browserContext.newPage();
545
+ const targetUrl = 'https://example.com/late-enable';
546
+ await page.goto(targetUrl);
547
+ await page.bringToFront();
548
+ // Wait for load
549
+ await page.waitForLoadState('domcontentloaded');
550
+ // 2. Enable extension for this page
551
+ await serviceWorker.evaluate(async () => {
552
+ await globalThis.toggleExtensionForActiveTab();
553
+ });
554
+ // 3. Verify via CDP that the correct URL is shown
555
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
556
+ // Wait for sync
557
+ await new Promise((r) => setTimeout(r, 100));
558
+ const cdpPage = browser
559
+ .contexts()[0]
560
+ .pages()
561
+ .find((p) => p.url() === targetUrl);
562
+ expect(cdpPage).toBeDefined();
563
+ expect(cdpPage?.url()).toBe(targetUrl);
564
+ await browser.close();
565
+ await page.close();
566
+ }, 60000);
567
+ it('should be able to reconnect after disconnecting everything', async () => {
568
+ const browserContext = getBrowserContext();
569
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
570
+ const pages = await browserContext.pages();
571
+ expect(pages.length).toBeGreaterThan(0);
572
+ const page = pages[0];
573
+ await page.goto('https://example.com/disconnect-test');
574
+ await page.waitForLoadState('domcontentloaded');
575
+ await page.bringToFront();
576
+ // Enable extension on this page
577
+ const initialEnable = await serviceWorker.evaluate(async () => {
578
+ return await globalThis.toggleExtensionForActiveTab();
579
+ });
580
+ console.log('Initial enable result:', initialEnable);
581
+ expect(initialEnable.isConnected).toBe(true);
582
+ // Wait for extension to fully connect
583
+ await new Promise((resolve) => setTimeout(resolve, 100));
584
+ // Verify MCP can see the page
585
+ const beforeDisconnect = await client.callTool({
586
+ name: 'execute',
587
+ arguments: {
588
+ code: js `
589
+ const pages = context.pages();
590
+ console.log('Pages before disconnect:', pages.length);
591
+ const testPage = pages.find(p => p.url().includes('disconnect-test'));
592
+ console.log('Found test page:', !!testPage);
593
+ return { pagesCount: pages.length, foundTestPage: !!testPage };
594
+ `,
595
+ },
596
+ });
597
+ const beforeOutput = beforeDisconnect.content[0].text;
598
+ expect(beforeOutput).toContain('foundTestPage');
599
+ console.log('Before disconnect:', beforeOutput);
600
+ // 2. Disconnect everything
601
+ console.log('Calling disconnectEverything...');
602
+ await serviceWorker.evaluate(async () => {
603
+ await globalThis.disconnectEverything();
604
+ });
605
+ // Wait for disconnect to complete
606
+ await new Promise((resolve) => setTimeout(resolve, 100));
607
+ // 3. Verify MCP cannot see the page anymore
608
+ const afterDisconnect = await client.callTool({
609
+ name: 'execute',
610
+ arguments: {
611
+ code: js `
612
+ const pages = context.pages();
613
+ console.log('Pages after disconnect:', pages.length);
614
+ return { pagesCount: pages.length };
615
+ `,
616
+ },
617
+ });
618
+ const afterDisconnectOutput = afterDisconnect.content[0].text;
619
+ console.log('After disconnect:', afterDisconnectOutput);
620
+ expect(afterDisconnectOutput).toContain('Pages after disconnect: 0');
621
+ // 4. Re-enable extension on the same page
622
+ console.log('Re-enabling extension...');
623
+ await page.bringToFront();
624
+ const reconnectResult = await serviceWorker.evaluate(async () => {
625
+ console.log('About to call toggleExtensionForActiveTab');
626
+ const result = await globalThis.toggleExtensionForActiveTab();
627
+ console.log('toggleExtensionForActiveTab result:', result);
628
+ return result;
629
+ });
630
+ console.log('Reconnect result:', reconnectResult);
631
+ expect(reconnectResult.isConnected).toBe(true);
632
+ // Wait for extension to fully reconnect and relay server to be ready
633
+ console.log('Waiting for reconnection to stabilize...');
634
+ await new Promise((resolve) => setTimeout(resolve, 100));
635
+ // 5. Reset the MCP client's playwright connection since it was closed by disconnectEverything
636
+ console.log('Resetting MCP playwright connection...');
637
+ const resetResult = await client.callTool({
638
+ name: 'execute',
639
+ arguments: {
640
+ code: js `
641
+ console.log('Resetting playwright connection');
642
+ const result = await resetPlaywright();
643
+ console.log('Reset complete, checking pages');
644
+ const pages = context.pages();
645
+ console.log('Pages after reset:', pages.length);
646
+ return { reset: true, pagesCount: pages.length };
647
+ `,
648
+ },
649
+ });
650
+ console.log('Reset result:', resetResult.content[0].text);
651
+ // 6. Verify MCP can see the page again
652
+ console.log('Attempting to access page via MCP...');
653
+ const afterReconnect = await client.callTool({
654
+ name: 'execute',
655
+ arguments: {
656
+ code: js `
657
+ console.log('Checking pages after reconnect...');
658
+ const pages = context.pages();
659
+ console.log('Pages after reconnect:', pages.length);
660
+
661
+ if (pages.length === 0) {
662
+ console.log('No pages found!');
663
+ return { pagesCount: 0, foundTestPage: false };
664
+ }
665
+
666
+ const testPage = pages.find(p => p.url().includes('disconnect-test'));
667
+ console.log('Found test page after reconnect:', !!testPage);
668
+
669
+ if (testPage) {
670
+ console.log('Test page URL:', testPage.url());
671
+ return { pagesCount: pages.length, foundTestPage: true, url: testPage.url() };
672
+ }
673
+
674
+ return { pagesCount: pages.length, foundTestPage: false };
675
+ `,
676
+ },
677
+ });
678
+ const afterReconnectOutput = afterReconnect.content[0].text;
679
+ console.log('After reconnect:', afterReconnectOutput);
680
+ expect(afterReconnectOutput).toContain('foundTestPage');
681
+ expect(afterReconnectOutput).toContain('disconnect-test');
682
+ // Clean up - navigate page back to about:blank to not interfere with other tests
683
+ await page.goto('about:blank');
684
+ });
685
+ it('should auto-reconnect MCP after extension WebSocket reconnects', async () => {
686
+ // This test verifies that the MCP automatically reconnects when the browser
687
+ // disconnects (e.g., when the extension WebSocket reconnects and the relay
688
+ // server closes all playwright clients). The fix adds browser.on('disconnected')
689
+ // handler that clears state.isConnected, so ensureConnection() creates a new connection.
690
+ const serviceWorker = await getExtensionServiceWorker(testCtx.browserContext);
691
+ // 1. Create a test page and enable extension
692
+ const page = await testCtx.browserContext.newPage();
693
+ await page.goto('https://example.com/auto-reconnect-test');
694
+ await page.waitForLoadState('domcontentloaded');
695
+ await page.bringToFront();
696
+ const initialEnable = await serviceWorker.evaluate(async () => {
697
+ return await globalThis.toggleExtensionForActiveTab();
698
+ });
699
+ expect(initialEnable.isConnected).toBe(true);
700
+ await new Promise((resolve) => setTimeout(resolve, 100));
701
+ // 2. Verify MCP can execute commands
702
+ const beforeResult = await client.callTool({
703
+ name: 'execute',
704
+ arguments: {
705
+ code: js `
706
+ const pages = context.pages();
707
+ const testPage = pages.find(p => p.url().includes('auto-reconnect-test'));
708
+ return { pagesCount: pages.length, foundTestPage: !!testPage };
709
+ `,
710
+ },
711
+ });
712
+ const beforeOutput = beforeResult.content[0].text;
713
+ expect(beforeOutput).toContain('foundTestPage');
714
+ expect(beforeOutput).toContain('true');
715
+ // 3. Simulate extension WebSocket reconnection
716
+ // This causes relay server to close all playwright client WebSockets
717
+ await serviceWorker.evaluate(async () => {
718
+ await globalThis.disconnectEverything();
719
+ });
720
+ await new Promise((resolve) => setTimeout(resolve, 100));
721
+ // Re-enable extension (simulates extension reconnecting)
722
+ await page.bringToFront();
723
+ const reconnectResult = await serviceWorker.evaluate(async () => {
724
+ return await globalThis.toggleExtensionForActiveTab();
725
+ });
726
+ expect(reconnectResult.isConnected).toBe(true);
727
+ await new Promise((resolve) => setTimeout(resolve, 100));
728
+ // 4. Execute command WITHOUT calling resetPlaywright()
729
+ // The browser.on('disconnected') handler should have cleared state.isConnected,
730
+ // causing ensureConnection() to automatically create a new connection
731
+ const afterResult = await client.callTool({
732
+ name: 'execute',
733
+ arguments: {
734
+ code: js `
735
+ const pages = context.pages();
736
+ const testPage = pages.find(p => p.url().includes('auto-reconnect-test'));
737
+ return { pagesCount: pages.length, foundTestPage: !!testPage, url: testPage?.url() };
738
+ `,
739
+ },
740
+ });
741
+ const afterOutput = afterResult.content[0].text;
742
+ // The command should succeed and find our test page
743
+ expect(afterOutput).toContain('foundTestPage');
744
+ expect(afterOutput).toContain('true');
745
+ expect(afterOutput).toContain('auto-reconnect-test');
746
+ // Should NOT contain error about extension not connected
747
+ expect(afterOutput).not.toContain('Extension not connected');
748
+ expect(afterResult.isError).not.toBe(true);
749
+ // Clean up
750
+ await page.goto('about:blank');
751
+ });
752
+ it('should capture browser console logs with getLatestLogs', async () => {
753
+ // Ensure clean state and clear any existing logs
754
+ const resetResult = await client.callTool({
755
+ name: 'execute',
756
+ arguments: {
757
+ code: js `
758
+ // Clear any existing logs from previous tests
759
+ clearAllLogs();
760
+ console.log('Cleared all existing logs');
761
+
762
+ // Verify connection is working
763
+ const pages = context.pages();
764
+ console.log('Current pages count:', pages.length);
765
+
766
+ return { success: true, pagesCount: pages.length };
767
+ `,
768
+ },
769
+ });
770
+ console.log('Cleanup result:', resetResult);
771
+ // Create a new page for this test
772
+ await client.callTool({
773
+ name: 'execute',
774
+ arguments: {
775
+ code: js `
776
+ const newPage = await context.newPage();
777
+ state.testLogPage = newPage;
778
+ await newPage.goto('about:blank');
779
+ `,
780
+ },
781
+ });
782
+ // Generate some console logs in the browser
783
+ await client.callTool({
784
+ name: 'execute',
785
+ arguments: {
786
+ code: js `
787
+ await state.testLogPage.evaluate(() => {
788
+ console.log('Test log 12345');
789
+ console.error('Test error 67890');
790
+ console.warn('Test warning 11111');
791
+ console.log('Test log 2 with', { data: 'object' });
792
+ });
793
+ // Wait for logs to be captured
794
+ await new Promise(resolve => setTimeout(resolve, 100));
795
+ `,
796
+ },
797
+ });
798
+ // Test getting all logs
799
+ const allLogsResult = await client.callTool({
800
+ name: 'execute',
801
+ arguments: {
802
+ code: js `
803
+ const logs = await getLatestLogs();
804
+ logs.forEach(log => console.log(log));
805
+ `,
806
+ },
807
+ });
808
+ const output = allLogsResult.content[0].text;
809
+ expect(output).toContain('[log] Test log 12345');
810
+ expect(output).toContain('[error] Test error 67890');
811
+ expect(output).toContain('[warning] Test warning 11111');
812
+ // Test filtering by search string
813
+ const errorLogsResult = await client.callTool({
814
+ name: 'execute',
815
+ arguments: {
816
+ code: js `
817
+ const logs = await getLatestLogs({ search: 'error' });
818
+ logs.forEach(log => console.log(log));
819
+ `,
820
+ },
821
+ });
822
+ const errorOutput = errorLogsResult.content[0].text;
823
+ expect(errorOutput).toContain('[error] Test error 67890');
824
+ expect(errorOutput).not.toContain('[log] Test log 12345');
825
+ // Test that logs are cleared on page reload
826
+ await client.callTool({
827
+ name: 'execute',
828
+ arguments: {
829
+ code: js `
830
+ // First add a log before reload
831
+ await state.testLogPage.evaluate(() => {
832
+ console.log('Before reload 99999');
833
+ });
834
+ await new Promise(resolve => setTimeout(resolve, 100));
835
+ `,
836
+ },
837
+ });
838
+ // Verify the log exists
839
+ const beforeReloadResult = await client.callTool({
840
+ name: 'execute',
841
+ arguments: {
842
+ code: js `
843
+ const logs = await getLatestLogs({ page: state.testLogPage });
844
+ console.log('Logs before reload:', logs.length);
845
+ logs.forEach(log => console.log(log));
846
+ `,
847
+ },
848
+ });
849
+ const beforeReloadOutput = beforeReloadResult.content[0].text;
850
+ expect(beforeReloadOutput).toContain('[log] Before reload 99999');
851
+ // Reload the page
852
+ await client.callTool({
853
+ name: 'execute',
854
+ arguments: {
855
+ code: js `
856
+ await state.testLogPage.reload();
857
+ await state.testLogPage.evaluate(() => {
858
+ console.log('After reload 88888');
859
+ });
860
+ await new Promise(resolve => setTimeout(resolve, 100));
861
+ `,
862
+ },
863
+ });
864
+ // Check logs after reload - old logs should be gone
865
+ const afterReloadResult = await client.callTool({
866
+ name: 'execute',
867
+ arguments: {
868
+ code: js `
869
+ const logs = await getLatestLogs({ page: state.testLogPage });
870
+ console.log('Logs after reload:', logs.length);
871
+ logs.forEach(log => console.log(log));
872
+ `,
873
+ },
874
+ });
875
+ const afterReloadOutput = afterReloadResult.content[0].text;
876
+ expect(afterReloadOutput).toContain('[log] After reload 88888');
877
+ expect(afterReloadOutput).not.toContain('[log] Before reload 99999');
878
+ // Clean up
879
+ await client.callTool({
880
+ name: 'execute',
881
+ arguments: {
882
+ code: js `
883
+ await state.testLogPage.close();
884
+ delete state.testLogPage;
885
+ `,
886
+ },
887
+ });
888
+ }, 30000);
889
+ it('should keep logs separate between different pages', async () => {
890
+ // Clear any existing logs from previous tests
891
+ await client.callTool({
892
+ name: 'execute',
893
+ arguments: {
894
+ code: js `
895
+ clearAllLogs();
896
+ console.log('Cleared all existing logs for second log test');
897
+ `,
898
+ },
899
+ });
900
+ // Create two pages
901
+ await client.callTool({
902
+ name: 'execute',
903
+ arguments: {
904
+ code: js `
905
+ state.pageA = await context.newPage();
906
+ state.pageB = await context.newPage();
907
+ await state.pageA.goto('about:blank');
908
+ await state.pageB.goto('about:blank');
909
+ `,
910
+ },
911
+ });
912
+ // Generate logs in page A
913
+ await client.callTool({
914
+ name: 'execute',
915
+ arguments: {
916
+ code: js `
917
+ await state.pageA.evaluate(() => {
918
+ console.log('PageA log 11111');
919
+ console.error('PageA error 22222');
920
+ });
921
+ await new Promise(resolve => setTimeout(resolve, 100));
922
+ `,
923
+ },
924
+ });
925
+ // Generate logs in page B
926
+ await client.callTool({
927
+ name: 'execute',
928
+ arguments: {
929
+ code: js `
930
+ await state.pageB.evaluate(() => {
931
+ console.log('PageB log 33333');
932
+ console.error('PageB error 44444');
933
+ });
934
+ await new Promise(resolve => setTimeout(resolve, 100));
935
+ `,
936
+ },
937
+ });
938
+ // Check logs for page A - should only have page A logs
939
+ const pageALogsResult = await client.callTool({
940
+ name: 'execute',
941
+ arguments: {
942
+ code: js `
943
+ const logs = await getLatestLogs({ page: state.pageA });
944
+ console.log('Page A logs:', logs.length);
945
+ logs.forEach(log => console.log(log));
946
+ `,
947
+ },
948
+ });
949
+ const pageAOutput = pageALogsResult.content[0].text;
950
+ expect(pageAOutput).toContain('[log] PageA log 11111');
951
+ expect(pageAOutput).toContain('[error] PageA error 22222');
952
+ expect(pageAOutput).not.toContain('PageB');
953
+ // Check logs for page B - should only have page B logs
954
+ const pageBLogsResult = await client.callTool({
955
+ name: 'execute',
956
+ arguments: {
957
+ code: js `
958
+ const logs = await getLatestLogs({ page: state.pageB });
959
+ console.log('Page B logs:', logs.length);
960
+ logs.forEach(log => console.log(log));
961
+ `,
962
+ },
963
+ });
964
+ const pageBOutput = pageBLogsResult.content[0].text;
965
+ expect(pageBOutput).toContain('[log] PageB log 33333');
966
+ expect(pageBOutput).toContain('[error] PageB error 44444');
967
+ expect(pageBOutput).not.toContain('PageA');
968
+ // Check all logs - should have logs from both pages
969
+ const allLogsResult = await client.callTool({
970
+ name: 'execute',
971
+ arguments: {
972
+ code: js `
973
+ const logs = await getLatestLogs();
974
+ console.log('All logs:', logs.length);
975
+ logs.forEach(log => console.log(log));
976
+ `,
977
+ },
978
+ });
979
+ const allOutput = allLogsResult.content[0].text;
980
+ expect(allOutput).toContain('[log] PageA log 11111');
981
+ expect(allOutput).toContain('[log] PageB log 33333');
982
+ // Test that reloading page A clears only page A logs
983
+ await client.callTool({
984
+ name: 'execute',
985
+ arguments: {
986
+ code: js `
987
+ await state.pageA.reload();
988
+ await state.pageA.evaluate(() => {
989
+ console.log('PageA after reload 55555');
990
+ });
991
+ await new Promise(resolve => setTimeout(resolve, 100));
992
+ `,
993
+ },
994
+ });
995
+ // Check page A logs - should only have new log
996
+ const pageAAfterReloadResult = await client.callTool({
997
+ name: 'execute',
998
+ arguments: {
999
+ code: js `
1000
+ const logs = await getLatestLogs({ page: state.pageA });
1001
+ console.log('Page A logs after reload:', logs.length);
1002
+ logs.forEach(log => console.log(log));
1003
+ `,
1004
+ },
1005
+ });
1006
+ const pageAAfterReloadOutput = pageAAfterReloadResult.content[0].text;
1007
+ expect(pageAAfterReloadOutput).toContain('[log] PageA after reload 55555');
1008
+ expect(pageAAfterReloadOutput).not.toContain('[log] PageA log 11111');
1009
+ // Check page B logs - should still have original logs
1010
+ const pageBAfterAReloadResult = await client.callTool({
1011
+ name: 'execute',
1012
+ arguments: {
1013
+ code: js `
1014
+ const logs = await getLatestLogs({ page: state.pageB });
1015
+ console.log('Page B logs after A reload:', logs.length);
1016
+ logs.forEach(log => console.log(log));
1017
+ `,
1018
+ },
1019
+ });
1020
+ const pageBAfterAReloadOutput = pageBAfterAReloadResult.content[0].text;
1021
+ expect(pageBAfterAReloadOutput).toContain('[log] PageB log 33333');
1022
+ expect(pageBAfterAReloadOutput).toContain('[error] PageB error 44444');
1023
+ // Test that logs are deleted when page is closed
1024
+ await client.callTool({
1025
+ name: 'execute',
1026
+ arguments: {
1027
+ code: js `
1028
+ // Close page A
1029
+ await state.pageA.close();
1030
+ await new Promise(resolve => setTimeout(resolve, 100));
1031
+ `,
1032
+ },
1033
+ });
1034
+ // Check all logs - page A logs should be gone
1035
+ const logsAfterCloseResult = await client.callTool({
1036
+ name: 'execute',
1037
+ arguments: {
1038
+ code: js `
1039
+ const logs = await getLatestLogs();
1040
+ console.log('All logs after closing page A:', logs.length);
1041
+ logs.forEach(log => console.log(log));
1042
+ `,
1043
+ },
1044
+ });
1045
+ const logsAfterCloseOutput = logsAfterCloseResult.content[0].text;
1046
+ expect(logsAfterCloseOutput).not.toContain('PageA');
1047
+ expect(logsAfterCloseOutput).toContain('[log] PageB log 33333');
1048
+ // Clean up remaining page
1049
+ await client.callTool({
1050
+ name: 'execute',
1051
+ arguments: {
1052
+ code: js `
1053
+ await state.pageB.close();
1054
+ delete state.pageA;
1055
+ delete state.pageB;
1056
+ `,
1057
+ },
1058
+ });
1059
+ }, 30000);
1060
+ it('should maintain correct page.url() with service worker pages', async () => {
1061
+ const browserContext = getBrowserContext();
1062
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1063
+ const page = await browserContext.newPage();
1064
+ const targetUrl = 'https://example.com/sw-test';
1065
+ await page.goto(targetUrl);
1066
+ await page.bringToFront();
1067
+ await serviceWorker.evaluate(async () => {
1068
+ await globalThis.toggleExtensionForActiveTab();
1069
+ });
1070
+ await new Promise((r) => setTimeout(r, 100));
1071
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1072
+ const pages = browser.contexts()[0].pages();
1073
+ const testPage = pages.find((p) => p.url().includes('sw-test'));
1074
+ expect(testPage).toBeDefined();
1075
+ expect(testPage?.url()).toContain('sw-test');
1076
+ expect(testPage?.url()).not.toContain('sw.js');
1077
+ await browser.close();
1078
+ await page.close();
1079
+ }, 30000);
1080
+ it('should maintain correct page.url() after repeated connections', async () => {
1081
+ const browserContext = getBrowserContext();
1082
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1083
+ const page = await browserContext.newPage();
1084
+ const targetUrl = 'https://example.com/repeated-test';
1085
+ await page.goto(targetUrl);
1086
+ await page.bringToFront();
1087
+ await serviceWorker.evaluate(async () => {
1088
+ await globalThis.toggleExtensionForActiveTab();
1089
+ });
1090
+ for (let i = 0; i < 5; i++) {
1091
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1092
+ const pages = browser.contexts()[0].pages();
1093
+ const testPage = pages.find((p) => p.url().includes('repeated-test'));
1094
+ expect(testPage).toBeDefined();
1095
+ expect(testPage?.url()).toBe(targetUrl);
1096
+ await browser.close();
1097
+ await new Promise((r) => setTimeout(r, 100));
1098
+ }
1099
+ await page.close();
1100
+ }, 30000);
1101
+ it('should maintain correct page.url() with concurrent MCP and CDP connections', async () => {
1102
+ const browserContext = getBrowserContext();
1103
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1104
+ const page = await browserContext.newPage();
1105
+ const targetUrl = 'https://example.com/concurrent-test';
1106
+ await page.goto(targetUrl);
1107
+ await page.bringToFront();
1108
+ await serviceWorker.evaluate(async () => {
1109
+ await globalThis.toggleExtensionForActiveTab();
1110
+ });
1111
+ await new Promise((r) => setTimeout(r, 400));
1112
+ const [mcpResult, cdpBrowser] = await Promise.all([
1113
+ client.callTool({
1114
+ name: 'execute',
1115
+ arguments: {
1116
+ code: js `
1117
+ const pages = context.pages();
1118
+ const testPage = pages.find(p => p.url().includes('concurrent-test'));
1119
+ return { url: testPage?.url(), found: !!testPage };
1120
+ `,
1121
+ },
1122
+ }),
1123
+ chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT })),
1124
+ ]);
1125
+ const mcpOutput = mcpResult.content[0].text;
1126
+ expect(mcpOutput).toContain(targetUrl);
1127
+ const cdpPages = cdpBrowser.contexts()[0].pages();
1128
+ const cdpPage = cdpPages.find((p) => p.url().includes('concurrent-test'));
1129
+ expect(cdpPage?.url()).toBe(targetUrl);
1130
+ await cdpBrowser.close();
1131
+ await page.close();
1132
+ }, 30000);
1133
+ it('should be usable after toggle with valid URL', async () => {
1134
+ // This test validates the extension properly waits for valid URLs before
1135
+ // sending Target.attachedToTarget. Uses Discord - a heavy React SPA.
1136
+ //
1137
+ // We use waitForEvent('page') to wait for Playwright to process the event.
1138
+ // The KEY assertion is that when the event fires, the URL is VALID (not empty).
1139
+ // Before the fix: event fired with empty URL -> page broken forever
1140
+ // After the fix: event fires with valid URL -> page works immediately
1141
+ const _browserContext = getBrowserContext();
1142
+ const serviceWorker = await getExtensionServiceWorker(_browserContext);
1143
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1144
+ const context = browser.contexts()[0];
1145
+ const page = await _browserContext.newPage();
1146
+ await page.goto('https://discord.com/login');
1147
+ await page.bringToFront();
1148
+ // Set up listener BEFORE toggle
1149
+ const pagePromise = context.waitForEvent('page', { timeout: 10000 });
1150
+ // Toggle extension - extension waits for valid URL before sending event
1151
+ await serviceWorker.evaluate(async () => {
1152
+ await globalThis.toggleExtensionForActiveTab();
1153
+ });
1154
+ // Wait for page event
1155
+ const targetPage = await pagePromise;
1156
+ console.log('Page URL when event fired:', targetPage.url());
1157
+ // KEY ASSERTION: URL must NOT be empty - this is what the extension fix guarantees
1158
+ expect(targetPage.url()).not.toBe('');
1159
+ expect(targetPage.url()).not.toBe(':');
1160
+ expect(targetPage.url()).toContain('discord.com');
1161
+ // evaluate() works immediately - no waiting needed
1162
+ const result = await targetPage.evaluate(() => window.location.href);
1163
+ expect(result).toContain('discord.com');
1164
+ await browser.close();
1165
+ await page.close();
1166
+ }, 60000);
1167
+ it('should have non-empty URLs when connecting to already-loaded pages', async () => {
1168
+ // This test validates that when we connect to a browser with already-loaded pages,
1169
+ // all pages have non-empty URLs. Empty URLs break Playwright permanently.
1170
+ const _browserContext = getBrowserContext();
1171
+ const serviceWorker = await getExtensionServiceWorker(_browserContext);
1172
+ // Create and fully load a heavy page BEFORE connecting
1173
+ const page = await _browserContext.newPage();
1174
+ await page.goto('https://discord.com/login', { waitUntil: 'load' });
1175
+ await page.bringToFront();
1176
+ // Toggle extension to attach to the loaded page
1177
+ await serviceWorker.evaluate(async () => {
1178
+ await globalThis.toggleExtensionForActiveTab();
1179
+ });
1180
+ // NOW connect via CDP - page should already be attached
1181
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1182
+ const context = browser.contexts()[0];
1183
+ // Get all pages and verify NONE have empty URLs
1184
+ const pages = context.pages();
1185
+ console.log('All page URLs:', pages.map((p) => p.url()));
1186
+ expect(pages.length).toBeGreaterThan(0);
1187
+ for (const p of pages) {
1188
+ expect(p.url()).not.toBe('');
1189
+ expect(p.url()).not.toBe(':');
1190
+ expect(p.url()).not.toBeUndefined();
1191
+ }
1192
+ // Find Discord page and verify it works
1193
+ const discordPage = pages.find((p) => p.url().includes('discord.com'));
1194
+ expect(discordPage).toBeDefined();
1195
+ const result = await discordPage.evaluate(() => window.location.href);
1196
+ expect(result).toContain('discord.com');
1197
+ await browser.close();
1198
+ await page.close();
1199
+ }, 60000);
1200
+ it('should maintain correct page.url() with iframe-heavy pages', async () => {
1201
+ const browserContext = getBrowserContext();
1202
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1203
+ const page = await browserContext.newPage();
1204
+ await page.setContent(`
1205
+ <html>
1206
+ <head><title>Iframe Test Page</title></head>
1207
+ <body>
1208
+ <h1>Iframe Heavy Page</h1>
1209
+ <iframe src="about:blank" id="frame1"></iframe>
1210
+ <iframe src="about:blank" id="frame2"></iframe>
1211
+ <iframe src="about:blank" id="frame3"></iframe>
1212
+ </body>
1213
+ </html>
1214
+ `);
1215
+ await page.bringToFront();
1216
+ await serviceWorker.evaluate(async () => {
1217
+ await globalThis.toggleExtensionForActiveTab();
1218
+ });
1219
+ await new Promise((r) => setTimeout(r, 100));
1220
+ for (let i = 0; i < 3; i++) {
1221
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1222
+ const pages = browser.contexts()[0].pages();
1223
+ let iframePage;
1224
+ for (const p of pages) {
1225
+ const html = await p.content();
1226
+ if (html.includes('Iframe Heavy Page')) {
1227
+ iframePage = p;
1228
+ break;
1229
+ }
1230
+ }
1231
+ expect(iframePage).toBeDefined();
1232
+ expect(iframePage?.url()).toContain('about:');
1233
+ await browser.close();
1234
+ await new Promise((r) => setTimeout(r, 100));
1235
+ }
1236
+ await page.close();
1237
+ }, 30000);
1238
+ it('should capture screenshot correctly', async () => {
1239
+ const browserContext = getBrowserContext();
1240
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1241
+ const page = await browserContext.newPage();
1242
+ await page.goto('https://example.com/');
1243
+ await page.bringToFront();
1244
+ await serviceWorker.evaluate(async () => {
1245
+ await globalThis.toggleExtensionForActiveTab();
1246
+ });
1247
+ await new Promise((r) => setTimeout(r, 100));
1248
+ const capturedCommands = [];
1249
+ const commandHandler = ({ command }) => {
1250
+ if (command.method === 'Page.captureScreenshot') {
1251
+ capturedCommands.push(command);
1252
+ }
1253
+ };
1254
+ testCtx.relayServer.on('cdp:command', commandHandler);
1255
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1256
+ const cdpPage = browser
1257
+ .contexts()[0]
1258
+ .pages()
1259
+ .find((p) => p.url().includes('example.com'));
1260
+ expect(cdpPage).toBeDefined();
1261
+ const viewportSize = cdpPage.viewportSize();
1262
+ console.log('Viewport size:', viewportSize);
1263
+ const viewportScreenshot = await cdpPage.screenshot();
1264
+ expect(viewportScreenshot).toBeDefined();
1265
+ const viewportDimensions = imageSize(viewportScreenshot);
1266
+ console.log('Viewport screenshot dimensions:', viewportDimensions);
1267
+ expect(viewportDimensions.width).toBeGreaterThan(0);
1268
+ expect(viewportDimensions.height).toBeGreaterThan(0);
1269
+ if (viewportSize) {
1270
+ expect(viewportDimensions.width).toBe(viewportSize.width);
1271
+ expect(viewportDimensions.height).toBe(viewportSize.height);
1272
+ }
1273
+ const fullPageScreenshot = await cdpPage.screenshot({ fullPage: true });
1274
+ expect(fullPageScreenshot).toBeDefined();
1275
+ const fullPageDimensions = imageSize(fullPageScreenshot);
1276
+ console.log('Full page screenshot dimensions:', fullPageDimensions);
1277
+ expect(fullPageDimensions.width).toBeGreaterThan(0);
1278
+ expect(fullPageDimensions.height).toBeGreaterThan(0);
1279
+ expect(fullPageDimensions.width).toBeGreaterThanOrEqual(viewportDimensions.width);
1280
+ testCtx.relayServer.off('cdp:command', commandHandler);
1281
+ expect(capturedCommands.length).toBe(2);
1282
+ expect(capturedCommands.map((c) => ({
1283
+ method: c.method,
1284
+ params: c.params,
1285
+ }))).toMatchInlineSnapshot(`
1286
+ [
1287
+ {
1288
+ "method": "Page.captureScreenshot",
1289
+ "params": {
1290
+ "captureBeyondViewport": false,
1291
+ "clip": {
1292
+ "height": 720,
1293
+ "scale": 1,
1294
+ "width": 1280,
1295
+ "x": 0,
1296
+ "y": 0,
1297
+ },
1298
+ "format": "png",
1299
+ },
1300
+ },
1301
+ {
1302
+ "method": "Page.captureScreenshot",
1303
+ "params": {
1304
+ "captureBeyondViewport": false,
1305
+ "clip": {
1306
+ "height": 581,
1307
+ "scale": 1,
1308
+ "width": 1280,
1309
+ "x": 0,
1310
+ "y": 0,
1311
+ },
1312
+ "format": "png",
1313
+ },
1314
+ },
1315
+ ]
1316
+ `);
1317
+ const screenshotPath = path.join(os.tmpdir(), 'e2e-pilot-test-screenshot.png');
1318
+ fs.writeFileSync(screenshotPath, viewportScreenshot);
1319
+ console.log('Screenshot saved to:', screenshotPath);
1320
+ await browser.close();
1321
+ await page.close();
1322
+ }, 60000);
1323
+ it('should capture element screenshot with correct coordinates', async () => {
1324
+ const browserContext = getBrowserContext();
1325
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1326
+ const target = { x: 200, y: 150, width: 300, height: 100 };
1327
+ const scrolledTarget = { x: 100, y: 1500, width: 200, height: 80 };
1328
+ const page = await browserContext.newPage();
1329
+ await page.setContent(`
1330
+ <html>
1331
+ <head>
1332
+ <style>
1333
+ body { margin: 0; padding: 0; height: 2000px; }
1334
+ #target {
1335
+ position: absolute;
1336
+ top: ${target.y}px;
1337
+ left: ${target.x}px;
1338
+ width: ${target.width}px;
1339
+ height: ${target.height}px;
1340
+ background: red;
1341
+ }
1342
+ #scrolled-target {
1343
+ position: absolute;
1344
+ top: ${scrolledTarget.y}px;
1345
+ left: ${scrolledTarget.x}px;
1346
+ width: ${scrolledTarget.width}px;
1347
+ height: ${scrolledTarget.height}px;
1348
+ background: blue;
1349
+ }
1350
+ </style>
1351
+ </head>
1352
+ <body>
1353
+ <div id="target">Target Element</div>
1354
+ <div id="scrolled-target">Scrolled Target</div>
1355
+ </body>
1356
+ </html>
1357
+ `);
1358
+ await page.bringToFront();
1359
+ await serviceWorker.evaluate(async () => {
1360
+ await globalThis.toggleExtensionForActiveTab();
1361
+ });
1362
+ await new Promise((r) => setTimeout(r, 100));
1363
+ const capturedCommands = [];
1364
+ const commandHandler = ({ command }) => {
1365
+ if (command.method === 'Page.captureScreenshot') {
1366
+ capturedCommands.push(command);
1367
+ }
1368
+ };
1369
+ testCtx.relayServer.on('cdp:command', commandHandler);
1370
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1371
+ let cdpPage;
1372
+ for (const p of browser.contexts()[0].pages()) {
1373
+ const html = await p.content();
1374
+ if (html.includes('scrolled-target')) {
1375
+ cdpPage = p;
1376
+ break;
1377
+ }
1378
+ }
1379
+ expect(cdpPage).toBeDefined();
1380
+ await cdpPage.locator('#target').screenshot();
1381
+ await cdpPage.locator('#scrolled-target').screenshot();
1382
+ testCtx.relayServer.off('cdp:command', commandHandler);
1383
+ expect(capturedCommands.length).toBe(2);
1384
+ const targetCmd = capturedCommands[0];
1385
+ expect(targetCmd.method).toBe('Page.captureScreenshot');
1386
+ const targetClip = targetCmd.params.clip;
1387
+ expect(targetClip.x).toBe(target.x);
1388
+ expect(targetClip.y).toBe(target.y);
1389
+ expect(targetClip.width).toBe(target.width);
1390
+ expect(targetClip.height).toBe(target.height);
1391
+ const scrolledCmd = capturedCommands[1];
1392
+ expect(scrolledCmd.method).toBe('Page.captureScreenshot');
1393
+ const scrolledClip = scrolledCmd.params.clip;
1394
+ expect(scrolledClip.x).toBe(scrolledTarget.x);
1395
+ expect(scrolledClip.y).toBe(scrolledTarget.y);
1396
+ expect(scrolledClip.width).toBe(scrolledTarget.width);
1397
+ expect(scrolledClip.height).toBe(scrolledTarget.height);
1398
+ await browser.close();
1399
+ await page.close();
1400
+ }, 60000);
1401
+ it('should get locator string for element using getLocatorStringForElement', async () => {
1402
+ const browserContext = getBrowserContext();
1403
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1404
+ const page = await browserContext.newPage();
1405
+ await page.setContent(`
1406
+ <html>
1407
+ <body>
1408
+ <button id="test-btn">Click Me</button>
1409
+ <input type="text" placeholder="Enter name" />
1410
+ </body>
1411
+ </html>
1412
+ `);
1413
+ await page.bringToFront();
1414
+ await serviceWorker.evaluate(async () => {
1415
+ await globalThis.toggleExtensionForActiveTab();
1416
+ });
1417
+ await new Promise((r) => setTimeout(r, 400));
1418
+ const result = await client.callTool({
1419
+ name: 'execute',
1420
+ arguments: {
1421
+ intend: 'Get locator string for button element',
1422
+ code: js `
1423
+ let testPage;
1424
+ for (const p of context.pages()) {
1425
+ const html = await p.content();
1426
+ if (html.includes('test-btn')) { testPage = p; break; }
1427
+ }
1428
+ if (!testPage) throw new Error('Test page not found');
1429
+ const btn = testPage.locator('#test-btn');
1430
+ const locatorString = await getLocatorStringForElement(btn);
1431
+ console.log('Locator string:', locatorString);
1432
+ const locatorFromString = eval('testPage.' + locatorString);
1433
+ const count = await locatorFromString.count();
1434
+ console.log('Locator count:', count);
1435
+ const text = await locatorFromString.textContent();
1436
+ console.log('Locator text:', text);
1437
+ `,
1438
+ timeout: 30000,
1439
+ },
1440
+ });
1441
+ expect(result.isError).toBeFalsy();
1442
+ const text = result.content[0]?.text || '';
1443
+ expect(text).toContain('Locator string:');
1444
+ expect(text).toContain("getByRole('button', { name: 'Click Me' })");
1445
+ expect(text).toContain('Locator count: 1');
1446
+ expect(text).toContain('Locator text: Click Me');
1447
+ await page.close();
1448
+ }, 60000);
1449
+ it('should get styles for element using getStylesForLocator', async () => {
1450
+ const browserContext = getBrowserContext();
1451
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1452
+ const page = await browserContext.newPage();
1453
+ await page.setContent(`
1454
+ <html>
1455
+ <head>
1456
+ <style>
1457
+ body { font-family: Arial, sans-serif; color: #333; }
1458
+ .container { padding: 20px; margin: 10px; }
1459
+ #main-btn { background-color: blue; color: white; border-radius: 4px; }
1460
+ .btn { padding: 8px 16px; }
1461
+ </style>
1462
+ </head>
1463
+ <body>
1464
+ <div class="container">
1465
+ <button id="main-btn" class="btn" style="font-weight: bold;">Click Me</button>
1466
+ </div>
1467
+ </body>
1468
+ </html>
1469
+ `);
1470
+ await page.bringToFront();
1471
+ await serviceWorker.evaluate(async () => {
1472
+ await globalThis.toggleExtensionForActiveTab();
1473
+ });
1474
+ await new Promise((r) => setTimeout(r, 400));
1475
+ const stylesResult = await client.callTool({
1476
+ name: 'execute',
1477
+ arguments: {
1478
+ code: js `
1479
+ let testPage;
1480
+ for (const p of context.pages()) {
1481
+ const html = await p.content();
1482
+ if (html.includes('main-btn')) { testPage = p; break; }
1483
+ }
1484
+ if (!testPage) throw new Error('Test page not found');
1485
+ const btn = testPage.locator('#main-btn');
1486
+ const styles = await getStylesForLocator({ locator: btn });
1487
+ return styles;
1488
+ `,
1489
+ timeout: 30000,
1490
+ },
1491
+ });
1492
+ expect(stylesResult.isError).toBeFalsy();
1493
+ const stylesText = stylesResult.content[0]?.text || '';
1494
+ expect(stylesText).toMatchInlineSnapshot(`
1495
+ "Return value:
1496
+ {
1497
+ "element": "button#main-btn.btn",
1498
+ "inlineStyle": {
1499
+ "font-weight": "bold"
1500
+ },
1501
+ "rules": [
1502
+ {
1503
+ "selector": ".btn",
1504
+ "source": null,
1505
+ "origin": "regular",
1506
+ "declarations": {
1507
+ "padding": "8px 16px",
1508
+ "padding-top": "8px",
1509
+ "padding-right": "16px",
1510
+ "padding-bottom": "8px",
1511
+ "padding-left": "16px"
1512
+ },
1513
+ "inheritedFrom": null
1514
+ },
1515
+ {
1516
+ "selector": "#main-btn",
1517
+ "source": null,
1518
+ "origin": "regular",
1519
+ "declarations": {
1520
+ "background-color": "blue",
1521
+ "color": "white",
1522
+ "border-radius": "4px",
1523
+ "border-top-left-radius": "4px",
1524
+ "border-top-right-radius": "4px",
1525
+ "border-bottom-right-radius": "4px",
1526
+ "border-bottom-left-radius": "4px"
1527
+ },
1528
+ "inheritedFrom": null
1529
+ },
1530
+ {
1531
+ "selector": ".container",
1532
+ "source": null,
1533
+ "origin": "regular",
1534
+ "declarations": {
1535
+ "padding": "20px",
1536
+ "margin": "10px",
1537
+ "padding-top": "20px",
1538
+ "padding-right": "20px",
1539
+ "padding-bottom": "20px",
1540
+ "padding-left": "20px",
1541
+ "margin-top": "10px",
1542
+ "margin-right": "10px",
1543
+ "margin-bottom": "10px",
1544
+ "margin-left": "10px"
1545
+ },
1546
+ "inheritedFrom": "ancestor[1]"
1547
+ },
1548
+ {
1549
+ "selector": "body",
1550
+ "source": null,
1551
+ "origin": "regular",
1552
+ "declarations": {
1553
+ "font-family": "Arial, sans-serif",
1554
+ "color": "rgb(51, 51, 51)"
1555
+ },
1556
+ "inheritedFrom": "ancestor[2]"
1557
+ }
1558
+ ]
1559
+ }"
1560
+ `);
1561
+ const formattedResult = await client.callTool({
1562
+ name: 'execute',
1563
+ arguments: {
1564
+ code: js `
1565
+ let testPage;
1566
+ for (const p of context.pages()) {
1567
+ const html = await p.content();
1568
+ if (html.includes('main-btn')) { testPage = p; break; }
1569
+ }
1570
+ if (!testPage) throw new Error('Test page not found');
1571
+ const btn = testPage.locator('#main-btn');
1572
+ const styles = await getStylesForLocator({ locator: btn });
1573
+ return formatStylesAsText(styles);
1574
+ `,
1575
+ timeout: 30000,
1576
+ },
1577
+ });
1578
+ expect(formattedResult.isError).toBeFalsy();
1579
+ const formattedText = formattedResult.content[0]?.text || '';
1580
+ expect(formattedText).toMatchInlineSnapshot(`
1581
+ "Return value:
1582
+ Element: button#main-btn.btn
1583
+
1584
+ Inline styles:
1585
+ font-weight: bold
1586
+
1587
+ Matched rules:
1588
+ .btn {
1589
+ padding: 8px 16px;
1590
+ padding-top: 8px;
1591
+ padding-right: 16px;
1592
+ padding-bottom: 8px;
1593
+ padding-left: 16px;
1594
+ }
1595
+ #main-btn {
1596
+ background-color: blue;
1597
+ color: white;
1598
+ border-radius: 4px;
1599
+ border-top-left-radius: 4px;
1600
+ border-top-right-radius: 4px;
1601
+ border-bottom-right-radius: 4px;
1602
+ border-bottom-left-radius: 4px;
1603
+ }
1604
+
1605
+ Inherited from ancestor[1]:
1606
+ .container {
1607
+ padding: 20px;
1608
+ margin: 10px;
1609
+ padding-top: 20px;
1610
+ padding-right: 20px;
1611
+ padding-bottom: 20px;
1612
+ padding-left: 20px;
1613
+ margin-top: 10px;
1614
+ margin-right: 10px;
1615
+ margin-bottom: 10px;
1616
+ margin-left: 10px;
1617
+ }
1618
+
1619
+ Inherited from ancestor[2]:
1620
+ body {
1621
+ font-family: Arial, sans-serif;
1622
+ color: rgb(51, 51, 51);
1623
+ }"
1624
+ `);
1625
+ await page.close();
1626
+ }, 60000);
1627
+ it('should return correct layout metrics via CDP', async () => {
1628
+ const browserContext = getBrowserContext();
1629
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1630
+ const page = await browserContext.newPage();
1631
+ await page.goto('https://example.com/');
1632
+ await page.bringToFront();
1633
+ await serviceWorker.evaluate(async () => {
1634
+ await globalThis.toggleExtensionForActiveTab();
1635
+ });
1636
+ await new Promise((r) => setTimeout(r, 100));
1637
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1638
+ const cdpPage = browser
1639
+ .contexts()[0]
1640
+ .pages()
1641
+ .find((p) => p.url().includes('example.com'));
1642
+ expect(cdpPage).toBeDefined();
1643
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
1644
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
1645
+ const layoutMetrics = await cdpSession.send('Page.getLayoutMetrics');
1646
+ const normalized = {
1647
+ cssLayoutViewport: layoutMetrics.cssLayoutViewport,
1648
+ cssVisualViewport: layoutMetrics.cssVisualViewport,
1649
+ layoutViewport: layoutMetrics.layoutViewport,
1650
+ visualViewport: layoutMetrics.visualViewport,
1651
+ devicePixelRatio: layoutMetrics.cssVisualViewport.clientWidth > 0
1652
+ ? layoutMetrics.visualViewport.clientWidth / layoutMetrics.cssVisualViewport.clientWidth
1653
+ : 1,
1654
+ };
1655
+ expect(normalized).toMatchInlineSnapshot(`
1656
+ {
1657
+ "cssLayoutViewport": {
1658
+ "clientHeight": 581,
1659
+ "clientWidth": 1280,
1660
+ "pageX": 0,
1661
+ "pageY": 0,
1662
+ },
1663
+ "cssVisualViewport": {
1664
+ "clientHeight": 581,
1665
+ "clientWidth": 1280,
1666
+ "offsetX": 0,
1667
+ "offsetY": 0,
1668
+ "pageX": 0,
1669
+ "pageY": 0,
1670
+ "scale": 1,
1671
+ "zoom": 1,
1672
+ },
1673
+ "devicePixelRatio": 1,
1674
+ "layoutViewport": {
1675
+ "clientHeight": 581,
1676
+ "clientWidth": 1280,
1677
+ "pageX": 0,
1678
+ "pageY": 0,
1679
+ },
1680
+ "visualViewport": {
1681
+ "clientHeight": 581,
1682
+ "clientWidth": 1280,
1683
+ "offsetX": 0,
1684
+ "offsetY": 0,
1685
+ "pageX": 0,
1686
+ "pageY": 0,
1687
+ "scale": 1,
1688
+ "zoom": 1,
1689
+ },
1690
+ }
1691
+ `);
1692
+ const windowDpr = await cdpPage.evaluate(() => globalThis.devicePixelRatio);
1693
+ console.log('window.devicePixelRatio:', windowDpr);
1694
+ expect(windowDpr).toBe(1);
1695
+ cdpSession.close();
1696
+ await browser.close();
1697
+ await page.close();
1698
+ }, 60000);
1699
+ it('should support getCDPSession through the relay', async () => {
1700
+ const browserContext = getBrowserContext();
1701
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1702
+ const page = await browserContext.newPage();
1703
+ await page.goto('https://example.com/');
1704
+ await page.bringToFront();
1705
+ await serviceWorker.evaluate(async () => {
1706
+ await globalThis.toggleExtensionForActiveTab();
1707
+ });
1708
+ await new Promise((r) => setTimeout(r, 100));
1709
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1710
+ const cdpPage = browser
1711
+ .contexts()[0]
1712
+ .pages()
1713
+ .find((p) => p.url().includes('example.com'));
1714
+ expect(cdpPage).toBeDefined();
1715
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
1716
+ const client = await getCDPSessionForPage({ page: cdpPage, wsUrl });
1717
+ const layoutMetrics = await client.send('Page.getLayoutMetrics');
1718
+ expect(layoutMetrics.cssVisualViewport).toBeDefined();
1719
+ expect(layoutMetrics.cssVisualViewport.clientWidth).toBeGreaterThan(0);
1720
+ client.close();
1721
+ await browser.close();
1722
+ await page.close();
1723
+ }, 60000);
1724
+ it('should work with stagehand', async () => {
1725
+ const browserContext = getBrowserContext();
1726
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1727
+ await serviceWorker.evaluate(async () => {
1728
+ await globalThis.disconnectEverything();
1729
+ });
1730
+ await new Promise((r) => setTimeout(r, 100));
1731
+ const targetUrl = 'https://example.com/';
1732
+ const enableResult = await serviceWorker.evaluate(async (url) => {
1733
+ const tab = await chrome.tabs.create({ url, active: true });
1734
+ await new Promise((r) => setTimeout(r, 100));
1735
+ return await globalThis.toggleExtensionForActiveTab();
1736
+ }, targetUrl);
1737
+ console.log('Extension enabled:', enableResult);
1738
+ expect(enableResult.isConnected).toBe(true);
1739
+ await new Promise((r) => setTimeout(r, 100));
1740
+ const { Stagehand } = await import('@browserbasehq/stagehand');
1741
+ const stagehand = new Stagehand({
1742
+ env: 'LOCAL',
1743
+ verbose: 1,
1744
+ disablePino: true,
1745
+ localBrowserLaunchOptions: {
1746
+ cdpUrl: getCdpUrl({ port: TEST_PORT }),
1747
+ },
1748
+ });
1749
+ console.log('Initializing Stagehand...');
1750
+ await stagehand.init();
1751
+ console.log('Stagehand initialized');
1752
+ const context = stagehand.context;
1753
+ // console.log('Stagehand context:', context)
1754
+ expect(context).toBeDefined();
1755
+ const pages = context.pages();
1756
+ console.log('Stagehand pages:', pages.length, pages.map((p) => p.url()));
1757
+ const stagehandPage = pages.find((p) => p.url().includes('example.com'));
1758
+ expect(stagehandPage).toBeDefined();
1759
+ const url = stagehandPage.url();
1760
+ console.log('Stagehand page URL:', url);
1761
+ expect(url).toContain('example.com');
1762
+ await stagehand.close();
1763
+ }, 60000);
1764
+ it('should preserve system color scheme instead of forcing light mode', async () => {
1765
+ const browserContext = getBrowserContext();
1766
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1767
+ const page = await browserContext.newPage();
1768
+ await page.goto('https://example.com');
1769
+ await page.bringToFront();
1770
+ const colorSchemeBefore = await page.evaluate(() => {
1771
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
1772
+ });
1773
+ console.log('Color scheme before MCP connection:', colorSchemeBefore);
1774
+ await serviceWorker.evaluate(async () => {
1775
+ await globalThis.toggleExtensionForActiveTab();
1776
+ });
1777
+ await new Promise((r) => setTimeout(r, 100));
1778
+ const result = await client.callTool({
1779
+ name: 'execute',
1780
+ arguments: {
1781
+ code: js `
1782
+ const pages = context.pages();
1783
+ const urls = pages.map(p => p.url());
1784
+ const targetPage = pages.find(p => p.url().includes('example.com'));
1785
+ if (!targetPage) {
1786
+ return { error: 'Page not found', urls };
1787
+ }
1788
+ const isDark = await targetPage.evaluate(() => window.matchMedia('(prefers-color-scheme: dark)').matches);
1789
+ const isLight = await targetPage.evaluate(() => window.matchMedia('(prefers-color-scheme: light)').matches);
1790
+ return { matchesDark: isDark, matchesLight: isLight };
1791
+ `,
1792
+ },
1793
+ });
1794
+ console.log('Color scheme after MCP connection:', result.content);
1795
+ expect(result.content).toMatchInlineSnapshot(`
1796
+ [
1797
+ {
1798
+ "text": "MCP error -32602: Input validation error: Invalid arguments for tool execute: [
1799
+ {
1800
+ "code": "invalid_type",
1801
+ "expected": "string",
1802
+ "received": "undefined",
1803
+ "path": [
1804
+ "intend"
1805
+ ],
1806
+ "message": "Required"
1807
+ }
1808
+ ]",
1809
+ "type": "text",
1810
+ },
1811
+ ]
1812
+ `);
1813
+ await page.close();
1814
+ }, 60000);
1815
+ it('should get aria ref for locator using getAriaSnapshot', async () => {
1816
+ const browserContext = getBrowserContext();
1817
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1818
+ const page = await browserContext.newPage();
1819
+ await page.setContent(`
1820
+ <html>
1821
+ <body>
1822
+ <button id="submit-btn">Submit Form</button>
1823
+ <a href="/about">About Us</a>
1824
+ <input type="text" placeholder="Enter your name" />
1825
+ </body>
1826
+ </html>
1827
+ `);
1828
+ await page.bringToFront();
1829
+ await serviceWorker.evaluate(async () => {
1830
+ await globalThis.toggleExtensionForActiveTab();
1831
+ });
1832
+ await new Promise((r) => setTimeout(r, 400));
1833
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1834
+ let cdpPage;
1835
+ for (const p of browser.contexts()[0].pages()) {
1836
+ const html = await p.content();
1837
+ if (html.includes('submit-btn')) {
1838
+ cdpPage = p;
1839
+ break;
1840
+ }
1841
+ }
1842
+ expect(cdpPage).toBeDefined();
1843
+ const { getAriaSnapshot } = await import('./aria-snapshot.js');
1844
+ // Get aria snapshot and verify we can get refs
1845
+ const ariaResult = await getAriaSnapshot({ page: cdpPage });
1846
+ expect(ariaResult.snapshot).toBeDefined();
1847
+ expect(ariaResult.snapshot.length).toBeGreaterThan(0);
1848
+ expect(ariaResult.snapshot).toContain('Submit Form');
1849
+ // Verify refToElement map is populated
1850
+ expect(ariaResult.refToElement.size).toBeGreaterThan(0);
1851
+ console.log('RefToElement map size:', ariaResult.refToElement.size);
1852
+ console.log('RefToElement entries:', [...ariaResult.refToElement.entries()]);
1853
+ // Verify we can select elements using aria-ref selectors
1854
+ const btnViaAriaRef = cdpPage.locator('aria-ref=e2');
1855
+ const btnTextViaRef = await btnViaAriaRef.textContent();
1856
+ console.log('Button text via aria-ref=e2:', btnTextViaRef);
1857
+ expect(btnTextViaRef).toBe('Submit Form');
1858
+ // Get ref for the submit button using getRefForLocator
1859
+ const submitBtn = cdpPage.locator('#submit-btn');
1860
+ const btnAriaRef = await ariaResult.getRefForLocator(submitBtn);
1861
+ console.log('Button ariaRef:', btnAriaRef);
1862
+ expect(btnAriaRef).toBeDefined();
1863
+ expect(btnAriaRef?.role).toBe('button');
1864
+ expect(btnAriaRef?.name).toBe('Submit Form');
1865
+ expect(btnAriaRef?.ref).toMatch(/^e\d+$/);
1866
+ // Verify the ref matches what we can use to select
1867
+ const btnFromRef = cdpPage.locator(`aria-ref=${btnAriaRef?.ref}`);
1868
+ const btnText = await btnFromRef.textContent();
1869
+ expect(btnText).toBe('Submit Form');
1870
+ // Test getRefStringForLocator
1871
+ const btnRefStr = await ariaResult.getRefStringForLocator(submitBtn);
1872
+ console.log('Button ref string:', btnRefStr);
1873
+ expect(btnRefStr).toBe(btnAriaRef?.ref);
1874
+ // Test link
1875
+ const aboutLink = cdpPage.locator('a');
1876
+ const linkAriaRef = await ariaResult.getRefForLocator(aboutLink);
1877
+ console.log('Link ariaRef:', linkAriaRef);
1878
+ expect(linkAriaRef).toBeDefined();
1879
+ expect(linkAriaRef?.role).toBe('link');
1880
+ expect(linkAriaRef?.name).toBe('About Us');
1881
+ // Verify the link ref works
1882
+ const linkFromRef = cdpPage.locator(`aria-ref=${linkAriaRef?.ref}`);
1883
+ const linkText = await linkFromRef.textContent();
1884
+ expect(linkText).toBe('About Us');
1885
+ // Test input field
1886
+ const inputField = cdpPage.locator('input');
1887
+ const inputAriaRef = await ariaResult.getRefForLocator(inputField);
1888
+ console.log('Input ariaRef:', inputAriaRef);
1889
+ expect(inputAriaRef).toBeDefined();
1890
+ expect(inputAriaRef?.role).toBe('textbox');
1891
+ // Test batch getRefsForLocators - single evaluate call for multiple elements
1892
+ const batchRefs = await ariaResult.getRefsForLocators([submitBtn, aboutLink, inputField]);
1893
+ console.log('Batch refs:', batchRefs);
1894
+ expect(batchRefs).toHaveLength(3);
1895
+ expect(batchRefs[0]?.ref).toBe(btnAriaRef?.ref);
1896
+ expect(batchRefs[1]?.ref).toBe(linkAriaRef?.ref);
1897
+ expect(batchRefs[2]?.ref).toBe(inputAriaRef?.ref);
1898
+ await browser.close();
1899
+ await page.close();
1900
+ }, 60000);
1901
+ it('should show aria ref labels on real pages and save screenshots', async () => {
1902
+ const browserContext = getBrowserContext();
1903
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1904
+ const { showAriaRefLabels, hideAriaRefLabels } = await import('./aria-snapshot.js');
1905
+ const fs = await import('node:fs');
1906
+ const path = await import('node:path');
1907
+ // Create assets folder for screenshots
1908
+ const assetsDir = path.join(path.dirname(new URL(import.meta.url).pathname), 'assets');
1909
+ if (!fs.existsSync(assetsDir)) {
1910
+ fs.mkdirSync(assetsDir, { recursive: true });
1911
+ }
1912
+ const testPages = [
1913
+ { name: 'hacker-news', url: 'https://news.ycombinator.com/' },
1914
+ { name: 'google', url: 'https://www.google.com/' },
1915
+ { name: 'github', url: 'https://github.com/' },
1916
+ ];
1917
+ // Create all pages and enable extension for each
1918
+ const pages = await Promise.all(testPages.map(async ({ name, url }) => {
1919
+ const page = await browserContext.newPage();
1920
+ await page.goto(url, { waitUntil: 'domcontentloaded' });
1921
+ return { name, url, page };
1922
+ }));
1923
+ // Enable extension for each tab (must be done sequentially as it uses active tab)
1924
+ for (const { page } of pages) {
1925
+ await page.bringToFront();
1926
+ await serviceWorker.evaluate(async () => {
1927
+ await globalThis.toggleExtensionForActiveTab();
1928
+ });
1929
+ }
1930
+ // Connect CDP and process all pages concurrently
1931
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
1932
+ await Promise.all(pages.map(async ({ name, url, page }) => {
1933
+ const cdpPage = browser
1934
+ .contexts()[0]
1935
+ .pages()
1936
+ .find((p) => p.url().includes(new URL(url).hostname));
1937
+ if (!cdpPage) {
1938
+ console.log(`Could not find CDP page for ${name}, skipping...`);
1939
+ return;
1940
+ }
1941
+ // Show aria ref labels
1942
+ const { snapshot, labelCount } = await showAriaRefLabels({ page: cdpPage });
1943
+ console.log(`${name}: ${labelCount} labels shown`);
1944
+ expect(labelCount).toBeGreaterThan(0);
1945
+ // Take screenshot with labels visible
1946
+ const screenshot = await cdpPage.screenshot({ type: 'png', fullPage: false });
1947
+ const screenshotPath = path.join(assetsDir, `aria-labels-${name}.png`);
1948
+ fs.writeFileSync(screenshotPath, screenshot);
1949
+ console.log(`Screenshot saved: ${screenshotPath}`);
1950
+ // Save snapshot text for reference
1951
+ const snapshotPath = path.join(assetsDir, `aria-labels-${name}-snapshot.txt`);
1952
+ fs.writeFileSync(snapshotPath, snapshot);
1953
+ // Verify labels are in DOM
1954
+ const labelElements = await cdpPage.evaluate(() => document.querySelectorAll('.__pw_label__').length);
1955
+ expect(labelElements).toBe(labelCount);
1956
+ // Cleanup
1957
+ await hideAriaRefLabels({ page: cdpPage });
1958
+ // Verify labels removed
1959
+ const labelsAfterHide = await cdpPage.evaluate(() => document.getElementById('__e2e_pilot_labels__'));
1960
+ expect(labelsAfterHide).toBeNull();
1961
+ await page.close();
1962
+ }));
1963
+ await browser.close();
1964
+ console.log(`Screenshots saved to: ${assetsDir}`);
1965
+ }, 120000);
1966
+ it('should take screenshot with accessibility labels via MCP execute tool', async () => {
1967
+ const browserContext = getBrowserContext();
1968
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
1969
+ const page = await browserContext.newPage();
1970
+ await page.setContent(`
1971
+ <html>
1972
+ <head>
1973
+ <style>
1974
+ body {
1975
+ margin: 0;
1976
+ background: #e8f4f8;
1977
+ position: relative;
1978
+ min-height: 100vh;
1979
+ }
1980
+ .controls {
1981
+ padding: 20px;
1982
+ position: relative;
1983
+ z-index: 10;
1984
+ }
1985
+ .grid-marker {
1986
+ position: absolute;
1987
+ background: rgba(255, 100, 100, 0.3);
1988
+ border: 1px solid #ff6464;
1989
+ font-size: 10px;
1990
+ color: #333;
1991
+ display: flex;
1992
+ align-items: center;
1993
+ justify-content: center;
1994
+ }
1995
+ .h-marker {
1996
+ left: 0;
1997
+ width: 100%;
1998
+ height: 20px;
1999
+ }
2000
+ .v-marker {
2001
+ top: 0;
2002
+ height: 100%;
2003
+ width: 20px;
2004
+ }
2005
+ </style>
2006
+ </head>
2007
+ <body>
2008
+ <div class="controls">
2009
+ <button id="submit-btn">Submit Form</button>
2010
+ <a href="/about">About Us</a>
2011
+ <input type="text" placeholder="Enter your name" />
2012
+ </div>
2013
+ <!-- Horizontal markers every 200px -->
2014
+ <div class="grid-marker h-marker" style="top: 200px;">200px</div>
2015
+ <div class="grid-marker h-marker" style="top: 400px;">400px</div>
2016
+ <div class="grid-marker h-marker" style="top: 600px;">600px</div>
2017
+ <!-- Vertical markers every 200px -->
2018
+ <div class="grid-marker v-marker" style="left: 200px;">200</div>
2019
+ <div class="grid-marker v-marker" style="left: 400px;">400</div>
2020
+ <div class="grid-marker v-marker" style="left: 600px;">600</div>
2021
+ <div class="grid-marker v-marker" style="left: 800px;">800</div>
2022
+ <div class="grid-marker v-marker" style="left: 1000px;">1000</div>
2023
+ <div class="grid-marker v-marker" style="left: 1200px;">1200</div>
2024
+ </body>
2025
+ </html>
2026
+ `);
2027
+ await page.bringToFront();
2028
+ await serviceWorker.evaluate(async () => {
2029
+ await globalThis.toggleExtensionForActiveTab();
2030
+ });
2031
+ await new Promise((r) => setTimeout(r, 400));
2032
+ // Take screenshot with accessibility labels via MCP
2033
+ const result = await client.callTool({
2034
+ name: 'execute',
2035
+ arguments: {
2036
+ code: js `
2037
+ let testPage;
2038
+ for (const p of context.pages()) {
2039
+ const html = await p.content();
2040
+ if (html.includes('submit-btn')) { testPage = p; break; }
2041
+ }
2042
+ if (!testPage) throw new Error('Test page not found');
2043
+ await screenshotWithAccessibilityLabels({ page: testPage });
2044
+ `,
2045
+ timeout: 15000,
2046
+ },
2047
+ });
2048
+ expect(result.isError).toBeFalsy();
2049
+ // Verify response has both text and image content
2050
+ const content = result.content;
2051
+ expect(content.length).toBe(2);
2052
+ // Check text content
2053
+ const textContent = content.find((c) => c.type === 'text');
2054
+ expect(textContent).toBeDefined();
2055
+ expect(textContent.text).toContain('Screenshot saved to:');
2056
+ expect(textContent.text).toContain('.jpg');
2057
+ expect(textContent.text).toContain('Labels shown:');
2058
+ expect(textContent.text).toContain('Accessibility snapshot:');
2059
+ expect(textContent.text).toContain('Submit Form');
2060
+ // Check image content
2061
+ const imageContent = content.find((c) => c.type === 'image');
2062
+ expect(imageContent).toBeDefined();
2063
+ expect(imageContent.mimeType).toBe('image/jpeg');
2064
+ expect(imageContent.data).toBeDefined();
2065
+ expect(imageContent.data.length).toBeGreaterThan(100); // base64 data should be substantial
2066
+ // Verify the image is valid JPEG by checking base64
2067
+ const buffer = Buffer.from(imageContent.data, 'base64');
2068
+ const dimensions = imageSize(buffer);
2069
+ // Get actual viewport size from page
2070
+ const viewport = await page.evaluate(() => ({
2071
+ innerWidth: window.innerWidth,
2072
+ innerHeight: window.innerHeight,
2073
+ outerWidth: window.outerWidth,
2074
+ outerHeight: window.outerHeight,
2075
+ }));
2076
+ console.log('Screenshot dimensions:', dimensions.width, 'x', dimensions.height);
2077
+ console.log('Window viewport:', viewport);
2078
+ expect(dimensions.type).toBe('jpg');
2079
+ expect(dimensions.width).toBeGreaterThan(0);
2080
+ expect(dimensions.height).toBeGreaterThan(0);
2081
+ await page.close();
2082
+ }, 60000);
2083
+ it('should capture network requests during execute', async () => {
2084
+ const browserContext = getBrowserContext();
2085
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2086
+ const page = await browserContext.newPage();
2087
+ await page.setContent(`
2088
+ <html>
2089
+ <body>
2090
+ <button id="fetch-btn">Fetch Data</button>
2091
+ <div id="result"></div>
2092
+ <script>
2093
+ document.getElementById('fetch-btn').addEventListener('click', async () => {
2094
+ const response = await fetch('https://httpbin.org/post', {
2095
+ method: 'POST',
2096
+ headers: { 'Content-Type': 'application/json' },
2097
+ body: JSON.stringify({ test: 'data' })
2098
+ });
2099
+ const data = await response.json();
2100
+ document.getElementById('result').textContent = JSON.stringify(data);
2101
+ });
2102
+ </script>
2103
+ </body>
2104
+ </html>
2105
+ `);
2106
+ await page.bringToFront();
2107
+ await serviceWorker.evaluate(async () => {
2108
+ await globalThis.toggleExtensionForActiveTab();
2109
+ });
2110
+ await new Promise((r) => setTimeout(r, 400));
2111
+ // Execute code that triggers a network request
2112
+ const result = await client.callTool({
2113
+ name: 'execute',
2114
+ arguments: {
2115
+ intend: 'Test network capture',
2116
+ code: js `
2117
+ let testPage;
2118
+ for (const p of context.pages()) {
2119
+ const html = await p.content();
2120
+ if (html.includes('fetch-btn')) { testPage = p; break; }
2121
+ }
2122
+ if (!testPage) throw new Error('Test page not found');
2123
+ await testPage.click('#fetch-btn');
2124
+ await testPage.waitForSelector('#result:not(:empty)', { timeout: 10000 });
2125
+ `,
2126
+ timeout: 15000,
2127
+ },
2128
+ });
2129
+ expect(result.isError).toBeFalsy();
2130
+ // Parse the response to get snapshot path
2131
+ const content = result.content;
2132
+ const textContent = content.find((c) => c.type === 'text');
2133
+ expect(textContent).toBeDefined();
2134
+ // Extract snapshot path from response
2135
+ const match = textContent.text.match(/Full snapshot: (.+\.e2e-pilot\/snapshots\/[^\/]+)\/full\.txt/);
2136
+ expect(match).toBeTruthy();
2137
+ const snapshotDir = match[1];
2138
+ const networksPath = path.join(snapshotDir, 'networks.jsonl');
2139
+ // Check that networks.jsonl was created
2140
+ expect(fs.existsSync(networksPath)).toBe(true);
2141
+ // Read and verify the network log
2142
+ const networksContent = fs.readFileSync(networksPath, 'utf-8');
2143
+ const lines = networksContent.trim().split('\n');
2144
+ expect(lines.length).toBeGreaterThan(0);
2145
+ // Parse and verify the captured request
2146
+ const networkEntry = JSON.parse(lines[0]);
2147
+ expect(networkEntry.method).toBe('POST');
2148
+ expect(networkEntry.url).toContain('httpbin.org/post');
2149
+ expect(networkEntry.status).toBe(200);
2150
+ expect(networkEntry.reqBody).toContain('test');
2151
+ expect(networkEntry.resBody).toBeDefined();
2152
+ await page.close();
2153
+ }, 60000);
2154
+ });
2155
+ function tryJsonParse(str) {
2156
+ try {
2157
+ return JSON.parse(str);
2158
+ }
2159
+ catch {
2160
+ return str;
2161
+ }
2162
+ }
2163
+ describe('CDP Session Tests', () => {
2164
+ let testCtx = null;
2165
+ beforeAll(async () => {
2166
+ testCtx = await setupTestContext({ tempDirPrefix: 'pw-cdp-test-' });
2167
+ const serviceWorker = await getExtensionServiceWorker(testCtx.browserContext);
2168
+ await serviceWorker.evaluate(async () => {
2169
+ await globalThis.disconnectEverything();
2170
+ });
2171
+ await new Promise((r) => setTimeout(r, 100));
2172
+ }, 600000);
2173
+ afterAll(async () => {
2174
+ await cleanupTestContext(testCtx);
2175
+ testCtx = null;
2176
+ });
2177
+ const getBrowserContext = () => {
2178
+ if (!testCtx?.browserContext)
2179
+ throw new Error('Browser not initialized');
2180
+ return testCtx.browserContext;
2181
+ };
2182
+ it('should use Debugger class to set breakpoints and inspect variables', async () => {
2183
+ const browserContext = getBrowserContext();
2184
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2185
+ const page = await browserContext.newPage();
2186
+ await page.goto('https://example.com/');
2187
+ await page.bringToFront();
2188
+ await serviceWorker.evaluate(async () => {
2189
+ await globalThis.toggleExtensionForActiveTab();
2190
+ });
2191
+ await new Promise((r) => setTimeout(r, 100));
2192
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2193
+ const cdpPage = browser
2194
+ .contexts()[0]
2195
+ .pages()
2196
+ .find((p) => p.url().includes('example.com'));
2197
+ expect(cdpPage).toBeDefined();
2198
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
2199
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
2200
+ const dbg = new Debugger({ cdp: cdpSession });
2201
+ await dbg.enable();
2202
+ expect(dbg.isPaused()).toBe(false);
2203
+ const pausedPromise = new Promise((resolve) => {
2204
+ cdpSession.on('Debugger.paused', () => {
2205
+ resolve();
2206
+ });
2207
+ });
2208
+ cdpPage.evaluate(`
2209
+ (function testFunction() {
2210
+ const localVar = 'hello';
2211
+ const numberVar = 42;
2212
+ debugger;
2213
+ return localVar + numberVar;
2214
+ })()
2215
+ `);
2216
+ await Promise.race([
2217
+ pausedPromise,
2218
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Debugger.paused timeout')), 5000)),
2219
+ ]);
2220
+ expect(dbg.isPaused()).toBe(true);
2221
+ const location = await dbg.getLocation();
2222
+ expect(location.callstack[0].functionName).toBe('testFunction');
2223
+ expect(location.sourceContext).toContain('debugger');
2224
+ const vars = await dbg.inspectLocalVariables();
2225
+ expect(vars).toMatchInlineSnapshot(`
2226
+ {
2227
+ "localVar": "hello",
2228
+ "numberVar": 42,
2229
+ }
2230
+ `);
2231
+ const evalResult = await dbg.evaluate({ expression: 'localVar + " world"' });
2232
+ expect(evalResult.value).toBe('hello world');
2233
+ await dbg.resume();
2234
+ await new Promise((r) => setTimeout(r, 100));
2235
+ expect(dbg.isPaused()).toBe(false);
2236
+ cdpSession.close();
2237
+ await browser.close();
2238
+ await page.close();
2239
+ }, 60000);
2240
+ it('should list scripts with Debugger class', async () => {
2241
+ const browserContext = getBrowserContext();
2242
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2243
+ const page = await browserContext.newPage();
2244
+ await page.setContent(`
2245
+ <html>
2246
+ <head>
2247
+ <script src="data:text/javascript,function testScript() { return 42; }"></script>
2248
+ </head>
2249
+ <body><h1>Script Test</h1></body>
2250
+ </html>
2251
+ `);
2252
+ await page.bringToFront();
2253
+ await serviceWorker.evaluate(async () => {
2254
+ await globalThis.toggleExtensionForActiveTab();
2255
+ });
2256
+ await new Promise((r) => setTimeout(r, 100));
2257
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2258
+ let cdpPage;
2259
+ for (const p of browser.contexts()[0].pages()) {
2260
+ const html = await p.content();
2261
+ if (html.includes('Script Test')) {
2262
+ cdpPage = p;
2263
+ break;
2264
+ }
2265
+ }
2266
+ expect(cdpPage).toBeDefined();
2267
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
2268
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
2269
+ const dbg = new Debugger({ cdp: cdpSession });
2270
+ const scripts = await dbg.listScripts();
2271
+ expect(scripts.length).toBeGreaterThan(0);
2272
+ expect(scripts[0]).toHaveProperty('scriptId');
2273
+ expect(scripts[0]).toHaveProperty('url');
2274
+ const dataScripts = await dbg.listScripts({ search: 'data:' });
2275
+ expect(dataScripts.length).toBeGreaterThan(0);
2276
+ cdpSession.close();
2277
+ await browser.close();
2278
+ await page.close();
2279
+ }, 60000);
2280
+ it('should manage breakpoints with Debugger class', async () => {
2281
+ const browserContext = getBrowserContext();
2282
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2283
+ const page = await browserContext.newPage();
2284
+ await page.setContent(`
2285
+ <html>
2286
+ <head>
2287
+ <script src="data:text/javascript,function testFunc() { return 42; }"></script>
2288
+ </head>
2289
+ <body></body>
2290
+ </html>
2291
+ `);
2292
+ await page.bringToFront();
2293
+ await serviceWorker.evaluate(async () => {
2294
+ await globalThis.toggleExtensionForActiveTab();
2295
+ });
2296
+ await new Promise((r) => setTimeout(r, 100));
2297
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2298
+ let cdpPage;
2299
+ for (const p of browser.contexts()[0].pages()) {
2300
+ const html = await p.content();
2301
+ if (html.includes('testFunc')) {
2302
+ cdpPage = p;
2303
+ break;
2304
+ }
2305
+ }
2306
+ expect(cdpPage).toBeDefined();
2307
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
2308
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
2309
+ const dbg = new Debugger({ cdp: cdpSession });
2310
+ await dbg.enable();
2311
+ expect(dbg.listBreakpoints()).toHaveLength(0);
2312
+ const bpId = await dbg.setBreakpoint({ file: 'https://example.com/test.js', line: 1 });
2313
+ expect(typeof bpId).toBe('string');
2314
+ expect(dbg.listBreakpoints()).toHaveLength(1);
2315
+ expect(dbg.listBreakpoints()[0]).toMatchObject({
2316
+ id: bpId,
2317
+ file: 'https://example.com/test.js',
2318
+ line: 1,
2319
+ });
2320
+ await dbg.deleteBreakpoint({ breakpointId: bpId });
2321
+ expect(dbg.listBreakpoints()).toHaveLength(0);
2322
+ cdpSession.close();
2323
+ await browser.close();
2324
+ await page.close();
2325
+ }, 60000);
2326
+ it('should step through code with Debugger class', async () => {
2327
+ const browserContext = getBrowserContext();
2328
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2329
+ const page = await browserContext.newPage();
2330
+ await page.goto('https://example.com/');
2331
+ await page.bringToFront();
2332
+ await serviceWorker.evaluate(async () => {
2333
+ await globalThis.toggleExtensionForActiveTab();
2334
+ });
2335
+ await new Promise((r) => setTimeout(r, 100));
2336
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2337
+ const cdpPage = browser
2338
+ .contexts()[0]
2339
+ .pages()
2340
+ .find((p) => p.url().includes('example.com'));
2341
+ expect(cdpPage).toBeDefined();
2342
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
2343
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
2344
+ const dbg = new Debugger({ cdp: cdpSession });
2345
+ await dbg.enable();
2346
+ const pausedPromise = new Promise((resolve) => {
2347
+ cdpSession.on('Debugger.paused', () => resolve());
2348
+ });
2349
+ cdpPage.evaluate(`
2350
+ (function outer() {
2351
+ function inner() {
2352
+ const x = 1;
2353
+ debugger;
2354
+ const y = 2;
2355
+ return x + y;
2356
+ }
2357
+ const result = inner();
2358
+ return result;
2359
+ })()
2360
+ `);
2361
+ await pausedPromise;
2362
+ expect(dbg.isPaused()).toBe(true);
2363
+ const location1 = await dbg.getLocation();
2364
+ expect(location1.callstack.length).toBeGreaterThanOrEqual(2);
2365
+ expect(location1.callstack[0].functionName).toBe('inner');
2366
+ expect(location1.callstack[1].functionName).toBe('outer');
2367
+ const stepOverPromise = new Promise((resolve) => {
2368
+ cdpSession.on('Debugger.paused', () => resolve());
2369
+ });
2370
+ await dbg.stepOver();
2371
+ await stepOverPromise;
2372
+ const location2 = await dbg.getLocation();
2373
+ expect(location2.lineNumber).toBeGreaterThan(location1.lineNumber);
2374
+ const stepOutPromise = new Promise((resolve) => {
2375
+ cdpSession.on('Debugger.paused', () => resolve());
2376
+ });
2377
+ await dbg.stepOut();
2378
+ await stepOutPromise;
2379
+ const location3 = await dbg.getLocation();
2380
+ expect(location3.callstack[0].functionName).toBe('outer');
2381
+ await dbg.resume();
2382
+ cdpSession.close();
2383
+ await browser.close();
2384
+ await page.close();
2385
+ }, 60000);
2386
+ it('should profile JavaScript execution using CDP Profiler', async () => {
2387
+ const browserContext = getBrowserContext();
2388
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2389
+ const page = await browserContext.newPage();
2390
+ await page.goto('https://example.com/');
2391
+ await page.bringToFront();
2392
+ await serviceWorker.evaluate(async () => {
2393
+ await globalThis.toggleExtensionForActiveTab();
2394
+ });
2395
+ await new Promise((r) => setTimeout(r, 100));
2396
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2397
+ const cdpPage = browser
2398
+ .contexts()[0]
2399
+ .pages()
2400
+ .find((p) => p.url().includes('example.com'));
2401
+ expect(cdpPage).toBeDefined();
2402
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
2403
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
2404
+ await cdpSession.send('Profiler.enable');
2405
+ await cdpSession.send('Profiler.start');
2406
+ await cdpPage.evaluate(`
2407
+ (() => {
2408
+ function fibonacci(n) {
2409
+ if (n <= 1) return n
2410
+ return fibonacci(n - 1) + fibonacci(n - 2)
2411
+ }
2412
+ for (let i = 0; i < 5; i++) {
2413
+ fibonacci(20)
2414
+ }
2415
+ for (let i = 0; i < 1000; i++) {
2416
+ document.querySelectorAll('*')
2417
+ }
2418
+ })()
2419
+ `);
2420
+ const stopResult = await cdpSession.send('Profiler.stop');
2421
+ const profile = stopResult.profile;
2422
+ const functionNames = profile.nodes
2423
+ .map((n) => n.callFrame.functionName)
2424
+ .filter((name) => name && name.length > 0)
2425
+ .slice(0, 10);
2426
+ expect(profile.nodes.length).toBeGreaterThan(0);
2427
+ expect(profile.endTime - profile.startTime).toBeGreaterThan(0);
2428
+ expect(functionNames.every((name) => typeof name === 'string')).toBe(true);
2429
+ await cdpSession.send('Profiler.disable');
2430
+ cdpSession.close();
2431
+ await browser.close();
2432
+ await page.close();
2433
+ }, 60000);
2434
+ it('should update Target.getTargets URL after page navigation', async () => {
2435
+ const browserContext = getBrowserContext();
2436
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2437
+ // Clear any existing connected tabs from previous tests
2438
+ await serviceWorker.evaluate(async () => {
2439
+ await globalThis.disconnectEverything();
2440
+ });
2441
+ const page = await browserContext.newPage();
2442
+ await page.goto('https://example.com/');
2443
+ await page.bringToFront();
2444
+ await serviceWorker.evaluate(async () => {
2445
+ await globalThis.toggleExtensionForActiveTab();
2446
+ });
2447
+ await new Promise((r) => setTimeout(r, 100));
2448
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2449
+ const cdpPage = browser
2450
+ .contexts()[0]
2451
+ .pages()
2452
+ .find((p) => p.url().includes('example.com'));
2453
+ expect(cdpPage).toBeDefined();
2454
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
2455
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
2456
+ const initialTargets = await cdpSession.send('Target.getTargets');
2457
+ const initialPageTarget = initialTargets.targetInfos.find((t) => t.type === 'page' && t.url.includes('example.com'));
2458
+ expect(initialPageTarget?.url).toBe('https://example.com/');
2459
+ await cdpPage.goto('https://example.org/', { waitUntil: 'domcontentloaded' });
2460
+ await new Promise((r) => setTimeout(r, 100));
2461
+ const afterNavTargets = await cdpSession.send('Target.getTargets');
2462
+ const allPageTargets = afterNavTargets.targetInfos.filter((t) => t.type === 'page');
2463
+ const aboutBlankTargets = allPageTargets.filter((t) => t.url === 'about:blank');
2464
+ expect(aboutBlankTargets).toHaveLength(0);
2465
+ const exampleComTargets = allPageTargets.filter((t) => t.url.includes('example.com'));
2466
+ expect(exampleComTargets).toHaveLength(0);
2467
+ const exampleOrgTargets = allPageTargets.filter((t) => t.url.includes('example.org'));
2468
+ expect(exampleOrgTargets).toHaveLength(1);
2469
+ cdpSession.close();
2470
+ await browser.close();
2471
+ await page.close();
2472
+ }, 60000);
2473
+ it('should return correct targets for multiple pages via Target.getTargets', async () => {
2474
+ const browserContext = getBrowserContext();
2475
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2476
+ // Clear any existing connected tabs from previous tests
2477
+ await serviceWorker.evaluate(async () => {
2478
+ await globalThis.disconnectEverything();
2479
+ });
2480
+ const page1 = await browserContext.newPage();
2481
+ await page1.goto('https://example.com/');
2482
+ await page1.bringToFront();
2483
+ await serviceWorker.evaluate(async () => {
2484
+ await globalThis.toggleExtensionForActiveTab();
2485
+ });
2486
+ const page2 = await browserContext.newPage();
2487
+ await page2.goto('https://example.org/');
2488
+ await page2.bringToFront();
2489
+ await serviceWorker.evaluate(async () => {
2490
+ await globalThis.toggleExtensionForActiveTab();
2491
+ });
2492
+ await new Promise((r) => setTimeout(r, 100));
2493
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2494
+ const cdpPage = browser
2495
+ .contexts()[0]
2496
+ .pages()
2497
+ .find((p) => p.url().includes('example.com'));
2498
+ expect(cdpPage).toBeDefined();
2499
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
2500
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
2501
+ const { targetInfos } = await cdpSession.send('Target.getTargets');
2502
+ const allPageTargets = targetInfos.filter((t) => t.type === 'page');
2503
+ const aboutBlankTargets = allPageTargets.filter((t) => t.url === 'about:blank');
2504
+ expect(aboutBlankTargets).toHaveLength(0);
2505
+ const pageTargets = allPageTargets
2506
+ .map((t) => ({ type: t.type, url: t.url }))
2507
+ .sort((a, b) => a.url.localeCompare(b.url));
2508
+ expect(pageTargets).toMatchInlineSnapshot(`
2509
+ [
2510
+ {
2511
+ "type": "page",
2512
+ "url": "https://example.com/",
2513
+ },
2514
+ {
2515
+ "type": "page",
2516
+ "url": "https://example.org/",
2517
+ },
2518
+ ]
2519
+ `);
2520
+ cdpSession.close();
2521
+ await browser.close();
2522
+ await page1.close();
2523
+ await page2.close();
2524
+ }, 60000);
2525
+ it('should create CDP session for page after navigation', async () => {
2526
+ const browserContext = getBrowserContext();
2527
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2528
+ const page = await browserContext.newPage();
2529
+ await page.goto('https://example.com/');
2530
+ await page.bringToFront();
2531
+ await serviceWorker.evaluate(async () => {
2532
+ await globalThis.toggleExtensionForActiveTab();
2533
+ });
2534
+ await new Promise((r) => setTimeout(r, 100));
2535
+ await page.goto('https://example.org/', { waitUntil: 'domcontentloaded' });
2536
+ await new Promise((r) => setTimeout(r, 100));
2537
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2538
+ const cdpPage = browser
2539
+ .contexts()[0]
2540
+ .pages()
2541
+ .find((p) => p.url().includes('example.org'));
2542
+ expect(cdpPage).toBeDefined();
2543
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
2544
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
2545
+ const evalResult = await cdpSession.send('Runtime.evaluate', {
2546
+ expression: 'document.title',
2547
+ returnByValue: true,
2548
+ });
2549
+ expect(evalResult.result.value).toContain('Example Domain');
2550
+ cdpSession.close();
2551
+ await browser.close();
2552
+ await page.close();
2553
+ }, 60000);
2554
+ it('should maintain CDP session functionality after page URL change', async () => {
2555
+ const browserContext = getBrowserContext();
2556
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2557
+ const page = await browserContext.newPage();
2558
+ const initialUrl = 'https://example.com/';
2559
+ await page.goto(initialUrl);
2560
+ await page.bringToFront();
2561
+ await serviceWorker.evaluate(async () => {
2562
+ await globalThis.toggleExtensionForActiveTab();
2563
+ });
2564
+ await new Promise((r) => setTimeout(r, 100));
2565
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2566
+ const cdpPage = browser
2567
+ .contexts()[0]
2568
+ .pages()
2569
+ .find((p) => p.url().includes('example.com'));
2570
+ expect(cdpPage).toBeDefined();
2571
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
2572
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
2573
+ const initialEvalResult = await cdpSession.send('Runtime.evaluate', {
2574
+ expression: 'document.title',
2575
+ returnByValue: true,
2576
+ });
2577
+ expect(initialEvalResult.result.value).toBe('Example Domain');
2578
+ const newUrl = 'https://example.org/';
2579
+ await cdpPage.goto(newUrl, { waitUntil: 'domcontentloaded' });
2580
+ expect(cdpPage.url()).toBe(newUrl);
2581
+ const layoutMetrics = await cdpSession.send('Page.getLayoutMetrics');
2582
+ expect(layoutMetrics.cssVisualViewport).toBeDefined();
2583
+ expect(layoutMetrics.cssVisualViewport.clientWidth).toBeGreaterThan(0);
2584
+ const afterNavEvalResult = await cdpSession.send('Runtime.evaluate', {
2585
+ expression: 'document.title',
2586
+ returnByValue: true,
2587
+ });
2588
+ expect(afterNavEvalResult.result.value).toContain('Example Domain');
2589
+ const locationResult = await cdpSession.send('Runtime.evaluate', {
2590
+ expression: 'window.location.href',
2591
+ returnByValue: true,
2592
+ });
2593
+ expect(locationResult.result.value).toBe(newUrl);
2594
+ cdpSession.close();
2595
+ await browser.close();
2596
+ await page.close();
2597
+ }, 60000);
2598
+ it('should pause on all exceptions with setPauseOnExceptions', async () => {
2599
+ const browserContext = getBrowserContext();
2600
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2601
+ const page = await browserContext.newPage();
2602
+ await page.goto('https://example.com/');
2603
+ await page.bringToFront();
2604
+ await serviceWorker.evaluate(async () => {
2605
+ await globalThis.toggleExtensionForActiveTab();
2606
+ });
2607
+ await new Promise((r) => setTimeout(r, 100));
2608
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2609
+ const cdpPage = browser
2610
+ .contexts()[0]
2611
+ .pages()
2612
+ .find((p) => p.url().includes('example.com'));
2613
+ expect(cdpPage).toBeDefined();
2614
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
2615
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
2616
+ const dbg = new Debugger({ cdp: cdpSession });
2617
+ await dbg.enable();
2618
+ await dbg.setPauseOnExceptions({ state: 'all' });
2619
+ const pausedPromise = new Promise((resolve) => {
2620
+ cdpSession.on('Debugger.paused', () => resolve());
2621
+ });
2622
+ cdpPage
2623
+ .evaluate(`
2624
+ (function() {
2625
+ try {
2626
+ throw new Error('Caught test error');
2627
+ } catch (e) {
2628
+ // caught but should still pause with state 'all'
2629
+ }
2630
+ })()
2631
+ `)
2632
+ .catch(() => { });
2633
+ await Promise.race([
2634
+ pausedPromise,
2635
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Debugger.paused timeout')), 5000)),
2636
+ ]);
2637
+ expect(dbg.isPaused()).toBe(true);
2638
+ const location = await dbg.getLocation();
2639
+ expect(location.sourceContext).toContain('throw');
2640
+ await dbg.resume();
2641
+ await dbg.setPauseOnExceptions({ state: 'none' });
2642
+ cdpSession.close();
2643
+ await browser.close();
2644
+ await page.close();
2645
+ }, 60000);
2646
+ it('should inspect local and global variables with inline snapshots', async () => {
2647
+ const browserContext = getBrowserContext();
2648
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2649
+ const page = await browserContext.newPage();
2650
+ await page.setContent(`
2651
+ <html>
2652
+ <head>
2653
+ <script>
2654
+ const GLOBAL_CONFIG = 'production';
2655
+ function runTest() {
2656
+ const userName = 'Alice';
2657
+ const userAge = 25;
2658
+ const settings = { theme: 'dark', lang: 'en' };
2659
+ const scores = [10, 20, 30];
2660
+ debugger;
2661
+ return userName;
2662
+ }
2663
+ </script>
2664
+ </head>
2665
+ <body>
2666
+ <button onclick="runTest()">Run</button>
2667
+ </body>
2668
+ </html>
2669
+ `);
2670
+ await page.bringToFront();
2671
+ await serviceWorker.evaluate(async () => {
2672
+ await globalThis.toggleExtensionForActiveTab();
2673
+ });
2674
+ await new Promise((r) => setTimeout(r, 100));
2675
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2676
+ let cdpPage;
2677
+ for (const p of browser.contexts()[0].pages()) {
2678
+ const html = await p.content();
2679
+ if (html.includes('runTest')) {
2680
+ cdpPage = p;
2681
+ break;
2682
+ }
2683
+ }
2684
+ expect(cdpPage).toBeDefined();
2685
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
2686
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
2687
+ const dbg = new Debugger({ cdp: cdpSession });
2688
+ await dbg.enable();
2689
+ const globalVars = await dbg.inspectGlobalVariables();
2690
+ expect(globalVars).toMatchInlineSnapshot(`
2691
+ [
2692
+ "GLOBAL_CONFIG",
2693
+ ]
2694
+ `);
2695
+ const pausedPromise = new Promise((resolve) => {
2696
+ cdpSession.on('Debugger.paused', () => resolve());
2697
+ });
2698
+ cdpPage.evaluate('runTest()');
2699
+ await pausedPromise;
2700
+ expect(dbg.isPaused()).toBe(true);
2701
+ const localVars = await dbg.inspectLocalVariables();
2702
+ expect(localVars).toMatchInlineSnapshot(`
2703
+ {
2704
+ "GLOBAL_CONFIG": "production",
2705
+ "scores": "[array]",
2706
+ "settings": "[object]",
2707
+ "userAge": 25,
2708
+ "userName": "Alice",
2709
+ }
2710
+ `);
2711
+ await dbg.resume();
2712
+ cdpSession.close();
2713
+ await browser.close();
2714
+ await page.close();
2715
+ }, 60000);
2716
+ it('should click at correct coordinates on high-DPI simulation', async () => {
2717
+ const browserContext = getBrowserContext();
2718
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2719
+ const page = await browserContext.newPage();
2720
+ await page.goto('https://example.com/');
2721
+ await page.bringToFront();
2722
+ await serviceWorker.evaluate(async () => {
2723
+ await globalThis.toggleExtensionForActiveTab();
2724
+ });
2725
+ await new Promise((r) => setTimeout(r, 100));
2726
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2727
+ const cdpPage = browser
2728
+ .contexts()[0]
2729
+ .pages()
2730
+ .find((p) => p.url().includes('example.com'));
2731
+ expect(cdpPage).toBeDefined();
2732
+ const h1Bounds = await cdpPage.locator('h1').boundingBox();
2733
+ expect(h1Bounds).toBeDefined();
2734
+ console.log('H1 bounding box:', h1Bounds);
2735
+ await cdpPage.evaluate(() => {
2736
+ ;
2737
+ window.clickedAt = null;
2738
+ document.addEventListener('click', (e) => {
2739
+ ;
2740
+ window.clickedAt = { x: e.clientX, y: e.clientY };
2741
+ });
2742
+ });
2743
+ await cdpPage.locator('h1').click();
2744
+ const clickedAt = await cdpPage.evaluate(() => window.clickedAt);
2745
+ console.log('Clicked at:', clickedAt);
2746
+ expect(clickedAt).toBeDefined();
2747
+ expect(clickedAt.x).toBeGreaterThan(0);
2748
+ expect(clickedAt.y).toBeGreaterThan(0);
2749
+ await browser.close();
2750
+ await page.close();
2751
+ }, 60000);
2752
+ it('should use Editor class to list, read, and edit scripts', async () => {
2753
+ const browserContext = getBrowserContext();
2754
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2755
+ const page = await browserContext.newPage();
2756
+ await page.goto('https://example.com/');
2757
+ await page.bringToFront();
2758
+ await serviceWorker.evaluate(async () => {
2759
+ await globalThis.toggleExtensionForActiveTab();
2760
+ });
2761
+ await new Promise((r) => setTimeout(r, 100));
2762
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2763
+ const cdpPage = browser
2764
+ .contexts()[0]
2765
+ .pages()
2766
+ .find((p) => p.url().includes('example.com'));
2767
+ expect(cdpPage).toBeDefined();
2768
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
2769
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
2770
+ const editor = new Editor({ cdp: cdpSession });
2771
+ await editor.enable();
2772
+ await cdpPage.addScriptTag({
2773
+ content: `
2774
+ function greetUser(name) {
2775
+ console.log('Hello, ' + name);
2776
+ return 'Hello, ' + name;
2777
+ }
2778
+ `,
2779
+ });
2780
+ await new Promise((r) => setTimeout(r, 100));
2781
+ const scripts = await editor.list();
2782
+ expect(scripts.length).toBeGreaterThan(0);
2783
+ const matches = await editor.grep({ regex: /greetUser/ });
2784
+ expect(matches.length).toBeGreaterThan(0);
2785
+ const match = matches[0];
2786
+ const { content, totalLines } = await editor.read({ url: match.url });
2787
+ expect(content).toContain('greetUser');
2788
+ expect(totalLines).toBeGreaterThan(0);
2789
+ await editor.edit({
2790
+ url: match.url,
2791
+ oldString: "console.log('Hello, ' + name);",
2792
+ newString: "console.log('Hello, ' + name); console.log('EDITOR_TEST_MARKER');",
2793
+ });
2794
+ const consoleLogs = [];
2795
+ cdpPage.on('console', (msg) => {
2796
+ consoleLogs.push(msg.text());
2797
+ });
2798
+ await cdpPage.evaluate(() => {
2799
+ ;
2800
+ window.greetUser('World');
2801
+ });
2802
+ await new Promise((r) => setTimeout(r, 100));
2803
+ expect(consoleLogs).toContain('Hello, World');
2804
+ expect(consoleLogs).toContain('EDITOR_TEST_MARKER');
2805
+ cdpSession.close();
2806
+ await browser.close();
2807
+ await page.close();
2808
+ }, 60000);
2809
+ it('editor can list, read, and edit CSS stylesheets', async () => {
2810
+ const browserContext = getBrowserContext();
2811
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2812
+ const page = await browserContext.newPage();
2813
+ await page.goto('https://example.com/');
2814
+ await page.bringToFront();
2815
+ await serviceWorker.evaluate(async () => {
2816
+ await globalThis.toggleExtensionForActiveTab();
2817
+ });
2818
+ await new Promise((r) => setTimeout(r, 100));
2819
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2820
+ const cdpPage = browser
2821
+ .contexts()[0]
2822
+ .pages()
2823
+ .find((p) => p.url().includes('example.com'));
2824
+ expect(cdpPage).toBeDefined();
2825
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
2826
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
2827
+ const editor = new Editor({ cdp: cdpSession });
2828
+ await editor.enable();
2829
+ await cdpPage.addStyleTag({
2830
+ content: `
2831
+ .editor-test-element {
2832
+ color: rgb(255, 0, 0);
2833
+ background-color: rgb(0, 0, 255);
2834
+ }
2835
+ `,
2836
+ });
2837
+ await new Promise((r) => setTimeout(r, 100));
2838
+ const stylesheets = await editor.list({ pattern: /inline-css:/ });
2839
+ expect(stylesheets.length).toBeGreaterThan(0);
2840
+ const cssMatches = await editor.grep({ regex: /editor-test-element/, pattern: /inline-css:/ });
2841
+ expect(cssMatches.length).toBeGreaterThan(0);
2842
+ const cssMatch = cssMatches[0];
2843
+ const { content, totalLines } = await editor.read({ url: cssMatch.url });
2844
+ expect(content).toContain('editor-test-element');
2845
+ expect(content).toContain('rgb(255, 0, 0)');
2846
+ expect(totalLines).toBeGreaterThan(0);
2847
+ await cdpPage.evaluate(() => {
2848
+ const el = document.createElement('div');
2849
+ el.className = 'editor-test-element';
2850
+ el.id = 'test-div';
2851
+ el.textContent = 'Test';
2852
+ document.body.appendChild(el);
2853
+ });
2854
+ const colorBefore = await cdpPage.evaluate(() => {
2855
+ const el = document.getElementById('test-div');
2856
+ return window.getComputedStyle(el).color;
2857
+ });
2858
+ expect(colorBefore).toBe('rgb(255, 0, 0)');
2859
+ await editor.edit({
2860
+ url: cssMatch.url,
2861
+ oldString: 'color: rgb(255, 0, 0);',
2862
+ newString: 'color: rgb(0, 255, 0);',
2863
+ });
2864
+ const colorAfter = await cdpPage.evaluate(() => {
2865
+ const el = document.getElementById('test-div');
2866
+ return window.getComputedStyle(el).color;
2867
+ });
2868
+ expect(colorAfter).toBe('rgb(0, 255, 0)');
2869
+ cdpSession.close();
2870
+ await browser.close();
2871
+ await page.close();
2872
+ }, 60000);
2873
+ it('should inject bippy and find React fiber with getReactSource', async () => {
2874
+ const browserContext = getBrowserContext();
2875
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2876
+ const page = await browserContext.newPage();
2877
+ await page.setContent(`
2878
+ <!DOCTYPE html>
2879
+ <html>
2880
+ <head>
2881
+ <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
2882
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
2883
+ </head>
2884
+ <body>
2885
+ <div id="root"></div>
2886
+ <script>
2887
+ function MyComponent() {
2888
+ return React.createElement('button', { id: 'react-btn' }, 'Click me');
2889
+ }
2890
+ const root = ReactDOM.createRoot(document.getElementById('root'));
2891
+ root.render(React.createElement(MyComponent));
2892
+ </script>
2893
+ </body>
2894
+ </html>
2895
+ `);
2896
+ await page.bringToFront();
2897
+ await serviceWorker.evaluate(async () => {
2898
+ await globalThis.toggleExtensionForActiveTab();
2899
+ });
2900
+ await new Promise((r) => setTimeout(r, 500));
2901
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2902
+ const pages = browser.contexts()[0].pages();
2903
+ const cdpPage = pages.find((p) => p.url().startsWith('about:'));
2904
+ expect(cdpPage).toBeDefined();
2905
+ const btn = cdpPage.locator('#react-btn');
2906
+ const btnCount = await btn.count();
2907
+ expect(btnCount).toBe(1);
2908
+ const hasBippyBefore = await cdpPage.evaluate(() => !!globalThis.__bippy);
2909
+ expect(hasBippyBefore).toBe(false);
2910
+ const wsUrl = getCdpUrl({ port: TEST_PORT });
2911
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
2912
+ const { getReactSource } = await import('./react-source.js');
2913
+ const source = await getReactSource({ locator: btn, cdp: cdpSession });
2914
+ const hasBippyAfter = await cdpPage.evaluate(() => !!globalThis.__bippy);
2915
+ expect(hasBippyAfter).toBe(true);
2916
+ const hasFiber = await btn.evaluate((el) => {
2917
+ const bippy = globalThis.__bippy;
2918
+ const fiber = bippy.getFiberFromHostInstance(el);
2919
+ return !!fiber;
2920
+ });
2921
+ expect(hasFiber).toBe(true);
2922
+ const componentName = await btn.evaluate((el) => {
2923
+ const bippy = globalThis.__bippy;
2924
+ const fiber = bippy.getFiberFromHostInstance(el);
2925
+ let current = fiber;
2926
+ while (current) {
2927
+ if (bippy.isCompositeFiber(current)) {
2928
+ return bippy.getDisplayName(current.type);
2929
+ }
2930
+ current = current.return;
2931
+ }
2932
+ return null;
2933
+ });
2934
+ expect(componentName).toBe('MyComponent');
2935
+ console.log('Component name from fiber:', componentName);
2936
+ console.log('Source location (null for UMD React, works on local dev servers with JSX transform):', source);
2937
+ await browser.close();
2938
+ await page.close();
2939
+ }, 60000);
2940
+ });
2941
+ describe('Auto-enable Tests', () => {
2942
+ let testCtx = null;
2943
+ // Set env var before any setup runs
2944
+ process.env.E2E_PILOT_AUTO_ENABLE = '1';
2945
+ beforeAll(async () => {
2946
+ testCtx = await setupTestContext({ tempDirPrefix: 'pw-auto-test-' });
2947
+ // Disconnect all tabs to start with a clean state
2948
+ const serviceWorker = await getExtensionServiceWorker(testCtx.browserContext);
2949
+ await serviceWorker.evaluate(async () => {
2950
+ await globalThis.disconnectEverything();
2951
+ });
2952
+ await new Promise((r) => setTimeout(r, 100));
2953
+ }, 600000);
2954
+ afterAll(async () => {
2955
+ delete process.env.E2E_PILOT_AUTO_ENABLE;
2956
+ await cleanupTestContext(testCtx);
2957
+ testCtx = null;
2958
+ });
2959
+ const getBrowserContext = () => {
2960
+ if (!testCtx?.browserContext)
2961
+ throw new Error('Browser not initialized');
2962
+ return testCtx.browserContext;
2963
+ };
2964
+ it('should auto-create a tab when Playwright connects and no tabs exist', async () => {
2965
+ const browserContext = getBrowserContext();
2966
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
2967
+ // Ensure clean state - disconnect any tabs from previous tests or setup
2968
+ await serviceWorker.evaluate(async () => {
2969
+ await globalThis.disconnectEverything();
2970
+ });
2971
+ await new Promise((r) => setTimeout(r, 100));
2972
+ // Verify no tabs are connected
2973
+ const tabCountBefore = await serviceWorker.evaluate(() => {
2974
+ const state = globalThis.getExtensionState();
2975
+ return state.tabs.size;
2976
+ });
2977
+ expect(tabCountBefore).toBe(0);
2978
+ // Connect Playwright - this should trigger auto-create
2979
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }));
2980
+ // Verify a page was auto-created
2981
+ const pages = browser.contexts()[0].pages();
2982
+ expect(pages.length).toBeGreaterThan(0);
2983
+ expect(pages.length).toBe(1);
2984
+ const autoCreatedPage = pages[0];
2985
+ expect(autoCreatedPage.url()).toBe('about:blank');
2986
+ // Verify extension state shows the tab as connected
2987
+ const tabCountAfter = await serviceWorker.evaluate(() => {
2988
+ const state = globalThis.getExtensionState();
2989
+ return state.tabs.size;
2990
+ });
2991
+ expect(tabCountAfter).toBe(1);
2992
+ // Verify we can interact with the auto-created page
2993
+ await autoCreatedPage.setContent('<h1>Auto-created page</h1>');
2994
+ const title = await autoCreatedPage.locator('h1').textContent();
2995
+ expect(title).toBe('Auto-created page');
2996
+ await browser.close();
2997
+ }, 60000);
2998
+ });
2999
+ //# sourceMappingURL=mcp.test.js.map