@flrande/browserctl 0.5.0 → 0.6.0

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 (136) hide show
  1. package/dist/client.d.ts +34 -0
  2. package/dist/client.js +138 -0
  3. package/dist/commandRegistry.d.ts +16 -0
  4. package/dist/commandRegistry.js +21 -0
  5. package/dist/help.d.ts +4 -0
  6. package/dist/help.js +24 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.js +23 -0
  9. package/dist/runCli.d.ts +5 -0
  10. package/dist/runCli.js +170 -0
  11. package/package.json +32 -57
  12. package/INSTALL-CN.md +0 -92
  13. package/INSTALL.md +0 -92
  14. package/LICENSE +0 -21
  15. package/README-CN.md +0 -69
  16. package/README.md +0 -69
  17. package/apps/browserctl/src/commands/a11y-snapshot.ts +0 -20
  18. package/apps/browserctl/src/commands/act.test.ts +0 -71
  19. package/apps/browserctl/src/commands/act.ts +0 -64
  20. package/apps/browserctl/src/commands/command-wrappers.test.ts +0 -688
  21. package/apps/browserctl/src/commands/common.test.ts +0 -87
  22. package/apps/browserctl/src/commands/common.ts +0 -191
  23. package/apps/browserctl/src/commands/console-list.test.ts +0 -102
  24. package/apps/browserctl/src/commands/console-list.ts +0 -108
  25. package/apps/browserctl/src/commands/cookie-clear.ts +0 -18
  26. package/apps/browserctl/src/commands/cookie-get.ts +0 -18
  27. package/apps/browserctl/src/commands/cookie-set.ts +0 -22
  28. package/apps/browserctl/src/commands/dialog-arm.ts +0 -20
  29. package/apps/browserctl/src/commands/dom-query-all.ts +0 -18
  30. package/apps/browserctl/src/commands/dom-query.ts +0 -18
  31. package/apps/browserctl/src/commands/download-trigger.ts +0 -22
  32. package/apps/browserctl/src/commands/download-wait.test.ts +0 -67
  33. package/apps/browserctl/src/commands/download-wait.ts +0 -27
  34. package/apps/browserctl/src/commands/element-screenshot.ts +0 -20
  35. package/apps/browserctl/src/commands/frame-list.ts +0 -16
  36. package/apps/browserctl/src/commands/frame-snapshot.ts +0 -18
  37. package/apps/browserctl/src/commands/har-export.test.ts +0 -112
  38. package/apps/browserctl/src/commands/har-export.ts +0 -120
  39. package/apps/browserctl/src/commands/memory-delete.ts +0 -20
  40. package/apps/browserctl/src/commands/memory-inspect.ts +0 -20
  41. package/apps/browserctl/src/commands/memory-list.ts +0 -90
  42. package/apps/browserctl/src/commands/memory-mode-set.ts +0 -29
  43. package/apps/browserctl/src/commands/memory-purge.ts +0 -16
  44. package/apps/browserctl/src/commands/memory-resolve.ts +0 -56
  45. package/apps/browserctl/src/commands/memory-status.ts +0 -16
  46. package/apps/browserctl/src/commands/memory-ttl-set.ts +0 -28
  47. package/apps/browserctl/src/commands/memory-upsert.ts +0 -142
  48. package/apps/browserctl/src/commands/network-list.test.ts +0 -110
  49. package/apps/browserctl/src/commands/network-list.ts +0 -112
  50. package/apps/browserctl/src/commands/network-wait-for.test.ts +0 -90
  51. package/apps/browserctl/src/commands/network-wait-for.ts +0 -100
  52. package/apps/browserctl/src/commands/profile-list.ts +0 -16
  53. package/apps/browserctl/src/commands/profile-use.ts +0 -18
  54. package/apps/browserctl/src/commands/response-body.ts +0 -24
  55. package/apps/browserctl/src/commands/screenshot.ts +0 -16
  56. package/apps/browserctl/src/commands/session-drop.test.ts +0 -36
  57. package/apps/browserctl/src/commands/session-drop.ts +0 -16
  58. package/apps/browserctl/src/commands/session-list.test.ts +0 -81
  59. package/apps/browserctl/src/commands/session-list.ts +0 -70
  60. package/apps/browserctl/src/commands/snapshot.ts +0 -16
  61. package/apps/browserctl/src/commands/status.ts +0 -10
  62. package/apps/browserctl/src/commands/storage-get.ts +0 -20
  63. package/apps/browserctl/src/commands/storage-set.ts +0 -22
  64. package/apps/browserctl/src/commands/tab-close.ts +0 -20
  65. package/apps/browserctl/src/commands/tab-focus.ts +0 -20
  66. package/apps/browserctl/src/commands/tab-open.ts +0 -19
  67. package/apps/browserctl/src/commands/tabs.ts +0 -13
  68. package/apps/browserctl/src/commands/trace-get.test.ts +0 -61
  69. package/apps/browserctl/src/commands/trace-get.ts +0 -62
  70. package/apps/browserctl/src/commands/upload-arm.ts +0 -26
  71. package/apps/browserctl/src/commands/wait-element.test.ts +0 -80
  72. package/apps/browserctl/src/commands/wait-element.ts +0 -76
  73. package/apps/browserctl/src/commands/wait-text.test.ts +0 -110
  74. package/apps/browserctl/src/commands/wait-text.ts +0 -93
  75. package/apps/browserctl/src/commands/wait-url.test.ts +0 -80
  76. package/apps/browserctl/src/commands/wait-url.ts +0 -76
  77. package/apps/browserctl/src/daemon-client.test.ts +0 -253
  78. package/apps/browserctl/src/daemon-client.ts +0 -632
  79. package/apps/browserctl/src/e2e.test.ts +0 -103
  80. package/apps/browserctl/src/main.dispatch.test.ts +0 -461
  81. package/apps/browserctl/src/main.test.ts +0 -334
  82. package/apps/browserctl/src/main.ts +0 -957
  83. package/apps/browserctl/src/smoke.e2e.test.ts +0 -97
  84. package/apps/browserctl/src/test-port.ts +0 -26
  85. package/apps/browserd/src/bootstrap.ts +0 -432
  86. package/apps/browserd/src/chrome-relay-extension-bridge.test.ts +0 -250
  87. package/apps/browserd/src/chrome-relay-extension-bridge.ts +0 -506
  88. package/apps/browserd/src/container.ts +0 -3088
  89. package/apps/browserd/src/main.test.ts +0 -1436
  90. package/apps/browserd/src/main.ts +0 -7
  91. package/apps/browserd/src/test-port.ts +0 -26
  92. package/apps/browserd/src/tool-matrix.test.ts +0 -887
  93. package/bin/browserctl.cjs +0 -21
  94. package/bin/browserd.cjs +0 -21
  95. package/extensions/chrome-relay/README-CN.md +0 -39
  96. package/extensions/chrome-relay/README.md +0 -39
  97. package/extensions/chrome-relay/background.js +0 -1687
  98. package/extensions/chrome-relay/manifest.json +0 -15
  99. package/extensions/chrome-relay/popup.html +0 -369
  100. package/extensions/chrome-relay/popup.js +0 -972
  101. package/packages/core/src/bootstrap.test.ts +0 -10
  102. package/packages/core/src/driver-registry.test.ts +0 -45
  103. package/packages/core/src/driver-registry.ts +0 -22
  104. package/packages/core/src/driver.ts +0 -47
  105. package/packages/core/src/index.ts +0 -6
  106. package/packages/core/src/navigation-memory.test.ts +0 -259
  107. package/packages/core/src/navigation-memory.ts +0 -360
  108. package/packages/core/src/ref-cache.test.ts +0 -61
  109. package/packages/core/src/ref-cache.ts +0 -28
  110. package/packages/core/src/session-store.test.ts +0 -82
  111. package/packages/core/src/session-store.ts +0 -138
  112. package/packages/core/src/types.ts +0 -9
  113. package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +0 -744
  114. package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +0 -2429
  115. package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.test.ts +0 -264
  116. package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.ts +0 -521
  117. package/packages/driver-chrome-relay/src/index.ts +0 -26
  118. package/packages/driver-managed/src/index.ts +0 -22
  119. package/packages/driver-managed/src/managed-driver.test.ts +0 -183
  120. package/packages/driver-managed/src/managed-driver.ts +0 -341
  121. package/packages/driver-managed/src/managed-local-driver.test.ts +0 -608
  122. package/packages/driver-managed/src/managed-local-driver.ts +0 -2243
  123. package/packages/driver-remote-cdp/src/index.ts +0 -19
  124. package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +0 -727
  125. package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +0 -2264
  126. package/packages/protocol/src/envelope.test.ts +0 -25
  127. package/packages/protocol/src/envelope.ts +0 -31
  128. package/packages/protocol/src/errors.test.ts +0 -17
  129. package/packages/protocol/src/errors.ts +0 -11
  130. package/packages/protocol/src/index.ts +0 -3
  131. package/packages/protocol/src/tools.ts +0 -3
  132. package/packages/transport-mcp-stdio/src/index.ts +0 -3
  133. package/packages/transport-mcp-stdio/src/sdk-server.ts +0 -139
  134. package/packages/transport-mcp-stdio/src/server.test.ts +0 -281
  135. package/packages/transport-mcp-stdio/src/server.ts +0 -183
  136. package/packages/transport-mcp-stdio/src/tool-map.ts +0 -84
