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
@@ -17,87 +17,92 @@ import './test-declarations.js'
17
17
  const TEST_PORT = 19991
18
18
 
19
19
  describe('Snapshot & Screenshot Tests', () => {
20
- let client: Awaited<ReturnType<typeof createMCPClient>>['client']
21
- let cleanup: (() => Promise<void>) | null = null
22
- let testCtx: TestContext | null = null
23
-
24
- beforeAll(async () => {
25
- testCtx = await setupTestContext({ port: TEST_PORT, tempDirPrefix: 'pw-snap-test-', toggleExtension: true })
26
-
27
- const result = await createMCPClient({ port: TEST_PORT })
28
- client = result.client
29
- cleanup = result.cleanup
30
- }, 600000)
31
-
32
- afterAll(async () => {
33
- await cleanupTestContext(testCtx, cleanup)
34
- cleanup = null
35
- testCtx = null
20
+ let client: Awaited<ReturnType<typeof createMCPClient>>['client']
21
+ let cleanup: (() => Promise<void>) | null = null
22
+ let testCtx: TestContext | null = null
23
+
24
+ beforeAll(async () => {
25
+ testCtx = await setupTestContext({ port: TEST_PORT, tempDirPrefix: 'pw-snap-test-', toggleExtension: true })
26
+
27
+ const result = await createMCPClient({ port: TEST_PORT })
28
+ client = result.client
29
+ cleanup = result.cleanup
30
+ }, 600000)
31
+
32
+ afterAll(async () => {
33
+ await cleanupTestContext(testCtx, cleanup)
34
+ cleanup = null
35
+ testCtx = null
36
+ })
37
+
38
+ const getBrowserContext = () => {
39
+ if (!testCtx?.browserContext) throw new Error('Browser not initialized')
40
+ return testCtx.browserContext
41
+ }
42
+
43
+ it('should capture screenshot correctly', async () => {
44
+ const browserContext = getBrowserContext()
45
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
46
+
47
+ const page = await browserContext.newPage()
48
+ await page.goto('https://example.com/')
49
+ await page.bringToFront()
50
+
51
+ await serviceWorker.evaluate(async () => {
52
+ await globalThis.toggleExtensionForActiveTab()
36
53
  })
37
54
 
38
- const getBrowserContext = () => {
39
- if (!testCtx?.browserContext) throw new Error('Browser not initialized')
40
- return testCtx.browserContext
41
- }
42
-
43
- it('should capture screenshot correctly', async () => {
44
- const browserContext = getBrowserContext()
45
- const serviceWorker = await getExtensionServiceWorker(browserContext)
46
-
47
- const page = await browserContext.newPage()
48
- await page.goto('https://example.com/')
49
- await page.bringToFront()
50
-
51
- await serviceWorker.evaluate(async () => {
52
- await globalThis.toggleExtensionForActiveTab()
53
- })
54
-
55
- await new Promise(r => setTimeout(r, 100))
55
+ await new Promise((r) => setTimeout(r, 100))
56
56
 
57
- const capturedCommands: CDPCommand[] = []
58
- const commandHandler = ({ command }: { clientId: string; command: CDPCommand }) => {
59
- if (command.method === 'Page.captureScreenshot') {
60
- capturedCommands.push(command)
61
- }
62
- }
63
- testCtx!.relayServer.on('cdp:command', commandHandler)
57
+ const capturedCommands: CDPCommand[] = []
58
+ const commandHandler = ({ command }: { clientId: string; command: CDPCommand }) => {
59
+ if (command.method === 'Page.captureScreenshot') {
60
+ capturedCommands.push(command)
61
+ }
62
+ }
63
+ testCtx!.relayServer.on('cdp:command', commandHandler)
64
64
 
65
- const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
66
- const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
65
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
66
+ const cdpPage = browser
67
+ .contexts()[0]
68
+ .pages()
69
+ .find((p) => p.url().includes('example.com'))
67
70
 
68
- expect(cdpPage).toBeDefined()
71
+ expect(cdpPage).toBeDefined()
69
72
 
70
- const viewportSize = cdpPage!.viewportSize()
71
- console.log('Viewport size:', viewportSize)
73
+ const viewportSize = cdpPage!.viewportSize()
74
+ console.log('Viewport size:', viewportSize)
72
75
 
73
- const viewportScreenshot = await cdpPage!.screenshot()
74
- expect(viewportScreenshot).toBeDefined()
76
+ const viewportScreenshot = await cdpPage!.screenshot()
77
+ expect(viewportScreenshot).toBeDefined()
75
78
 
76
- const viewportDimensions = imageSize(viewportScreenshot)
77
- console.log('Viewport screenshot dimensions:', viewportDimensions)
78
- expect(viewportDimensions.width).toBeGreaterThan(0)
79
- expect(viewportDimensions.height).toBeGreaterThan(0)
80
- if (viewportSize) {
81
- expect(viewportDimensions.width).toBe(viewportSize.width)
82
- expect(viewportDimensions.height).toBe(viewportSize.height)
83
- }
79
+ const viewportDimensions = imageSize(viewportScreenshot)
80
+ console.log('Viewport screenshot dimensions:', viewportDimensions)
81
+ expect(viewportDimensions.width).toBeGreaterThan(0)
82
+ expect(viewportDimensions.height).toBeGreaterThan(0)
83
+ if (viewportSize) {
84
+ expect(viewportDimensions.width).toBe(viewportSize.width)
85
+ expect(viewportDimensions.height).toBe(viewportSize.height)
86
+ }
84
87
 
85
- const fullPageScreenshot = await cdpPage!.screenshot({ fullPage: true })
86
- expect(fullPageScreenshot).toBeDefined()
88
+ const fullPageScreenshot = await cdpPage!.screenshot({ fullPage: true })
89
+ expect(fullPageScreenshot).toBeDefined()
87
90
 
88
- const fullPageDimensions = imageSize(fullPageScreenshot)
89
- console.log('Full page screenshot dimensions:', fullPageDimensions)
90
- expect(fullPageDimensions.width).toBeGreaterThan(0)
91
- expect(fullPageDimensions.height).toBeGreaterThan(0)
92
- expect(fullPageDimensions.width).toBeGreaterThanOrEqual(viewportDimensions.width!)
91
+ const fullPageDimensions = imageSize(fullPageScreenshot)
92
+ console.log('Full page screenshot dimensions:', fullPageDimensions)
93
+ expect(fullPageDimensions.width).toBeGreaterThan(0)
94
+ expect(fullPageDimensions.height).toBeGreaterThan(0)
95
+ expect(fullPageDimensions.width).toBeGreaterThanOrEqual(viewportDimensions.width!)
93
96
 
94
- testCtx!.relayServer.off('cdp:command', commandHandler)
97
+ testCtx!.relayServer.off('cdp:command', commandHandler)
95
98
 
96
- expect(capturedCommands.length).toBe(2)
97
- expect(capturedCommands.map(c => ({
98
- method: c.method,
99
- params: c.params
100
- }))).toMatchInlineSnapshot(`
99
+ expect(capturedCommands.length).toBe(2)
100
+ expect(
101
+ capturedCommands.map((c) => ({
102
+ method: c.method,
103
+ params: c.params,
104
+ })),
105
+ ).toMatchInlineSnapshot(`
101
106
  [
102
107
  {
103
108
  "method": "Page.captureScreenshot",
@@ -130,23 +135,65 @@ describe('Snapshot & Screenshot Tests', () => {
130
135
  ]
131
136
  `)
132
137
 
133
- const screenshotPath = path.join(os.tmpdir(), 'playwriter-test-screenshot.png')
134
- fs.writeFileSync(screenshotPath, viewportScreenshot)
135
- console.log('Screenshot saved to:', screenshotPath)
138
+ const screenshotPath = path.join(os.tmpdir(), 'playwriter-test-screenshot.png')
139
+ fs.writeFileSync(screenshotPath, viewportScreenshot)
140
+ console.log('Screenshot saved to:', screenshotPath)
136
141
 
137
- await browser.close()
138
- await page.close()
139
- }, 60000)
142
+ await browser.close()
143
+ await page.close()
144
+ }, 60000)
140
145
 
141
- it('should capture element screenshot with correct coordinates', async () => {
142
- const browserContext = getBrowserContext()
143
- const serviceWorker = await getExtensionServiceWorker(browserContext)
146
+ it('should match window.innerWidth/Height without clip param', async () => {
147
+ // Proves that in connectOverCDP mode, Playwright already queries
148
+ // window.innerWidth/innerHeight for viewport screenshots, so passing
149
+ // a manual clip is redundant.
150
+ const browserContext = getBrowserContext()
151
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
144
152
 
145
- const target = { x: 200, y: 150, width: 300, height: 100 }
146
- const scrolledTarget = { x: 100, y: 1500, width: 200, height: 80 }
153
+ const page = await browserContext.newPage()
154
+ await page.goto('https://example.com/')
155
+ await page.bringToFront()
147
156
 
148
- const page = await browserContext.newPage()
149
- await page.setContent(`
157
+ await serviceWorker.evaluate(async () => {
158
+ await globalThis.toggleExtensionForActiveTab()
159
+ })
160
+ await new Promise((r) => setTimeout(r, 100))
161
+
162
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
163
+ const cdpPage = browser
164
+ .contexts()[0]
165
+ .pages()
166
+ .find((p) => p.url().includes('example.com'))
167
+ expect(cdpPage).toBeDefined()
168
+
169
+ // Get actual browser viewport via JS
170
+ const actualViewport = await cdpPage!.evaluate(() => ({
171
+ width: window.innerWidth,
172
+ height: window.innerHeight,
173
+ }))
174
+ console.log('Actual viewport (window.inner*):', actualViewport)
175
+
176
+ // Plain screenshot with scale:'css', NO clip
177
+ const screenshot = await cdpPage!.screenshot({ scale: 'css' })
178
+ const dimensions = imageSize(screenshot)
179
+ console.log('Screenshot dimensions (no clip):', dimensions)
180
+
181
+ expect(dimensions.width).toBe(actualViewport.width)
182
+ expect(dimensions.height).toBe(actualViewport.height)
183
+
184
+ await browser.close()
185
+ await page.close()
186
+ }, 60000)
187
+
188
+ it('should capture element screenshot with correct coordinates', async () => {
189
+ const browserContext = getBrowserContext()
190
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
191
+
192
+ const target = { x: 200, y: 150, width: 300, height: 100 }
193
+ const scrolledTarget = { x: 100, y: 1500, width: 200, height: 80 }
194
+
195
+ const page = await browserContext.newPage()
196
+ await page.setContent(`
150
197
  <html>
151
198
  <head>
152
199
  <style>
@@ -175,67 +222,67 @@ describe('Snapshot & Screenshot Tests', () => {
175
222
  </body>
176
223
  </html>
177
224
  `)
178
- await page.bringToFront()
225
+ await page.bringToFront()
179
226
 
180
- await serviceWorker.evaluate(async () => {
181
- await globalThis.toggleExtensionForActiveTab()
182
- })
227
+ await serviceWorker.evaluate(async () => {
228
+ await globalThis.toggleExtensionForActiveTab()
229
+ })
183
230
 
184
- await new Promise(r => setTimeout(r, 400))
231
+ await new Promise((r) => setTimeout(r, 400))
185
232
 
186
- const capturedCommands: CDPCommand[] = []
187
- const commandHandler = ({ command }: { clientId: string; command: CDPCommand }) => {
188
- if (command.method === 'Page.captureScreenshot') {
189
- capturedCommands.push(command)
190
- }
191
- }
192
- testCtx!.relayServer.on('cdp:command', commandHandler)
193
-
194
- const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
195
- let cdpPage
196
- for (const p of browser.contexts()[0].pages()) {
197
- const html = await p.content()
198
- if (html.includes('scrolled-target')) {
199
- cdpPage = p
200
- break
201
- }
202
- }
203
- expect(cdpPage).toBeDefined()
233
+ const capturedCommands: CDPCommand[] = []
234
+ const commandHandler = ({ command }: { clientId: string; command: CDPCommand }) => {
235
+ if (command.method === 'Page.captureScreenshot') {
236
+ capturedCommands.push(command)
237
+ }
238
+ }
239
+ testCtx!.relayServer.on('cdp:command', commandHandler)
240
+
241
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
242
+ let cdpPage
243
+ for (const p of browser.contexts()[0].pages()) {
244
+ const html = await p.content()
245
+ if (html.includes('scrolled-target')) {
246
+ cdpPage = p
247
+ break
248
+ }
249
+ }
250
+ expect(cdpPage).toBeDefined()
204
251
 
205
- await cdpPage!.locator('#target').screenshot()
252
+ await cdpPage!.locator('#target').screenshot()
206
253
 
207
- await cdpPage!.locator('#scrolled-target').screenshot()
254
+ await cdpPage!.locator('#scrolled-target').screenshot()
208
255
 
209
- testCtx!.relayServer.off('cdp:command', commandHandler)
256
+ testCtx!.relayServer.off('cdp:command', commandHandler)
210
257
 
211
- expect(capturedCommands.length).toBe(2)
258
+ expect(capturedCommands.length).toBe(2)
212
259
 
213
- const targetCmd = capturedCommands[0]
214
- expect(targetCmd.method).toBe('Page.captureScreenshot')
215
- const targetClip = (targetCmd.params as any).clip
216
- expect(targetClip.x).toBe(target.x)
217
- expect(targetClip.y).toBe(target.y)
218
- expect(targetClip.width).toBe(target.width)
219
- expect(targetClip.height).toBe(target.height)
260
+ const targetCmd = capturedCommands[0]
261
+ expect(targetCmd.method).toBe('Page.captureScreenshot')
262
+ const targetClip = (targetCmd.params as any).clip
263
+ expect(targetClip.x).toBe(target.x)
264
+ expect(targetClip.y).toBe(target.y)
265
+ expect(targetClip.width).toBe(target.width)
266
+ expect(targetClip.height).toBe(target.height)
220
267
 
221
- const scrolledCmd = capturedCommands[1]
222
- expect(scrolledCmd.method).toBe('Page.captureScreenshot')
223
- const scrolledClip = (scrolledCmd.params as any).clip
224
- expect(scrolledClip.x).toBe(scrolledTarget.x)
225
- expect(scrolledClip.y).toBe(scrolledTarget.y)
226
- expect(scrolledClip.width).toBe(scrolledTarget.width)
227
- expect(scrolledClip.height).toBe(scrolledTarget.height)
268
+ const scrolledCmd = capturedCommands[1]
269
+ expect(scrolledCmd.method).toBe('Page.captureScreenshot')
270
+ const scrolledClip = (scrolledCmd.params as any).clip
271
+ expect(scrolledClip.x).toBe(scrolledTarget.x)
272
+ expect(scrolledClip.y).toBe(scrolledTarget.y)
273
+ expect(scrolledClip.width).toBe(scrolledTarget.width)
274
+ expect(scrolledClip.height).toBe(scrolledTarget.height)
228
275
 
229
- await browser.close()
230
- await page.close()
231
- }, 60000)
276
+ await browser.close()
277
+ await page.close()
278
+ }, 60000)
232
279
 
233
- it('should get locator string for element using getLocatorStringForElement', async () => {
234
- const browserContext = getBrowserContext()
235
- const serviceWorker = await getExtensionServiceWorker(browserContext)
280
+ it('should get locator string for element using getLocatorStringForElement', async () => {
281
+ const browserContext = getBrowserContext()
282
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
236
283
 
237
- const page = await browserContext.newPage()
238
- await page.setContent(`
284
+ const page = await browserContext.newPage()
285
+ await page.setContent(`
239
286
  <html>
240
287
  <body>
241
288
  <button id="test-btn">Click Me</button>
@@ -243,18 +290,18 @@ describe('Snapshot & Screenshot Tests', () => {
243
290
  </body>
244
291
  </html>
245
292
  `)
246
- await page.bringToFront()
293
+ await page.bringToFront()
247
294
 
248
- await serviceWorker.evaluate(async () => {
249
- await globalThis.toggleExtensionForActiveTab()
250
- })
295
+ await serviceWorker.evaluate(async () => {
296
+ await globalThis.toggleExtensionForActiveTab()
297
+ })
251
298
 
252
- await new Promise(r => setTimeout(r, 400))
299
+ await new Promise((r) => setTimeout(r, 400))
253
300
 
254
- const result = await client.callTool({
255
- name: 'execute',
256
- arguments: {
257
- code: js`
301
+ const result = await client.callTool({
302
+ name: 'execute',
303
+ arguments: {
304
+ code: js`
258
305
  let testPage;
259
306
  for (const p of context.pages()) {
260
307
  const html = await p.content();
@@ -270,27 +317,27 @@ describe('Snapshot & Screenshot Tests', () => {
270
317
  const text = await locatorFromString.textContent();
271
318
  console.log('Locator text:', text);
272
319
  `,
273
- timeout: 30000,
274
- },
275
- })
320
+ timeout: 30000,
321
+ },
322
+ })
276
323
 
277
- expect(result.isError).toBeFalsy()
278
- const text = (result.content as any)[0]?.text || ''
279
- expect(text).toContain('Locator string:')
280
- expect(text).toContain("getByRole('button', { name: 'Click Me' })")
281
- expect(text).toContain('Locator count:')
282
- expect(text).toContain('Locator text:')
283
- expect(text).toContain('Click Me')
324
+ expect(result.isError).toBeFalsy()
325
+ const text = (result.content as any)[0]?.text || ''
326
+ expect(text).toContain('Locator string:')
327
+ expect(text).toContain("getByRole('button', { name: 'Click Me' })")
328
+ expect(text).toContain('Locator count:')
329
+ expect(text).toContain('Locator text:')
330
+ expect(text).toContain('Click Me')
284
331
 
285
- await page.close()
286
- }, 60000)
332
+ await page.close()
333
+ }, 60000)
287
334
 
288
- it('should get styles for element using getStylesForLocator', async () => {
289
- const browserContext = getBrowserContext()
290
- const serviceWorker = await getExtensionServiceWorker(browserContext)
335
+ it('should get styles for element using getStylesForLocator', async () => {
336
+ const browserContext = getBrowserContext()
337
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
291
338
 
292
- const page = await browserContext.newPage()
293
- await page.setContent(`
339
+ const page = await browserContext.newPage()
340
+ await page.setContent(`
294
341
  <html>
295
342
  <head>
296
343
  <style>
@@ -307,18 +354,18 @@ describe('Snapshot & Screenshot Tests', () => {
307
354
  </body>
308
355
  </html>
309
356
  `)
310
- await page.bringToFront()
357
+ await page.bringToFront()
311
358
 
312
- await serviceWorker.evaluate(async () => {
313
- await globalThis.toggleExtensionForActiveTab()
314
- })
359
+ await serviceWorker.evaluate(async () => {
360
+ await globalThis.toggleExtensionForActiveTab()
361
+ })
315
362
 
316
- await new Promise(r => setTimeout(r, 400))
363
+ await new Promise((r) => setTimeout(r, 400))
317
364
 
318
- const stylesResult = await client.callTool({
319
- name: 'execute',
320
- arguments: {
321
- code: js`
365
+ const stylesResult = await client.callTool({
366
+ name: 'execute',
367
+ arguments: {
368
+ code: js`
322
369
  let testPage;
323
370
  for (const p of context.pages()) {
324
371
  const html = await p.content();
@@ -329,13 +376,13 @@ describe('Snapshot & Screenshot Tests', () => {
329
376
  const styles = await getStylesForLocator({ locator: btn });
330
377
  return styles;
331
378
  `,
332
- timeout: 30000,
333
- },
334
- })
379
+ timeout: 30000,
380
+ },
381
+ })
335
382
 
336
- expect(stylesResult.isError).toBeFalsy()
337
- const stylesText = (stylesResult.content as any)[0]?.text || ''
338
- expect(stylesText).toMatchInlineSnapshot(`
383
+ expect(stylesResult.isError).toBeFalsy()
384
+ const stylesText = (stylesResult.content as any)[0]?.text || ''
385
+ expect(stylesText).toMatchInlineSnapshot(`
339
386
  "[return value] {
340
387
  element: 'button#main-btn.btn',
341
388
  inlineStyle: { 'font-weight': 'bold' },
@@ -397,10 +444,10 @@ describe('Snapshot & Screenshot Tests', () => {
397
444
  }"
398
445
  `)
399
446
 
400
- const formattedResult = await client.callTool({
401
- name: 'execute',
402
- arguments: {
403
- code: js`
447
+ const formattedResult = await client.callTool({
448
+ name: 'execute',
449
+ arguments: {
450
+ code: js`
404
451
  let testPage;
405
452
  for (const p of context.pages()) {
406
453
  const html = await p.content();
@@ -411,13 +458,13 @@ describe('Snapshot & Screenshot Tests', () => {
411
458
  const styles = await getStylesForLocator({ locator: btn });
412
459
  return formatStylesAsText(styles);
413
460
  `,
414
- timeout: 30000,
415
- },
416
- })
461
+ timeout: 30000,
462
+ },
463
+ })
417
464
 
418
- expect(formattedResult.isError).toBeFalsy()
419
- const formattedText = (formattedResult.content as any)[0]?.text || ''
420
- expect(formattedText).toMatchInlineSnapshot(`
465
+ expect(formattedResult.isError).toBeFalsy()
466
+ const formattedText = (formattedResult.content as any)[0]?.text || ''
467
+ expect(formattedText).toMatchInlineSnapshot(`
421
468
  "[return value] Element: button#main-btn.btn
422
469
 
423
470
  Inline styles:
@@ -462,42 +509,46 @@ describe('Snapshot & Screenshot Tests', () => {
462
509
  }"
463
510
  `)
464
511
 
465
- await page.close()
466
- }, 60000)
512
+ await page.close()
513
+ }, 60000)
467
514
 
468
- it('should return correct layout metrics via CDP', async () => {
469
- const browserContext = getBrowserContext()
470
- const serviceWorker = await getExtensionServiceWorker(browserContext)
515
+ it('should return correct layout metrics via CDP', async () => {
516
+ const browserContext = getBrowserContext()
517
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
471
518
 
472
- const page = await browserContext.newPage()
473
- await page.goto('https://example.com/')
474
- await page.bringToFront()
519
+ const page = await browserContext.newPage()
520
+ await page.goto('https://example.com/')
521
+ await page.bringToFront()
475
522
 
476
- await serviceWorker.evaluate(async () => {
477
- await globalThis.toggleExtensionForActiveTab()
478
- })
523
+ await serviceWorker.evaluate(async () => {
524
+ await globalThis.toggleExtensionForActiveTab()
525
+ })
479
526
 
480
- await new Promise(r => setTimeout(r, 100))
527
+ await new Promise((r) => setTimeout(r, 100))
481
528
 
482
- const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
483
- const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
484
- expect(cdpPage).toBeDefined()
529
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
530
+ const cdpPage = browser
531
+ .contexts()[0]
532
+ .pages()
533
+ .find((p) => p.url().includes('example.com'))
534
+ expect(cdpPage).toBeDefined()
485
535
 
486
- const cdpSession = await getCDPSessionForPage({ page: cdpPage! })
536
+ const cdpSession = await getCDPSessionForPage({ page: cdpPage! })
487
537
 
488
- const layoutMetrics = await cdpSession.send('Page.getLayoutMetrics')
538
+ const layoutMetrics = await cdpSession.send('Page.getLayoutMetrics')
489
539
 
490
- const normalized = {
491
- cssLayoutViewport: layoutMetrics.cssLayoutViewport,
492
- cssVisualViewport: layoutMetrics.cssVisualViewport,
493
- layoutViewport: layoutMetrics.layoutViewport,
494
- visualViewport: layoutMetrics.visualViewport,
495
- devicePixelRatio: layoutMetrics.cssVisualViewport.clientWidth > 0
496
- ? layoutMetrics.visualViewport.clientWidth / layoutMetrics.cssVisualViewport.clientWidth
497
- : 1,
498
- }
540
+ const normalized = {
541
+ cssLayoutViewport: layoutMetrics.cssLayoutViewport,
542
+ cssVisualViewport: layoutMetrics.cssVisualViewport,
543
+ layoutViewport: layoutMetrics.layoutViewport,
544
+ visualViewport: layoutMetrics.visualViewport,
545
+ devicePixelRatio:
546
+ layoutMetrics.cssVisualViewport.clientWidth > 0
547
+ ? layoutMetrics.visualViewport.clientWidth / layoutMetrics.cssVisualViewport.clientWidth
548
+ : 1,
549
+ }
499
550
 
500
- expect(normalized).toMatchInlineSnapshot(`
551
+ expect(normalized).toMatchInlineSnapshot(`
501
552
  {
502
553
  "cssLayoutViewport": {
503
554
  "clientHeight": 720,
@@ -535,59 +586,62 @@ describe('Snapshot & Screenshot Tests', () => {
535
586
  }
536
587
  `)
537
588
 
538
- const windowDpr = await cdpPage!.evaluate(() => (globalThis as any).devicePixelRatio)
539
- console.log('window.devicePixelRatio:', windowDpr)
540
- expect(windowDpr).toBe(1)
541
-
542
- await cdpSession.detach()
543
- await browser.close()
544
- await page.close()
545
- }, 60000)
546
-
547
- it('should support getExistingCDPSession through the relay (reusing Playwright WS)', async () => {
548
- const browserContext = getBrowserContext()
549
- const serviceWorker = await getExtensionServiceWorker(browserContext)
550
-
551
- const page = await browserContext.newPage()
552
- await page.goto('https://example.com/')
553
- await page.bringToFront()
554
-
555
- await serviceWorker.evaluate(async () => {
556
- await globalThis.toggleExtensionForActiveTab()
557
- })
558
-
559
- await new Promise(r => setTimeout(r, 100))
560
-
561
- const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
562
- const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
563
- expect(cdpPage).toBeDefined()
564
-
565
- // Use the new getCDPSessionForPage which reuses Playwright's internal WS
566
- const cdpClient = await getCDPSessionForPage({ page: cdpPage! })
589
+ const windowDpr = await cdpPage!.evaluate(() => (globalThis as any).devicePixelRatio)
590
+ console.log('window.devicePixelRatio:', windowDpr)
591
+ expect(windowDpr).toBe(1)
567
592
 
568
- // Should be able to send CDP commands just like the regular getCDPSessionForPage
569
- const layoutMetrics = await cdpClient.send('Page.getLayoutMetrics')
570
- expect(layoutMetrics).toBeDefined()
571
- const metrics = layoutMetrics as { cssVisualViewport?: { clientWidth?: number } }
572
- expect(metrics.cssVisualViewport).toBeDefined()
573
- expect(metrics.cssVisualViewport!.clientWidth).toBeGreaterThan(0)
593
+ await cdpSession.detach()
594
+ await browser.close()
595
+ await page.close()
596
+ }, 60000)
574
597
 
575
- // Test DOM access
576
- const document = await cdpClient.send('DOM.getDocument')
577
- expect(document).toBeDefined()
598
+ it('should support getExistingCDPSession through the relay (reusing Playwright WS)', async () => {
599
+ const browserContext = getBrowserContext()
600
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
578
601
 
579
- await cdpClient.detach()
580
- await browser.close()
581
- await page.close()
582
- }, 60000)
602
+ const page = await browserContext.newPage()
603
+ await page.goto('https://example.com/')
604
+ await page.bringToFront()
583
605
 
584
- it('should get aria ref for locator using getAriaSnapshot', async () => {
585
- const browserContext = getBrowserContext()
586
- const serviceWorker = await getExtensionServiceWorker(browserContext)
606
+ await serviceWorker.evaluate(async () => {
607
+ await globalThis.toggleExtensionForActiveTab()
608
+ })
587
609
 
588
- const page = await browserContext.newPage()
589
- // Use data-testid for stable refs, regular id for the button
590
- await page.setContent(`
610
+ await new Promise((r) => setTimeout(r, 100))
611
+
612
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
613
+ const cdpPage = browser
614
+ .contexts()[0]
615
+ .pages()
616
+ .find((p) => p.url().includes('example.com'))
617
+ expect(cdpPage).toBeDefined()
618
+
619
+ // Use the new getCDPSessionForPage which reuses Playwright's internal WS
620
+ const cdpClient = await getCDPSessionForPage({ page: cdpPage! })
621
+
622
+ // Should be able to send CDP commands just like the regular getCDPSessionForPage
623
+ const layoutMetrics = await cdpClient.send('Page.getLayoutMetrics')
624
+ expect(layoutMetrics).toBeDefined()
625
+ const metrics = layoutMetrics as { cssVisualViewport?: { clientWidth?: number } }
626
+ expect(metrics.cssVisualViewport).toBeDefined()
627
+ expect(metrics.cssVisualViewport!.clientWidth).toBeGreaterThan(0)
628
+
629
+ // Test DOM access
630
+ const document = await cdpClient.send('DOM.getDocument')
631
+ expect(document).toBeDefined()
632
+
633
+ await cdpClient.detach()
634
+ await browser.close()
635
+ await page.close()
636
+ }, 60000)
637
+
638
+ it('should get aria ref for locator using getAriaSnapshot', async () => {
639
+ const browserContext = getBrowserContext()
640
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
641
+
642
+ const page = await browserContext.newPage()
643
+ // Use data-testid for stable refs, regular id for the button
644
+ await page.setContent(`
591
645
  <html>
592
646
  <body>
593
647
  <button data-testid="submit-btn">Submit Form</button>
@@ -596,257 +650,255 @@ describe('Snapshot & Screenshot Tests', () => {
596
650
  </body>
597
651
  </html>
598
652
  `)
599
- await page.bringToFront()
600
-
601
- await serviceWorker.evaluate(async () => {
602
- await globalThis.toggleExtensionForActiveTab()
603
- })
604
- await new Promise(r => setTimeout(r, 400))
605
-
606
- const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
607
- let cdpPage
608
- for (const p of browser.contexts()[0].pages()) {
609
- const html = await p.content()
610
- if (html.includes('submit-btn')) {
611
- cdpPage = p
612
- break
613
- }
614
- }
615
- expect(cdpPage).toBeDefined()
616
-
617
- const { getAriaSnapshot } = await import('./aria-snapshot.js')
618
-
619
- const ariaResult = await getAriaSnapshot({
620
- page: cdpPage!,
621
- })
622
-
623
- expect(ariaResult.snapshot).toBeDefined()
624
- expect(ariaResult.snapshot.length).toBeGreaterThan(0)
625
- expect(ariaResult.snapshot).toContain('Submit Form')
626
- // Snapshot lines include Playwright locators for interactive elements
627
- expect(ariaResult.snapshot).toContain('[data-testid="submit-btn"]')
628
- expect(ariaResult.snapshot).toContain('[data-testid="about-link"]')
629
- expect(ariaResult.snapshot).toContain('[data-testid="name-input"]')
630
-
631
- const flattenNodes = (nodes: AriaSnapshotNode[]): AriaSnapshotNode[] => {
632
- return nodes.flatMap((node) => {
633
- return [node, ...flattenNodes(node.children)]
634
- })
635
- }
653
+ await page.bringToFront()
636
654
 
637
- const allNodes = flattenNodes(ariaResult.tree)
638
- const findByLocator = (locator: string) => {
639
- return allNodes.find((node) => node.locator === locator)
640
- }
655
+ await serviceWorker.evaluate(async () => {
656
+ await globalThis.toggleExtensionForActiveTab()
657
+ })
658
+ await new Promise((r) => setTimeout(r, 400))
659
+
660
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
661
+ let cdpPage
662
+ for (const p of browser.contexts()[0].pages()) {
663
+ const html = await p.content()
664
+ if (html.includes('submit-btn')) {
665
+ cdpPage = p
666
+ break
667
+ }
668
+ }
669
+ expect(cdpPage).toBeDefined()
641
670
 
642
- const submitNode = findByLocator('[data-testid="submit-btn"]')
643
- const aboutNode = findByLocator('[data-testid="about-link"]')
644
- const nameNode = findByLocator('[data-testid="name-input"]')
671
+ const { getAriaSnapshot } = await import('./aria-snapshot.js')
645
672
 
646
- expect(submitNode).toBeDefined()
647
- expect(aboutNode).toBeDefined()
648
- expect(nameNode).toBeDefined()
673
+ const ariaResult = await getAriaSnapshot({
674
+ page: cdpPage!,
675
+ })
649
676
 
650
- const submitLocator = cdpPage!.locator(submitNode!.locator!)
651
- const aboutLocator = cdpPage!.locator(aboutNode!.locator!)
652
- const nameLocator = cdpPage!.locator(nameNode!.locator!)
677
+ expect(ariaResult.snapshot).toBeDefined()
678
+ expect(ariaResult.snapshot.length).toBeGreaterThan(0)
679
+ expect(ariaResult.snapshot).toContain('Submit Form')
680
+ // Snapshot lines include Playwright locators for interactive elements
681
+ expect(ariaResult.snapshot).toContain('[data-testid="submit-btn"]')
682
+ expect(ariaResult.snapshot).toContain('[data-testid="about-link"]')
683
+ expect(ariaResult.snapshot).toContain('[data-testid="name-input"]')
684
+
685
+ const flattenNodes = (nodes: AriaSnapshotNode[]): AriaSnapshotNode[] => {
686
+ return nodes.flatMap((node) => {
687
+ return [node, ...flattenNodes(node.children)]
688
+ })
689
+ }
653
690
 
654
- expect(await submitLocator.count()).toBe(1)
655
- expect(await aboutLocator.count()).toBe(1)
656
- expect(await nameLocator.count()).toBe(1)
691
+ const allNodes = flattenNodes(ariaResult.tree)
692
+ const findByLocator = (locator: string) => {
693
+ return allNodes.find((node) => node.locator === locator)
694
+ }
657
695
 
658
- expect(await submitLocator.textContent()).toBe('Submit Form')
659
- expect(await aboutLocator.textContent()).toBe('About Us')
660
- expect(await nameLocator.getAttribute('placeholder')).toBe('Enter your name')
696
+ const submitNode = findByLocator('[data-testid="submit-btn"]')
697
+ const aboutNode = findByLocator('[data-testid="about-link"]')
698
+ const nameNode = findByLocator('[data-testid="name-input"]')
661
699
 
662
- expect(ariaResult.refToElement.size).toBeGreaterThan(0)
663
- console.log('RefToElement map size:', ariaResult.refToElement.size)
664
- console.log('RefToElement entries:', [...ariaResult.refToElement.entries()])
700
+ expect(submitNode).toBeDefined()
701
+ expect(aboutNode).toBeDefined()
702
+ expect(nameNode).toBeDefined()
665
703
 
666
- // Verify refs are stable test IDs
667
- expect(ariaResult.refToElement.has('submit-btn')).toBe(true)
668
- expect(ariaResult.refToElement.has('about-link')).toBe(true)
669
- expect(ariaResult.refToElement.has('name-input')).toBe(true)
704
+ const submitLocator = cdpPage!.locator(submitNode!.locator!)
705
+ const aboutLocator = cdpPage!.locator(aboutNode!.locator!)
706
+ const nameLocator = cdpPage!.locator(nameNode!.locator!)
670
707
 
671
- // Use getSelectorForRef to get CSS selector for a ref
672
- const btnSelector = ariaResult.getSelectorForRef('submit-btn')
673
- expect(btnSelector).toBeDefined()
674
- console.log('Button selector:', btnSelector)
708
+ expect(await submitLocator.count()).toBe(1)
709
+ expect(await aboutLocator.count()).toBe(1)
710
+ expect(await nameLocator.count()).toBe(1)
675
711
 
676
- // Verify the selector works
677
- const btnViaSelector = cdpPage!.locator(btnSelector!)
678
- const btnTextViaRef = await btnViaSelector.textContent()
679
- console.log('Button text via selector:', btnTextViaRef)
680
- expect(btnTextViaRef).toBe('Submit Form')
712
+ expect(await submitLocator.textContent()).toBe('Submit Form')
713
+ expect(await aboutLocator.textContent()).toBe('About Us')
714
+ expect(await nameLocator.getAttribute('placeholder')).toBe('Enter your name')
681
715
 
682
- // Test role and name
683
- const btnInfo = ariaResult.refToElement.get('submit-btn')
684
- expect(btnInfo?.role).toBe('button')
685
- expect(btnInfo?.name).toBe('Submit Form')
716
+ expect(ariaResult.refToElement.size).toBeGreaterThan(0)
717
+ console.log('RefToElement map size:', ariaResult.refToElement.size)
718
+ console.log('RefToElement entries:', [...ariaResult.refToElement.entries()])
686
719
 
687
- const linkInfo = ariaResult.refToElement.get('about-link')
688
- expect(linkInfo?.role).toBe('link')
689
- expect(linkInfo?.name).toBe('About Us')
720
+ // Verify refs are stable test IDs
721
+ expect(ariaResult.refToElement.has('submit-btn')).toBe(true)
722
+ expect(ariaResult.refToElement.has('about-link')).toBe(true)
723
+ expect(ariaResult.refToElement.has('name-input')).toBe(true)
690
724
 
691
- const inputInfo = ariaResult.refToElement.get('name-input')
692
- expect(inputInfo?.role).toBe('textbox')
725
+ // Use getSelectorForRef to get CSS selector for a ref
726
+ const btnSelector = ariaResult.getSelectorForRef('submit-btn')
727
+ expect(btnSelector).toBeDefined()
728
+ console.log('Button selector:', btnSelector)
693
729
 
694
- await browser.close()
695
- await page.close()
696
- }, 60000)
730
+ // Verify the selector works
731
+ const btnViaSelector = cdpPage!.locator(btnSelector!)
732
+ const btnTextViaRef = await btnViaSelector.textContent()
733
+ console.log('Button text via selector:', btnTextViaRef)
734
+ expect(btnTextViaRef).toBe('Submit Form')
697
735
 
698
- it('should show aria ref labels on real pages and save screenshots', async () => {
699
- const browserContext = getBrowserContext()
700
- const serviceWorker = await getExtensionServiceWorker(browserContext)
736
+ // Test role and name
737
+ const btnInfo = ariaResult.refToElement.get('submit-btn')
738
+ expect(btnInfo?.role).toBe('button')
739
+ expect(btnInfo?.name).toBe('Submit Form')
701
740
 
702
- const { showAriaRefLabels, hideAriaRefLabels } = await import('./aria-snapshot.js')
741
+ const linkInfo = ariaResult.refToElement.get('about-link')
742
+ expect(linkInfo?.role).toBe('link')
743
+ expect(linkInfo?.name).toBe('About Us')
703
744
 
704
- // Create assets folder for screenshots
705
- const assetsDir = path.join(path.dirname(new URL(import.meta.url).pathname), 'assets')
706
- if (!fs.existsSync(assetsDir)) {
707
- fs.mkdirSync(assetsDir, { recursive: true })
708
- }
745
+ const inputInfo = ariaResult.refToElement.get('name-input')
746
+ expect(inputInfo?.role).toBe('textbox')
709
747
 
710
- const testPages = [
711
- { name: 'hacker-news', url: 'https://news.ycombinator.com/' },
712
- { name: 'google', url: 'https://www.google.com/' },
713
- { name: 'github', url: 'https://github.com/' },
714
- ]
715
-
716
- const loadPageWithRetries = async ({ name, url }: { name: string; url: string }) => {
717
- const page = await browserContext.newPage()
718
- page.setDefaultNavigationTimeout(60000)
719
-
720
- const attempts = 2
721
- let lastError: unknown = null
722
- for (let attempt = 1; attempt <= attempts; attempt += 1) {
723
- try {
724
- console.log(`[labels] opening ${name}: ${url} (attempt ${attempt}/${attempts})`)
725
- await page.goto(url, { waitUntil: 'domcontentloaded' })
726
- await page.waitForLoadState('networkidle', { timeout: 15000 })
727
- console.log(`[labels] loaded ${name}: ${page.url()}`)
728
- return { name, url, page }
729
- } catch (error) {
730
- lastError = error
731
- }
732
- }
748
+ await browser.close()
749
+ await page.close()
750
+ }, 60000)
733
751
 
734
- await page.close()
735
- throw new Error(`Failed to load ${name} after ${attempts} attempts`, { cause: lastError instanceof Error ? lastError : undefined })
736
- }
752
+ it('should show aria ref labels on real pages and save screenshots', async () => {
753
+ const browserContext = getBrowserContext()
754
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
737
755
 
738
- const pages: Array<{ name: string; url: string; page: Page }> = []
739
- for (const testPage of testPages) {
740
- pages.push(await loadPageWithRetries(testPage))
741
- }
756
+ const { showAriaRefLabels, hideAriaRefLabels } = await import('./aria-snapshot.js')
742
757
 
743
- for (const { page } of pages) {
744
- await page.bringToFront()
745
- await serviceWorker.evaluate(async () => {
746
- await globalThis.toggleExtensionForActiveTab()
747
- })
748
- }
758
+ // Create assets folder for screenshots
759
+ const assetsDir = path.join(path.dirname(new URL(import.meta.url).pathname), 'assets')
760
+ if (!fs.existsSync(assetsDir)) {
761
+ fs.mkdirSync(assetsDir, { recursive: true })
762
+ }
749
763
 
750
- const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
751
-
752
- const withTimeout = async <T,>(label: string, task: () => Promise<T>, timeoutMs: number): Promise<T> => {
753
- let timeoutId: NodeJS.Timeout | null = null
754
- const timeoutPromise = new Promise<never>((_, reject) => {
755
- timeoutId = setTimeout(() => {
756
- reject(new Error(`Timed out after ${timeoutMs}ms: ${label}`))
757
- }, timeoutMs)
758
- })
759
-
760
- try {
761
- return await Promise.race([task(), timeoutPromise])
762
- } finally {
763
- if (timeoutId) {
764
- clearTimeout(timeoutId)
765
- }
766
- }
764
+ const testPages = [
765
+ { name: 'old-reddit', url: 'https://old.reddit.com/' },
766
+ { name: 'hacker-news', url: 'https://news.ycombinator.com/' },
767
+ ]
768
+
769
+ const loadPageWithRetries = async ({ name, url }: { name: string; url: string }) => {
770
+ const page = await browserContext.newPage()
771
+ page.setDefaultNavigationTimeout(60000)
772
+
773
+ const attempts = 2
774
+ let lastError: unknown = null
775
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
776
+ try {
777
+ console.log(`[labels] opening ${name}: ${url} (attempt ${attempt}/${attempts})`)
778
+ await page.goto(url, { waitUntil: 'load' })
779
+ console.log(`[labels] loaded ${name}: ${page.url()}`)
780
+ return { name, url, page }
781
+ } catch (error) {
782
+ lastError = error
767
783
  }
784
+ }
768
785
 
769
- const wsUrl = getCdpUrl({ port: TEST_PORT })
770
-
771
- for (const { name, url, page } of pages) {
772
- console.log(`[labels] start ${name}`)
773
- const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes(new URL(url).hostname))
774
-
775
- if (!cdpPage) {
776
- throw new Error(`Could not find CDP page for ${name}`)
777
- }
778
-
779
- console.log(`[labels] show labels ${name}`)
780
- const { labelCount } = await withTimeout(
781
- `showAriaRefLabels(${name})`,
782
- async () => {
783
- return await showAriaRefLabels({ page: cdpPage })
784
- },
785
- 60000
786
- )
787
- console.log(`${name}: ${labelCount} labels shown`)
788
- if (name !== 'google') {
789
- expect(labelCount).toBeGreaterThan(0)
790
- }
786
+ await page.close()
787
+ throw new Error(`Failed to load ${name} after ${attempts} attempts`, {
788
+ cause: lastError instanceof Error ? lastError : undefined,
789
+ })
790
+ }
791
791
 
792
- console.log(`[labels] screenshot ${name}`)
793
- const screenshot = await withTimeout(
794
- `screenshot(${name})`,
795
- async () => {
796
- return await cdpPage.screenshot({ type: 'png', fullPage: false })
797
- },
798
- 30000
799
- )
800
- const screenshotPath = path.join(assetsDir, `aria-labels-${name}.png`)
801
- fs.writeFileSync(screenshotPath, screenshot)
802
- console.log(`Screenshot saved: ${screenshotPath}`)
803
-
804
- console.log(`[labels] count dom labels ${name}`)
805
- const labelElements = await withTimeout(
806
- `countLabels(${name})`,
807
- async () => {
808
- return await cdpPage.evaluate(() =>
809
- document.querySelectorAll('.__pw_label__').length
810
- )
811
- },
812
- 10000
813
- )
814
- expect(labelElements).toBe(labelCount)
815
-
816
- console.log(`[labels] hide labels ${name}`)
817
- await withTimeout(
818
- `hideAriaRefLabels(${name})`,
819
- async () => {
820
- await hideAriaRefLabels({ page: cdpPage })
821
- },
822
- 10000
823
- )
824
-
825
- const labelsAfterHide = await withTimeout(
826
- `verifyHide(${name})`,
827
- async () => {
828
- return await cdpPage.evaluate(() =>
829
- document.getElementById('__playwriter_labels__')
830
- )
831
- },
832
- 10000
833
- )
834
- expect(labelsAfterHide).toBeNull()
792
+ const pages = await Promise.all(
793
+ testPages.map((testPage) => {
794
+ return loadPageWithRetries(testPage)
795
+ }),
796
+ )
797
+
798
+ for (const { page } of pages) {
799
+ await page.bringToFront()
800
+ await serviceWorker.evaluate(async () => {
801
+ await globalThis.toggleExtensionForActiveTab()
802
+ })
803
+ }
835
804
 
836
- console.log(`[labels] close page ${name}`)
837
- await page.close()
805
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
806
+
807
+ const withTimeout = async <T>(label: string, task: () => Promise<T>, timeoutMs: number): Promise<T> => {
808
+ let timeoutId: NodeJS.Timeout | null = null
809
+ const timeoutPromise = new Promise<never>((_, reject) => {
810
+ timeoutId = setTimeout(() => {
811
+ reject(new Error(`Timed out after ${timeoutMs}ms: ${label}`))
812
+ }, timeoutMs)
813
+ })
814
+
815
+ try {
816
+ return await Promise.race([task(), timeoutPromise])
817
+ } finally {
818
+ if (timeoutId) {
819
+ clearTimeout(timeoutId)
838
820
  }
821
+ }
822
+ }
823
+
824
+ const wsUrl = getCdpUrl({ port: TEST_PORT })
825
+
826
+ for (const { name, url, page } of pages) {
827
+ console.log(`[labels] start ${name}`)
828
+ const cdpPage = browser
829
+ .contexts()[0]
830
+ .pages()
831
+ .find((p) => p.url().includes(new URL(url).hostname))
832
+
833
+ if (!cdpPage) {
834
+ throw new Error(`Could not find CDP page for ${name}`)
835
+ }
836
+
837
+ console.log(`[labels] show labels ${name}`)
838
+ const { labelCount } = await withTimeout(
839
+ `showAriaRefLabels(${name})`,
840
+ async () => {
841
+ return await showAriaRefLabels({ page: cdpPage })
842
+ },
843
+ 60000,
844
+ )
845
+ console.log(`${name}: ${labelCount} labels shown`)
846
+ expect(labelCount).toBeGreaterThan(0)
847
+
848
+ console.log(`[labels] screenshot ${name}`)
849
+ const screenshot = await withTimeout(
850
+ `screenshot(${name})`,
851
+ async () => {
852
+ return await cdpPage.screenshot({ type: 'png', fullPage: false })
853
+ },
854
+ 30000,
855
+ )
856
+ const screenshotPath = path.join(assetsDir, `aria-labels-${name}.png`)
857
+ fs.writeFileSync(screenshotPath, screenshot)
858
+ console.log(`Screenshot saved: ${screenshotPath}`)
859
+
860
+ console.log(`[labels] count dom labels ${name}`)
861
+ const labelElements = await withTimeout(
862
+ `countLabels(${name})`,
863
+ async () => {
864
+ return await cdpPage.evaluate(() => document.querySelectorAll('.__pw_label__').length)
865
+ },
866
+ 10000,
867
+ )
868
+ expect(labelElements).toBe(labelCount)
869
+
870
+ console.log(`[labels] hide labels ${name}`)
871
+ await withTimeout(
872
+ `hideAriaRefLabels(${name})`,
873
+ async () => {
874
+ await hideAriaRefLabels({ page: cdpPage })
875
+ },
876
+ 10000,
877
+ )
878
+
879
+ const labelsAfterHide = await withTimeout(
880
+ `verifyHide(${name})`,
881
+ async () => {
882
+ return await cdpPage.evaluate(() => document.getElementById('__playwriter_labels__'))
883
+ },
884
+ 10000,
885
+ )
886
+ expect(labelsAfterHide).toBeNull()
887
+
888
+ console.log(`[labels] close page ${name}`)
889
+ await page.close()
890
+ }
839
891
 
840
- await browser.close()
841
- console.log(`Screenshots saved to: ${assetsDir}`)
842
- }, 180000)
892
+ await browser.close()
893
+ console.log(`Screenshots saved to: ${assetsDir}`)
894
+ }, 180000)
843
895
 
844
- it('should take screenshot with accessibility labels via MCP execute tool', async () => {
845
- const browserContext = getBrowserContext()
846
- const serviceWorker = await getExtensionServiceWorker(browserContext)
896
+ it('should take screenshot with accessibility labels via MCP execute tool', async () => {
897
+ const browserContext = getBrowserContext()
898
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
847
899
 
848
- const page = await browserContext.newPage()
849
- await page.setContent(`
900
+ const page = await browserContext.newPage()
901
+ await page.setContent(`
850
902
  <html>
851
903
  <head>
852
904
  <style>
@@ -903,17 +955,17 @@ describe('Snapshot & Screenshot Tests', () => {
903
955
  </body>
904
956
  </html>
905
957
  `)
906
- await page.bringToFront()
958
+ await page.bringToFront()
907
959
 
908
- await serviceWorker.evaluate(async () => {
909
- await globalThis.toggleExtensionForActiveTab()
910
- })
911
- await new Promise(r => setTimeout(r, 400))
960
+ await serviceWorker.evaluate(async () => {
961
+ await globalThis.toggleExtensionForActiveTab()
962
+ })
963
+ await new Promise((r) => setTimeout(r, 400))
912
964
 
913
- const result = await client.callTool({
914
- name: 'execute',
915
- arguments: {
916
- code: js`
965
+ const result = await client.callTool({
966
+ name: 'execute',
967
+ arguments: {
968
+ code: js`
917
969
  let testPage;
918
970
  for (const p of context.pages()) {
919
971
  const html = await p.content();
@@ -922,45 +974,45 @@ describe('Snapshot & Screenshot Tests', () => {
922
974
  if (!testPage) throw new Error('Test page not found');
923
975
  await screenshotWithAccessibilityLabels({ page: testPage });
924
976
  `,
925
- timeout: 15000,
926
- },
927
- })
928
-
929
- expect(result.isError).toBeFalsy()
930
-
931
- const content = result.content as any[]
932
- expect(content.length).toBe(2)
933
-
934
- const textContent = content.find(c => c.type === 'text')
935
- expect(textContent).toBeDefined()
936
- expect(textContent.text).toContain('Screenshot saved to:')
937
- expect(textContent.text).toContain('.jpg')
938
- expect(textContent.text).toContain('Labels shown:')
939
- expect(textContent.text).toContain('Accessibility snapshot:')
940
- expect(textContent.text).toContain('Submit Form')
941
-
942
- const imageContent = content.find(c => c.type === 'image')
943
- expect(imageContent).toBeDefined()
944
- expect(imageContent.mimeType).toBe('image/jpeg')
945
- expect(imageContent.data).toBeDefined()
946
- expect(imageContent.data.length).toBeGreaterThan(100)
947
-
948
- const buffer = Buffer.from(imageContent.data, 'base64')
949
- const dimensions = imageSize(buffer)
950
-
951
- const viewport = await page.evaluate(() => ({
952
- innerWidth: window.innerWidth,
953
- innerHeight: window.innerHeight,
954
- outerWidth: window.outerWidth,
955
- outerHeight: window.outerHeight,
956
- }))
957
- console.log('Screenshot dimensions:', dimensions.width, 'x', dimensions.height)
958
- console.log('Window viewport:', viewport)
959
-
960
- expect(dimensions.type).toBe('jpg')
961
- expect(dimensions.width).toBeGreaterThan(0)
962
- expect(dimensions.height).toBeGreaterThan(0)
963
-
964
- await page.close()
965
- }, 60000)
977
+ timeout: 15000,
978
+ },
979
+ })
980
+
981
+ expect(result.isError).toBeFalsy()
982
+
983
+ const content = result.content as any[]
984
+ expect(content.length).toBe(2)
985
+
986
+ const textContent = content.find((c) => c.type === 'text')
987
+ expect(textContent).toBeDefined()
988
+ expect(textContent.text).toContain('Screenshot saved to:')
989
+ expect(textContent.text).toContain('.jpg')
990
+ expect(textContent.text).toContain('Labels shown:')
991
+ expect(textContent.text).toContain('Accessibility snapshot:')
992
+ expect(textContent.text).toContain('Submit Form')
993
+
994
+ const imageContent = content.find((c) => c.type === 'image')
995
+ expect(imageContent).toBeDefined()
996
+ expect(imageContent.mimeType).toBe('image/jpeg')
997
+ expect(imageContent.data).toBeDefined()
998
+ expect(imageContent.data.length).toBeGreaterThan(100)
999
+
1000
+ const buffer = Buffer.from(imageContent.data, 'base64')
1001
+ const dimensions = imageSize(buffer)
1002
+
1003
+ const viewport = await page.evaluate(() => ({
1004
+ innerWidth: window.innerWidth,
1005
+ innerHeight: window.innerHeight,
1006
+ outerWidth: window.outerWidth,
1007
+ outerHeight: window.outerHeight,
1008
+ }))
1009
+ console.log('Screenshot dimensions:', dimensions.width, 'x', dimensions.height)
1010
+ console.log('Window viewport:', viewport)
1011
+
1012
+ expect(dimensions.type).toBe('jpg')
1013
+ expect(dimensions.width).toBeGreaterThan(0)
1014
+ expect(dimensions.height).toBeGreaterThan(0)
1015
+
1016
+ await page.close()
1017
+ }, 60000)
966
1018
  })