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
@@ -0,0 +1,497 @@
1
+ /**
2
+ * Centralized relay state: one immutable atom for domain state + runtime resources.
3
+ *
4
+ * Follows the zustand-centralized-state skill pattern:
5
+ * - Single Zustand vanilla store holds all relay state
6
+ * - setState() callbacks remain deterministic data transitions
7
+ * - Runtime resources (WebSocket/timers/pending callbacks) are co-located on
8
+ * each extension entry in the same map
9
+ *
10
+ * See docs/plan-centralize-relay-state.md for the full refactor plan.
11
+ */
12
+ import { createStore, type StoreApi } from 'zustand/vanilla'
13
+ import type { WSContext } from 'hono/ws'
14
+ import type { Protocol } from './cdp-types.js'
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Types
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export type ConnectedTarget = {
21
+ sessionId: string
22
+ targetId: string
23
+ targetInfo: Protocol.Target.TargetInfo
24
+ frameIds: Set<string>
25
+ }
26
+
27
+ export type ExtensionInfo = {
28
+ browser?: string
29
+ email?: string
30
+ id?: string
31
+ /** playwriter package version the extension was built with (sent as ?v= query param) */
32
+ version?: string
33
+ }
34
+
35
+ export type ExtensionPendingRequest = {
36
+ resolve: (result: unknown) => void
37
+ reject: (error: Error) => void
38
+ }
39
+
40
+ /**
41
+ * Single aggregated extension object: domain state + runtime I/O.
42
+ */
43
+ export type ExtensionEntry = {
44
+ id: string
45
+ info: ExtensionInfo
46
+ stableKey: string
47
+ connectedTargets: Map<string, ConnectedTarget>
48
+ // Runtime I/O fields
49
+ ws: WSContext | null
50
+ pendingRequests: Map<number, ExtensionPendingRequest>
51
+ messageId: number
52
+ pingInterval: ReturnType<typeof setInterval> | null
53
+ }
54
+
55
+ export type PlaywrightClient = {
56
+ id: string
57
+ extensionId: string | null
58
+ ws: WSContext
59
+ }
60
+
61
+ export type RelayState = {
62
+ extensions: Map<string, ExtensionEntry>
63
+ playwrightClients: Map<string, PlaywrightClient>
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Store factory
68
+ // ---------------------------------------------------------------------------
69
+
70
+ export function createRelayStore(): StoreApi<RelayState> {
71
+ return createStore<RelayState>(() => ({
72
+ extensions: new Map(),
73
+ playwrightClients: new Map(),
74
+ }))
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Derivation helpers
79
+ // ---------------------------------------------------------------------------
80
+
81
+ /**
82
+ * Linear scan over extensions to find one by stableKey. With <10 extensions this is free.
83
+ * Returns the LAST (newest) match because during reconnect both old and new connections
84
+ * coexist briefly. Map iteration order is insertion order, so last wins.
85
+ */
86
+ export function findExtensionByStableKey(state: RelayState, stableKey: string): ExtensionEntry | undefined {
87
+ let match: ExtensionEntry | undefined
88
+ for (const ext of state.extensions.values()) {
89
+ if (ext.stableKey === stableKey) {
90
+ match = ext
91
+ }
92
+ }
93
+ return match
94
+ }
95
+
96
+ /** Find which extension owns a CDP tab sessionId (e.g. "pw-tab-1"). */
97
+ export function findExtensionIdByCdpSession(state: RelayState, cdpSessionId: string): string | null {
98
+ for (const [connectionId, ext] of state.extensions.entries()) {
99
+ if (ext.connectedTargets.has(cdpSessionId)) {
100
+ return connectionId
101
+ }
102
+ }
103
+ return null
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Pure state transition functions
108
+ //
109
+ // Each takes RelayState + event data and returns a new RelayState.
110
+ // No I/O, no side effects. Testable with data in / data out.
111
+ // ---------------------------------------------------------------------------
112
+
113
+ /**
114
+ * Add a new extension connection.
115
+ * Does NOT remove an existing connection with the same stableKey — that old
116
+ * connection stays routable until its WebSocket onClose fires and calls
117
+ * removeExtension(). This preserves in-flight message routing during reconnect.
118
+ * findExtensionByStableKey() returns the newest match so stableKey lookups
119
+ * resolve to the new connection immediately.
120
+ */
121
+ export function addExtension(
122
+ state: RelayState,
123
+ {
124
+ id,
125
+ info,
126
+ stableKey,
127
+ ws,
128
+ }: {
129
+ id: string
130
+ info: ExtensionInfo
131
+ stableKey: string
132
+ ws: WSContext | null
133
+ },
134
+ ): RelayState {
135
+ const newExtensions = new Map(state.extensions)
136
+
137
+ newExtensions.set(id, {
138
+ id,
139
+ info,
140
+ stableKey,
141
+ connectedTargets: new Map(),
142
+ ws,
143
+ pendingRequests: new Map(),
144
+ messageId: 0,
145
+ pingInterval: null,
146
+ })
147
+ return { ...state, extensions: newExtensions }
148
+ }
149
+
150
+ /** Remove an extension, its targets, and any playwright clients bound to it. */
151
+ export function removeExtension(state: RelayState, { extensionId }: { extensionId: string }): RelayState {
152
+ if (!state.extensions.has(extensionId)) {
153
+ return state
154
+ }
155
+ const newExtensions = new Map(state.extensions)
156
+ newExtensions.delete(extensionId)
157
+
158
+ // Also remove playwright clients bound to this extension
159
+ const clientsToRemove = Array.from(state.playwrightClients.values())
160
+ .filter((client) => client.extensionId === extensionId)
161
+ if (clientsToRemove.length === 0) {
162
+ return { ...state, extensions: newExtensions }
163
+ }
164
+
165
+ const newClients = new Map(state.playwrightClients)
166
+ for (const client of clientsToRemove) {
167
+ newClients.delete(client.id)
168
+ }
169
+ return { ...state, extensions: newExtensions, playwrightClients: newClients }
170
+ }
171
+
172
+ /** Add a playwright client (state + ws handle co-located). */
173
+ export function addPlaywrightClient(
174
+ state: RelayState,
175
+ { id, extensionId, ws }: { id: string; extensionId: string | null; ws: WSContext },
176
+ ): RelayState {
177
+ const newClients = new Map(state.playwrightClients)
178
+ newClients.set(id, { id, extensionId, ws })
179
+ return { ...state, playwrightClients: newClients }
180
+ }
181
+
182
+ /** Remove a playwright client. */
183
+ export function removePlaywrightClient(state: RelayState, { clientId }: { clientId: string }): RelayState {
184
+ if (!state.playwrightClients.has(clientId)) {
185
+ return state
186
+ }
187
+ const newClients = new Map(state.playwrightClients)
188
+ newClients.delete(clientId)
189
+ return { ...state, playwrightClients: newClients }
190
+ }
191
+
192
+ /** Rebind all clients from one extension id to another. */
193
+ export function rebindClientsToExtension(
194
+ state: RelayState,
195
+ { fromExtensionId, toExtensionId }: { fromExtensionId: string; toExtensionId: string },
196
+ ): RelayState {
197
+ if (fromExtensionId === toExtensionId) {
198
+ return state
199
+ }
200
+
201
+ let updated = false
202
+ const newClients = new Map(state.playwrightClients)
203
+ for (const [clientId, client] of newClients) {
204
+ if (client.extensionId !== fromExtensionId) {
205
+ continue
206
+ }
207
+ newClients.set(clientId, { ...client, extensionId: toExtensionId })
208
+ updated = true
209
+ }
210
+
211
+ if (!updated) {
212
+ return state
213
+ }
214
+
215
+ return { ...state, playwrightClients: newClients }
216
+ }
217
+
218
+ /** Update an extension entry's I/O fields (ws, pingInterval). */
219
+ export function updateExtensionIO(
220
+ state: RelayState,
221
+ {
222
+ extensionId,
223
+ ws,
224
+ pingInterval,
225
+ }: {
226
+ extensionId: string
227
+ ws?: WSContext | null
228
+ pingInterval?: ReturnType<typeof setInterval> | null
229
+ },
230
+ ): RelayState {
231
+ const ext = state.extensions.get(extensionId)
232
+ if (!ext) {
233
+ return state
234
+ }
235
+ const newExtensions = new Map(state.extensions)
236
+ newExtensions.set(extensionId, {
237
+ ...ext,
238
+ ...(ws !== undefined ? { ws } : {}),
239
+ ...(pingInterval !== undefined ? { pingInterval } : {}),
240
+ })
241
+ return { ...state, extensions: newExtensions }
242
+ }
243
+
244
+ /** Add or replace one pending extension request callback pair. */
245
+ export function addExtensionPendingRequest(
246
+ state: RelayState,
247
+ {
248
+ extensionId,
249
+ requestId,
250
+ pendingRequest,
251
+ }: {
252
+ extensionId: string
253
+ requestId: number
254
+ pendingRequest: ExtensionPendingRequest
255
+ },
256
+ ): RelayState {
257
+ const ext = state.extensions.get(extensionId)
258
+ if (!ext) {
259
+ return state
260
+ }
261
+
262
+ const pendingRequests = new Map(ext.pendingRequests)
263
+ pendingRequests.set(requestId, pendingRequest)
264
+ const newExtensions = new Map(state.extensions)
265
+ newExtensions.set(extensionId, { ...ext, pendingRequests })
266
+ return { ...state, extensions: newExtensions }
267
+ }
268
+
269
+ /** Remove one pending extension request callback pair. */
270
+ export function removeExtensionPendingRequest(
271
+ state: RelayState,
272
+ { extensionId, requestId }: { extensionId: string; requestId: number },
273
+ ): RelayState {
274
+ const ext = state.extensions.get(extensionId)
275
+ if (!ext || !ext.pendingRequests.has(requestId)) {
276
+ return state
277
+ }
278
+
279
+ const pendingRequests = new Map(ext.pendingRequests)
280
+ pendingRequests.delete(requestId)
281
+ const newExtensions = new Map(state.extensions)
282
+ newExtensions.set(extensionId, { ...ext, pendingRequests })
283
+ return { ...state, extensions: newExtensions }
284
+ }
285
+
286
+ /** Add a target to an extension's connectedTargets. No-op if extension doesn't exist. */
287
+ export function addTarget(
288
+ state: RelayState,
289
+ {
290
+ extensionId,
291
+ sessionId,
292
+ targetId,
293
+ targetInfo,
294
+ existingFrameIds,
295
+ }: {
296
+ extensionId: string
297
+ sessionId: string
298
+ targetId: string
299
+ targetInfo: Protocol.Target.TargetInfo
300
+ /** Preserve existing frameIds if target already existed (update scenario). */
301
+ existingFrameIds?: Set<string>
302
+ },
303
+ ): RelayState {
304
+ const ext = state.extensions.get(extensionId)
305
+ if (!ext) {
306
+ return state
307
+ }
308
+
309
+ const existingTarget = ext.connectedTargets.get(sessionId)
310
+ const newTargets = new Map(ext.connectedTargets)
311
+ newTargets.set(sessionId, {
312
+ sessionId,
313
+ targetId,
314
+ targetInfo,
315
+ frameIds: existingFrameIds ?? existingTarget?.frameIds ?? new Set(),
316
+ })
317
+
318
+ const newExtensions = new Map(state.extensions)
319
+ newExtensions.set(extensionId, { ...ext, connectedTargets: newTargets })
320
+ return { ...state, extensions: newExtensions }
321
+ }
322
+
323
+ /** Remove a target by sessionId. No-op if extension or target doesn't exist. */
324
+ export function removeTarget(
325
+ state: RelayState,
326
+ { extensionId, sessionId }: { extensionId: string; sessionId: string },
327
+ ): RelayState {
328
+ const ext = state.extensions.get(extensionId)
329
+ if (!ext || !ext.connectedTargets.has(sessionId)) {
330
+ return state
331
+ }
332
+
333
+ const newTargets = new Map(ext.connectedTargets)
334
+ newTargets.delete(sessionId)
335
+
336
+ const newExtensions = new Map(state.extensions)
337
+ newExtensions.set(extensionId, { ...ext, connectedTargets: newTargets })
338
+ return { ...state, extensions: newExtensions }
339
+ }
340
+
341
+ /** Remove a crashed target by targetId (not sessionId). */
342
+ export function removeTargetByCrash(
343
+ state: RelayState,
344
+ { extensionId, targetId }: { extensionId: string; targetId: string },
345
+ ): RelayState {
346
+ const ext = state.extensions.get(extensionId)
347
+ if (!ext) {
348
+ return state
349
+ }
350
+
351
+ let found = false
352
+ const newTargets = new Map(ext.connectedTargets)
353
+ for (const [sid, target] of newTargets) {
354
+ if (target.targetId === targetId) {
355
+ newTargets.delete(sid)
356
+ found = true
357
+ break
358
+ }
359
+ }
360
+
361
+ if (!found) {
362
+ return state
363
+ }
364
+
365
+ const newExtensions = new Map(state.extensions)
366
+ newExtensions.set(extensionId, { ...ext, connectedTargets: newTargets })
367
+ return { ...state, extensions: newExtensions }
368
+ }
369
+
370
+ /** Update targetInfo on a target matched by targetId. */
371
+ export function updateTargetInfo(
372
+ state: RelayState,
373
+ { extensionId, targetInfo }: { extensionId: string; targetInfo: Protocol.Target.TargetInfo },
374
+ ): RelayState {
375
+ const ext = state.extensions.get(extensionId)
376
+ if (!ext) {
377
+ return state
378
+ }
379
+
380
+ let updated = false
381
+ const newTargets = new Map(ext.connectedTargets)
382
+ for (const [sid, target] of newTargets) {
383
+ if (target.targetId === targetInfo.targetId) {
384
+ newTargets.set(sid, { ...target, targetInfo })
385
+ updated = true
386
+ break
387
+ }
388
+ }
389
+
390
+ if (!updated) {
391
+ return state
392
+ }
393
+
394
+ const newExtensions = new Map(state.extensions)
395
+ newExtensions.set(extensionId, { ...ext, connectedTargets: newTargets })
396
+ return { ...state, extensions: newExtensions }
397
+ }
398
+
399
+ /** Add a frameId to a target's frameIds set. */
400
+ export function addFrameId(
401
+ state: RelayState,
402
+ { extensionId, sessionId, frameId }: { extensionId: string; sessionId: string; frameId: string },
403
+ ): RelayState {
404
+ const ext = state.extensions.get(extensionId)
405
+ if (!ext) {
406
+ return state
407
+ }
408
+ const target = ext.connectedTargets.get(sessionId)
409
+ if (!target) {
410
+ return state
411
+ }
412
+
413
+ // Already present — no-op
414
+ if (target.frameIds.has(frameId)) {
415
+ return state
416
+ }
417
+
418
+ const newFrameIds = new Set(target.frameIds)
419
+ newFrameIds.add(frameId)
420
+
421
+ const newTargets = new Map(ext.connectedTargets)
422
+ newTargets.set(sessionId, { ...target, frameIds: newFrameIds })
423
+
424
+ const newExtensions = new Map(state.extensions)
425
+ newExtensions.set(extensionId, { ...ext, connectedTargets: newTargets })
426
+ return { ...state, extensions: newExtensions }
427
+ }
428
+
429
+ /** Remove a frameId from the target that owns it (scans all targets in the extension). */
430
+ export function removeFrameId(
431
+ state: RelayState,
432
+ { extensionId, frameId }: { extensionId: string; frameId: string },
433
+ ): RelayState {
434
+ const ext = state.extensions.get(extensionId)
435
+ if (!ext) {
436
+ return state
437
+ }
438
+
439
+ for (const [sid, target] of ext.connectedTargets) {
440
+ if (target.frameIds.has(frameId)) {
441
+ const newFrameIds = new Set(target.frameIds)
442
+ newFrameIds.delete(frameId)
443
+
444
+ const newTargets = new Map(ext.connectedTargets)
445
+ newTargets.set(sid, { ...target, frameIds: newFrameIds })
446
+
447
+ const newExtensions = new Map(state.extensions)
448
+ newExtensions.set(extensionId, { ...ext, connectedTargets: newTargets })
449
+ return { ...state, extensions: newExtensions }
450
+ }
451
+ }
452
+
453
+ return state
454
+ }
455
+
456
+ /**
457
+ * Update URL (and optionally title) on a target.
458
+ * Used by Page.frameNavigated (top-level) and Page.navigatedWithinDocument.
459
+ */
460
+ export function updateTargetUrl(
461
+ state: RelayState,
462
+ {
463
+ extensionId,
464
+ sessionId,
465
+ url,
466
+ title,
467
+ }: {
468
+ extensionId: string
469
+ sessionId: string
470
+ url: string
471
+ title?: string
472
+ },
473
+ ): RelayState {
474
+ const ext = state.extensions.get(extensionId)
475
+ if (!ext) {
476
+ return state
477
+ }
478
+ const target = ext.connectedTargets.get(sessionId)
479
+ if (!target) {
480
+ return state
481
+ }
482
+
483
+ const newTargetInfo = {
484
+ ...target.targetInfo,
485
+ url,
486
+ ...(title !== undefined ? { title } : {}),
487
+ }
488
+
489
+ const newTargets = new Map(ext.connectedTargets)
490
+ newTargets.set(sessionId, { ...target, targetInfo: newTargetInfo })
491
+
492
+ const newExtensions = new Map(state.extensions)
493
+ newExtensions.set(extensionId, { ...ext, connectedTargets: newTargets })
494
+ return { ...state, extensions: newExtensions }
495
+ }
496
+
497
+
package/src/resource.md CHANGED
@@ -1,5 +1,3 @@
1
-
2
-
3
1
  You can also find `getByRole` to get elements on the page.
4
2
 
5
3
  ```javascript
@@ -14,9 +12,7 @@ await page.getByRole('link', { name: 'About' }).click()
14
12
  await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com')
15
13
 
16
14
  // For a heading with { "role": "heading", "name": "Welcome to Example.com" }
17
- const headingText = await page
18
- .getByRole('heading', { name: 'Welcome to Example.com' })
19
- .textContent()
15
+ const headingText = await page.getByRole('heading', { name: 'Welcome to Example.com' }).textContent()
20
16
  console.log('Heading text:', headingText)
21
17
  ```
22
18
 
@@ -223,17 +219,14 @@ const pageTitle = await page.evaluate(() => document.title)
223
219
 
224
220
  // Modify page
225
221
  await page.evaluate(() => {
226
- document.body.style.backgroundColor = 'red'
222
+ document.body.style.backgroundColor = 'red'
227
223
  })
228
224
 
229
225
  // Pass arguments to page context
230
226
  const sum = await page.evaluate(([a, b]) => a + b, [5, 3])
231
227
 
232
228
  // Work with elements
233
- const elementText = await page.evaluate(
234
- (el) => el.textContent,
235
- await page.getByRole('heading'),
236
- )
229
+ const elementText = await page.evaluate((el) => el.textContent, await page.getByRole('heading'))
237
230
  ```
238
231
 
239
232
  ### Execute JavaScript on Element
@@ -244,8 +237,8 @@ const href = await page.getByRole('link').evaluate((el) => el.href)
244
237
 
245
238
  // Modify element
246
239
  await page.getByRole('button').evaluate((el) => {
247
- el.style.backgroundColor = 'green'
248
- el.disabled = true
240
+ el.style.backgroundColor = 'green'
241
+ el.disabled = true
249
242
  })
250
243
 
251
244
  // Scroll element into view
@@ -261,9 +254,7 @@ await page.getByText('Section').evaluate((el) => el.scrollIntoView())
261
254
  await page.getByLabel('Upload file').setInputFiles('/path/to/file.pdf')
262
255
 
263
256
  // Upload multiple files
264
- await page
265
- .getByLabel('Upload files')
266
- .setInputFiles(['/path/to/file1.pdf', '/path/to/file2.pdf'])
257
+ await page.getByLabel('Upload files').setInputFiles(['/path/to/file1.pdf', '/path/to/file2.pdf'])
267
258
 
268
259
  // Clear file input
269
260
  await page.getByLabel('Upload file').setInputFiles([])
@@ -280,8 +271,7 @@ await page.locator('input[type="file"]').setInputFiles('/path/to/file.pdf')
280
271
  ```javascript
281
272
  // Wait for a specific request to complete and get its response
282
273
  const response = await page.waitForResponse(
283
- (response) =>
284
- response.url().includes('/api/user') && response.status() === 200,
274
+ (response) => response.url().includes('/api/user') && response.status() === 200,
285
275
  )
286
276
 
287
277
  // Get response data
@@ -295,11 +285,11 @@ console.log('Request method:', request.method())
295
285
 
296
286
  // Get all resources loaded by the page
297
287
  const resources = await page.evaluate(() =>
298
- performance.getEntriesByType('resource').map((r) => ({
299
- name: r.name,
300
- duration: r.duration,
301
- size: r.transferSize,
302
- })),
288
+ performance.getEntriesByType('resource').map((r) => ({
289
+ name: r.name,
290
+ duration: r.duration,
291
+ size: r.transferSize,
292
+ })),
303
293
  )
304
294
  console.log('Page resources:', resources)
305
295
  ```
@@ -314,9 +304,9 @@ console.log('Page resources:', resources)
314
304
 
315
305
  // To trigger console messages from the page:
316
306
  await page.evaluate(() => {
317
- console.log('This message will be captured')
318
- console.error('This error will be captured')
319
- console.warn('This warning will be captured')
307
+ console.log('This message will be captured')
308
+ console.error('This error will be captured')
309
+ console.warn('This warning will be captured')
320
310
  })
321
311
 
322
312
  // Then use the console_logs MCP tool to retrieve all captured messages
@@ -338,10 +328,7 @@ await page.waitForURL(/github\.com.*\/pull/)
338
328
  await page.waitForURL(/\/new-org/)
339
329
 
340
330
  // Wait for text to appear
341
- await page.waitForFunction(
342
- (text) => document.body.textContent.includes(text),
343
- 'Success!',
344
- )
331
+ await page.waitForFunction((text) => document.body.textContent.includes(text), 'Success!')
345
332
 
346
333
  // Wait for navigation
347
334
  await page.waitForURL('**/success')
@@ -350,10 +337,7 @@ await page.waitForURL('**/success')
350
337
  await waitForPageLoad({ page })
351
338
 
352
339
  // Wait for specific condition
353
- await page.waitForFunction(
354
- (text) => document.querySelector('.status')?.textContent === text,
355
- 'Ready',
356
- )
340
+ await page.waitForFunction((text) => document.querySelector('.status')?.textContent === text, 'Ready')
357
341
  ```
358
342
 
359
343
  ### Wait for Text to Appear or Disappear
@@ -374,30 +358,18 @@ await page.getByText('Success!').first().waitFor({ state: 'visible' })
374
358
  console.log('Processing finished and success message appeared')
375
359
 
376
360
  // Example: Wait for error message to disappear before proceeding
377
- await page
378
- .getByText('Error: Please try again')
379
- .first()
380
- .waitFor({ state: 'hidden' })
361
+ await page.getByText('Error: Please try again').first().waitFor({ state: 'hidden' })
381
362
  await page.getByRole('button', { name: 'Submit' }).click()
382
363
 
383
364
  // Example: Wait for confirmation text after form submission
384
365
  await page.getByRole('button', { name: 'Save' }).click()
385
- await page
386
- .getByText('Your changes have been saved')
387
- .first()
388
- .waitFor({ state: 'visible' })
366
+ await page.getByText('Your changes have been saved').first().waitFor({ state: 'visible' })
389
367
  console.log('Save confirmed')
390
368
 
391
369
  // Example: Wait for dynamic content to load
392
370
  await page.getByRole('button', { name: 'Load More' }).click()
393
- await page
394
- .getByText('Loading more items...')
395
- .first()
396
- .waitFor({ state: 'visible' })
397
- await page
398
- .getByText('Loading more items...')
399
- .first()
400
- .waitFor({ state: 'hidden' })
371
+ await page.getByText('Loading more items...').first().waitFor({ state: 'visible' })
372
+ await page.getByText('Loading more items...').first().waitFor({ state: 'hidden' })
401
373
  console.log('Additional items loaded')
402
374
  ```
403
375
 
package/src/scoped-fs.ts CHANGED
@@ -150,7 +150,9 @@ export class ScopedFS {
150
150
  // Verify the real path is also within allowed directories (handles symlinks)
151
151
  const realStr = real.toString()
152
152
  if (!this.isPathAllowed(realStr)) {
153
- const error = new Error(`EPERM: operation not permitted, realpath escapes allowed directories`) as NodeJS.ErrnoException
153
+ const error = new Error(
154
+ `EPERM: operation not permitted, realpath escapes allowed directories`,
155
+ ) as NodeJS.ErrnoException
154
156
  error.code = 'EPERM'
155
157
  throw error
156
158
  }
@@ -168,7 +170,9 @@ export class ScopedFS {
168
170
  const linkDir = path.dirname(resolvedLink)
169
171
  const resolvedTarget = path.resolve(linkDir, target.toString())
170
172
  if (!this.isPathAllowed(resolvedTarget)) {
171
- const error = new Error(`EPERM: operation not permitted, symlink target outside allowed directories`) as NodeJS.ErrnoException
173
+ const error = new Error(
174
+ `EPERM: operation not permitted, symlink target outside allowed directories`,
175
+ ) as NodeJS.ErrnoException
172
176
  error.code = 'EPERM'
173
177
  throw error
174
178
  }
@@ -368,7 +372,9 @@ export class ScopedFS {
368
372
  const real = await fs.promises.realpath(resolved, options)
369
373
  const realStr = real.toString()
370
374
  if (!self.isPathAllowed(realStr)) {
371
- const error = new Error(`EPERM: operation not permitted, realpath escapes allowed directories`) as NodeJS.ErrnoException
375
+ const error = new Error(
376
+ `EPERM: operation not permitted, realpath escapes allowed directories`,
377
+ ) as NodeJS.ErrnoException
372
378
  error.code = 'EPERM'
373
379
  throw error
374
380
  }