playwriter 0.0.63 → 0.0.89

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 (223) hide show
  1. package/dist/a11y-client.js +18 -8
  2. package/dist/aria-snapshot.d.ts +41 -3
  3. package/dist/aria-snapshot.d.ts.map +1 -1
  4. package/dist/aria-snapshot.js +134 -55
  5. package/dist/aria-snapshot.js.map +1 -1
  6. package/dist/aria-snapshot.test.js +5 -2
  7. package/dist/aria-snapshot.test.js.map +1 -1
  8. package/dist/aria-snapshot.unit.test.js +83 -41
  9. package/dist/aria-snapshot.unit.test.js.map +1 -1
  10. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts +5 -0
  11. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts.map +1 -0
  12. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js +5 -0
  13. package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js.map +1 -0
  14. package/dist/bippy.js +1 -1
  15. package/dist/cdp-log.d.ts +1 -1
  16. package/dist/cdp-log.d.ts.map +1 -1
  17. package/dist/cdp-log.js +1 -1
  18. package/dist/cdp-log.js.map +1 -1
  19. package/dist/cdp-relay.d.ts.map +1 -1
  20. package/dist/cdp-relay.js +492 -298
  21. package/dist/cdp-relay.js.map +1 -1
  22. package/dist/cdp-session.d.ts.map +1 -1
  23. package/dist/cdp-session.js.map +1 -1
  24. package/dist/cdp-types.d.ts.map +1 -1
  25. package/dist/cdp-types.js +7 -7
  26. package/dist/cdp-types.js.map +1 -1
  27. package/dist/clean-html.d.ts.map +1 -1
  28. package/dist/clean-html.js +4 -5
  29. package/dist/clean-html.js.map +1 -1
  30. package/dist/cli.js +45 -27
  31. package/dist/cli.js.map +1 -1
  32. package/dist/create-logger.d.ts.map +1 -1
  33. package/dist/create-logger.js +3 -1
  34. package/dist/create-logger.js.map +1 -1
  35. package/dist/debugger-examples-types.d.ts.map +1 -1
  36. package/dist/debugger.d.ts.map +1 -1
  37. package/dist/debugger.js +1 -3
  38. package/dist/debugger.js.map +1 -1
  39. package/dist/diff-utils.d.ts.map +1 -1
  40. package/dist/diff-utils.js +1 -4
  41. package/dist/diff-utils.js.map +1 -1
  42. package/dist/editor-api.md +12 -2
  43. package/dist/editor-examples.d.ts +1 -1
  44. package/dist/editor-examples.d.ts.map +1 -1
  45. package/dist/editor-examples.js +1 -1
  46. package/dist/editor-examples.js.map +1 -1
  47. package/dist/editor.d.ts +1 -1
  48. package/dist/editor.d.ts.map +1 -1
  49. package/dist/editor.js +1 -1
  50. package/dist/editor.js.map +1 -1
  51. package/dist/executor.d.ts +26 -3
  52. package/dist/executor.d.ts.map +1 -1
  53. package/dist/executor.js +297 -64
  54. package/dist/executor.js.map +1 -1
  55. package/dist/executor.unit.test.js +38 -1
  56. package/dist/executor.unit.test.js.map +1 -1
  57. package/dist/extension-connection.test.js +139 -36
  58. package/dist/extension-connection.test.js.map +1 -1
  59. package/dist/ffmpeg.d.ts +148 -0
  60. package/dist/ffmpeg.d.ts.map +1 -0
  61. package/dist/ffmpeg.js +523 -0
  62. package/dist/ffmpeg.js.map +1 -0
  63. package/dist/ghost-browser.d.ts.map +1 -1
  64. package/dist/ghost-browser.js.map +1 -1
  65. package/dist/ghost-cursor-client.js +287 -0
  66. package/dist/ghost-cursor.d.ts +27 -0
  67. package/dist/ghost-cursor.d.ts.map +1 -0
  68. package/dist/ghost-cursor.js +63 -0
  69. package/dist/ghost-cursor.js.map +1 -0
  70. package/dist/htmlrewrite.d.ts.map +1 -1
  71. package/dist/htmlrewrite.js +17 -55
  72. package/dist/htmlrewrite.js.map +1 -1
  73. package/dist/htmlrewrite.test.js.map +1 -1
  74. package/dist/kill-port.d.ts.map +1 -1
  75. package/dist/kill-port.js +1 -3
  76. package/dist/kill-port.js.map +1 -1
  77. package/dist/locator-selector.test.d.ts +2 -0
  78. package/dist/locator-selector.test.d.ts.map +1 -0
  79. package/dist/locator-selector.test.js +96 -0
  80. package/dist/locator-selector.test.js.map +1 -0
  81. package/dist/mcp-client.js.map +1 -1
  82. package/dist/mcp.d.ts.map +1 -1
  83. package/dist/mcp.js +8 -3
  84. package/dist/mcp.js.map +1 -1
  85. package/dist/on-mouse-action.test.d.ts +2 -0
  86. package/dist/on-mouse-action.test.d.ts.map +1 -0
  87. package/dist/on-mouse-action.test.js +155 -0
  88. package/dist/on-mouse-action.test.js.map +1 -0
  89. package/dist/page-markdown.js +4 -4
  90. package/dist/page-markdown.js.map +1 -1
  91. package/dist/prompt.md +450 -377
  92. package/dist/protocol.d.ts +4 -0
  93. package/dist/protocol.d.ts.map +1 -1
  94. package/dist/readability.js +16 -2
  95. package/dist/recording-ghost-cursor.d.ts +41 -0
  96. package/dist/recording-ghost-cursor.d.ts.map +1 -0
  97. package/dist/recording-ghost-cursor.js +79 -0
  98. package/dist/recording-ghost-cursor.js.map +1 -0
  99. package/dist/recording-relay.d.ts.map +1 -1
  100. package/dist/recording-relay.js +8 -8
  101. package/dist/recording-relay.js.map +1 -1
  102. package/dist/relay-client.d.ts +17 -4
  103. package/dist/relay-client.d.ts.map +1 -1
  104. package/dist/relay-client.js +45 -11
  105. package/dist/relay-client.js.map +1 -1
  106. package/dist/relay-core.test.d.ts.map +1 -1
  107. package/dist/relay-core.test.js +515 -26
  108. package/dist/relay-core.test.js.map +1 -1
  109. package/dist/relay-navigation.test.d.ts.map +1 -1
  110. package/dist/relay-navigation.test.js +169 -31
  111. package/dist/relay-navigation.test.js.map +1 -1
  112. package/dist/relay-session.test.d.ts.map +1 -1
  113. package/dist/relay-session.test.js +113 -65
  114. package/dist/relay-session.test.js.map +1 -1
  115. package/dist/relay-state.d.ts +158 -0
  116. package/dist/relay-state.d.ts.map +1 -0
  117. package/dist/relay-state.js +306 -0
  118. package/dist/relay-state.js.map +1 -0
  119. package/dist/relay-state.test.d.ts +2 -0
  120. package/dist/relay-state.test.d.ts.map +1 -0
  121. package/dist/relay-state.test.js +472 -0
  122. package/dist/relay-state.test.js.map +1 -0
  123. package/dist/scoped-fs.d.ts.map +1 -1
  124. package/dist/scoped-fs.js.map +1 -1
  125. package/dist/screen-recording.d.ts +66 -4
  126. package/dist/screen-recording.d.ts.map +1 -1
  127. package/dist/screen-recording.js +150 -13
  128. package/dist/screen-recording.js.map +1 -1
  129. package/dist/screen-recording.test.d.ts +2 -0
  130. package/dist/screen-recording.test.d.ts.map +1 -0
  131. package/dist/screen-recording.test.js +102 -0
  132. package/dist/screen-recording.test.js.map +1 -0
  133. package/dist/selector-generator.js +1 -1
  134. package/dist/snapshot-tools.test.js +71 -28
  135. package/dist/snapshot-tools.test.js.map +1 -1
  136. package/dist/start-relay-server.d.ts +1 -1
  137. package/dist/start-relay-server.d.ts.map +1 -1
  138. package/dist/start-relay-server.js +1 -1
  139. package/dist/start-relay-server.js.map +1 -1
  140. package/dist/styles-api.md +8 -1
  141. package/dist/styles-examples.d.ts +1 -1
  142. package/dist/styles-examples.d.ts.map +1 -1
  143. package/dist/styles-examples.js +1 -1
  144. package/dist/styles-examples.js.map +1 -1
  145. package/dist/styles.d.ts.map +1 -1
  146. package/dist/styles.js +1 -3
  147. package/dist/styles.js.map +1 -1
  148. package/dist/test-declarations.d.ts.map +1 -1
  149. package/dist/test-utils.d.ts +1 -1
  150. package/dist/test-utils.d.ts.map +1 -1
  151. package/dist/test-utils.js +7 -5
  152. package/dist/test-utils.js.map +1 -1
  153. package/dist/utils.d.ts.map +1 -1
  154. package/dist/utils.js.map +1 -1
  155. package/dist/wait-for-page-load.d.ts.map +1 -1
  156. package/dist/wait-for-page-load.js +1 -1
  157. package/dist/wait-for-page-load.js.map +1 -1
  158. package/package.json +4 -3
  159. package/src/a11y-client.ts +5 -4
  160. package/src/aria-snapshot.test.ts +5 -2
  161. package/src/aria-snapshot.ts +306 -117
  162. package/src/aria-snapshot.unit.test.ts +199 -141
  163. package/src/aria-snapshots/github-interactive.txt +2 -0
  164. package/src/aria-snapshots/github-raw.txt +5 -1
  165. package/src/aria-snapshots/hackernews-interactive.txt +238 -241
  166. package/src/aria-snapshots/hackernews-raw.txt +265 -269
  167. package/src/assets/aria-labels-example.png +0 -0
  168. package/src/assets/aria-labels-github.png +0 -0
  169. package/src/assets/aria-labels-hacker-news.png +0 -0
  170. package/src/assets/aria-labels-old-reddit.png +0 -0
  171. package/src/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.ts +5 -0
  172. package/src/assets/cursors/screen-studio/pointer-macos-tahoe.svg +18 -0
  173. package/src/cdp-log.ts +4 -1
  174. package/src/cdp-relay.ts +1059 -737
  175. package/src/cdp-session.ts +12 -3
  176. package/src/cdp-types.ts +51 -51
  177. package/src/clean-html.ts +4 -5
  178. package/src/cli.ts +82 -55
  179. package/src/create-logger.ts +5 -3
  180. package/src/debugger-examples-types.ts +4 -1
  181. package/src/debugger.ts +1 -5
  182. package/src/diff-utils.ts +2 -5
  183. package/src/editor-examples.ts +11 -1
  184. package/src/editor.ts +10 -2
  185. package/src/executor.ts +374 -73
  186. package/src/executor.unit.test.ts +48 -1
  187. package/src/extension-connection.test.ts +612 -488
  188. package/src/ffmpeg.ts +769 -0
  189. package/src/ghost-browser.ts +4 -6
  190. package/src/ghost-cursor-client.ts +369 -0
  191. package/src/ghost-cursor.ts +110 -0
  192. package/src/htmlrewrite.test.ts +6 -2
  193. package/src/htmlrewrite.ts +348 -386
  194. package/src/kill-port.ts +1 -3
  195. package/src/locator-selector.test.ts +115 -0
  196. package/src/mcp-client.ts +1 -1
  197. package/src/mcp.ts +21 -15
  198. package/src/on-mouse-action.test.ts +196 -0
  199. package/src/page-markdown.ts +7 -7
  200. package/src/protocol.ts +73 -57
  201. package/src/recording-ghost-cursor.ts +113 -0
  202. package/src/recording-relay.ts +20 -12
  203. package/src/relay-client.ts +85 -18
  204. package/src/relay-core.test.ts +1117 -578
  205. package/src/relay-navigation.test.ts +648 -483
  206. package/src/relay-session.test.ts +984 -929
  207. package/src/relay-state.test.ts +570 -0
  208. package/src/relay-state.ts +497 -0
  209. package/src/resource.md +21 -49
  210. package/src/scoped-fs.ts +9 -3
  211. package/src/screen-recording.test.ts +111 -0
  212. package/src/screen-recording.ts +256 -31
  213. package/src/skill.md +476 -396
  214. package/src/snapshot-tools.test.ts +580 -528
  215. package/src/snapshots/shadcn-ui-accessibility-full.md +8 -8
  216. package/src/snapshots/shadcn-ui-accessibility-interactive.md +8 -8
  217. package/src/start-relay-server.ts +14 -11
  218. package/src/styles-examples.ts +8 -1
  219. package/src/styles.ts +20 -21
  220. package/src/test-declarations.ts +6 -6
  221. package/src/test-utils.ts +104 -91
  222. package/src/utils.ts +2 -1
  223. package/src/wait-for-page-load.ts +6 -1