@@ -1,2264 +0,0 @@
1
- import type {
2
- BrowserDriver,
3
- BrowserDriverScreenshot,
4
- DriverObject
5
- } from "../../core/src/driver";
6
- import type { ProfileId, TargetId } from "../../core/src/types";
7
-
8
- export type RemoteCdpDriverConfig = {
9
- cdpUrl: string;
10
- runtime?: RemoteCdpDriverRuntime;
11
- };
12
-
13
- export type RemoteCdpEndpoint = {
14
- url: string;
15
- protocol: string;
16
- host: string;
17
- };
18
-
19
- export type RemoteCdpDriverStatus = {
20
- kind: "remote-cdp";
21
- connected: boolean;
22
- endpoint: RemoteCdpEndpoint;
23
- };
24
-
25
- export type RemoteCdpConsoleEntryLocation = {
26
- url?: string;
27
- lineNumber?: number;
28
- columnNumber?: number;
29
- };
30
-
31
- export type RemoteCdpConsoleEntry = {
32
- type: string;
33
- text: string;
34
- timestamp: string;
35
- location?: RemoteCdpConsoleEntryLocation;
36
- };
37
-
38
- export type RemoteCdpNetworkRequestSummary = {
39
- requestId: string;
40
- url: string;
41
- method?: string;
42
- resourceType?: string;
43
- status?: number;
44
- timestamp: string;
45
- };
46
-
47
- export type RemoteCdpNetworkResponseBody = {
48
- body: string;
49
- encoding: "utf8" | "base64";
50
- };
51
-
52
- export type RemoteCdpSnapshot = {
53
- kind: "remote-cdp";
54
- profile: ProfileId;
55
- targetId: TargetId;
56
- endpoint: RemoteCdpEndpoint;
57
- hasTarget: boolean;
58
- url?: string;
59
- title?: string;
60
- html?: string;
61
- requestSummaries?: RemoteCdpNetworkRequestSummary[];
62
- };
63
-
64
- export type RemoteCdpScreenshot = {
65
- kind: "remote-cdp";
66
- profile: ProfileId;
67
- targetId: TargetId;
68
- endpoint: RemoteCdpEndpoint;
69
- hasTarget: boolean;
70
- mimeType?: string;
71
- encoding?: "base64";
72
- imageBase64?: string;
73
- width?: number;
74
- height?: number;
75
- };
76
-
77
- export type RemoteCdpTelemetryDriverExtensions = {
78
- getConsoleEntries?(targetId: TargetId, profile?: ProfileId): RemoteCdpConsoleEntry[];
79
- getNetworkResponseBody?(
80
- requestId: string,
81
- targetId: TargetId,
82
- profile?: ProfileId
83
- ): RemoteCdpNetworkResponseBody | undefined;
84
- };
85
-
86
- export type RemoteCdpLocator = {
87
- click(): Promise<void>;
88
- fill(value: string): Promise<void>;
89
- type(value: string): Promise<void>;
90
- };
91
-
92
- export type RemoteCdpKeyboard = {
93
- press(key: string): Promise<void>;
94
- };
95
-
96
- export type RemoteCdpPage = {
97
- goto(url: string): Promise<void>;
98
- bringToFront(): Promise<void>;
99
- close(): Promise<void>;
100
- url(): string;
101
- title(): Promise<string>;
102
- content(): Promise<string>;
103
- screenshot?(options?: Record<string, unknown>): Promise<unknown>;
104
- locator(selector: string): RemoteCdpLocator;
105
- route?(url: string, handler: RemoteCdpNetworkMockHandler): Promise<void>;
106
- unroute?(url: string, handler?: RemoteCdpNetworkMockHandler): Promise<void>;
107
- keyboard?: RemoteCdpKeyboard;
108
- on?(eventName: string, listener: (payload: unknown) => unknown): void;
109
- };
110
-
111
- export type RemoteCdpBrowserContext = {
112
- newPage(): Promise<RemoteCdpPage>;
113
- close?(): Promise<void>;
114
- };
115
-
116
- export type RemoteCdpBrowser = {
117
- contexts(): RemoteCdpBrowserContext[];
118
- newContext?(): Promise<RemoteCdpBrowserContext>;
119
- close(): Promise<void>;
120
- };
121
-
122
- export type RemoteCdpDriverRuntime = {
123
- connectOverCDP(endpointUrl: string): Promise<RemoteCdpBrowser>;
124
- };
125
-
126
- const DEFAULT_PROFILE_ID: ProfileId = "profile:remote-cdp:default";
127
- const TELEMETRY_EVENT_LIMIT = 200;
128
-
129
- type RemoteCdpTab = {
130
- url: string;
131
- page: RemoteCdpPage;
132
- };
133
-
134
- type RemoteCdpProfileState = {
135
- nextTargetNumber: number;
136
- tabs: Map<TargetId, RemoteCdpTab>;
137
- tabOrder: TargetId[];
138
- focusedTargetId?: TargetId;
139
- };
140
-
141
- type RemoteCdpTargetOperationState = {
142
- uploadFiles: string[];
143
- dialogArmedCount: number;
144
- triggerCount: number;
145
- nextNetworkMockNumber: number;
146
- networkMocks: Map<string, RemoteCdpNetworkMockBinding>;
147
- requestedDownloadPath?: string;
148
- downloadInFlight?: Promise<RemoteCdpDownloadArtifact>;
149
- latestDownload?: RemoteCdpDownloadArtifact;
150
- latestRawDownload?: unknown;
151
- };
152
-
153
- type RemoteCdpDownloadArtifact = {
154
- path: string;
155
- suggestedFilename?: string;
156
- url?: string;
157
- mimeType?: string;
158
- };
159
-
160
- type RemoteCdpNetworkMockHandler = (...args: unknown[]) => Promise<void>;
161
- type RemoteCdpNetworkMockBinding = {
162
- urlPattern: string;
163
- handler: RemoteCdpNetworkMockHandler;
164
- };
165
-
166
- type RemoteCdpTargetTelemetryState = {
167
- consoleEntries: RemoteCdpConsoleEntry[];
168
- requestSummaries: RemoteCdpNetworkRequestSummary[];
169
- networkResponseBodies: Map<string, RemoteCdpNetworkResponseBody>;
170
- nextRequestNumber: number;
171
- };
172
-
173
- function resolveProfileId(profile?: ProfileId): ProfileId {
174
- return profile ?? DEFAULT_PROFILE_ID;
175
- }
176
-
177
- function parseEndpoint(cdpUrl: string): RemoteCdpEndpoint {
178
- const trimmedCdpUrl = cdpUrl.trim();
179
- if (trimmedCdpUrl.length === 0) {
180
- throw new Error("Invalid cdpUrl: value must not be empty.");
181
- }
182
-
183
- try {
184
- const endpoint = new URL(trimmedCdpUrl);
185
- return {
186
- url: endpoint.toString(),
187
- protocol: endpoint.protocol,
188
- host: endpoint.host
189
- };
190
- } catch {
191
- throw new Error(`Invalid cdpUrl: ${trimmedCdpUrl}`);
192
- }
193
- }
194
-
195
- function createTargetId(profileId: ProfileId, targetNumber: number): TargetId {
196
- return `target:remote-cdp:${profileId}:${targetNumber}`;
197
- }
198
-
199
- function createProfileState(): RemoteCdpProfileState {
200
- return {
201
- nextTargetNumber: 1,
202
- tabs: new Map<TargetId, RemoteCdpTab>(),
203
- tabOrder: []
204
- };
205
- }
206
-
207
- function createTargetOperationState(): RemoteCdpTargetOperationState {
208
- return {
209
- uploadFiles: [],
210
- dialogArmedCount: 0,
211
- triggerCount: 0,
212
- nextNetworkMockNumber: 1,
213
- networkMocks: new Map<string, RemoteCdpNetworkMockBinding>()
214
- };
215
- }
216
-
217
- function createTargetTelemetryState(): RemoteCdpTargetTelemetryState {
218
- return {
219
- consoleEntries: [],
220
- requestSummaries: [],
221
- networkResponseBodies: new Map<string, RemoteCdpNetworkResponseBody>(),
222
- nextRequestNumber: 1
223
- };
224
- }
225
-
226
- function trimConsoleEntries(telemetryState: RemoteCdpTargetTelemetryState): void {
227
- if (telemetryState.consoleEntries.length <= TELEMETRY_EVENT_LIMIT) {
228
- return;
229
- }
230
-
231
- telemetryState.consoleEntries.splice(
232
- 0,
233
- telemetryState.consoleEntries.length - TELEMETRY_EVENT_LIMIT
234
- );
235
- }
236
-
237
- function trimNetworkTelemetry(telemetryState: RemoteCdpTargetTelemetryState): void {
238
- if (telemetryState.requestSummaries.length > TELEMETRY_EVENT_LIMIT) {
239
- const removedSummaries = telemetryState.requestSummaries.splice(
240
- 0,
241
- telemetryState.requestSummaries.length - TELEMETRY_EVENT_LIMIT
242
- );
243
- for (const removed of removedSummaries) {
244
- telemetryState.networkResponseBodies.delete(removed.requestId);
245
- }
246
- }
247
-
248
- const knownRequestIds = new Set(
249
- telemetryState.requestSummaries.map((summary) => summary.requestId)
250
- );
251
- for (const requestId of telemetryState.networkResponseBodies.keys()) {
252
- if (!knownRequestIds.has(requestId)) {
253
- telemetryState.networkResponseBodies.delete(requestId);
254
- }
255
- }
256
- }
257
-
258
- function createDownloadPath(profileId: ProfileId, targetId: TargetId, triggerCount: number): string {
259
- return `remote-cdp-${encodeURIComponent(profileId)}-${encodeURIComponent(targetId)}-${triggerCount}.bin`;
260
- }
261
-
262
- function createRequestId(profileId: ProfileId, targetId: TargetId, requestNumber: number): string {
263
- return `request:remote-cdp:${encodeURIComponent(profileId)}:${encodeURIComponent(targetId)}:${requestNumber}`;
264
- }
265
-
266
- function toErrorMessage(error: unknown): string {
267
- return error instanceof Error ? error.message : "Unexpected remote-cdp driver failure.";
268
- }
269
-
270
- function readActionPayloadString(payload: DriverObject | undefined, key: string): string | undefined {
271
- const value = payload?.[key];
272
- return typeof value === "string" && value.trim().length > 0 ? value : undefined;
273
- }
274
-
275
- function readActionPayloadNumber(payload: DriverObject | undefined, key: string): number | undefined {
276
- const value = payload?.[key];
277
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
278
- }
279
-
280
- function isObjectRecord(value: unknown): value is Record<string, unknown> {
281
- return typeof value === "object" && value !== null && !Array.isArray(value);
282
- }
283
-
284
- function readObjectMethod<TValue>(value: unknown, methodName: string): TValue | undefined {
285
- if (typeof value !== "object" || value === null) {
286
- return undefined;
287
- }
288
-
289
- const method = (value as Record<string, unknown>)[methodName];
290
- if (typeof method !== "function") {
291
- return undefined;
292
- }
293
-
294
- try {
295
- return (method as () => TValue).call(value);
296
- } catch {
297
- return undefined;
298
- }
299
- }
300
-
301
- function getObjectMethod(
302
- value: unknown,
303
- methodName: string
304
- ): ((...args: unknown[]) => unknown) | undefined {
305
- if (typeof value !== "object" || value === null) {
306
- return undefined;
307
- }
308
-
309
- const method = (value as Record<string, unknown>)[methodName];
310
- return typeof method === "function" ? (method as (...args: unknown[]) => unknown) : undefined;
311
- }
312
-
313
- function readObjectStringProperty(value: unknown, key: string): string | undefined {
314
- if (typeof value !== "object" || value === null) {
315
- return undefined;
316
- }
317
-
318
- const property = (value as Record<string, unknown>)[key];
319
- return typeof property === "string" ? property : undefined;
320
- }
321
-
322
- function readObjectNumberProperty(value: unknown, key: string): number | undefined {
323
- if (typeof value !== "object" || value === null) {
324
- return undefined;
325
- }
326
-
327
- const property = (value as Record<string, unknown>)[key];
328
- return typeof property === "number" ? property : undefined;
329
- }
330
-
331
- function readConsoleEntry(value: unknown): RemoteCdpConsoleEntry {
332
- const locationValue = readObjectMethod<unknown>(value, "location");
333
- const locationUrl = readObjectStringProperty(locationValue, "url");
334
- const locationLineNumber = readObjectNumberProperty(locationValue, "lineNumber");
335
- const locationColumnNumber = readObjectNumberProperty(locationValue, "columnNumber");
336
-
337
- const hasLocation =
338
- locationUrl !== undefined || locationLineNumber !== undefined || locationColumnNumber !== undefined;
339
-
340
- return {
341
- type: readObjectMethod<string>(value, "type") ?? "log",
342
- text: readObjectMethod<string>(value, "text") ?? "",
343
- timestamp: new Date().toISOString(),
344
- ...(hasLocation
345
- ? {
346
- location: {
347
- ...(locationUrl !== undefined ? { url: locationUrl } : {}),
348
- ...(locationLineNumber !== undefined ? { lineNumber: locationLineNumber } : {}),
349
- ...(locationColumnNumber !== undefined ? { columnNumber: locationColumnNumber } : {})
350
- }
351
- }
352
- : {})
353
- };
354
- }
355
-
356
- function readNetworkSummary(value: unknown, requestId: string): RemoteCdpNetworkRequestSummary {
357
- const requestValue = readObjectMethod<unknown>(value, "request");
358
- const method = readObjectMethod<string>(requestValue, "method");
359
- const resourceType = readObjectMethod<string>(requestValue, "resourceType");
360
- const status = readObjectMethod<number>(value, "status");
361
-
362
- return {
363
- requestId,
364
- url: readObjectMethod<string>(value, "url") ?? "",
365
- ...(typeof method === "string" ? { method } : {}),
366
- ...(typeof resourceType === "string" ? { resourceType } : {}),
367
- ...(typeof status === "number" ? { status } : {}),
368
- timestamp: new Date().toISOString()
369
- };
370
- }
371
-
372
- function toBase64(value: ArrayBuffer | Uint8Array): string {
373
- const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
374
- return Buffer.from(bytes).toString("base64");
375
- }
376
-
377
- function parseScreenshotBase64(
378
- value: string
379
- ): { mimeType: string; imageBase64: string } | undefined {
380
- const trimmedValue = value.trim();
381
- if (trimmedValue.length === 0) {
382
- return undefined;
383
- }
384
-
385
- const dataUrlMatch = /^data:([^;,]+);base64,(.+)$/i.exec(trimmedValue);
386
- if (dataUrlMatch !== null) {
387
- const mimeType = dataUrlMatch[1] ?? "image/png";
388
- const imageBase64 = dataUrlMatch[2] ?? "";
389
- if (imageBase64.length === 0) {
390
- return undefined;
391
- }
392
-
393
- return {
394
- mimeType,
395
- imageBase64
396
- };
397
- }
398
-
399
- return {
400
- mimeType: "image/png",
401
- imageBase64: trimmedValue
402
- };
403
- }
404
-
405
- type RemoteCdpScreenshotImage = {
406
- mimeType: string;
407
- encoding: "base64";
408
- imageBase64: string;
409
- width?: number;
410
- height?: number;
411
- };
412
-
413
- function toOptionalPositiveNumber(value: unknown): number | undefined {
414
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
415
- }
416
-
417
- function readScreenshotImage(value: unknown): RemoteCdpScreenshotImage | undefined {
418
- if (value instanceof Uint8Array || value instanceof ArrayBuffer) {
419
- return {
420
- mimeType: "image/png",
421
- encoding: "base64",
422
- imageBase64: toBase64(value)
423
- };
424
- }
425
-
426
- if (typeof value === "string") {
427
- const parsed = parseScreenshotBase64(value);
428
- if (parsed === undefined) {
429
- return undefined;
430
- }
431
-
432
- return {
433
- mimeType: parsed.mimeType,
434
- encoding: "base64",
435
- imageBase64: parsed.imageBase64
436
- };
437
- }
438
-
439
- if (typeof value !== "object" || value === null) {
440
- return undefined;
441
- }
442
-
443
- const imageBase64 = readObjectStringProperty(value, "imageBase64");
444
- if (imageBase64 === undefined || imageBase64.trim().length === 0) {
445
- return undefined;
446
- }
447
-
448
- const rawEncoding = readObjectStringProperty(value, "encoding");
449
- if (rawEncoding !== undefined && rawEncoding !== "base64") {
450
- return undefined;
451
- }
452
-
453
- const mimeType = readObjectStringProperty(value, "mimeType") ?? "image/png";
454
- const width = toOptionalPositiveNumber(readObjectNumberProperty(value, "width"));
455
- const height = toOptionalPositiveNumber(readObjectNumberProperty(value, "height"));
456
-
457
- return {
458
- mimeType,
459
- encoding: "base64",
460
- imageBase64: imageBase64.trim(),
461
- ...(width !== undefined ? { width } : {}),
462
- ...(height !== undefined ? { height } : {})
463
- };
464
- }
465
-
466
- async function capturePageScreenshot(page: RemoteCdpPage): Promise<RemoteCdpScreenshotImage> {
467
- const screenshotMethod = getObjectMethod(page, "screenshot");
468
- if (typeof screenshotMethod !== "function") {
469
- throw new Error("Screenshot is not supported by this driver runtime.");
470
- }
471
-
472
- const screenshot = await screenshotMethod.call(page, {
473
- type: "png"
474
- });
475
- const parsedScreenshot = readScreenshotImage(screenshot);
476
- if (parsedScreenshot === undefined) {
477
- throw new Error("Unexpected screenshot payload from driver runtime.");
478
- }
479
-
480
- return parsedScreenshot;
481
- }
482
-
483
- type RemoteCdpCookie = {
484
- name: string;
485
- value: string;
486
- domain?: string;
487
- path?: string;
488
- expires?: number;
489
- httpOnly?: boolean;
490
- secure?: boolean;
491
- sameSite?: string;
492
- url?: string;
493
- };
494
-
495
- function normalizeCookies(value: unknown): RemoteCdpCookie[] {
496
- if (!Array.isArray(value)) {
497
- return [];
498
- }
499
-
500
- const cookies: RemoteCdpCookie[] = [];
501
- for (const item of value) {
502
- if (!isObjectRecord(item)) {
503
- continue;
504
- }
505
-
506
- const name = readObjectStringProperty(item, "name");
507
- const cookieValue = readObjectStringProperty(item, "value");
508
- if (name === undefined || cookieValue === undefined) {
509
- continue;
510
- }
511
-
512
- const domain = readObjectStringProperty(item, "domain");
513
- const path = readObjectStringProperty(item, "path");
514
- const sameSite = readObjectStringProperty(item, "sameSite");
515
- const url = readObjectStringProperty(item, "url");
516
- const expiresRaw = readObjectNumberProperty(item, "expires");
517
- const httpOnlyRaw = item.httpOnly;
518
- const secureRaw = item.secure;
519
-
520
- cookies.push({
521
- name,
522
- value: cookieValue,
523
- ...(domain !== undefined ? { domain } : {}),
524
- ...(path !== undefined ? { path } : {}),
525
- ...(typeof expiresRaw === "number" && Number.isFinite(expiresRaw) ? { expires: expiresRaw } : {}),
526
- ...(typeof httpOnlyRaw === "boolean" ? { httpOnly: httpOnlyRaw } : {}),
527
- ...(typeof secureRaw === "boolean" ? { secure: secureRaw } : {}),
528
- ...(sameSite !== undefined ? { sameSite } : {}),
529
- ...(url !== undefined ? { url } : {})
530
- });
531
- }
532
-
533
- return cookies;
534
- }
535
-
536
- function toCookieInput(cookie: RemoteCdpCookie): Record<string, unknown> | undefined {
537
- const base: Record<string, unknown> = {
538
- name: cookie.name,
539
- value: cookie.value
540
- };
541
-
542
- if (cookie.url !== undefined) {
543
- return {
544
- ...base,
545
- url: cookie.url,
546
- ...(cookie.expires !== undefined ? { expires: cookie.expires } : {}),
547
- ...(cookie.httpOnly !== undefined ? { httpOnly: cookie.httpOnly } : {}),
548
- ...(cookie.secure !== undefined ? { secure: cookie.secure } : {}),
549
- ...(cookie.sameSite !== undefined ? { sameSite: cookie.sameSite } : {})
550
- };
551
- }
552
-
553
- if (cookie.domain !== undefined && cookie.path !== undefined) {
554
- return {
555
- ...base,
556
- domain: cookie.domain,
557
- path: cookie.path,
558
- ...(cookie.expires !== undefined ? { expires: cookie.expires } : {}),
559
- ...(cookie.httpOnly !== undefined ? { httpOnly: cookie.httpOnly } : {}),
560
- ...(cookie.secure !== undefined ? { secure: cookie.secure } : {}),
561
- ...(cookie.sameSite !== undefined ? { sameSite: cookie.sameSite } : {})
562
- };
563
- }
564
-
565
- return undefined;
566
- }
567
-
568
- function parseStorageScope(scope: string | undefined): "local" | "session" | undefined {
569
- if (scope === "local" || scope === "session") {
570
- return scope;
571
- }
572
-
573
- return undefined;
574
- }
575
-
576
- function parseFrameIndex(frameId: string | undefined): number | undefined {
577
- if (typeof frameId !== "string") {
578
- return undefined;
579
- }
580
-
581
- const match = /^frame:(\d+)$/.exec(frameId.trim());
582
- if (match === null) {
583
- return undefined;
584
- }
585
-
586
- const parsed = Number.parseInt(match[1] ?? "", 10);
587
- return Number.isFinite(parsed) ? parsed : undefined;
588
- }
589
-
590
- async function evaluateInPage<TResult>(
591
- page: RemoteCdpPage,
592
- callback: (arg: unknown) => unknown,
593
- arg: unknown
594
- ): Promise<TResult> {
595
- const evaluateMethod = getObjectMethod(page, "evaluate");
596
- if (typeof evaluateMethod !== "function") {
597
- throw new Error("Driver runtime page does not support evaluate.");
598
- }
599
-
600
- const result = await evaluateMethod.call(page, callback, arg);
601
- return result as TResult;
602
- }
603
-
604
- async function readNetworkResponseBody(
605
- value: unknown
606
- ): Promise<RemoteCdpNetworkResponseBody | undefined> {
607
- const textMethod = getObjectMethod(value, "text");
608
- if (typeof textMethod === "function") {
609
- try {
610
- const textResult = await textMethod.call(value);
611
- if (typeof textResult === "string") {
612
- return {
613
- body: textResult,
614
- encoding: "utf8"
615
- };
616
- }
617
- } catch {
618
- // Ignore body extraction failures from optional runtime hooks.
619
- }
620
- }
621
-
622
- const bodyMethod = getObjectMethod(value, "body");
623
- if (typeof bodyMethod !== "function") {
624
- return undefined;
625
- }
626
-
627
- try {
628
- const bodyResult = await bodyMethod.call(value);
629
- if (typeof bodyResult === "string") {
630
- return {
631
- body: bodyResult,
632
- encoding: "utf8"
633
- };
634
- }
635
-
636
- if (bodyResult instanceof Uint8Array || bodyResult instanceof ArrayBuffer) {
637
- return {
638
- body: toBase64(bodyResult),
639
- encoding: "base64"
640
- };
641
- }
642
-
643
- return undefined;
644
- } catch {
645
- return undefined;
646
- }
647
- }
648
-
649
- function attachTelemetryListeners(
650
- page: RemoteCdpPage,
651
- profileId: ProfileId,
652
- targetId: TargetId,
653
- telemetryState: RemoteCdpTargetTelemetryState,
654
- operationState: RemoteCdpTargetOperationState
655
- ): void {
656
- if (typeof page.on !== "function") {
657
- return;
658
- }
659
-
660
- page.on("console", (message) => {
661
- telemetryState.consoleEntries.push(readConsoleEntry(message));
662
- trimConsoleEntries(telemetryState);
663
- });
664
-
665
- page.on("response", async (response) => {
666
- const requestId = createRequestId(profileId, targetId, telemetryState.nextRequestNumber);
667
- telemetryState.nextRequestNumber += 1;
668
- telemetryState.requestSummaries.push(readNetworkSummary(response, requestId));
669
- trimNetworkTelemetry(telemetryState);
670
-
671
- const body = await readNetworkResponseBody(response);
672
- if (body !== undefined) {
673
- telemetryState.networkResponseBodies.set(requestId, body);
674
- trimNetworkTelemetry(telemetryState);
675
- }
676
- });
677
-
678
- page.on("dialog", async (dialog) => {
679
- if (operationState.dialogArmedCount <= 0) {
680
- return;
681
- }
682
-
683
- operationState.dialogArmedCount -= 1;
684
- const acceptMethod = getObjectMethod(dialog, "accept");
685
- if (typeof acceptMethod === "function") {
686
- try {
687
- await acceptMethod.call(dialog);
688
- return;
689
- } catch {
690
- // Fall through to dismiss.
691
- }
692
- }
693
-
694
- const dismissMethod = getObjectMethod(dialog, "dismiss");
695
- if (typeof dismissMethod === "function") {
696
- try {
697
- await dismissMethod.call(dialog);
698
- } catch {
699
- // Ignore dialog dismissal failures.
700
- }
701
- }
702
- });
703
- }
704
-
705
- async function readDownloadArtifact(
706
- value: unknown,
707
- fallbackPath: string
708
- ): Promise<RemoteCdpDownloadArtifact> {
709
- let resolvedPath = fallbackPath;
710
- const pathMethod = getObjectMethod(value, "path");
711
- if (typeof pathMethod === "function") {
712
- try {
713
- const pathResult = await pathMethod.call(value);
714
- if (typeof pathResult === "string" && pathResult.trim().length > 0) {
715
- resolvedPath = pathResult;
716
- }
717
- } catch {
718
- // Ignore path() failures and keep fallback path.
719
- }
720
- }
721
-
722
- const saveAsMethod = getObjectMethod(value, "saveAs");
723
- if (typeof saveAsMethod === "function") {
724
- try {
725
- await saveAsMethod.call(value, fallbackPath);
726
- resolvedPath = fallbackPath;
727
- } catch {
728
- // Ignore saveAs failures and keep best-effort path.
729
- }
730
- }
731
-
732
- const suggestedFilename = readObjectMethod<string>(value, "suggestedFilename");
733
- const url = readObjectMethod<string>(value, "url");
734
- const mimeType = readObjectMethod<string>(value, "mimeType");
735
-
736
- return {
737
- path: resolvedPath,
738
- ...(typeof suggestedFilename === "string" ? { suggestedFilename } : {}),
739
- ...(typeof url === "string" ? { url } : {}),
740
- ...(typeof mimeType === "string" ? { mimeType } : {})
741
- };
742
- }
743
-
744
- function createPlaywrightRuntime(): RemoteCdpDriverRuntime {
745
- return {
746
- connectOverCDP: async (endpointUrl) => {
747
- const playwright = await import("playwright-core");
748
- return (await playwright.chromium.connectOverCDP(endpointUrl)) as unknown as RemoteCdpBrowser;
749
- }
750
- };
751
- }
752
-
753
- function cloneConsoleEntry(entry: RemoteCdpConsoleEntry): RemoteCdpConsoleEntry {
754
- return {
755
- ...entry,
756
- ...(entry.location !== undefined ? { location: { ...entry.location } } : {})
757
- };
758
- }
759
-
760
- function cloneRequestSummary(summary: RemoteCdpNetworkRequestSummary): RemoteCdpNetworkRequestSummary {
761
- return { ...summary };
762
- }
763
-
764
- export function createRemoteCdpDriver(
765
- config: RemoteCdpDriverConfig
766
- ): BrowserDriver<RemoteCdpDriverStatus, RemoteCdpSnapshot> &
767
- RemoteCdpTelemetryDriverExtensions &
768
- BrowserDriverScreenshot<RemoteCdpScreenshot> {
769
- const endpoint = parseEndpoint(config.cdpUrl);
770
- const runtime = config.runtime ?? createPlaywrightRuntime();
771
- const profileStates = new Map<ProfileId, RemoteCdpProfileState>();
772
- const targetOperationStates = new Map<ProfileId, Map<TargetId, RemoteCdpTargetOperationState>>();
773
- const targetTelemetryStates = new Map<ProfileId, Map<TargetId, RemoteCdpTargetTelemetryState>>();
774
- let browser: RemoteCdpBrowser | undefined;
775
- let browserConnectInFlight: Promise<RemoteCdpBrowser> | undefined;
776
- let browserContext: RemoteCdpBrowserContext | undefined;
777
- let browserContextInFlight: Promise<RemoteCdpBrowserContext> | undefined;
778
-
779
- async function ensureBrowser(): Promise<RemoteCdpBrowser> {
780
- if (browser !== undefined) {
781
- return browser;
782
- }
783
-
784
- if (browserConnectInFlight === undefined) {
785
- browserConnectInFlight = runtime.connectOverCDP(endpoint.url)
786
- .then((createdBrowser) => {
787
- browser = createdBrowser;
788
- return createdBrowser;
789
- })
790
- .finally(() => {
791
- browserConnectInFlight = undefined;
792
- });
793
- }
794
-
795
- return browserConnectInFlight;
796
- }
797
-
798
- async function ensureBrowserContext(): Promise<RemoteCdpBrowserContext> {
799
- if (browserContext !== undefined) {
800
- return browserContext;
801
- }
802
-
803
- if (browserContextInFlight === undefined) {
804
- browserContextInFlight = (async () => {
805
- const connectedBrowser = await ensureBrowser();
806
- const existingContext = connectedBrowser.contexts()[0];
807
- if (existingContext !== undefined) {
808
- browserContext = existingContext;
809
- return existingContext;
810
- }
811
-
812
- if (typeof connectedBrowser.newContext !== "function") {
813
- throw new Error("No browser context available from remote CDP connection.");
814
- }
815
-
816
- const createdContext = await connectedBrowser.newContext();
817
- browserContext = createdContext;
818
- return createdContext;
819
- })().finally(() => {
820
- browserContextInFlight = undefined;
821
- });
822
- }
823
-
824
- return browserContextInFlight;
825
- }
826
-
827
- function getOrCreateProfileState(profileId: ProfileId): RemoteCdpProfileState {
828
- const existingState = profileStates.get(profileId);
829
- if (existingState !== undefined) {
830
- return existingState;
831
- }
832
-
833
- const createdState = createProfileState();
834
- profileStates.set(profileId, createdState);
835
- return createdState;
836
- }
837
-
838
- function requireTargetInProfile(
839
- profileId: ProfileId,
840
- targetId: TargetId
841
- ): { profileState: RemoteCdpProfileState; tab: RemoteCdpTab } {
842
- const profileState = profileStates.get(profileId);
843
- const tab = profileState?.tabs.get(targetId);
844
- if (profileState === undefined || tab === undefined) {
845
- throw new Error(`Unknown targetId: ${targetId} (profile: ${profileId})`);
846
- }
847
-
848
- return { profileState, tab };
849
- }
850
-
851
- function getOrCreateTargetOperationState(
852
- profileId: ProfileId,
853
- targetId: TargetId,
854
- validateTarget = true
855
- ): RemoteCdpTargetOperationState {
856
- if (validateTarget) {
857
- requireTargetInProfile(profileId, targetId);
858
- }
859
-
860
- let profileOperationStates = targetOperationStates.get(profileId);
861
- if (profileOperationStates === undefined) {
862
- profileOperationStates = new Map<TargetId, RemoteCdpTargetOperationState>();
863
- targetOperationStates.set(profileId, profileOperationStates);
864
- }
865
-
866
- const existingState = profileOperationStates.get(targetId);
867
- if (existingState !== undefined) {
868
- return existingState;
869
- }
870
-
871
- const createdState = createTargetOperationState();
872
- profileOperationStates.set(targetId, createdState);
873
- return createdState;
874
- }
875
-
876
- function getOrCreateProfileTelemetryStates(
877
- profileId: ProfileId
878
- ): Map<TargetId, RemoteCdpTargetTelemetryState> {
879
- const existingStates = targetTelemetryStates.get(profileId);
880
- if (existingStates !== undefined) {
881
- return existingStates;
882
- }
883
-
884
- const createdStates = new Map<TargetId, RemoteCdpTargetTelemetryState>();
885
- targetTelemetryStates.set(profileId, createdStates);
886
- return createdStates;
887
- }
888
-
889
- function findTelemetryState(
890
- targetId: TargetId,
891
- profile?: ProfileId
892
- ): RemoteCdpTargetTelemetryState | undefined {
893
- if (profile !== undefined) {
894
- return targetTelemetryStates.get(resolveProfileId(profile))?.get(targetId);
895
- }
896
-
897
- for (const profileTelemetryStates of targetTelemetryStates.values()) {
898
- const telemetryState = profileTelemetryStates.get(targetId);
899
- if (telemetryState !== undefined) {
900
- return telemetryState;
901
- }
902
- }
903
-
904
- return undefined;
905
- }
906
-
907
- function ensureDownloadInFlight(
908
- profileId: ProfileId,
909
- targetId: TargetId,
910
- tab: RemoteCdpTab,
911
- operationState: RemoteCdpTargetOperationState,
912
- requestedPath?: string
913
- ): Promise<RemoteCdpDownloadArtifact> {
914
- if (requestedPath !== undefined) {
915
- operationState.requestedDownloadPath = requestedPath;
916
- }
917
-
918
- if (operationState.downloadInFlight !== undefined) {
919
- return operationState.downloadInFlight;
920
- }
921
-
922
- const fallbackPath =
923
- operationState.requestedDownloadPath ??
924
- createDownloadPath(profileId, targetId, Math.max(operationState.triggerCount, 1));
925
- const waitForEventMethod = getObjectMethod(tab.page, "waitForEvent");
926
- if (typeof waitForEventMethod !== "function") {
927
- const artifact: RemoteCdpDownloadArtifact = { path: fallbackPath };
928
- operationState.requestedDownloadPath = undefined;
929
- operationState.latestDownload = artifact;
930
- operationState.latestRawDownload = undefined;
931
- const resolved = Promise.resolve(artifact);
932
- operationState.downloadInFlight = resolved;
933
- return resolved;
934
- }
935
-
936
- const inFlight = (async () => {
937
- const rawDownload = await waitForEventMethod.call(tab.page, "download");
938
- operationState.latestRawDownload = rawDownload;
939
- const persistedPath =
940
- operationState.requestedDownloadPath ??
941
- createDownloadPath(profileId, targetId, Math.max(operationState.triggerCount, 1));
942
- const artifact = await readDownloadArtifact(rawDownload, persistedPath);
943
- operationState.latestDownload = artifact;
944
- operationState.requestedDownloadPath = undefined;
945
- return artifact;
946
- })().finally(() => {
947
- if (operationState.downloadInFlight === inFlight) {
948
- operationState.downloadInFlight = undefined;
949
- }
950
- });
951
-
952
- operationState.downloadInFlight = inFlight;
953
- return inFlight;
954
- }
955
-
956
- async function resolveDownloadArtifactPath(
957
- operationState: RemoteCdpTargetOperationState,
958
- artifact: RemoteCdpDownloadArtifact,
959
- requestedPath?: string
960
- ): Promise<RemoteCdpDownloadArtifact> {
961
- if (requestedPath === undefined || requestedPath.trim().length === 0 || artifact.path === requestedPath) {
962
- return artifact;
963
- }
964
-
965
- const failurePrefix = `Failed to persist download to requested path: ${requestedPath}`;
966
- const saveAsMethod = getObjectMethod(operationState.latestRawDownload, "saveAs");
967
- if (typeof saveAsMethod !== "function") {
968
- throw new Error(failurePrefix);
969
- }
970
-
971
- try {
972
- await saveAsMethod.call(operationState.latestRawDownload, requestedPath);
973
- return { ...artifact, path: requestedPath };
974
- } catch (error) {
975
- const message = error instanceof Error ? error.message : String(error);
976
- throw new Error(message.length > 0 ? `${failurePrefix} (${message})` : failurePrefix);
977
- }
978
- }
979
-
980
- return {
981
- status: async () => ({ kind: "remote-cdp", connected: browser !== undefined, endpoint }),
982
- listProfiles: async () => {
983
- const knownProfiles = new Set<ProfileId>([DEFAULT_PROFILE_ID]);
984
- for (const profileId of profileStates.keys()) {
985
- knownProfiles.add(profileId);
986
- }
987
-
988
- return Array.from(knownProfiles).sort();
989
- },
990
- listTabs: async (profile) => {
991
- const profileId = resolveProfileId(profile);
992
- const profileState = profileStates.get(profileId);
993
- return profileState === undefined ? [] : [...profileState.tabOrder];
994
- },
995
- openTab: async (url, profile) => {
996
- const profileId = resolveProfileId(profile);
997
- const profileState = getOrCreateProfileState(profileId);
998
- const targetId = createTargetId(profileId, profileState.nextTargetNumber);
999
- const page = await (await ensureBrowserContext()).newPage();
1000
- const telemetryState = createTargetTelemetryState();
1001
- const operationState = getOrCreateTargetOperationState(profileId, targetId, false);
1002
- attachTelemetryListeners(page, profileId, targetId, telemetryState, operationState);
1003
- await page.goto(url);
1004
-
1005
- profileState.nextTargetNumber += 1;
1006
- profileState.tabs.set(targetId, { url, page });
1007
- profileState.tabOrder.push(targetId);
1008
- getOrCreateProfileTelemetryStates(profileId).set(targetId, telemetryState);
1009
- if (profileState.focusedTargetId === undefined) {
1010
- profileState.focusedTargetId = targetId;
1011
- }
1012
-
1013
- return targetId;
1014
- },
1015
- focusTab: async (targetId, profile) => {
1016
- const profileId = resolveProfileId(profile);
1017
- const { profileState, tab } = requireTargetInProfile(profileId, targetId);
1018
- await tab.page.bringToFront();
1019
- profileState.focusedTargetId = targetId;
1020
- },
1021
- closeTab: async (targetId, profile) => {
1022
- const profileId = resolveProfileId(profile);
1023
- const { profileState, tab } = requireTargetInProfile(profileId, targetId);
1024
- await tab.page.close();
1025
-
1026
- profileState.tabs.delete(targetId);
1027
- profileState.tabOrder = profileState.tabOrder.filter((existingTargetId) => existingTargetId !== targetId);
1028
- if (profileState.focusedTargetId === targetId) {
1029
- profileState.focusedTargetId = profileState.tabOrder[0];
1030
- }
1031
-
1032
- const profileOperationStates = targetOperationStates.get(profileId);
1033
- profileOperationStates?.delete(targetId);
1034
- if (profileOperationStates !== undefined && profileOperationStates.size === 0) {
1035
- targetOperationStates.delete(profileId);
1036
- }
1037
-
1038
- const profileTelemetryStates = targetTelemetryStates.get(profileId);
1039
- profileTelemetryStates?.delete(targetId);
1040
- if (profileTelemetryStates !== undefined && profileTelemetryStates.size === 0) {
1041
- targetTelemetryStates.delete(profileId);
1042
- }
1043
-
1044
- if (profileState.tabOrder.length === 0) {
1045
- profileStates.delete(profileId);
1046
- }
1047
- },
1048
- snapshot: async (targetId, profile) => {
1049
- const profileId = resolveProfileId(profile);
1050
- const tab = profileStates.get(profileId)?.tabs.get(targetId);
1051
- if (tab === undefined) {
1052
- return {
1053
- kind: "remote-cdp",
1054
- profile: profileId,
1055
- targetId,
1056
- endpoint,
1057
- hasTarget: false
1058
- };
1059
- }
1060
-
1061
- const requestSummaries =
1062
- targetTelemetryStates
1063
- .get(profileId)
1064
- ?.get(targetId)
1065
- ?.requestSummaries.map(cloneRequestSummary) ?? [];
1066
-
1067
- return {
1068
- kind: "remote-cdp",
1069
- profile: profileId,
1070
- targetId,
1071
- endpoint,
1072
- hasTarget: true,
1073
- url: tab.page.url(),
1074
- title: await tab.page.title(),
1075
- html: await tab.page.content(),
1076
- requestSummaries
1077
- };
1078
- },
1079
- screenshot: async (targetId, profile) => {
1080
- const profileId = resolveProfileId(profile);
1081
- const tab = profileStates.get(profileId)?.tabs.get(targetId);
1082
- if (tab === undefined) {
1083
- return {
1084
- kind: "remote-cdp",
1085
- profile: profileId,
1086
- targetId,
1087
- endpoint,
1088
- hasTarget: false
1089
- };
1090
- }
1091
-
1092
- const screenshot = await capturePageScreenshot(tab.page);
1093
- return {
1094
- kind: "remote-cdp",
1095
- profile: profileId,
1096
- targetId,
1097
- endpoint,
1098
- hasTarget: true,
1099
- ...screenshot
1100
- };
1101
- },
1102
- act: async (action, targetId, profile) => {
1103
- const profileId = resolveProfileId(profile);
1104
- const tab = profileStates.get(profileId)?.tabs.get(targetId);
1105
- if (tab === undefined) {
1106
- return {
1107
- actionType: action.type,
1108
- profile: profileId,
1109
- targetId,
1110
- endpoint,
1111
- targetKnown: false,
1112
- ok: false,
1113
- executed: false,
1114
- error: `Unknown targetId: ${targetId} (profile: ${profileId})`
1115
- };
1116
- }
1117
-
1118
- const payload = action.payload;
1119
- const operationState = getOrCreateTargetOperationState(profileId, targetId);
1120
- const baseResult = {
1121
- actionType: action.type,
1122
- profile: profileId,
1123
- targetId,
1124
- endpoint,
1125
- targetKnown: true
1126
- };
1127
-
1128
- const performAction = async (): Promise<{
1129
- ok: boolean;
1130
- executed: boolean;
1131
- error?: string;
1132
- data?: Record<string, unknown>;
1133
- }> => {
1134
- switch (action.type) {
1135
- case "navigate":
1136
- case "goto": {
1137
- const nextUrl = readActionPayloadString(payload, "url");
1138
- if (nextUrl === undefined) {
1139
- return {
1140
- ok: false,
1141
- executed: false,
1142
- error: "action.payload.url is required for navigate action."
1143
- };
1144
- }
1145
-
1146
- await tab.page.goto(nextUrl);
1147
- tab.url = nextUrl;
1148
- return { ok: true, executed: true };
1149
- }
1150
- case "click": {
1151
- const selector = readActionPayloadString(payload, "selector");
1152
- if (selector === undefined) {
1153
- return {
1154
- ok: false,
1155
- executed: false,
1156
- error: "action.payload.selector is required for click action."
1157
- };
1158
- }
1159
-
1160
- const locator = tab.page.locator(selector);
1161
- if (operationState.uploadFiles.length > 0) {
1162
- const setInputFilesMethod = getObjectMethod(locator, "setInputFiles");
1163
- if (typeof setInputFilesMethod === "function") {
1164
- await setInputFilesMethod.call(locator, [...operationState.uploadFiles]);
1165
- operationState.uploadFiles = [];
1166
- return { ok: true, executed: true };
1167
- }
1168
- }
1169
-
1170
- await locator.click();
1171
- return { ok: true, executed: true };
1172
- }
1173
- case "fill": {
1174
- const selector = readActionPayloadString(payload, "selector");
1175
- const value = readActionPayloadString(payload, "value");
1176
- if (selector === undefined || value === undefined) {
1177
- return {
1178
- ok: false,
1179
- executed: false,
1180
- error: "action.payload.selector and action.payload.value are required for fill action."
1181
- };
1182
- }
1183
-
1184
- await tab.page.locator(selector).fill(value);
1185
- return { ok: true, executed: true };
1186
- }
1187
- case "type": {
1188
- const selector = readActionPayloadString(payload, "selector");
1189
- const text = readActionPayloadString(payload, "text");
1190
- if (selector === undefined || text === undefined) {
1191
- return {
1192
- ok: false,
1193
- executed: false,
1194
- error: "action.payload.selector and action.payload.text are required for type action."
1195
- };
1196
- }
1197
-
1198
- await tab.page.locator(selector).type(text);
1199
- return { ok: true, executed: true };
1200
- }
1201
- case "press": {
1202
- const key = readActionPayloadString(payload, "key");
1203
- if (key === undefined || tab.page.keyboard === undefined) {
1204
- return {
1205
- ok: false,
1206
- executed: false,
1207
- error: "action.payload.key is required for press action."
1208
- };
1209
- }
1210
-
1211
- await tab.page.keyboard.press(key);
1212
- return { ok: true, executed: true };
1213
- }
1214
- case "networkMockAdd": {
1215
- const urlPattern = readActionPayloadString(payload, "urlPattern");
1216
- if (urlPattern === undefined) {
1217
- return {
1218
- ok: false,
1219
- executed: false,
1220
- error: "action.payload.urlPattern is required for networkMockAdd action."
1221
- };
1222
- }
1223
-
1224
- const routeMethod = getObjectMethod(tab.page, "route");
1225
- if (typeof routeMethod !== "function") {
1226
- return {
1227
- ok: true,
1228
- executed: false
1229
- };
1230
- }
1231
-
1232
- const methodFilter = readActionPayloadString(payload, "method")?.toUpperCase();
1233
- const status = readActionPayloadNumber(payload, "status") ?? 200;
1234
- if (!Number.isInteger(status) || status < 100 || status > 599) {
1235
- return {
1236
- ok: false,
1237
- executed: false,
1238
- error: "action.payload.status must be an integer between 100 and 599 for networkMockAdd action."
1239
- };
1240
- }
1241
- const body = readActionPayloadString(payload, "body") ?? "";
1242
- const contentType = readActionPayloadString(payload, "contentType") ?? "text/plain";
1243
- const mockId = `mock:${operationState.nextNetworkMockNumber}`;
1244
- operationState.nextNetworkMockNumber += 1;
1245
- const handler: RemoteCdpNetworkMockHandler = async (...args) => {
1246
- const route = args[0];
1247
- const explicitRequest = args[1];
1248
- const request = explicitRequest ?? readObjectMethod<unknown>(route, "request");
1249
- if (methodFilter !== undefined) {
1250
- const requestMethod = readObjectMethod<string>(request, "method");
1251
- if (requestMethod?.toUpperCase() !== methodFilter) {
1252
- const continueMethod = getObjectMethod(route, "continue");
1253
- if (typeof continueMethod === "function") {
1254
- await continueMethod.call(route);
1255
- return;
1256
- }
1257
-
1258
- const fallbackMethod = getObjectMethod(route, "fallback");
1259
- if (typeof fallbackMethod === "function") {
1260
- await fallbackMethod.call(route);
1261
- return;
1262
- }
1263
-
1264
- return;
1265
- }
1266
- }
1267
-
1268
- const fulfillMethod = getObjectMethod(route, "fulfill");
1269
- if (typeof fulfillMethod !== "function") {
1270
- return;
1271
- }
1272
-
1273
- await fulfillMethod.call(route, {
1274
- status,
1275
- body,
1276
- headers: {
1277
- "content-type": contentType
1278
- }
1279
- });
1280
- };
1281
-
1282
- await routeMethod.call(tab.page, urlPattern, handler);
1283
- operationState.networkMocks.set(mockId, {
1284
- urlPattern,
1285
- handler
1286
- });
1287
- return {
1288
- ok: true,
1289
- executed: true,
1290
- data: {
1291
- mockId,
1292
- urlPattern,
1293
- ...(methodFilter !== undefined ? { method: methodFilter } : {}),
1294
- status
1295
- }
1296
- };
1297
- }
1298
- case "networkMockClear": {
1299
- const unrouteMethod = getObjectMethod(tab.page, "unroute");
1300
- if (typeof unrouteMethod !== "function") {
1301
- return {
1302
- ok: true,
1303
- executed: false
1304
- };
1305
- }
1306
-
1307
- const mockId = readActionPayloadString(payload, "mockId");
1308
- if (mockId !== undefined) {
1309
- const binding = operationState.networkMocks.get(mockId);
1310
- if (binding === undefined) {
1311
- return {
1312
- ok: false,
1313
- executed: false,
1314
- error: `Unknown network mock id: ${mockId}`
1315
- };
1316
- }
1317
-
1318
- await unrouteMethod.call(tab.page, binding.urlPattern, binding.handler);
1319
- operationState.networkMocks.delete(mockId);
1320
- return {
1321
- ok: true,
1322
- executed: true,
1323
- data: {
1324
- cleared: 1,
1325
- mockId
1326
- }
1327
- };
1328
- }
1329
-
1330
- let cleared = 0;
1331
- for (const [registeredId, binding] of operationState.networkMocks.entries()) {
1332
- try {
1333
- await unrouteMethod.call(tab.page, binding.urlPattern, binding.handler);
1334
- } catch {
1335
- // Ignore per-route cleanup failures.
1336
- }
1337
- operationState.networkMocks.delete(registeredId);
1338
- cleared += 1;
1339
- }
1340
-
1341
- return {
1342
- ok: true,
1343
- executed: true,
1344
- data: {
1345
- cleared
1346
- }
1347
- };
1348
- }
1349
- case "domQuery": {
1350
- const selector = readActionPayloadString(payload, "selector");
1351
- if (selector === undefined) {
1352
- return {
1353
- ok: false,
1354
- executed: false,
1355
- error: "action.payload.selector is required for domQuery action."
1356
- };
1357
- }
1358
-
1359
- const customDomQueryMethod = getObjectMethod(tab.page, "domQuery");
1360
- if (typeof customDomQueryMethod === "function") {
1361
- const customResult = await customDomQueryMethod.call(tab.page, selector);
1362
- return {
1363
- ok: true,
1364
- executed: true,
1365
- data: isObjectRecord(customResult)
1366
- ? customResult
1367
- : {
1368
- selector,
1369
- found: false
1370
- }
1371
- };
1372
- }
1373
-
1374
- const queryResult = await evaluateInPage<Record<string, unknown>>(
1375
- tab.page,
1376
- (rawArg) => {
1377
- const selectorValue =
1378
- rawArg !== null &&
1379
- typeof rawArg === "object" &&
1380
- typeof (rawArg as { selector?: unknown }).selector === "string"
1381
- ? (rawArg as { selector: string }).selector
1382
- : "";
1383
- const element = selectorValue.length > 0 ? document.querySelector(selectorValue) : null;
1384
- if (element === null) {
1385
- return {
1386
- selector: selectorValue,
1387
- found: false
1388
- };
1389
- }
1390
-
1391
- const attributes: Record<string, string> = {};
1392
- for (const attribute of Array.from(element.attributes)) {
1393
- attributes[attribute.name] = attribute.value;
1394
- }
1395
-
1396
- return {
1397
- selector: selectorValue,
1398
- found: true,
1399
- node: {
1400
- tagName: element.tagName.toLowerCase(),
1401
- id: element.id || undefined,
1402
- className: element.className || undefined,
1403
- text: (element.textContent || "").trim().slice(0, 300),
1404
- attributes
1405
- }
1406
- };
1407
- },
1408
- { selector }
1409
- );
1410
-
1411
- return {
1412
- ok: true,
1413
- executed: true,
1414
- data: isObjectRecord(queryResult)
1415
- ? queryResult
1416
- : {
1417
- selector,
1418
- found: false
1419
- }
1420
- };
1421
- }
1422
- case "domQueryAll": {
1423
- const selector = readActionPayloadString(payload, "selector");
1424
- if (selector === undefined) {
1425
- return {
1426
- ok: false,
1427
- executed: false,
1428
- error: "action.payload.selector is required for domQueryAll action."
1429
- };
1430
- }
1431
-
1432
- const customDomQueryAllMethod = getObjectMethod(tab.page, "domQueryAll");
1433
- if (typeof customDomQueryAllMethod === "function") {
1434
- const customResult = await customDomQueryAllMethod.call(tab.page, selector);
1435
- return {
1436
- ok: true,
1437
- executed: true,
1438
- data: isObjectRecord(customResult)
1439
- ? customResult
1440
- : {
1441
- selector,
1442
- count: 0,
1443
- nodes: []
1444
- }
1445
- };
1446
- }
1447
-
1448
- const queryResult = await evaluateInPage<Record<string, unknown>>(
1449
- tab.page,
1450
- (rawArg) => {
1451
- const selectorValue =
1452
- rawArg !== null &&
1453
- typeof rawArg === "object" &&
1454
- typeof (rawArg as { selector?: unknown }).selector === "string"
1455
- ? (rawArg as { selector: string }).selector
1456
- : "";
1457
- const elements = selectorValue.length > 0 ? document.querySelectorAll(selectorValue) : [];
1458
- const nodes = Array.from(elements).map((element, index) => {
1459
- const attributes: Record<string, string> = {};
1460
- for (const attribute of Array.from(element.attributes)) {
1461
- attributes[attribute.name] = attribute.value;
1462
- }
1463
-
1464
- return {
1465
- index,
1466
- tagName: element.tagName.toLowerCase(),
1467
- id: element.id || undefined,
1468
- className: element.className || undefined,
1469
- text: (element.textContent || "").trim().slice(0, 300),
1470
- attributes
1471
- };
1472
- });
1473
-
1474
- return {
1475
- selector: selectorValue,
1476
- count: nodes.length,
1477
- nodes
1478
- };
1479
- },
1480
- { selector }
1481
- );
1482
-
1483
- return {
1484
- ok: true,
1485
- executed: true,
1486
- data: isObjectRecord(queryResult)
1487
- ? queryResult
1488
- : {
1489
- selector,
1490
- count: 0,
1491
- nodes: []
1492
- }
1493
- };
1494
- }
1495
- case "elementScreenshot": {
1496
- const selector = readActionPayloadString(payload, "selector");
1497
- if (selector === undefined) {
1498
- return {
1499
- ok: false,
1500
- executed: false,
1501
- error: "action.payload.selector is required for elementScreenshot action."
1502
- };
1503
- }
1504
-
1505
- const customElementScreenshotMethod = getObjectMethod(tab.page, "elementScreenshot");
1506
- if (typeof customElementScreenshotMethod === "function") {
1507
- const customResult = await customElementScreenshotMethod.call(tab.page, selector);
1508
- return {
1509
- ok: true,
1510
- executed: true,
1511
- data: isObjectRecord(customResult)
1512
- ? customResult
1513
- : {
1514
- selector,
1515
- found: false
1516
- }
1517
- };
1518
- }
1519
-
1520
- const found = await evaluateInPage<boolean>(
1521
- tab.page,
1522
- (rawArg) => {
1523
- const selectorValue =
1524
- rawArg !== null &&
1525
- typeof rawArg === "object" &&
1526
- typeof (rawArg as { selector?: unknown }).selector === "string"
1527
- ? (rawArg as { selector: string }).selector
1528
- : "";
1529
- return selectorValue.length > 0 && document.querySelector(selectorValue) !== null;
1530
- },
1531
- { selector }
1532
- );
1533
- if (!found) {
1534
- return {
1535
- ok: true,
1536
- executed: true,
1537
- data: {
1538
- selector,
1539
- found: false
1540
- }
1541
- };
1542
- }
1543
-
1544
- const locator = tab.page.locator(selector);
1545
- const locatorScreenshotMethod = getObjectMethod(locator, "screenshot");
1546
- if (typeof locatorScreenshotMethod !== "function") {
1547
- return {
1548
- ok: true,
1549
- executed: false
1550
- };
1551
- }
1552
-
1553
- const rawScreenshot = await locatorScreenshotMethod.call(locator, { type: "png" });
1554
- const parsedScreenshot = readScreenshotImage(rawScreenshot);
1555
- if (parsedScreenshot === undefined) {
1556
- return {
1557
- ok: false,
1558
- executed: false,
1559
- error: "Unable to parse element screenshot payload."
1560
- };
1561
- }
1562
-
1563
- return {
1564
- ok: true,
1565
- executed: true,
1566
- data: {
1567
- selector,
1568
- found: true,
1569
- ...parsedScreenshot
1570
- }
1571
- };
1572
- }
1573
- case "a11ySnapshot": {
1574
- const selector = readActionPayloadString(payload, "selector");
1575
- const customA11ySnapshotMethod = getObjectMethod(tab.page, "a11ySnapshot");
1576
- if (typeof customA11ySnapshotMethod === "function") {
1577
- const customResult = await customA11ySnapshotMethod.call(tab.page, selector);
1578
- return {
1579
- ok: true,
1580
- executed: true,
1581
- data: isObjectRecord(customResult)
1582
- ? customResult
1583
- : {
1584
- found: false
1585
- }
1586
- };
1587
- }
1588
-
1589
- const accessibilityValue =
1590
- typeof tab.page === "object" && tab.page !== null
1591
- ? (tab.page as Record<string, unknown>).accessibility
1592
- : undefined;
1593
- const accessibilitySnapshotMethod = getObjectMethod(accessibilityValue, "snapshot");
1594
- if (typeof accessibilitySnapshotMethod === "function") {
1595
- const snapshot = await accessibilitySnapshotMethod.call(accessibilityValue, {});
1596
- return {
1597
- ok: true,
1598
- executed: true,
1599
- data: {
1600
- selector,
1601
- found: true,
1602
- snapshot
1603
- }
1604
- };
1605
- }
1606
-
1607
- const fallbackSnapshot = await evaluateInPage<Record<string, unknown>>(
1608
- tab.page,
1609
- (rawArg) => {
1610
- const selectorValue =
1611
- rawArg !== null &&
1612
- typeof rawArg === "object" &&
1613
- typeof (rawArg as { selector?: unknown }).selector === "string"
1614
- ? (rawArg as { selector: string }).selector
1615
- : "";
1616
- const root =
1617
- selectorValue.length > 0
1618
- ? document.querySelector(selectorValue)
1619
- : document.body ?? document.documentElement;
1620
- if (root === null) {
1621
- return {
1622
- selector: selectorValue,
1623
- found: false
1624
- };
1625
- }
1626
-
1627
- const build = (element: Element, depth: number): Record<string, unknown> => {
1628
- const role =
1629
- element.getAttribute("role") ??
1630
- (element.tagName.toLowerCase() === "a"
1631
- ? "link"
1632
- : element.tagName.toLowerCase() === "button"
1633
- ? "button"
1634
- : element.tagName.toLowerCase() === "input"
1635
- ? "textbox"
1636
- : "generic");
1637
- const name =
1638
- element.getAttribute("aria-label") ??
1639
- element.getAttribute("alt") ??
1640
- (element.textContent || "").trim().slice(0, 120);
1641
- if (depth >= 5) {
1642
- return {
1643
- role,
1644
- ...(name.length > 0 ? { name } : {})
1645
- };
1646
- }
1647
-
1648
- const children = Array.from(element.children)
1649
- .slice(0, 30)
1650
- .map((child) => build(child, depth + 1));
1651
- return {
1652
- role,
1653
- ...(name.length > 0 ? { name } : {}),
1654
- ...(children.length > 0 ? { children } : {})
1655
- };
1656
- };
1657
-
1658
- return {
1659
- selector: selectorValue,
1660
- found: true,
1661
- snapshot: build(root, 0)
1662
- };
1663
- },
1664
- { selector: selector ?? "" }
1665
- );
1666
-
1667
- return {
1668
- ok: true,
1669
- executed: true,
1670
- data: isObjectRecord(fallbackSnapshot)
1671
- ? fallbackSnapshot
1672
- : {
1673
- selector,
1674
- found: false
1675
- }
1676
- };
1677
- }
1678
- case "cookieGet": {
1679
- const cookieName = readActionPayloadString(payload, "name");
1680
- const customCookieGetMethod = getObjectMethod(tab.page, "cookieGet");
1681
- if (typeof customCookieGetMethod === "function") {
1682
- const customResult = await customCookieGetMethod.call(tab.page, cookieName);
1683
- return {
1684
- ok: true,
1685
- executed: true,
1686
- data: isObjectRecord(customResult)
1687
- ? customResult
1688
- : {
1689
- cookies: []
1690
- }
1691
- };
1692
- }
1693
-
1694
- const contextMethod = getObjectMethod(tab.page, "context");
1695
- if (typeof contextMethod !== "function") {
1696
- return {
1697
- ok: true,
1698
- executed: false
1699
- };
1700
- }
1701
-
1702
- const context = await contextMethod.call(tab.page);
1703
- const cookiesMethod = getObjectMethod(context, "cookies");
1704
- if (typeof cookiesMethod !== "function") {
1705
- return {
1706
- ok: true,
1707
- executed: false
1708
- };
1709
- }
1710
-
1711
- let rawCookies: unknown;
1712
- try {
1713
- rawCookies = await cookiesMethod.call(context, [tab.page.url()]);
1714
- } catch {
1715
- rawCookies = await cookiesMethod.call(context);
1716
- }
1717
- const cookies = normalizeCookies(rawCookies).filter(
1718
- (cookie) => cookieName === undefined || cookie.name === cookieName
1719
- );
1720
- return {
1721
- ok: true,
1722
- executed: true,
1723
- data: {
1724
- cookies,
1725
- ...(cookieName !== undefined ? { name: cookieName } : {})
1726
- }
1727
- };
1728
- }
1729
- case "cookieSet": {
1730
- const cookieName = readActionPayloadString(payload, "name");
1731
- const cookieValue = readActionPayloadString(payload, "value");
1732
- const cookieUrl = readActionPayloadString(payload, "url") ?? tab.page.url();
1733
- if (cookieName === undefined || cookieValue === undefined) {
1734
- return {
1735
- ok: false,
1736
- executed: false,
1737
- error: "action.payload.name and action.payload.value are required for cookieSet action."
1738
- };
1739
- }
1740
-
1741
- const customCookieSetMethod = getObjectMethod(tab.page, "cookieSet");
1742
- if (typeof customCookieSetMethod === "function") {
1743
- const customResult = await customCookieSetMethod.call(tab.page, {
1744
- name: cookieName,
1745
- value: cookieValue,
1746
- url: cookieUrl
1747
- });
1748
- return {
1749
- ok: true,
1750
- executed: true,
1751
- data: isObjectRecord(customResult)
1752
- ? customResult
1753
- : {
1754
- set: true,
1755
- cookie: {
1756
- name: cookieName,
1757
- value: cookieValue,
1758
- url: cookieUrl
1759
- }
1760
- }
1761
- };
1762
- }
1763
-
1764
- const contextMethod = getObjectMethod(tab.page, "context");
1765
- if (typeof contextMethod !== "function") {
1766
- return {
1767
- ok: true,
1768
- executed: false
1769
- };
1770
- }
1771
-
1772
- const context = await contextMethod.call(tab.page);
1773
- const addCookiesMethod = getObjectMethod(context, "addCookies");
1774
- if (typeof addCookiesMethod !== "function") {
1775
- return {
1776
- ok: true,
1777
- executed: false
1778
- };
1779
- }
1780
-
1781
- await addCookiesMethod.call(context, [
1782
- {
1783
- name: cookieName,
1784
- value: cookieValue,
1785
- url: cookieUrl
1786
- }
1787
- ]);
1788
-
1789
- return {
1790
- ok: true,
1791
- executed: true,
1792
- data: {
1793
- set: true,
1794
- cookie: {
1795
- name: cookieName,
1796
- value: cookieValue,
1797
- url: cookieUrl
1798
- }
1799
- }
1800
- };
1801
- }
1802
- case "cookieClear": {
1803
- const cookieName = readActionPayloadString(payload, "name");
1804
- const customCookieClearMethod = getObjectMethod(tab.page, "cookieClear");
1805
- if (typeof customCookieClearMethod === "function") {
1806
- const customResult = await customCookieClearMethod.call(tab.page, cookieName);
1807
- return {
1808
- ok: true,
1809
- executed: true,
1810
- data: isObjectRecord(customResult)
1811
- ? customResult
1812
- : {
1813
- cleared: true
1814
- }
1815
- };
1816
- }
1817
-
1818
- const contextMethod = getObjectMethod(tab.page, "context");
1819
- if (typeof contextMethod !== "function") {
1820
- return {
1821
- ok: true,
1822
- executed: false
1823
- };
1824
- }
1825
-
1826
- const context = await contextMethod.call(tab.page);
1827
- const clearCookiesMethod = getObjectMethod(context, "clearCookies");
1828
- if (typeof clearCookiesMethod !== "function") {
1829
- return {
1830
- ok: true,
1831
- executed: false
1832
- };
1833
- }
1834
-
1835
- if (cookieName === undefined) {
1836
- await clearCookiesMethod.call(context);
1837
- return {
1838
- ok: true,
1839
- executed: true,
1840
- data: {
1841
- cleared: true,
1842
- count: -1
1843
- }
1844
- };
1845
- }
1846
-
1847
- const cookiesMethod = getObjectMethod(context, "cookies");
1848
- const addCookiesMethod = getObjectMethod(context, "addCookies");
1849
- if (typeof cookiesMethod !== "function" || typeof addCookiesMethod !== "function") {
1850
- return {
1851
- ok: true,
1852
- executed: false
1853
- };
1854
- }
1855
-
1856
- let rawCookies: unknown;
1857
- try {
1858
- rawCookies = await cookiesMethod.call(context, [tab.page.url()]);
1859
- } catch {
1860
- rawCookies = await cookiesMethod.call(context);
1861
- }
1862
- const currentCookies = normalizeCookies(rawCookies);
1863
- const keptCookieInputs = currentCookies
1864
- .filter((cookie) => cookie.name !== cookieName)
1865
- .map((cookie) => toCookieInput(cookie))
1866
- .filter((cookie): cookie is Record<string, unknown> => cookie !== undefined);
1867
- const removedCount = currentCookies.length - keptCookieInputs.length;
1868
-
1869
- await clearCookiesMethod.call(context);
1870
- if (keptCookieInputs.length > 0) {
1871
- await addCookiesMethod.call(context, keptCookieInputs);
1872
- }
1873
-
1874
- return {
1875
- ok: true,
1876
- executed: true,
1877
- data: {
1878
- cleared: true,
1879
- name: cookieName,
1880
- count: removedCount
1881
- }
1882
- };
1883
- }
1884
- case "storageGet": {
1885
- const scope = parseStorageScope(readActionPayloadString(payload, "scope"));
1886
- const key = readActionPayloadString(payload, "key");
1887
- if (scope === undefined || key === undefined) {
1888
- return {
1889
- ok: false,
1890
- executed: false,
1891
- error: "action.payload.scope and action.payload.key are required for storageGet action."
1892
- };
1893
- }
1894
-
1895
- const customStorageGetMethod = getObjectMethod(tab.page, "storageGet");
1896
- if (typeof customStorageGetMethod === "function") {
1897
- const customResult = await customStorageGetMethod.call(tab.page, scope, key);
1898
- return {
1899
- ok: true,
1900
- executed: true,
1901
- data: isObjectRecord(customResult)
1902
- ? customResult
1903
- : {
1904
- scope,
1905
- key,
1906
- exists: false
1907
- }
1908
- };
1909
- }
1910
-
1911
- const result = await evaluateInPage<Record<string, unknown>>(
1912
- tab.page,
1913
- (rawArg) => {
1914
- const scopeValue =
1915
- rawArg !== null &&
1916
- typeof rawArg === "object" &&
1917
- typeof (rawArg as { scope?: unknown }).scope === "string"
1918
- ? (rawArg as { scope: string }).scope
1919
- : "local";
1920
- const keyValue =
1921
- rawArg !== null &&
1922
- typeof rawArg === "object" &&
1923
- typeof (rawArg as { key?: unknown }).key === "string"
1924
- ? (rawArg as { key: string }).key
1925
- : "";
1926
- const storage = scopeValue === "session" ? window.sessionStorage : window.localStorage;
1927
- const value = storage.getItem(keyValue);
1928
- return {
1929
- scope: scopeValue,
1930
- key: keyValue,
1931
- exists: value !== null,
1932
- ...(value !== null ? { value } : {})
1933
- };
1934
- },
1935
- { scope, key }
1936
- );
1937
-
1938
- return {
1939
- ok: true,
1940
- executed: true,
1941
- data: isObjectRecord(result)
1942
- ? result
1943
- : {
1944
- scope,
1945
- key,
1946
- exists: false
1947
- }
1948
- };
1949
- }
1950
- case "storageSet": {
1951
- const scope = parseStorageScope(readActionPayloadString(payload, "scope"));
1952
- const key = readActionPayloadString(payload, "key");
1953
- const value = readActionPayloadString(payload, "value");
1954
- if (scope === undefined || key === undefined || value === undefined) {
1955
- return {
1956
- ok: false,
1957
- executed: false,
1958
- error: "action.payload.scope, action.payload.key and action.payload.value are required for storageSet action."
1959
- };
1960
- }
1961
-
1962
- const customStorageSetMethod = getObjectMethod(tab.page, "storageSet");
1963
- if (typeof customStorageSetMethod === "function") {
1964
- const customResult = await customStorageSetMethod.call(tab.page, scope, key, value);
1965
- return {
1966
- ok: true,
1967
- executed: true,
1968
- data: isObjectRecord(customResult)
1969
- ? customResult
1970
- : {
1971
- scope,
1972
- key,
1973
- value,
1974
- set: true
1975
- }
1976
- };
1977
- }
1978
-
1979
- await evaluateInPage(
1980
- tab.page,
1981
- (rawArg) => {
1982
- const scopeValue =
1983
- rawArg !== null &&
1984
- typeof rawArg === "object" &&
1985
- typeof (rawArg as { scope?: unknown }).scope === "string"
1986
- ? (rawArg as { scope: string }).scope
1987
- : "local";
1988
- const keyValue =
1989
- rawArg !== null &&
1990
- typeof rawArg === "object" &&
1991
- typeof (rawArg as { key?: unknown }).key === "string"
1992
- ? (rawArg as { key: string }).key
1993
- : "";
1994
- const valueText =
1995
- rawArg !== null &&
1996
- typeof rawArg === "object" &&
1997
- typeof (rawArg as { value?: unknown }).value === "string"
1998
- ? (rawArg as { value: string }).value
1999
- : "";
2000
- const storage = scopeValue === "session" ? window.sessionStorage : window.localStorage;
2001
- storage.setItem(keyValue, valueText);
2002
- },
2003
- { scope, key, value }
2004
- );
2005
-
2006
- return {
2007
- ok: true,
2008
- executed: true,
2009
- data: {
2010
- scope,
2011
- key,
2012
- value,
2013
- set: true
2014
- }
2015
- };
2016
- }
2017
- case "frameList": {
2018
- const customFrameListMethod = getObjectMethod(tab.page, "frameList");
2019
- if (typeof customFrameListMethod === "function") {
2020
- const customResult = await customFrameListMethod.call(tab.page);
2021
- return {
2022
- ok: true,
2023
- executed: true,
2024
- data: isObjectRecord(customResult)
2025
- ? customResult
2026
- : {
2027
- frames: []
2028
- }
2029
- };
2030
- }
2031
-
2032
- const framesMethod = getObjectMethod(tab.page, "frames");
2033
- if (typeof framesMethod !== "function") {
2034
- return {
2035
- ok: true,
2036
- executed: false
2037
- };
2038
- }
2039
-
2040
- const rawFrames = await framesMethod.call(tab.page);
2041
- if (!Array.isArray(rawFrames)) {
2042
- return {
2043
- ok: true,
2044
- executed: true,
2045
- data: {
2046
- frames: []
2047
- }
2048
- };
2049
- }
2050
-
2051
- const mainFrameMethod = getObjectMethod(tab.page, "mainFrame");
2052
- const mainFrame = typeof mainFrameMethod === "function" ? await mainFrameMethod.call(tab.page) : undefined;
2053
- const frames = rawFrames.map((frame, index) => {
2054
- const name = readObjectMethod<string>(frame, "name");
2055
- const url = readObjectMethod<string>(frame, "url") ?? "";
2056
- return {
2057
- frameId: `frame:${index}`,
2058
- index,
2059
- ...(typeof name === "string" && name.length > 0 ? { name } : {}),
2060
- url,
2061
- isMainFrame: frame === mainFrame
2062
- };
2063
- });
2064
-
2065
- return {
2066
- ok: true,
2067
- executed: true,
2068
- data: {
2069
- frames
2070
- }
2071
- };
2072
- }
2073
- case "frameSnapshot": {
2074
- const frameId = readActionPayloadString(payload, "frameId");
2075
- if (frameId === undefined) {
2076
- return {
2077
- ok: false,
2078
- executed: false,
2079
- error: "action.payload.frameId is required for frameSnapshot action."
2080
- };
2081
- }
2082
-
2083
- const customFrameSnapshotMethod = getObjectMethod(tab.page, "frameSnapshot");
2084
- if (typeof customFrameSnapshotMethod === "function") {
2085
- const customResult = await customFrameSnapshotMethod.call(tab.page, frameId);
2086
- return {
2087
- ok: true,
2088
- executed: true,
2089
- data: isObjectRecord(customResult)
2090
- ? customResult
2091
- : {
2092
- frameId,
2093
- found: false
2094
- }
2095
- };
2096
- }
2097
-
2098
- const frameIndex = parseFrameIndex(frameId);
2099
- if (frameIndex === undefined) {
2100
- return {
2101
- ok: false,
2102
- executed: false,
2103
- error: `Invalid frameId: ${frameId}`
2104
- };
2105
- }
2106
-
2107
- const framesMethod = getObjectMethod(tab.page, "frames");
2108
- if (typeof framesMethod !== "function") {
2109
- return {
2110
- ok: true,
2111
- executed: false
2112
- };
2113
- }
2114
-
2115
- const rawFrames = await framesMethod.call(tab.page);
2116
- if (!Array.isArray(rawFrames) || frameIndex >= rawFrames.length) {
2117
- return {
2118
- ok: true,
2119
- executed: true,
2120
- data: {
2121
- frameId,
2122
- found: false
2123
- }
2124
- };
2125
- }
2126
-
2127
- const frame = rawFrames[frameIndex];
2128
- const contentMethod = getObjectMethod(frame, "content");
2129
- if (typeof contentMethod !== "function") {
2130
- return {
2131
- ok: true,
2132
- executed: false
2133
- };
2134
- }
2135
-
2136
- const mainFrameMethod = getObjectMethod(tab.page, "mainFrame");
2137
- const mainFrame = typeof mainFrameMethod === "function" ? await mainFrameMethod.call(tab.page) : undefined;
2138
- const frameName = readObjectMethod<string>(frame, "name");
2139
- const frameUrl = readObjectMethod<string>(frame, "url") ?? "";
2140
- const frameHtml = await contentMethod.call(frame);
2141
-
2142
- return {
2143
- ok: true,
2144
- executed: true,
2145
- data: {
2146
- frameId,
2147
- index: frameIndex,
2148
- found: true,
2149
- ...(typeof frameName === "string" && frameName.length > 0 ? { name: frameName } : {}),
2150
- url: frameUrl,
2151
- isMainFrame: frame === mainFrame,
2152
- html: typeof frameHtml === "string" ? frameHtml : ""
2153
- }
2154
- };
2155
- }
2156
- default:
2157
- return {
2158
- ok: true,
2159
- executed: false
2160
- };
2161
- }
2162
- };
2163
-
2164
- try {
2165
- return {
2166
- ...baseResult,
2167
- ...(await performAction())
2168
- };
2169
- } catch (error) {
2170
- return {
2171
- ...baseResult,
2172
- ok: false,
2173
- executed: false,
2174
- error: toErrorMessage(error)
2175
- };
2176
- }
2177
- },
2178
- armUpload: async (targetId, files, profile) => {
2179
- const profileId = resolveProfileId(profile);
2180
- const operationState = getOrCreateTargetOperationState(profileId, targetId);
2181
- operationState.uploadFiles = [...files];
2182
- },
2183
- armDialog: async (targetId, profile) => {
2184
- const profileId = resolveProfileId(profile);
2185
- const operationState = getOrCreateTargetOperationState(profileId, targetId);
2186
- operationState.dialogArmedCount += 1;
2187
- },
2188
- waitDownload: async (targetId, profile, path) => {
2189
- const profileId = resolveProfileId(profile);
2190
- const { tab } = requireTargetInProfile(profileId, targetId);
2191
- const operationState = getOrCreateTargetOperationState(profileId, targetId);
2192
- const requestedPath = path !== undefined && path.trim().length > 0 ? path : undefined;
2193
- if (operationState.downloadInFlight === undefined && operationState.latestDownload !== undefined) {
2194
- const download = await resolveDownloadArtifactPath(
2195
- operationState,
2196
- { ...operationState.latestDownload },
2197
- requestedPath
2198
- );
2199
- operationState.latestDownload = undefined;
2200
- operationState.latestRawDownload = undefined;
2201
- operationState.requestedDownloadPath = undefined;
2202
- return {
2203
- path: download.path,
2204
- profile: profileId,
2205
- targetId,
2206
- endpoint,
2207
- uploadFiles: [...operationState.uploadFiles],
2208
- dialogArmedCount: operationState.dialogArmedCount,
2209
- triggerCount: operationState.triggerCount,
2210
- ...(download.suggestedFilename !== undefined
2211
- ? { suggestedFilename: download.suggestedFilename }
2212
- : {}),
2213
- ...(download.url !== undefined ? { url: download.url } : {}),
2214
- ...(download.mimeType !== undefined ? { mimeType: download.mimeType } : {})
2215
- };
2216
- }
2217
-
2218
- const inFlightDownload = await ensureDownloadInFlight(
2219
- profileId,
2220
- targetId,
2221
- tab,
2222
- operationState,
2223
- requestedPath
2224
- );
2225
- const download = await resolveDownloadArtifactPath(operationState, inFlightDownload, requestedPath);
2226
- operationState.latestDownload = undefined;
2227
- operationState.latestRawDownload = undefined;
2228
- operationState.requestedDownloadPath = undefined;
2229
-
2230
- return {
2231
- path: download.path,
2232
- profile: profileId,
2233
- targetId,
2234
- endpoint,
2235
- uploadFiles: [...operationState.uploadFiles],
2236
- dialogArmedCount: operationState.dialogArmedCount,
2237
- triggerCount: operationState.triggerCount,
2238
- ...(download.suggestedFilename !== undefined
2239
- ? { suggestedFilename: download.suggestedFilename }
2240
- : {}),
2241
- ...(download.url !== undefined ? { url: download.url } : {}),
2242
- ...(download.mimeType !== undefined ? { mimeType: download.mimeType } : {})
2243
- };
2244
- },
2245
- triggerDownload: async (targetId, profile) => {
2246
- const profileId = resolveProfileId(profile);
2247
- const { tab } = requireTargetInProfile(profileId, targetId);
2248
- const operationState = getOrCreateTargetOperationState(profileId, targetId);
2249
- operationState.triggerCount += 1;
2250
- operationState.latestDownload = undefined;
2251
- operationState.latestRawDownload = undefined;
2252
- operationState.requestedDownloadPath = undefined;
2253
- void ensureDownloadInFlight(profileId, targetId, tab, operationState);
2254
- },
2255
- getConsoleEntries: (targetId, profile) => {
2256
- const telemetryState = findTelemetryState(targetId, profile);
2257
- return telemetryState === undefined ? [] : telemetryState.consoleEntries.map(cloneConsoleEntry);
2258
- },
2259
- getNetworkResponseBody: (requestId, targetId, profile) => {
2260
- const body = findTelemetryState(targetId, profile)?.networkResponseBodies.get(requestId);
2261
- return body === undefined ? undefined : { ...body };
2262
- }
2263
- };
2264
- }