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.
- package/dist/a11y-client.js +18 -8
- package/dist/aria-snapshot.d.ts +41 -3
- package/dist/aria-snapshot.d.ts.map +1 -1
- package/dist/aria-snapshot.js +134 -55
- package/dist/aria-snapshot.js.map +1 -1
- package/dist/aria-snapshot.test.js +5 -2
- package/dist/aria-snapshot.test.js.map +1 -1
- package/dist/aria-snapshot.unit.test.js +83 -41
- package/dist/aria-snapshot.unit.test.js.map +1 -1
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts +5 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts.map +1 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js +5 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js.map +1 -0
- package/dist/bippy.js +1 -1
- package/dist/cdp-log.d.ts +1 -1
- package/dist/cdp-log.d.ts.map +1 -1
- package/dist/cdp-log.js +1 -1
- package/dist/cdp-log.js.map +1 -1
- package/dist/cdp-relay.d.ts.map +1 -1
- package/dist/cdp-relay.js +492 -298
- package/dist/cdp-relay.js.map +1 -1
- package/dist/cdp-session.d.ts.map +1 -1
- package/dist/cdp-session.js.map +1 -1
- package/dist/cdp-types.d.ts.map +1 -1
- package/dist/cdp-types.js +7 -7
- package/dist/cdp-types.js.map +1 -1
- package/dist/clean-html.d.ts.map +1 -1
- package/dist/clean-html.js +4 -5
- package/dist/clean-html.js.map +1 -1
- package/dist/cli.js +45 -27
- package/dist/cli.js.map +1 -1
- package/dist/create-logger.d.ts.map +1 -1
- package/dist/create-logger.js +3 -1
- package/dist/create-logger.js.map +1 -1
- package/dist/debugger-examples-types.d.ts.map +1 -1
- package/dist/debugger.d.ts.map +1 -1
- package/dist/debugger.js +1 -3
- package/dist/debugger.js.map +1 -1
- package/dist/diff-utils.d.ts.map +1 -1
- package/dist/diff-utils.js +1 -4
- package/dist/diff-utils.js.map +1 -1
- package/dist/editor-api.md +12 -2
- package/dist/editor-examples.d.ts +1 -1
- package/dist/editor-examples.d.ts.map +1 -1
- package/dist/editor-examples.js +1 -1
- package/dist/editor-examples.js.map +1 -1
- package/dist/editor.d.ts +1 -1
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +1 -1
- package/dist/editor.js.map +1 -1
- package/dist/executor.d.ts +26 -3
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +297 -64
- package/dist/executor.js.map +1 -1
- package/dist/executor.unit.test.js +38 -1
- package/dist/executor.unit.test.js.map +1 -1
- package/dist/extension-connection.test.js +139 -36
- package/dist/extension-connection.test.js.map +1 -1
- package/dist/ffmpeg.d.ts +148 -0
- package/dist/ffmpeg.d.ts.map +1 -0
- package/dist/ffmpeg.js +523 -0
- package/dist/ffmpeg.js.map +1 -0
- package/dist/ghost-browser.d.ts.map +1 -1
- package/dist/ghost-browser.js.map +1 -1
- package/dist/ghost-cursor-client.js +287 -0
- package/dist/ghost-cursor.d.ts +27 -0
- package/dist/ghost-cursor.d.ts.map +1 -0
- package/dist/ghost-cursor.js +63 -0
- package/dist/ghost-cursor.js.map +1 -0
- package/dist/htmlrewrite.d.ts.map +1 -1
- package/dist/htmlrewrite.js +17 -55
- package/dist/htmlrewrite.js.map +1 -1
- package/dist/htmlrewrite.test.js.map +1 -1
- package/dist/kill-port.d.ts.map +1 -1
- package/dist/kill-port.js +1 -3
- package/dist/kill-port.js.map +1 -1
- package/dist/locator-selector.test.d.ts +2 -0
- package/dist/locator-selector.test.d.ts.map +1 -0
- package/dist/locator-selector.test.js +96 -0
- package/dist/locator-selector.test.js.map +1 -0
- package/dist/mcp-client.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +8 -3
- package/dist/mcp.js.map +1 -1
- package/dist/on-mouse-action.test.d.ts +2 -0
- package/dist/on-mouse-action.test.d.ts.map +1 -0
- package/dist/on-mouse-action.test.js +155 -0
- package/dist/on-mouse-action.test.js.map +1 -0
- package/dist/page-markdown.js +4 -4
- package/dist/page-markdown.js.map +1 -1
- package/dist/prompt.md +450 -377
- package/dist/protocol.d.ts +4 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/readability.js +16 -2
- package/dist/recording-ghost-cursor.d.ts +41 -0
- package/dist/recording-ghost-cursor.d.ts.map +1 -0
- package/dist/recording-ghost-cursor.js +79 -0
- package/dist/recording-ghost-cursor.js.map +1 -0
- package/dist/recording-relay.d.ts.map +1 -1
- package/dist/recording-relay.js +8 -8
- package/dist/recording-relay.js.map +1 -1
- package/dist/relay-client.d.ts +17 -4
- package/dist/relay-client.d.ts.map +1 -1
- package/dist/relay-client.js +45 -11
- package/dist/relay-client.js.map +1 -1
- package/dist/relay-core.test.d.ts.map +1 -1
- package/dist/relay-core.test.js +515 -26
- package/dist/relay-core.test.js.map +1 -1
- package/dist/relay-navigation.test.d.ts.map +1 -1
- package/dist/relay-navigation.test.js +169 -31
- package/dist/relay-navigation.test.js.map +1 -1
- package/dist/relay-session.test.d.ts.map +1 -1
- package/dist/relay-session.test.js +113 -65
- package/dist/relay-session.test.js.map +1 -1
- package/dist/relay-state.d.ts +158 -0
- package/dist/relay-state.d.ts.map +1 -0
- package/dist/relay-state.js +306 -0
- package/dist/relay-state.js.map +1 -0
- package/dist/relay-state.test.d.ts +2 -0
- package/dist/relay-state.test.d.ts.map +1 -0
- package/dist/relay-state.test.js +472 -0
- package/dist/relay-state.test.js.map +1 -0
- package/dist/scoped-fs.d.ts.map +1 -1
- package/dist/scoped-fs.js.map +1 -1
- package/dist/screen-recording.d.ts +66 -4
- package/dist/screen-recording.d.ts.map +1 -1
- package/dist/screen-recording.js +150 -13
- package/dist/screen-recording.js.map +1 -1
- package/dist/screen-recording.test.d.ts +2 -0
- package/dist/screen-recording.test.d.ts.map +1 -0
- package/dist/screen-recording.test.js +102 -0
- package/dist/screen-recording.test.js.map +1 -0
- package/dist/selector-generator.js +1 -1
- package/dist/snapshot-tools.test.js +71 -28
- package/dist/snapshot-tools.test.js.map +1 -1
- package/dist/start-relay-server.d.ts +1 -1
- package/dist/start-relay-server.d.ts.map +1 -1
- package/dist/start-relay-server.js +1 -1
- package/dist/start-relay-server.js.map +1 -1
- package/dist/styles-api.md +8 -1
- package/dist/styles-examples.d.ts +1 -1
- package/dist/styles-examples.d.ts.map +1 -1
- package/dist/styles-examples.js +1 -1
- package/dist/styles-examples.js.map +1 -1
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +1 -3
- package/dist/styles.js.map +1 -1
- package/dist/test-declarations.d.ts.map +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/dist/test-utils.d.ts.map +1 -1
- package/dist/test-utils.js +7 -5
- package/dist/test-utils.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js.map +1 -1
- package/dist/wait-for-page-load.d.ts.map +1 -1
- package/dist/wait-for-page-load.js +1 -1
- package/dist/wait-for-page-load.js.map +1 -1
- package/package.json +4 -3
- package/src/a11y-client.ts +5 -4
- package/src/aria-snapshot.test.ts +5 -2
- package/src/aria-snapshot.ts +306 -117
- package/src/aria-snapshot.unit.test.ts +199 -141
- package/src/aria-snapshots/github-interactive.txt +2 -0
- package/src/aria-snapshots/github-raw.txt +5 -1
- package/src/aria-snapshots/hackernews-interactive.txt +238 -241
- package/src/aria-snapshots/hackernews-raw.txt +265 -269
- package/src/assets/aria-labels-example.png +0 -0
- package/src/assets/aria-labels-github.png +0 -0
- package/src/assets/aria-labels-hacker-news.png +0 -0
- package/src/assets/aria-labels-old-reddit.png +0 -0
- package/src/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.ts +5 -0
- package/src/assets/cursors/screen-studio/pointer-macos-tahoe.svg +18 -0
- package/src/cdp-log.ts +4 -1
- package/src/cdp-relay.ts +1059 -737
- package/src/cdp-session.ts +12 -3
- package/src/cdp-types.ts +51 -51
- package/src/clean-html.ts +4 -5
- package/src/cli.ts +82 -55
- package/src/create-logger.ts +5 -3
- package/src/debugger-examples-types.ts +4 -1
- package/src/debugger.ts +1 -5
- package/src/diff-utils.ts +2 -5
- package/src/editor-examples.ts +11 -1
- package/src/editor.ts +10 -2
- package/src/executor.ts +374 -73
- package/src/executor.unit.test.ts +48 -1
- package/src/extension-connection.test.ts +612 -488
- package/src/ffmpeg.ts +769 -0
- package/src/ghost-browser.ts +4 -6
- package/src/ghost-cursor-client.ts +369 -0
- package/src/ghost-cursor.ts +110 -0
- package/src/htmlrewrite.test.ts +6 -2
- package/src/htmlrewrite.ts +348 -386
- package/src/kill-port.ts +1 -3
- package/src/locator-selector.test.ts +115 -0
- package/src/mcp-client.ts +1 -1
- package/src/mcp.ts +21 -15
- package/src/on-mouse-action.test.ts +196 -0
- package/src/page-markdown.ts +7 -7
- package/src/protocol.ts +73 -57
- package/src/recording-ghost-cursor.ts +113 -0
- package/src/recording-relay.ts +20 -12
- package/src/relay-client.ts +85 -18
- package/src/relay-core.test.ts +1117 -578
- package/src/relay-navigation.test.ts +648 -483
- package/src/relay-session.test.ts +984 -929
- package/src/relay-state.test.ts +570 -0
- package/src/relay-state.ts +497 -0
- package/src/resource.md +21 -49
- package/src/scoped-fs.ts +9 -3
- package/src/screen-recording.test.ts +111 -0
- package/src/screen-recording.ts +256 -31
- package/src/skill.md +476 -396
- package/src/snapshot-tools.test.ts +580 -528
- package/src/snapshots/shadcn-ui-accessibility-full.md +8 -8
- package/src/snapshots/shadcn-ui-accessibility-interactive.md +8 -8
- package/src/start-relay-server.ts +14 -11
- package/src/styles-examples.ts +8 -1
- package/src/styles.ts +20 -21
- package/src/test-declarations.ts +6 -6
- package/src/test-utils.ts +104 -91
- package/src/utils.ts +2 -1
- 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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
}
|