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
@@ -8,273 +8,281 @@ import './test-declarations.js'
8
8
  const TEST_PORT = 19990
9
9
 
10
10
  describe('Extension Connection Tests', () => {
11
- let client: Awaited<ReturnType<typeof createMCPClient>>['client']
12
- let cleanup: (() => Promise<void>) | null = null
13
- let testCtx: TestContext | null = null
11
+ let client: Awaited<ReturnType<typeof createMCPClient>>['client']
12
+ let cleanup: (() => Promise<void>) | null = null
13
+ let testCtx: TestContext | null = null
14
+
15
+ beforeAll(async () => {
16
+ testCtx = await setupTestContext({ port: TEST_PORT, tempDirPrefix: 'pw-conn-test-', toggleExtension: true })
17
+
18
+ const result = await createMCPClient({ port: TEST_PORT })
19
+ client = result.client
20
+ cleanup = result.cleanup
21
+ }, 600000)
22
+
23
+ afterAll(async () => {
24
+ await cleanupTestContext(testCtx, cleanup)
25
+ cleanup = null
26
+ testCtx = null
27
+ })
28
+
29
+ const getBrowserContext = () => {
30
+ if (!testCtx?.browserContext) throw new Error('Browser not initialized')
31
+ return testCtx.browserContext
32
+ }
33
+
34
+ it('should handle new pages and toggling with new connections', async () => {
35
+ const browserContext = getBrowserContext()
36
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
37
+
38
+ // 1. Create a new page
39
+ const page = await browserContext.newPage()
40
+ const testUrl = 'https://example.com/'
41
+ await page.goto(testUrl)
42
+
43
+ await page.bringToFront()
44
+
45
+ // 2. Enable extension on this new tab
46
+ const result = await serviceWorker.evaluate(async () => {
47
+ return await globalThis.toggleExtensionForActiveTab()
48
+ })
49
+ expect(result.isConnected).toBe(true)
50
+
51
+ // 3. Verify we can connect via direct CDP and see the page
52
+ let directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
53
+ let contexts = directBrowser.contexts()
54
+ let pages = contexts[0].pages()
55
+
56
+ let foundPage = pages.find((p) => p.url() === testUrl)
57
+ expect(foundPage).toBeDefined()
58
+ expect(foundPage?.url()).toBe(testUrl)
59
+
60
+ const sum1 = await foundPage?.evaluate(() => 1 + 1)
61
+ expect(sum1).toBe(2)
62
+
63
+ await directBrowser.close()
64
+
65
+ // 4. Disable extension on this tab
66
+ const resultDisabled = await serviceWorker.evaluate(async () => {
67
+ return await globalThis.toggleExtensionForActiveTab()
68
+ })
69
+ expect(resultDisabled.isConnected).toBe(false)
70
+
71
+ // 5. Connect again - page should NOT be visible
72
+ directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
73
+ contexts = directBrowser.contexts()
74
+ pages = contexts[0].pages()
75
+
76
+ foundPage = pages.find((p) => p.url() === testUrl)
77
+ expect(foundPage).toBeUndefined()
78
+
79
+ await directBrowser.close()
80
+
81
+ // 6. Re-enable extension
82
+ const resultEnabled = await serviceWorker.evaluate(async () => {
83
+ return await globalThis.toggleExtensionForActiveTab()
84
+ })
85
+ expect(resultEnabled.isConnected).toBe(true)
86
+
87
+ // 7. Verify page is back
88
+ directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
89
+ await new Promise((r) => setTimeout(r, 100))
90
+
91
+ contexts = directBrowser.contexts()
92
+ if (contexts[0].pages().length === 0) {
93
+ await new Promise((r) => setTimeout(r, 100))
94
+ }
95
+ pages = contexts[0].pages()
96
+
97
+ foundPage = pages.find((p) => p.url() === testUrl)
98
+ expect(foundPage).toBeDefined()
99
+ expect(foundPage?.url()).toBe(testUrl)
100
+
101
+ const sum2 = await foundPage?.evaluate(() => 2 + 2)
102
+ expect(sum2).toBe(4)
103
+
104
+ await directBrowser.close()
105
+ await page.close()
106
+ }, 120000)
107
+
108
+ it('should handle new pages and toggling with persistent connection', async () => {
109
+ const browserContext = getBrowserContext()
110
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
111
+
112
+ const directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
113
+ await new Promise((r) => setTimeout(r, 100))
114
+
115
+ // 1. Create a new page
116
+ const page = await browserContext.newPage()
117
+ const testUrl = 'https://example.com/persistent'
118
+ await page.goto(testUrl)
119
+ await page.bringToFront()
120
+
121
+ // 2. Enable extension
122
+ await serviceWorker.evaluate(async () => {
123
+ await globalThis.toggleExtensionForActiveTab()
124
+ })
125
+
126
+ // 3. Verify page appears (polling)
127
+ let foundPage
128
+ for (let i = 0; i < 50; i++) {
129
+ const pages = directBrowser.contexts()[0].pages()
130
+ foundPage = pages.find((p) => p.url() === testUrl)
131
+ if (foundPage) break
132
+ await new Promise((r) => setTimeout(r, 100))
133
+ }
134
+ expect(foundPage).toBeDefined()
135
+ expect(foundPage?.url()).toBe(testUrl)
136
+
137
+ const sum1 = await foundPage?.evaluate(() => 10 + 20)
138
+ expect(sum1).toBe(30)
14
139
 
15
- beforeAll(async () => {
16
- testCtx = await setupTestContext({ port: TEST_PORT, tempDirPrefix: 'pw-conn-test-', toggleExtension: true })
140
+ // 4. Disable extension
141
+ await serviceWorker.evaluate(async () => {
142
+ await globalThis.toggleExtensionForActiveTab()
143
+ })
17
144
 
18
- const result = await createMCPClient({ port: TEST_PORT })
19
- client = result.client
20
- cleanup = result.cleanup
21
- }, 600000)
145
+ // 5. Verify page disappears (polling)
146
+ for (let i = 0; i < 50; i++) {
147
+ const pages = directBrowser.contexts()[0].pages()
148
+ foundPage = pages.find((p) => p.url() === testUrl)
149
+ if (!foundPage) break
150
+ await new Promise((r) => setTimeout(r, 100))
151
+ }
152
+ expect(foundPage).toBeUndefined()
22
153
 
23
- afterAll(async () => {
24
- await cleanupTestContext(testCtx, cleanup)
25
- cleanup = null
26
- testCtx = null
154
+ // 6. Re-enable extension
155
+ await serviceWorker.evaluate(async () => {
156
+ await globalThis.toggleExtensionForActiveTab()
27
157
  })
28
158
 
29
- const getBrowserContext = () => {
30
- if (!testCtx?.browserContext) throw new Error('Browser not initialized')
31
- return testCtx.browserContext
159
+ // 7. Verify page reappears (polling)
160
+ for (let i = 0; i < 50; i++) {
161
+ const pages = directBrowser.contexts()[0].pages()
162
+ foundPage = pages.find((p) => p.url() === testUrl)
163
+ if (foundPage) break
164
+ await new Promise((r) => setTimeout(r, 100))
32
165
  }
166
+ expect(foundPage).toBeDefined()
167
+ expect(foundPage?.url()).toBe(testUrl)
168
+
169
+ const sum2 = await foundPage?.evaluate(() => 30 + 40)
170
+ expect(sum2).toBe(70)
171
+
172
+ await page.close()
173
+ await directBrowser.close()
174
+ })
175
+
176
+ it('should maintain connection across reloads and navigation', async () => {
177
+ const browserContext = getBrowserContext()
178
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
179
+
180
+ // 1. Setup page
181
+ const page = await browserContext.newPage()
182
+ const initialUrl = 'https://example.com/'
183
+ await page.goto(initialUrl)
184
+ await page.bringToFront()
185
+
186
+ // 2. Enable extension
187
+ await serviceWorker.evaluate(async () => {
188
+ await globalThis.toggleExtensionForActiveTab()
189
+ })
190
+
191
+ // 3. Connect via CDP
192
+ const cdpUrl = getCdpUrl({ port: TEST_PORT })
193
+ const directBrowser = await chromium.connectOverCDP(cdpUrl)
194
+ const connectedPage = directBrowser
195
+ .contexts()[0]
196
+ .pages()
197
+ .find((p) => p.url() === initialUrl)
198
+ expect(connectedPage).toBeDefined()
199
+
200
+ expect(await connectedPage?.evaluate(() => 1 + 1)).toBe(2)
201
+
202
+ // 4. Reload
203
+ await connectedPage?.reload()
204
+ await connectedPage?.waitForLoadState('domcontentloaded')
205
+ expect(await connectedPage?.title()).toBe('Example Domain')
206
+
207
+ expect(await connectedPage?.evaluate(() => 2 + 2)).toBe(4)
208
+
209
+ // 5. Navigate to new URL
210
+ const newUrl = 'https://example.org/'
211
+ await connectedPage?.goto(newUrl)
212
+ await connectedPage?.waitForLoadState('domcontentloaded')
213
+
214
+ expect(connectedPage?.url()).toBe(newUrl)
215
+ expect(await connectedPage?.title()).toContain('Example Domain')
216
+
217
+ expect(await connectedPage?.evaluate(() => 3 + 3)).toBe(6)
218
+
219
+ await directBrowser.close()
220
+ await page.close()
221
+ })
222
+
223
+ it('should support multiple concurrent tabs', async () => {
224
+ const browserContext = getBrowserContext()
225
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
226
+ await new Promise((resolve) => setTimeout(resolve, 100))
227
+
228
+ // Tab A
229
+ const pageA = await browserContext.newPage()
230
+ await pageA.goto('https://example.com/tab-a')
231
+ await pageA.bringToFront()
232
+ await new Promise((resolve) => setTimeout(resolve, 100))
233
+ await serviceWorker.evaluate(async () => {
234
+ await globalThis.toggleExtensionForActiveTab()
235
+ })
236
+
237
+ // Tab B
238
+ const pageB = await browserContext.newPage()
239
+ await pageB.goto('https://example.com/tab-b')
240
+ await pageB.bringToFront()
241
+ await new Promise((resolve) => setTimeout(resolve, 100))
242
+ await serviceWorker.evaluate(async () => {
243
+ await globalThis.toggleExtensionForActiveTab()
244
+ })
245
+
246
+ // Get target IDs for both
247
+ const targetIds = await serviceWorker.evaluate(async () => {
248
+ const state = globalThis.getExtensionState()
249
+ const chrome = globalThis.chrome
250
+ const tabs = await chrome.tabs.query({})
251
+ const tabA = tabs.find((t: any) => t.url?.includes('tab-a'))
252
+ const tabB = tabs.find((t: any) => t.url?.includes('tab-b'))
253
+ return {
254
+ idA: state.tabs.get(tabA?.id ?? -1)?.targetId,
255
+ idB: state.tabs.get(tabB?.id ?? -1)?.targetId,
256
+ }
257
+ })
33
258
 
34
- it('should handle new pages and toggling with new connections', async () => {
35
- const browserContext = getBrowserContext()
36
- const serviceWorker = await getExtensionServiceWorker(browserContext)
37
-
38
- // 1. Create a new page
39
- const page = await browserContext.newPage()
40
- const testUrl = 'https://example.com/'
41
- await page.goto(testUrl)
42
-
43
- await page.bringToFront()
44
-
45
- // 2. Enable extension on this new tab
46
- const result = await serviceWorker.evaluate(async () => {
47
- return await globalThis.toggleExtensionForActiveTab()
48
- })
49
- expect(result.isConnected).toBe(true)
50
-
51
- // 3. Verify we can connect via direct CDP and see the page
52
- let directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
53
- let contexts = directBrowser.contexts()
54
- let pages = contexts[0].pages()
55
-
56
- let foundPage = pages.find(p => p.url() === testUrl)
57
- expect(foundPage).toBeDefined()
58
- expect(foundPage?.url()).toBe(testUrl)
59
-
60
- const sum1 = await foundPage?.evaluate(() => 1 + 1)
61
- expect(sum1).toBe(2)
62
-
63
- await directBrowser.close()
64
-
65
- // 4. Disable extension on this tab
66
- const resultDisabled = await serviceWorker.evaluate(async () => {
67
- return await globalThis.toggleExtensionForActiveTab()
68
- })
69
- expect(resultDisabled.isConnected).toBe(false)
70
-
71
- // 5. Connect again - page should NOT be visible
72
- directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
73
- contexts = directBrowser.contexts()
74
- pages = contexts[0].pages()
75
-
76
- foundPage = pages.find(p => p.url() === testUrl)
77
- expect(foundPage).toBeUndefined()
78
-
79
- await directBrowser.close()
80
-
81
- // 6. Re-enable extension
82
- const resultEnabled = await serviceWorker.evaluate(async () => {
83
- return await globalThis.toggleExtensionForActiveTab()
84
- })
85
- expect(resultEnabled.isConnected).toBe(true)
86
-
87
- // 7. Verify page is back
88
- directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
89
- await new Promise(r => setTimeout(r, 100))
90
-
91
- contexts = directBrowser.contexts()
92
- if (contexts[0].pages().length === 0) {
93
- await new Promise(r => setTimeout(r, 100))
94
- }
95
- pages = contexts[0].pages()
96
-
97
- foundPage = pages.find(p => p.url() === testUrl)
98
- expect(foundPage).toBeDefined()
99
- expect(foundPage?.url()).toBe(testUrl)
100
-
101
- const sum2 = await foundPage?.evaluate(() => 2 + 2)
102
- expect(sum2).toBe(4)
103
-
104
- await directBrowser.close()
105
- await page.close()
106
- }, 120000)
107
-
108
- it('should handle new pages and toggling with persistent connection', async () => {
109
- const browserContext = getBrowserContext()
110
- const serviceWorker = await getExtensionServiceWorker(browserContext)
111
-
112
- const directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
113
- await new Promise(r => setTimeout(r, 100))
114
-
115
- // 1. Create a new page
116
- const page = await browserContext.newPage()
117
- const testUrl = 'https://example.com/persistent'
118
- await page.goto(testUrl)
119
- await page.bringToFront()
120
-
121
- // 2. Enable extension
122
- await serviceWorker.evaluate(async () => {
123
- await globalThis.toggleExtensionForActiveTab()
124
- })
125
-
126
- // 3. Verify page appears (polling)
127
- let foundPage
128
- for (let i = 0; i < 50; i++) {
129
- const pages = directBrowser.contexts()[0].pages()
130
- foundPage = pages.find(p => p.url() === testUrl)
131
- if (foundPage) break
132
- await new Promise(r => setTimeout(r, 100))
133
- }
134
- expect(foundPage).toBeDefined()
135
- expect(foundPage?.url()).toBe(testUrl)
136
-
137
- const sum1 = await foundPage?.evaluate(() => 10 + 20)
138
- expect(sum1).toBe(30)
139
-
140
- // 4. Disable extension
141
- await serviceWorker.evaluate(async () => {
142
- await globalThis.toggleExtensionForActiveTab()
143
- })
144
-
145
- // 5. Verify page disappears (polling)
146
- for (let i = 0; i < 50; i++) {
147
- const pages = directBrowser.contexts()[0].pages()
148
- foundPage = pages.find(p => p.url() === testUrl)
149
- if (!foundPage) break
150
- await new Promise(r => setTimeout(r, 100))
151
- }
152
- expect(foundPage).toBeUndefined()
153
-
154
- // 6. Re-enable extension
155
- await serviceWorker.evaluate(async () => {
156
- await globalThis.toggleExtensionForActiveTab()
157
- })
158
-
159
- // 7. Verify page reappears (polling)
160
- for (let i = 0; i < 50; i++) {
161
- const pages = directBrowser.contexts()[0].pages()
162
- foundPage = pages.find(p => p.url() === testUrl)
163
- if (foundPage) break
164
- await new Promise(r => setTimeout(r, 100))
165
- }
166
- expect(foundPage).toBeDefined()
167
- expect(foundPage?.url()).toBe(testUrl)
168
-
169
- const sum2 = await foundPage?.evaluate(() => 30 + 40)
170
- expect(sum2).toBe(70)
171
-
172
- await page.close()
173
- await directBrowser.close()
174
- })
175
-
176
- it('should maintain connection across reloads and navigation', async () => {
177
- const browserContext = getBrowserContext()
178
- const serviceWorker = await getExtensionServiceWorker(browserContext)
179
-
180
- // 1. Setup page
181
- const page = await browserContext.newPage()
182
- const initialUrl = 'https://example.com/'
183
- await page.goto(initialUrl)
184
- await page.bringToFront()
185
-
186
- // 2. Enable extension
187
- await serviceWorker.evaluate(async () => {
188
- await globalThis.toggleExtensionForActiveTab()
189
- })
190
-
191
- // 3. Connect via CDP
192
- const cdpUrl = getCdpUrl({ port: TEST_PORT })
193
- const directBrowser = await chromium.connectOverCDP(cdpUrl)
194
- const connectedPage = directBrowser.contexts()[0].pages().find(p => p.url() === initialUrl)
195
- expect(connectedPage).toBeDefined()
196
-
197
- expect(await connectedPage?.evaluate(() => 1 + 1)).toBe(2)
198
-
199
- // 4. Reload
200
- await connectedPage?.reload()
201
- await connectedPage?.waitForLoadState('domcontentloaded')
202
- expect(await connectedPage?.title()).toBe('Example Domain')
203
-
204
- expect(await connectedPage?.evaluate(() => 2 + 2)).toBe(4)
205
-
206
- // 5. Navigate to new URL
207
- const newUrl = 'https://example.org/'
208
- await connectedPage?.goto(newUrl)
209
- await connectedPage?.waitForLoadState('domcontentloaded')
210
-
211
- expect(connectedPage?.url()).toBe(newUrl)
212
- expect(await connectedPage?.title()).toContain('Example Domain')
213
-
214
- expect(await connectedPage?.evaluate(() => 3 + 3)).toBe(6)
215
-
216
- await directBrowser.close()
217
- await page.close()
218
- })
219
-
220
- it('should support multiple concurrent tabs', async () => {
221
- const browserContext = getBrowserContext()
222
- const serviceWorker = await getExtensionServiceWorker(browserContext)
223
- await new Promise(resolve => setTimeout(resolve, 100))
224
-
225
- // Tab A
226
- const pageA = await browserContext.newPage()
227
- await pageA.goto('https://example.com/tab-a')
228
- await pageA.bringToFront()
229
- await new Promise(resolve => setTimeout(resolve, 100))
230
- await serviceWorker.evaluate(async () => {
231
- await globalThis.toggleExtensionForActiveTab()
232
- })
233
-
234
- // Tab B
235
- const pageB = await browserContext.newPage()
236
- await pageB.goto('https://example.com/tab-b')
237
- await pageB.bringToFront()
238
- await new Promise(resolve => setTimeout(resolve, 100))
239
- await serviceWorker.evaluate(async () => {
240
- await globalThis.toggleExtensionForActiveTab()
241
- })
242
-
243
- // Get target IDs for both
244
- const targetIds = await serviceWorker.evaluate(async () => {
245
- const state = globalThis.getExtensionState()
246
- const chrome = globalThis.chrome
247
- const tabs = await chrome.tabs.query({})
248
- const tabA = tabs.find((t: any) => t.url?.includes('tab-a'))
249
- const tabB = tabs.find((t: any) => t.url?.includes('tab-b'))
250
- return {
251
- idA: state.tabs.get(tabA?.id ?? -1)?.targetId,
252
- idB: state.tabs.get(tabB?.id ?? -1)?.targetId
253
- }
254
- })
255
-
256
- expect(targetIds).toMatchInlineSnapshot({
257
- idA: expect.any(String),
258
- idB: expect.any(String)
259
- }, `
259
+ expect(targetIds).toMatchInlineSnapshot(
260
+ {
261
+ idA: expect.any(String),
262
+ idB: expect.any(String),
263
+ },
264
+ `
260
265
  {
261
266
  "idA": Any<String>,
262
267
  "idB": Any<String>,
263
268
  }
264
- `)
265
- expect(targetIds.idA).not.toBe(targetIds.idB)
269
+ `,
270
+ )
271
+ expect(targetIds.idA).not.toBe(targetIds.idB)
266
272
 
267
- // Verify independent connections
268
- const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
273
+ // Verify independent connections
274
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
269
275
 
270
- const pages = browser.contexts()[0].pages()
276
+ const pages = browser.contexts()[0].pages()
271
277
 
272
- const results = await Promise.all(pages.map(async (p) => ({
273
- url: p.url(),
274
- title: await p.title()
275
- })))
278
+ const results = await Promise.all(
279
+ pages.map(async (p) => ({
280
+ url: p.url(),
281
+ title: await p.title(),
282
+ })),
283
+ )
276
284
 
277
- expect(results).toMatchInlineSnapshot(`
285
+ expect(results).toMatchInlineSnapshot(`
278
286
  [
279
287
  {
280
288
  "title": "",
@@ -291,140 +299,256 @@ describe('Extension Connection Tests', () => {
291
299
  ]
292
300
  `)
293
301
 
294
- // Verify execution on both pages
295
- const pageA_CDP = pages.find(p => p.url().includes('tab-a'))
296
- const pageB_CDP = pages.find(p => p.url().includes('tab-b'))
302
+ // Verify execution on both pages
303
+ const pageA_CDP = pages.find((p) => p.url().includes('tab-a'))
304
+ const pageB_CDP = pages.find((p) => p.url().includes('tab-b'))
305
+
306
+ expect(await pageA_CDP?.evaluate(() => 10 + 10)).toBe(20)
307
+ expect(await pageB_CDP?.evaluate(() => 20 + 20)).toBe(40)
297
308
 
298
- expect(await pageA_CDP?.evaluate(() => 10 + 10)).toBe(20)
299
- expect(await pageB_CDP?.evaluate(() => 20 + 20)).toBe(40)
309
+ await browser.close()
310
+ await pageA.close()
311
+ await pageB.close()
312
+ })
300
313
 
301
- await browser.close()
302
- await pageA.close()
303
- await pageB.close()
314
+ it('should warn and switch page when the active page closes', async () => {
315
+ const browserContext = getBrowserContext()
316
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
317
+
318
+ const pageA = await browserContext.newPage()
319
+ await pageA.goto('https://example.com/close-warning-a')
320
+ await pageA.bringToFront()
321
+ await serviceWorker.evaluate(async () => {
322
+ await globalThis.toggleExtensionForActiveTab()
304
323
  })
305
324
 
306
- it('should show correct url when enabling extension after navigation', async () => {
307
- const browserContext = getBrowserContext()
308
- const serviceWorker = await getExtensionServiceWorker(browserContext)
325
+ const pageB = await browserContext.newPage()
326
+ await pageB.goto('https://example.com/close-warning-b')
327
+ await pageB.bringToFront()
328
+ await serviceWorker.evaluate(async () => {
329
+ await globalThis.toggleExtensionForActiveTab()
330
+ })
309
331
 
310
- const page = await browserContext.newPage()
311
- const targetUrl = 'https://example.com/late-enable'
312
- await page.goto(targetUrl)
313
- await page.bringToFront()
332
+ const closeResult = await client.callTool({
333
+ name: 'execute',
334
+ arguments: {
335
+ code: js`
336
+ state.page = page;
337
+ const closedUrl = state.page.url();
338
+ await state.page.close();
339
+ return { closedUrl, remainingPages: context.pages().length };
340
+ `,
341
+ },
342
+ })
314
343
 
315
- await page.waitForLoadState('domcontentloaded')
344
+ const closeOutput = (closeResult as any).content[0].text
345
+ expect(closeOutput).toContain('[WARNING] The current page in state.page was closed')
346
+ expect(closeOutput).toContain('Switched active page to index')
347
+ expect((closeResult as any).isError).not.toBe(true)
316
348
 
317
- await serviceWorker.evaluate(async () => {
318
- await globalThis.toggleExtensionForActiveTab()
319
- })
349
+ const nextResult = await client.callTool({
350
+ name: 'execute',
351
+ arguments: {
352
+ code: js`
353
+ return { pageUrl: page.url(), pagesCount: context.pages().length };
354
+ `,
355
+ },
356
+ })
320
357
 
321
- const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
322
- await new Promise(r => setTimeout(r, 100))
358
+ const nextOutput = (nextResult as any).content[0].text
359
+ expect(nextOutput).toContain('pagesCount')
360
+ expect(nextOutput).not.toContain('No Playwright pages are available')
361
+ expect(nextOutput).not.toContain('[WARNING] The current page was closed')
362
+ expect((nextResult as any).isError).not.toBe(true)
323
363
 
324
- const cdpPage = browser.contexts()[0].pages().find(p => p.url() === targetUrl)
364
+ if (!pageA.isClosed()) {
365
+ await pageA.close()
366
+ }
367
+ if (!pageB.isClosed()) {
368
+ await pageB.close()
369
+ }
370
+ })
325
371
 
326
- expect(cdpPage).toBeDefined()
327
- expect(cdpPage?.url()).toBe(targetUrl)
372
+ it('should switch page without warning when closed page is not stored in state', async () => {
373
+ const browserContext = getBrowserContext()
374
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
328
375
 
329
- await browser.close()
330
- await page.close()
331
- }, 60000)
376
+ const pageA = await browserContext.newPage()
377
+ await pageA.goto('https://example.com/close-no-state-warning-a')
378
+ await pageA.bringToFront()
379
+ await serviceWorker.evaluate(async () => {
380
+ await globalThis.toggleExtensionForActiveTab()
381
+ })
332
382
 
333
- it('should be able to reconnect after disconnecting everything', async () => {
334
- const browserContext = getBrowserContext()
335
- const serviceWorker = await getExtensionServiceWorker(browserContext)
383
+ const pageB = await browserContext.newPage()
384
+ await pageB.goto('https://example.com/close-no-state-warning-b')
385
+ await pageB.bringToFront()
386
+ await serviceWorker.evaluate(async () => {
387
+ await globalThis.toggleExtensionForActiveTab()
388
+ })
389
+
390
+ const closeResult = await client.callTool({
391
+ name: 'execute',
392
+ arguments: {
393
+ code: js`
394
+ const closedUrl = page.url();
395
+ await page.close();
396
+ return { closedUrl, remainingPages: context.pages().length };
397
+ `,
398
+ },
399
+ })
400
+
401
+ const closeOutput = (closeResult as any).content[0].text
402
+ expect(closeOutput).not.toContain('[WARNING] The current page in state.page was closed')
403
+ expect(closeOutput).not.toContain('Switched active page to index')
404
+ expect((closeResult as any).isError).not.toBe(true)
405
+
406
+ const nextResult = await client.callTool({
407
+ name: 'execute',
408
+ arguments: {
409
+ code: js`
410
+ return { pageUrl: page.url(), pagesCount: context.pages().length };
411
+ `,
412
+ },
413
+ })
414
+
415
+ const nextOutput = (nextResult as any).content[0].text
416
+ expect(nextOutput).toContain('pagesCount')
417
+ expect((nextResult as any).isError).not.toBe(true)
418
+
419
+ if (!pageA.isClosed()) {
420
+ await pageA.close()
421
+ }
422
+ if (!pageB.isClosed()) {
423
+ await pageB.close()
424
+ }
425
+ })
426
+
427
+ it('should show correct url when enabling extension after navigation', async () => {
428
+ const browserContext = getBrowserContext()
429
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
336
430
 
337
- const pages = await browserContext.pages()
338
- expect(pages.length).toBeGreaterThan(0)
339
- const page = pages[0]
431
+ const page = await browserContext.newPage()
432
+ const targetUrl = 'https://example.com/late-enable'
433
+ await page.goto(targetUrl)
434
+ await page.bringToFront()
340
435
 
341
- await page.goto('https://example.com/disconnect-test')
342
- await page.waitForLoadState('domcontentloaded')
343
- await page.bringToFront()
436
+ await page.waitForLoadState('domcontentloaded')
344
437
 
345
- // Enable extension on this page
346
- const initialEnable = await serviceWorker.evaluate(async () => {
347
- return await globalThis.toggleExtensionForActiveTab()
348
- })
349
- console.log('Initial enable result:', initialEnable)
350
- expect(initialEnable.isConnected).toBe(true)
438
+ await serviceWorker.evaluate(async () => {
439
+ await globalThis.toggleExtensionForActiveTab()
440
+ })
441
+
442
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
443
+ await new Promise((r) => setTimeout(r, 100))
444
+
445
+ const cdpPage = browser
446
+ .contexts()[0]
447
+ .pages()
448
+ .find((p) => p.url() === targetUrl)
449
+
450
+ expect(cdpPage).toBeDefined()
451
+ expect(cdpPage?.url()).toBe(targetUrl)
452
+
453
+ await browser.close()
454
+ await page.close()
455
+ }, 60000)
456
+
457
+ it('should be able to reconnect after disconnecting everything', async () => {
458
+ const browserContext = getBrowserContext()
459
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
460
+
461
+ const pages = await browserContext.pages()
462
+ expect(pages.length).toBeGreaterThan(0)
463
+ const page = pages[0]
351
464
 
352
- await new Promise(resolve => setTimeout(resolve, 100))
465
+ await page.goto('https://example.com/disconnect-test')
466
+ await page.waitForLoadState('domcontentloaded')
467
+ await page.bringToFront()
353
468
 
354
- // Verify MCP can see the page
355
- const beforeDisconnect = await client.callTool({
356
- name: 'execute',
357
- arguments: {
358
- code: js`
469
+ // Enable extension on this page
470
+ const initialEnable = await serviceWorker.evaluate(async () => {
471
+ return await globalThis.toggleExtensionForActiveTab()
472
+ })
473
+ console.log('Initial enable result:', initialEnable)
474
+ expect(initialEnable.isConnected).toBe(true)
475
+
476
+ await new Promise((resolve) => setTimeout(resolve, 100))
477
+
478
+ // Verify MCP can see the page
479
+ const beforeDisconnect = await client.callTool({
480
+ name: 'execute',
481
+ arguments: {
482
+ code: js`
359
483
  const pages = context.pages();
360
484
  console.log('Pages before disconnect:', pages.length);
361
485
  const testPage = pages.find(p => p.url().includes('disconnect-test'));
362
486
  console.log('Found test page:', !!testPage);
363
487
  return { pagesCount: pages.length, foundTestPage: !!testPage };
364
488
  `,
365
- },
366
- })
489
+ },
490
+ })
367
491
 
368
- const beforeOutput = (beforeDisconnect as any).content[0].text
369
- expect(beforeOutput).toContain('foundTestPage')
370
- console.log('Before disconnect:', beforeOutput)
492
+ const beforeOutput = (beforeDisconnect as any).content[0].text
493
+ expect(beforeOutput).toContain('foundTestPage')
494
+ console.log('Before disconnect:', beforeOutput)
371
495
 
372
- // 2. Disconnect everything
373
- console.log('Calling disconnectEverything...')
374
- await serviceWorker.evaluate(async () => {
375
- await globalThis.disconnectEverything()
376
- })
496
+ // 2. Disconnect everything
497
+ console.log('Calling disconnectEverything...')
498
+ await serviceWorker.evaluate(async () => {
499
+ await globalThis.disconnectEverything()
500
+ })
377
501
 
378
- await new Promise(resolve => setTimeout(resolve, 100))
502
+ await new Promise((resolve) => setTimeout(resolve, 100))
379
503
 
380
- // 3. Verify MCP cannot execute code anymore (no pages available)
381
- const afterDisconnect = await client.callTool({
382
- name: 'execute',
383
- arguments: {
384
- code: js`
504
+ // 3. Verify MCP cannot execute code anymore (no pages available)
505
+ const afterDisconnect = await client.callTool({
506
+ name: 'execute',
507
+ arguments: {
508
+ code: js`
385
509
  const pages = context.pages();
386
510
  console.log('Pages after disconnect:', pages.length);
387
511
  return { pagesCount: pages.length };
388
512
  `,
389
- },
390
- })
391
-
392
- const afterDisconnectOutput = (afterDisconnect as any).content[0].text
393
- console.log('After disconnect:', afterDisconnectOutput)
394
- expect((afterDisconnect as any).isError).toBe(true)
395
- expect(afterDisconnectOutput).toContain('No Playwright pages are available')
396
-
397
- // 4. Re-enable extension on the same page
398
- console.log('Re-enabling extension...')
399
- await page.bringToFront()
400
- const reconnectResult = await serviceWorker.evaluate(async () => {
401
- console.log('About to call toggleExtensionForActiveTab')
402
- const result = await globalThis.toggleExtensionForActiveTab()
403
- console.log('toggleExtensionForActiveTab result:', result)
404
- return result
405
- })
406
-
407
- console.log('Reconnect result:', reconnectResult)
408
- expect(reconnectResult.isConnected).toBe(true)
409
-
410
- console.log('Waiting for reconnection to stabilize...')
411
- await new Promise(resolve => setTimeout(resolve, 100))
412
-
413
- // 5. Reset the MCP client's playwright connection
414
- console.log('Resetting MCP playwright connection...')
415
- const resetResult = await client.callTool({
416
- name: 'reset',
417
- arguments: {},
418
- })
419
- console.log('Reset result:', (resetResult as any).content[0].text)
420
- expect((resetResult as any).content[0].text).toContain('Connection reset successfully')
421
-
422
- // 6. Verify MCP can see the page again
423
- console.log('Attempting to access page via MCP...')
424
- const afterReconnect = await client.callTool({
425
- name: 'execute',
426
- arguments: {
427
- code: js`
513
+ },
514
+ })
515
+
516
+ const afterDisconnectOutput = (afterDisconnect as any).content[0].text
517
+ console.log('After disconnect:', afterDisconnectOutput)
518
+ expect((afterDisconnect as any).isError).toBe(true)
519
+ expect(afterDisconnectOutput).toContain('No Playwright pages are available')
520
+
521
+ // 4. Re-enable extension on the same page
522
+ console.log('Re-enabling extension...')
523
+ await page.bringToFront()
524
+ const reconnectResult = await serviceWorker.evaluate(async () => {
525
+ console.log('About to call toggleExtensionForActiveTab')
526
+ const result = await globalThis.toggleExtensionForActiveTab()
527
+ console.log('toggleExtensionForActiveTab result:', result)
528
+ return result
529
+ })
530
+
531
+ console.log('Reconnect result:', reconnectResult)
532
+ expect(reconnectResult.isConnected).toBe(true)
533
+
534
+ console.log('Waiting for reconnection to stabilize...')
535
+ await new Promise((resolve) => setTimeout(resolve, 100))
536
+
537
+ // 5. Reset the MCP client's playwright connection
538
+ console.log('Resetting MCP playwright connection...')
539
+ const resetResult = await client.callTool({
540
+ name: 'reset',
541
+ arguments: {},
542
+ })
543
+ console.log('Reset result:', (resetResult as any).content[0].text)
544
+ expect((resetResult as any).content[0].text).toContain('Connection reset successfully')
545
+
546
+ // 6. Verify MCP can see the page again
547
+ console.log('Attempting to access page via MCP...')
548
+ const afterReconnect = await client.callTool({
549
+ name: 'execute',
550
+ arguments: {
551
+ code: js`
428
552
  console.log('Checking pages after reconnect...');
429
553
  const pages = context.pages();
430
554
  console.log('Pages after reconnect:', pages.length);
@@ -444,38 +568,38 @@ describe('Extension Connection Tests', () => {
444
568
 
445
569
  return { pagesCount: pages.length, foundTestPage: false };
446
570
  `,
447
- },
448
- })
449
-
450
- const afterReconnectOutput = (afterReconnect as any).content[0].text
451
- console.log('After reconnect:', afterReconnectOutput)
452
- expect(afterReconnectOutput).toContain('foundTestPage')
453
- expect(afterReconnectOutput).toContain('disconnect-test')
454
-
455
- // Clean up
456
- await page.goto('about:blank')
571
+ },
457
572
  })
458
573
 
459
- it('should auto-reconnect MCP after extension WebSocket reconnects', async () => {
460
- const serviceWorker = await getExtensionServiceWorker(testCtx!.browserContext)
574
+ const afterReconnectOutput = (afterReconnect as any).content[0].text
575
+ console.log('After reconnect:', afterReconnectOutput)
576
+ expect(afterReconnectOutput).toContain('foundTestPage')
577
+ expect(afterReconnectOutput).toContain('disconnect-test')
578
+
579
+ // Clean up
580
+ await page.goto('about:blank')
581
+ })
461
582
 
462
- // 1. Create a test page and enable extension
463
- const page = await testCtx!.browserContext.newPage()
464
- await page.goto('https://example.com/auto-reconnect-test')
465
- await page.waitForLoadState('domcontentloaded')
466
- await page.bringToFront()
583
+ it('should auto-reconnect MCP after extension WebSocket reconnects', async () => {
584
+ const serviceWorker = await getExtensionServiceWorker(testCtx!.browserContext)
467
585
 
468
- const initialEnable = await serviceWorker.evaluate(async () => {
469
- return await globalThis.toggleExtensionForActiveTab()
470
- })
471
- expect(initialEnable.isConnected).toBe(true)
472
- await new Promise(resolve => setTimeout(resolve, 100))
586
+ // 1. Create a test page and enable extension
587
+ const page = await testCtx!.browserContext.newPage()
588
+ await page.goto('https://example.com/auto-reconnect-test')
589
+ await page.waitForLoadState('domcontentloaded')
590
+ await page.bringToFront()
473
591
 
474
- // 2. Verify MCP can execute commands
475
- const beforeResult = await client.callTool({
476
- name: 'execute',
477
- arguments: {
478
- code: js`
592
+ const initialEnable = await serviceWorker.evaluate(async () => {
593
+ return await globalThis.toggleExtensionForActiveTab()
594
+ })
595
+ expect(initialEnable.isConnected).toBe(true)
596
+ await new Promise((resolve) => setTimeout(resolve, 100))
597
+
598
+ // 2. Verify MCP can execute commands
599
+ const beforeResult = await client.callTool({
600
+ name: 'execute',
601
+ arguments: {
602
+ code: js`
479
603
  let testPage;
480
604
  for (let i = 0; i < 20; i++) {
481
605
  const pages = context.pages();
@@ -486,32 +610,32 @@ describe('Extension Connection Tests', () => {
486
610
  const pages = context.pages();
487
611
  return { pagesCount: pages.length, foundTestPage: !!testPage, url: testPage?.url() };
488
612
  `,
489
- },
490
- })
491
- const beforeOutput = (beforeResult as any).content[0].text
492
- expect(beforeOutput).toContain('foundTestPage')
493
- expect(beforeOutput).toContain('true')
494
- expect(beforeOutput).toContain('auto-reconnect-test')
495
-
496
- // 3. Simulate extension WebSocket reconnection
497
- await serviceWorker.evaluate(async () => {
498
- await globalThis.disconnectEverything()
499
- })
500
- await new Promise(resolve => setTimeout(resolve, 100))
501
-
502
- // Re-enable extension
503
- await page.bringToFront()
504
- const reconnectResult = await serviceWorker.evaluate(async () => {
505
- return await globalThis.toggleExtensionForActiveTab()
506
- })
507
- expect(reconnectResult.isConnected).toBe(true)
508
- await new Promise(resolve => setTimeout(resolve, 100))
509
-
510
- // 4. Execute command WITHOUT calling resetPlaywright()
511
- const afterResult = await client.callTool({
512
- name: 'execute',
513
- arguments: {
514
- code: js`
613
+ },
614
+ })
615
+ const beforeOutput = (beforeResult as any).content[0].text
616
+ expect(beforeOutput).toContain('foundTestPage')
617
+ expect(beforeOutput).toContain('true')
618
+ expect(beforeOutput).toContain('auto-reconnect-test')
619
+
620
+ // 3. Simulate extension WebSocket reconnection
621
+ await serviceWorker.evaluate(async () => {
622
+ await globalThis.disconnectEverything()
623
+ })
624
+ await new Promise((resolve) => setTimeout(resolve, 100))
625
+
626
+ // Re-enable extension
627
+ await page.bringToFront()
628
+ const reconnectResult = await serviceWorker.evaluate(async () => {
629
+ return await globalThis.toggleExtensionForActiveTab()
630
+ })
631
+ expect(reconnectResult.isConnected).toBe(true)
632
+ await new Promise((resolve) => setTimeout(resolve, 100))
633
+
634
+ // 4. Execute command WITHOUT calling resetPlaywright()
635
+ const afterResult = await client.callTool({
636
+ name: 'execute',
637
+ arguments: {
638
+ code: js`
515
639
  let testPage;
516
640
  for (let i = 0; i < 20; i++) {
517
641
  const pages = context.pages();
@@ -522,112 +646,112 @@ describe('Extension Connection Tests', () => {
522
646
  const pages = context.pages();
523
647
  return { pagesCount: pages.length, foundTestPage: !!testPage, url: testPage?.url() };
524
648
  `,
525
- },
526
- })
649
+ },
650
+ })
527
651
 
528
- const afterOutput = (afterResult as any).content[0].text
529
- expect(afterOutput).toContain('foundTestPage')
530
- expect(afterOutput).toContain('true')
531
- expect(afterOutput).toContain('auto-reconnect-test')
532
- expect(afterOutput).not.toContain('Extension not connected')
533
- expect((afterResult as any).isError).not.toBe(true)
652
+ const afterOutput = (afterResult as any).content[0].text
653
+ expect(afterOutput).toContain('foundTestPage')
654
+ expect(afterOutput).toContain('true')
655
+ expect(afterOutput).toContain('auto-reconnect-test')
656
+ expect(afterOutput).not.toContain('Extension not connected')
657
+ expect((afterResult as any).isError).not.toBe(true)
534
658
 
535
- // Clean up
536
- await page.goto('about:blank')
537
- })
659
+ // Clean up
660
+ await page.goto('about:blank')
661
+ })
538
662
 
539
- it('should maintain correct page.url() with service worker pages', async () => {
540
- const browserContext = getBrowserContext()
541
- const serviceWorker = await getExtensionServiceWorker(browserContext)
663
+ it('should maintain correct page.url() with service worker pages', async () => {
664
+ const browserContext = getBrowserContext()
665
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
542
666
 
543
- const page = await browserContext.newPage()
544
- const targetUrl = 'https://example.com/sw-test'
545
- await page.goto(targetUrl)
546
- await page.bringToFront()
667
+ const page = await browserContext.newPage()
668
+ const targetUrl = 'https://example.com/sw-test'
669
+ await page.goto(targetUrl)
670
+ await page.bringToFront()
547
671
 
548
- await serviceWorker.evaluate(async () => {
549
- await globalThis.toggleExtensionForActiveTab()
550
- })
672
+ await serviceWorker.evaluate(async () => {
673
+ await globalThis.toggleExtensionForActiveTab()
674
+ })
551
675
 
552
- await new Promise(r => setTimeout(r, 100))
676
+ await new Promise((r) => setTimeout(r, 100))
553
677
 
554
- const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
555
- const cdpPages = browser.contexts()[0].pages()
556
- const testPage = cdpPages.find(p => p.url().includes('sw-test'))
678
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
679
+ const cdpPages = browser.contexts()[0].pages()
680
+ const testPage = cdpPages.find((p) => p.url().includes('sw-test'))
557
681
 
558
- expect(testPage).toBeDefined()
559
- expect(testPage?.url()).toContain('sw-test')
560
- expect(testPage?.url()).not.toContain('sw.js')
682
+ expect(testPage).toBeDefined()
683
+ expect(testPage?.url()).toContain('sw-test')
684
+ expect(testPage?.url()).not.toContain('sw.js')
561
685
 
562
- await browser.close()
563
- await page.close()
564
- }, 30000)
686
+ await browser.close()
687
+ await page.close()
688
+ }, 30000)
565
689
 
566
- it('should maintain correct page.url() after repeated connections', async () => {
567
- const browserContext = getBrowserContext()
568
- const serviceWorker = await getExtensionServiceWorker(browserContext)
690
+ it('should maintain correct page.url() after repeated connections', async () => {
691
+ const browserContext = getBrowserContext()
692
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
569
693
 
570
- const page = await browserContext.newPage()
571
- const targetUrl = 'https://example.com/repeated-test'
572
- await page.goto(targetUrl)
573
- await page.bringToFront()
694
+ const page = await browserContext.newPage()
695
+ const targetUrl = 'https://example.com/repeated-test'
696
+ await page.goto(targetUrl)
697
+ await page.bringToFront()
574
698
 
575
- await serviceWorker.evaluate(async () => {
576
- await globalThis.toggleExtensionForActiveTab()
577
- })
699
+ await serviceWorker.evaluate(async () => {
700
+ await globalThis.toggleExtensionForActiveTab()
701
+ })
578
702
 
579
- for (let i = 0; i < 5; i++) {
580
- const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
581
- const cdpPages = browser.contexts()[0].pages()
582
- const testPage = cdpPages.find(p => p.url().includes('repeated-test'))
703
+ for (let i = 0; i < 5; i++) {
704
+ const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
705
+ const cdpPages = browser.contexts()[0].pages()
706
+ const testPage = cdpPages.find((p) => p.url().includes('repeated-test'))
583
707
 
584
- expect(testPage).toBeDefined()
585
- expect(testPage?.url()).toBe(targetUrl)
708
+ expect(testPage).toBeDefined()
709
+ expect(testPage?.url()).toBe(targetUrl)
586
710
 
587
- await browser.close()
588
- await new Promise(r => setTimeout(r, 100))
589
- }
711
+ await browser.close()
712
+ await new Promise((r) => setTimeout(r, 100))
713
+ }
590
714
 
591
- await page.close()
592
- }, 30000)
715
+ await page.close()
716
+ }, 30000)
593
717
 
594
- it('should maintain correct page.url() with concurrent MCP and CDP connections', async () => {
595
- const browserContext = getBrowserContext()
596
- const serviceWorker = await getExtensionServiceWorker(browserContext)
718
+ it('should maintain correct page.url() with concurrent MCP and CDP connections', async () => {
719
+ const browserContext = getBrowserContext()
720
+ const serviceWorker = await getExtensionServiceWorker(browserContext)
597
721
 
598
- const page = await browserContext.newPage()
599
- const targetUrl = 'https://example.com/concurrent-test'
600
- await page.goto(targetUrl)
601
- await page.bringToFront()
722
+ const page = await browserContext.newPage()
723
+ const targetUrl = 'https://example.com/concurrent-test'
724
+ await page.goto(targetUrl)
725
+ await page.bringToFront()
602
726
 
603
- await serviceWorker.evaluate(async () => {
604
- await globalThis.toggleExtensionForActiveTab()
605
- })
727
+ await serviceWorker.evaluate(async () => {
728
+ await globalThis.toggleExtensionForActiveTab()
729
+ })
606
730
 
607
- await new Promise(r => setTimeout(r, 400))
731
+ await new Promise((r) => setTimeout(r, 400))
608
732
 
609
- const [mcpResult, cdpBrowser] = await Promise.all([
610
- client.callTool({
611
- name: 'execute',
612
- arguments: {
613
- code: js`
733
+ const [mcpResult, cdpBrowser] = await Promise.all([
734
+ client.callTool({
735
+ name: 'execute',
736
+ arguments: {
737
+ code: js`
614
738
  const pages = context.pages();
615
739
  const testPage = pages.find(p => p.url().includes('concurrent-test'));
616
740
  return { url: testPage?.url(), found: !!testPage };
617
741
  `,
618
- },
619
- }),
620
- chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
621
- ])
742
+ },
743
+ }),
744
+ chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT })),
745
+ ])
622
746
 
623
- const mcpOutput = (mcpResult as any).content[0].text
624
- expect(mcpOutput).toContain(targetUrl)
747
+ const mcpOutput = (mcpResult as any).content[0].text
748
+ expect(mcpOutput).toContain(targetUrl)
625
749
 
626
- const cdpPages = cdpBrowser.contexts()[0].pages()
627
- const cdpPage = cdpPages.find(p => p.url().includes('concurrent-test'))
628
- expect(cdpPage?.url()).toBe(targetUrl)
750
+ const cdpPages = cdpBrowser.contexts()[0].pages()
751
+ const cdpPage = cdpPages.find((p) => p.url().includes('concurrent-test'))
752
+ expect(cdpPage?.url()).toBe(targetUrl)
629
753
 
630
- await cdpBrowser.close()
631
- await page.close()
632
- }, 30000)
754
+ await cdpBrowser.close()
755
+ await page.close()
756
+ }, 30000)
633
757
  })