@@ -1,7 +1,10 @@
1
1
  import { createMCPClient } from './mcp-client.js';
2
2
  import { describe, it, expect, beforeAll, afterAll } from 'vitest';
3
+ import { chromium } from '@xmorse/playwright-core';
3
4
  import { getCDPSessionForPage } from './cdp-session.js';
4
- import { setupTestContext, cleanupTestContext, getExtensionServiceWorker, withTimeout, js, tryJsonParse } from './test-utils.js';
5
+ import { getCdpUrl, LOG_CDP_FILE_PATH } from './utils.js';
6
+ import fs from 'node:fs';
7
+ import { setupTestContext, cleanupTestContext, getExtensionServiceWorker, withTimeout, js, tryJsonParse, createSimpleServer, } from './test-utils.js';
5
8
  import './test-declarations.js';
6
9
  const TEST_PORT = 19987;
7
10
  describe('Relay Core Tests', () => {
@@ -24,6 +27,26 @@ describe('Relay Core Tests', () => {
24
27
  throw new Error('Browser not initialized');
25
28
  return testCtx.browserContext;
26
29
  };
30
+ const ensureConnectedTabForExecute = async () => {
31
+ const browserContext = getBrowserContext();
32
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
33
+ const connectedTabCount = await serviceWorker.evaluate(async () => {
34
+ const state = globalThis.getExtensionState();
35
+ return state.tabs.size;
36
+ });
37
+ if (connectedTabCount > 0) {
38
+ return;
39
+ }
40
+ const page = await browserContext.newPage();
41
+ await page.goto('about:blank');
42
+ await page.bringToFront();
43
+ await serviceWorker.evaluate(async () => {
44
+ await globalThis.toggleExtensionForActiveTab();
45
+ });
46
+ await new Promise((r) => {
47
+ setTimeout(r, 100);
48
+ });
49
+ };
27
50
  it('should inject script via addScriptTag through CDP relay', async () => {
28
51
  const browserContext = getBrowserContext();
29
52
  const serviceWorker = await withTimeout({
@@ -43,7 +66,9 @@ describe('Relay Core Tests', () => {
43
66
  timeoutMs: 10000,
44
67
  errorMessage: 'Timed out toggling extension for active tab',
45
68
  });
46
- await new Promise((r) => { setTimeout(r, 100); });
69
+ await new Promise((r) => {
70
+ setTimeout(r, 100);
71
+ });
47
72
  const cdpSession = await withTimeout({
48
73
  promise: getCDPSessionForPage({ page }),
49
74
  timeoutMs: 10000,
@@ -71,6 +96,138 @@ describe('Relay Core Tests', () => {
71
96
  await cdpSession.detach();
72
97
  await page.close();
73
98
  }, 60000);
99
+ it('should emit download events for both Browser and Page domains in extension mode', async () => {
100
+ const browserContext = getBrowserContext();
101
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
102
+ const logFilePath = LOG_CDP_FILE_PATH;
103
+ const logLineCountBefore = fs.existsSync(logFilePath)
104
+ ? fs
105
+ .readFileSync(logFilePath, 'utf-8')
106
+ .split('\n')
107
+ .filter((line) => {
108
+ return line.trim().length > 0;
109
+ }).length
110
+ : 0;
111
+ const server = await createSimpleServer({
112
+ routes: {
113
+ '/': `<!doctype html>
114
+ <html>
115
+ <body>
116
+ <button id="download-button">Download</button>
117
+ <script>
118
+ const button = document.getElementById('download-button');
119
+ button.addEventListener('click', () => {
120
+ const blob = new Blob(['playwriter-download-test'], { type: 'text/plain' });
121
+ const url = URL.createObjectURL(blob);
122
+ const anchor = document.createElement('a');
123
+ anchor.href = url;
124
+ anchor.download = 'playwriter-download-test.txt';
125
+ document.body.appendChild(anchor);
126
+ anchor.click();
127
+ anchor.remove();
128
+ setTimeout(() => {
129
+ URL.revokeObjectURL(url);
130
+ }, 1000);
131
+ });
132
+ </script>
133
+ </body>
134
+ </html>`,
135
+ },
136
+ });
137
+ const page = await browserContext.newPage();
138
+ await page.goto(server.baseUrl, { waitUntil: 'domcontentloaded' });
139
+ await page.bringToFront();
140
+ await serviceWorker.evaluate(async () => {
141
+ await globalThis.toggleExtensionForActiveTab();
142
+ });
143
+ const directBrowser = await withTimeout({
144
+ promise: chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT })),
145
+ timeoutMs: 10000,
146
+ errorMessage: 'Timed out connecting over CDP for download reproduction test',
147
+ });
148
+ const connectedPage = directBrowser
149
+ .contexts()[0]
150
+ .pages()
151
+ .find((candidatePage) => {
152
+ return candidatePage.url() === server.baseUrl + '/';
153
+ });
154
+ if (!connectedPage) {
155
+ throw new Error('Connected page not found for download reproduction test');
156
+ }
157
+ const downloadResult = await Promise.all([
158
+ connectedPage.waitForEvent('download', { timeout: 3000 }).then((download) => {
159
+ return { timedOut: false, suggestedFilename: download.suggestedFilename() };
160
+ }, (error) => {
161
+ return { timedOut: true, errorMessage: error.message };
162
+ }),
163
+ connectedPage.click('#download-button'),
164
+ ]);
165
+ expect(downloadResult[0]).toMatchInlineSnapshot(`
166
+ {
167
+ "suggestedFilename": "playwriter-download-test.txt",
168
+ "timedOut": false,
169
+ }
170
+ `);
171
+ await directBrowser.close();
172
+ await page.close();
173
+ await server.close();
174
+ const logLinesAfter = fs
175
+ .readFileSync(logFilePath, 'utf-8')
176
+ .split('\n')
177
+ .filter((line) => {
178
+ return line.trim().length > 0;
179
+ })
180
+ .slice(logLineCountBefore);
181
+ const newEntries = logLinesAfter
182
+ .map((line) => {
183
+ return tryJsonParse(line);
184
+ })
185
+ .filter((entry) => {
186
+ return Boolean(entry && typeof entry === 'object' && 'direction' in entry && 'message' in entry);
187
+ });
188
+ const methods = newEntries
189
+ .map((entry) => {
190
+ return {
191
+ direction: entry.direction,
192
+ method: typeof entry.message?.method === 'string' ? entry.message.method : 'response',
193
+ };
194
+ })
195
+ .filter((entry) => {
196
+ return (entry.method.includes('download') ||
197
+ entry.method === 'Browser.setDownloadBehavior' ||
198
+ entry.method === 'Page.setDownloadBehavior');
199
+ });
200
+ const summary = {
201
+ hasBrowserSetDownloadBehavior: methods.some((entry) => {
202
+ return entry.direction === 'from-playwright' && entry.method === 'Browser.setDownloadBehavior';
203
+ }),
204
+ hasPageSetDownloadBehavior: methods.some((entry) => {
205
+ return entry.direction === 'to-extension' && entry.method === 'Page.setDownloadBehavior';
206
+ }),
207
+ hasPageDownloadWillBegin: methods.some((entry) => {
208
+ return entry.method === 'Page.downloadWillBegin';
209
+ }),
210
+ hasPageDownloadProgress: methods.some((entry) => {
211
+ return entry.method === 'Page.downloadProgress';
212
+ }),
213
+ hasBrowserDownloadWillBegin: methods.some((entry) => {
214
+ return entry.method === 'Browser.downloadWillBegin';
215
+ }),
216
+ hasBrowserDownloadProgress: methods.some((entry) => {
217
+ return entry.method === 'Browser.downloadProgress';
218
+ }),
219
+ };
220
+ expect(summary).toMatchInlineSnapshot(`
221
+ {
222
+ "hasBrowserDownloadProgress": false,
223
+ "hasBrowserDownloadWillBegin": false,
224
+ "hasBrowserSetDownloadBehavior": true,
225
+ "hasPageDownloadProgress": false,
226
+ "hasPageDownloadWillBegin": false,
227
+ "hasPageSetDownloadBehavior": true,
228
+ }
229
+ `);
230
+ }, 120000);
74
231
  it('should execute code and capture console output', async () => {
75
232
  await client.callTool({
76
233
  name: 'execute',
@@ -107,6 +264,108 @@ describe('Relay Core Tests', () => {
107
264
  `);
108
265
  expect(result.content).toBeDefined();
109
266
  }, 30000);
267
+ // Repro test for https://github.com/remorses/playwriter/issues/66.
268
+ // Current limitation: extension-mode routing does not support root-session
269
+ // Storage.getCookies in playwriter. MUST use Network.getCookies via page CDP
270
+ // session instead (see test below), so this repro stays skipped.
271
+ it.skip('should reproduce page.route failure in MCP execute path (issue #66)', async () => {
272
+ const server = await createSimpleServer({
273
+ routes: {
274
+ '/': '<!doctype html><html><body>route issue repro</body></html>',
275
+ '/api/data': '{"ok":true}',
276
+ },
277
+ });
278
+ try {
279
+ const result = await client.callTool({
280
+ name: 'execute',
281
+ arguments: {
282
+ code: js `
283
+ const newPage = await context.newPage();
284
+ state.issue66Page = newPage;
285
+ await newPage.goto('${server.baseUrl}', { waitUntil: 'domcontentloaded' });
286
+
287
+ let routeFetchError = null;
288
+ await newPage.route('**/api/**', async (route) => {
289
+ try {
290
+ const response = await route.fetch();
291
+ await route.fulfill({ response });
292
+ } catch (error) {
293
+ routeFetchError = error instanceof Error ? error.message : String(error);
294
+ await route.abort();
295
+ }
296
+ });
297
+
298
+ await newPage.evaluate(async () => {
299
+ await fetch('/api/data').catch(() => null);
300
+ });
301
+
302
+ return { routeFetchError };
303
+ `,
304
+ },
305
+ });
306
+ const resultWithContent = result;
307
+ const content = Array.isArray(resultWithContent.content) ? resultWithContent.content : [];
308
+ const firstContent = content[0];
309
+ const output = typeof firstContent === 'object' && firstContent !== null && 'text' in firstContent
310
+ ? String(firstContent.text ?? '')
311
+ : '';
312
+ expect(output).toContain('routeFetchError');
313
+ expect(output).toContain('Storage.getCookies');
314
+ expect(output).toContain('No tab found for method Storage.getCookies');
315
+ }
316
+ finally {
317
+ try {
318
+ await client.callTool({
319
+ name: 'execute',
320
+ arguments: {
321
+ code: js `
322
+ if (state.issue66Page && !state.issue66Page.isClosed()) {
323
+ await state.issue66Page.close();
324
+ }
325
+ delete state.issue66Page;
326
+ `,
327
+ },
328
+ });
329
+ }
330
+ catch {
331
+ // Ignore cleanup failure if MCP disconnected due to the repro.
332
+ }
333
+ await server.close();
334
+ }
335
+ }, 30000);
336
+ it('should read cookies via Network.getCookies through page CDP session', async () => {
337
+ const browserContext = getBrowserContext();
338
+ const serviceWorker = await getExtensionServiceWorker(browserContext);
339
+ const server = await createSimpleServer({
340
+ routes: {
341
+ '/': '<!doctype html><html><body>cookies test</body></html>',
342
+ },
343
+ });
344
+ const page = await browserContext.newPage();
345
+ try {
346
+ await page.goto(server.baseUrl, { waitUntil: 'domcontentloaded' });
347
+ await page.bringToFront();
348
+ await serviceWorker.evaluate(async () => {
349
+ await globalThis.toggleExtensionForActiveTab();
350
+ });
351
+ await new Promise((r) => {
352
+ setTimeout(r, 200);
353
+ });
354
+ await page.evaluate(() => {
355
+ document.cookie = 'issue66=ok; path=/';
356
+ });
357
+ const cdpSession = await getCDPSessionForPage({ page });
358
+ const cookiesResult = await cdpSession.send('Network.getCookies', { urls: [page.url()] });
359
+ const cookie = cookiesResult.cookies.find((value) => {
360
+ return value.name === 'issue66';
361
+ });
362
+ expect(cookie?.value).toBe('ok');
363
+ }
364
+ finally {
365
+ await page.close();
366
+ await server.close();
367
+ }
368
+ }, 30000);
110
369
  it('should show extension as connected for pages created via newPage()', async () => {
111
370
  const browserContext = getBrowserContext();
112
371
  const serviceWorker = await getExtensionServiceWorker(browserContext);
@@ -131,7 +390,7 @@ describe('Relay Core Tests', () => {
131
390
  connected: !!testTab && !!testTab.id && state.tabs.has(testTab.id),
132
391
  tabId: testTab?.id,
133
392
  tabInfo: testTab?.id ? state.tabs.get(testTab.id) : null,
134
- connectionState: state.connectionState
393
+ connectionState: state.connectionState,
135
394
  };
136
395
  });
137
396
  expect(extensionState.connected).toBe(true);
@@ -150,7 +409,7 @@ describe('Relay Core Tests', () => {
150
409
  },
151
410
  });
152
411
  }, 30000);
153
- const accessibilitySnapshotTestCases = [
412
+ const snapshotTestCases = [
154
413
  {
155
414
  name: 'hacker-news',
156
415
  url: 'https://news.ycombinator.com/item?id=1',
@@ -162,7 +421,7 @@ describe('Relay Core Tests', () => {
162
421
  expectedContent: ['shadcn'],
163
422
  },
164
423
  ];
165
- for (const testCase of accessibilitySnapshotTestCases) {
424
+ for (const testCase of snapshotTestCases) {
166
425
  it(`should get accessibility snapshot of ${testCase.name}`, async () => {
167
426
  await client.callTool({
168
427
  name: 'execute',
@@ -181,8 +440,8 @@ describe('Relay Core Tests', () => {
181
440
  arguments: {
182
441
  code: js `
183
442
  await state.page.goto('${testCase.url}', { waitUntil: 'domcontentloaded' });
184
- const snapshot = await accessibilitySnapshot({ page: state.page, showDiffSinceLastCall: false, interactiveOnly: true });
185
- return snapshot;
443
+ const snap = await snapshot({ page: state.page, showDiffSinceLastCall: false, interactiveOnly: true });
444
+ return snap;
186
445
  `,
187
446
  },
188
447
  });
@@ -199,8 +458,8 @@ describe('Relay Core Tests', () => {
199
458
  name: 'execute',
200
459
  arguments: {
201
460
  code: js `
202
- const snapshot = await accessibilitySnapshot({ page: state.page, showDiffSinceLastCall: false, interactiveOnly: false });
203
- return snapshot;
461
+ const snap = await snapshot({ page: state.page, showDiffSinceLastCall: false, interactiveOnly: false });
462
+ return snap;
204
463
  `,
205
464
  },
206
465
  });
@@ -541,21 +800,92 @@ describe('Relay Core Tests', () => {
541
800
  },
542
801
  });
543
802
  }, 30000);
544
- // right now our extension always forces light mode because of a playwright cdp bug
545
- it.todo('should preserve system color scheme instead of forcing light mode', async () => {
803
+ it('should capture console logs from cross-origin iframes', async () => {
804
+ // Two servers on different ports = different origins
805
+ const iframeServer = await createSimpleServer({
806
+ routes: {
807
+ '/iframe.html': `<!doctype html><html><body>
808
+ <script>
809
+ console.log('iframe-log-ALPHA');
810
+ console.error('iframe-error-BETA');
811
+ console.warn('iframe-warn-GAMMA');
812
+ </script>
813
+ <p>cross-origin iframe</p>
814
+ </body></html>`,
815
+ },
816
+ });
817
+ const parentServer = await createSimpleServer({
818
+ routes: {
819
+ '/': `<!doctype html><html><body>
820
+ <script>console.log('parent-log-DELTA');</script>
821
+ <iframe src="${iframeServer.baseUrl}/iframe.html"></iframe>
822
+ </body></html>`,
823
+ },
824
+ });
825
+ try {
826
+ // Clear logs and navigate to the parent page with cross-origin iframe
827
+ await client.callTool({
828
+ name: 'execute',
829
+ arguments: {
830
+ code: js `
831
+ clearAllLogs();
832
+ state.iframePage = await context.newPage();
833
+ await state.iframePage.goto('${parentServer.baseUrl}', { waitUntil: 'networkidle' });
834
+ // Wait for iframe to load and logs to be captured
835
+ await state.iframePage.frameLocator('iframe').locator('p').waitFor({ timeout: 5000 });
836
+ await new Promise(resolve => setTimeout(resolve, 500));
837
+ `,
838
+ },
839
+ });
840
+ // Retrieve logs and verify both parent and iframe logs are captured
841
+ const logsResult = await client.callTool({
842
+ name: 'execute',
843
+ arguments: {
844
+ code: js `
845
+ const logs = await getLatestLogs({ page: state.iframePage });
846
+ console.log('Cross-origin iframe logs count:', logs.length);
847
+ logs.forEach(log => console.log(log));
848
+ `,
849
+ },
850
+ });
851
+ const output = logsResult.content[0].text;
852
+ // Parent page log
853
+ expect(output).toContain('parent-log-DELTA');
854
+ // Cross-origin iframe logs
855
+ expect(output).toContain('iframe-log-ALPHA');
856
+ expect(output).toContain('iframe-error-BETA');
857
+ expect(output).toContain('iframe-warn-GAMMA');
858
+ // Clean up
859
+ await client.callTool({
860
+ name: 'execute',
861
+ arguments: {
862
+ code: js `
863
+ await state.iframePage.close();
864
+ delete state.iframePage;
865
+ `,
866
+ },
867
+ });
868
+ }
869
+ finally {
870
+ await Promise.all([parentServer.close(), iframeServer.close()]);
871
+ }
872
+ }, 60000);
873
+ it('should preserve system color scheme instead of forcing light mode', async () => {
546
874
  const browserContext = getBrowserContext();
547
875
  const serviceWorker = await getExtensionServiceWorker(browserContext);
548
876
  const page = await browserContext.newPage();
549
877
  await page.goto('https://example.com');
550
878
  await page.bringToFront();
879
+ // test-utils launches with colorScheme: 'dark', so before MCP connection
880
+ // the browser should report dark mode
551
881
  const colorSchemeBefore = await page.evaluate(() => {
552
882
  return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
553
883
  });
554
- console.log('Color scheme before MCP connection:', colorSchemeBefore);
884
+ expect(colorSchemeBefore).toBe('dark');
555
885
  await serviceWorker.evaluate(async () => {
556
886
  await globalThis.toggleExtensionForActiveTab();
557
887
  });
558
- await new Promise(r => setTimeout(r, 100));
888
+ await new Promise((r) => setTimeout(r, 500));
559
889
  const result = await client.callTool({
560
890
  name: 'execute',
561
891
  arguments: {
@@ -573,14 +903,17 @@ describe('Relay Core Tests', () => {
573
903
  },
574
904
  });
575
905
  console.log('Color scheme after MCP connection:', result.content);
906
+ // After MCP connection, color scheme should NOT be forced to light.
907
+ // The page.ts default is now 'no-override', so the browser's actual
908
+ // color scheme (dark, from test-utils launch config) should be preserved.
576
909
  expect(result.content).toMatchInlineSnapshot(`
577
- [
578
- {
579
- "text": "[return value] { error: 'Page not found', urls: [ 'about:blank' ] }",
580
- "type": "text",
581
- },
582
- ]
583
- `);
910
+ [
911
+ {
912
+ "text": "[return value] { matchesDark: true, matchesLight: false }",
913
+ "type": "text",
914
+ },
915
+ ]
916
+ `);
584
917
  await page.close();
585
918
  }, 60000);
586
919
  it('should get clean HTML with getCleanHTML', async () => {
@@ -607,7 +940,7 @@ describe('Relay Core Tests', () => {
607
940
  await serviceWorker.evaluate(async () => {
608
941
  await globalThis.toggleExtensionForActiveTab();
609
942
  });
610
- await new Promise(r => setTimeout(r, 400));
943
+ await new Promise((r) => setTimeout(r, 400));
611
944
  // Test basic getCleanHTML
612
945
  const result = await client.callTool({
613
946
  name: 'execute',
@@ -698,7 +1031,7 @@ describe('Relay Core Tests', () => {
698
1031
  await serviceWorker.evaluate(async () => {
699
1032
  await globalThis.toggleExtensionForActiveTab();
700
1033
  });
701
- await new Promise(r => setTimeout(r, 400));
1034
+ await new Promise((r) => setTimeout(r, 400));
702
1035
  // Test basic getPageMarkdown
703
1036
  const result = await client.callTool({
704
1037
  name: 'execute',
@@ -759,7 +1092,7 @@ describe('Relay Core Tests', () => {
759
1092
  await serviceWorker.evaluate(async () => {
760
1093
  await globalThis.disconnectEverything();
761
1094
  });
762
- await new Promise(r => setTimeout(r, 100));
1095
+ await new Promise((r) => setTimeout(r, 100));
763
1096
  // 2. Create first page and enable extension
764
1097
  const page1 = await browserContext.newPage();
765
1098
  await page1.goto('https://example.com/first-page');
@@ -767,7 +1100,7 @@ describe('Relay Core Tests', () => {
767
1100
  await serviceWorker.evaluate(async () => {
768
1101
  await globalThis.toggleExtensionForActiveTab();
769
1102
  });
770
- await new Promise(r => setTimeout(r, 100));
1103
+ await new Promise((r) => setTimeout(r, 100));
771
1104
  // 3. Reset MCP to ensure page1 becomes the default page (only page available)
772
1105
  const resetResult = await client.callTool({
773
1106
  name: 'reset',
@@ -793,10 +1126,10 @@ describe('Relay Core Tests', () => {
793
1126
  await serviceWorker.evaluate(async () => {
794
1127
  await globalThis.toggleExtensionForActiveTab();
795
1128
  });
796
- await new Promise(r => setTimeout(r, 100));
1129
+ await new Promise((r) => setTimeout(r, 100));
797
1130
  // 6. Close the first page (which is the default `page` in MCP scope)
798
1131
  await page1.close();
799
- await new Promise(r => setTimeout(r, 100));
1132
+ await new Promise((r) => setTimeout(r, 100));
800
1133
  // 7. Execute code via MCP - should NOT fail with "page closed" error
801
1134
  // Instead, it should automatically switch to the second page
802
1135
  const afterCloseResult = await client.callTool({
@@ -819,5 +1152,161 @@ describe('Relay Core Tests', () => {
819
1152
  // Cleanup
820
1153
  await page2.close();
821
1154
  }, 60000);
1155
+ it('should show descriptive error when clicking a hidden element', async () => {
1156
+ await ensureConnectedTabForExecute();
1157
+ // Create a fresh page and set content with a collapsed details element
1158
+ await client.callTool({
1159
+ name: 'execute',
1160
+ arguments: {
1161
+ code: js `
1162
+ state.errorTestPage = await context.newPage();
1163
+ await state.errorTestPage.setContent(\`
1164
+ <details>
1165
+ <summary>Toggle</summary>
1166
+ <button id="hidden-btn">Hidden Button</button>
1167
+ </details>
1168
+ \`);
1169
+ `,
1170
+ },
1171
+ });
1172
+ const result = await client.callTool({
1173
+ name: 'execute',
1174
+ arguments: {
1175
+ code: js `
1176
+ await state.errorTestPage.click('#hidden-btn', { timeout: 100 });
1177
+ `,
1178
+ },
1179
+ });
1180
+ expect(result).toMatchInlineSnapshot(`
1181
+ {
1182
+ "content": [
1183
+ {
1184
+ "text": "
1185
+ Error executing code: page.click: Timeout 100ms exceeded. Element is not visible — it may be hidden by CSS, inside a collapsed <details>, inactive tab, or closed accordion. Try: interact with the page to reveal it first, or use { force: true } to skip visibility checks
1186
+ Call log:
1187
+  - waiting for locator('#hidden-btn')
1188
+  - locator resolved to <button id="hidden-btn">Hidden Button</button>
1189
+  - attempting click action
1190
+  2 × waiting for element to be visible, enabled and stable
1191
+  - element is not visible
1192
+  - retrying click action
1193
+  - waiting 20ms
1194
+  - waiting for element to be visible, enabled and stable
1195
+  - element is not visible
1196
+  - retrying click action
1197
+  - waiting 100ms
1198
+ ",
1199
+ "type": "text",
1200
+ },
1201
+ ],
1202
+ "isError": true,
1203
+ }
1204
+ `);
1205
+ // Cleanup
1206
+ await client.callTool({ name: 'execute', arguments: { code: js `await state.errorTestPage.close(); delete state.errorTestPage;` } });
1207
+ }, 30000);
1208
+ it('should show descriptive error when clicking an element covered by another', async () => {
1209
+ await ensureConnectedTabForExecute();
1210
+ await client.callTool({
1211
+ name: 'execute',
1212
+ arguments: {
1213
+ code: js `
1214
+ state.errorTestPage = await context.newPage();
1215
+ await state.errorTestPage.setContent(\`
1216
+ <div style="position:relative">
1217
+ <button id="covered-btn" style="position:absolute;top:0;left:0">Covered</button>
1218
+ <div id="overlay" style="position:absolute;top:0;left:0;width:200px;height:200px;background:red;z-index:10">Overlay</div>
1219
+ </div>
1220
+ \`);
1221
+ `,
1222
+ },
1223
+ });
1224
+ const result = await client.callTool({
1225
+ name: 'execute',
1226
+ arguments: {
1227
+ code: js `
1228
+ await state.errorTestPage.click('#covered-btn', { timeout: 100 });
1229
+ `,
1230
+ },
1231
+ });
1232
+ expect(result).toMatchInlineSnapshot(`
1233
+ {
1234
+ "content": [
1235
+ {
1236
+ "text": "
1237
+ Error executing code: page.click: Timeout 100ms exceeded. <div id="overlay">Overlay</div> intercepts pointer events
1238
+ Call log:
1239
+  - waiting for locator('#covered-btn')
1240
+  - locator resolved to <button id="covered-btn">Covered</button>
1241
+  - attempting click action
1242
+  2 × waiting for element to be visible, enabled and stable
1243
+  - element is visible, enabled and stable
1244
+  - scrolling into view if needed
1245
+  - done scrolling
1246
+  - <div id="overlay">Overlay</div> intercepts pointer events
1247
+  - retrying click action
1248
+  - waiting 20ms
1249
+  - waiting for element to be visible, enabled and stable
1250
+  - element is visible, enabled and stable
1251
+  - scrolling into view if needed
1252
+  - done scrolling
1253
+  - <div id="overlay">Overlay</div> intercepts pointer events
1254
+  - retrying click action
1255
+  - waiting 100ms
1256
+ ",
1257
+ "type": "text",
1258
+ },
1259
+ ],
1260
+ "isError": true,
1261
+ }
1262
+ `);
1263
+ await client.callTool({ name: 'execute', arguments: { code: js `await state.errorTestPage.close(); delete state.errorTestPage;` } });
1264
+ }, 30000);
1265
+ it('should show descriptive error when clicking a display:none element', async () => {
1266
+ await ensureConnectedTabForExecute();
1267
+ await client.callTool({
1268
+ name: 'execute',
1269
+ arguments: {
1270
+ code: js `
1271
+ state.errorTestPage = await context.newPage();
1272
+ await state.errorTestPage.setContent('<button id="invisible" style="display:none">Invisible</button>');
1273
+ `,
1274
+ },
1275
+ });
1276
+ const result = await client.callTool({
1277
+ name: 'execute',
1278
+ arguments: {
1279
+ code: js `
1280
+ await state.errorTestPage.click('#invisible', { timeout: 100 });
1281
+ `,
1282
+ },
1283
+ });
1284
+ expect(result).toMatchInlineSnapshot(`
1285
+ {
1286
+ "content": [
1287
+ {
1288
+ "text": "
1289
+ Error executing code: page.click: Timeout 100ms exceeded. Element is not visible — it may be hidden by CSS, inside a collapsed <details>, inactive tab, or closed accordion. Try: interact with the page to reveal it first, or use { force: true } to skip visibility checks
1290
+ Call log:
1291
+  - waiting for locator('#invisible')
1292
+  - locator resolved to <button id="invisible">Invisible</button>
1293
+  - attempting click action
1294
+  2 × waiting for element to be visible, enabled and stable
1295
+  - element is not visible
1296
+  - retrying click action
1297
+  - waiting 20ms
1298
+  - waiting for element to be visible, enabled and stable
1299
+  - element is not visible
1300
+  - retrying click action
1301
+  - waiting 100ms
1302
+ ",
1303
+ "type": "text",
1304
+ },
1305
+ ],
1306
+ "isError": true,
1307
+ }
1308
+ `);
1309
+ await client.callTool({ name: 'execute', arguments: { code: js `await state.errorTestPage.close(); delete state.errorTestPage;` } });
1310
+ }, 30000);
822
1311
  });
823
1312
  //# sourceMappingURL=relay-core.test.js.map