@flrande/browserctl 0.5.0-dev.22.1 → 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 -59
  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 -512
  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 -1522
  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,727 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
-
3
- import { createRemoteCdpDriver } from "./index";
4
-
5
- type MockConsoleMessage = {
6
- type(): string;
7
- text(): string;
8
- location?(): {
9
- url?: string;
10
- lineNumber?: number;
11
- columnNumber?: number;
12
- };
13
- };
14
-
15
- type MockNetworkResponse = {
16
- url(): string;
17
- status(): number;
18
- request(): {
19
- method(): string;
20
- resourceType?(): string;
21
- };
22
- text?(): Promise<string>;
23
- body?(): Promise<Uint8Array | ArrayBuffer | string>;
24
- };
25
-
26
- type MockDialog = {
27
- accept?(): Promise<void>;
28
- dismiss?(): Promise<void>;
29
- };
30
-
31
- type MockDownload = {
32
- path?(): Promise<string>;
33
- saveAs?(path: string): Promise<void>;
34
- suggestedFilename?(): string;
35
- url?(): string;
36
- mimeType?(): string;
37
- };
38
-
39
- type MockPage = {
40
- goto(url: string): Promise<void>;
41
- bringToFront(): Promise<void>;
42
- close(): Promise<void>;
43
- url(): string;
44
- title(): Promise<string>;
45
- content(): Promise<string>;
46
- screenshot?(options?: Record<string, unknown>): Promise<unknown>;
47
- locator(selector: string): {
48
- click(): Promise<void>;
49
- fill(value: string): Promise<void>;
50
- type(value: string): Promise<void>;
51
- setInputFiles?(files: string[]): Promise<void>;
52
- };
53
- route?(url: string, handler: (...args: unknown[]) => Promise<void>): Promise<void>;
54
- unroute?(url: string, handler?: (...args: unknown[]) => Promise<void>): Promise<void>;
55
- keyboard?: {
56
- press(key: string): Promise<void>;
57
- };
58
- on?(eventName: string, listener: (payload: unknown) => unknown): void;
59
- waitForEvent?(eventName: string): Promise<unknown>;
60
- };
61
-
62
- type MockPageRecord = {
63
- page: MockPage;
64
- goto: ReturnType<typeof vi.fn>;
65
- bringToFront: ReturnType<typeof vi.fn>;
66
- close: ReturnType<typeof vi.fn>;
67
- title: ReturnType<typeof vi.fn>;
68
- content: ReturnType<typeof vi.fn>;
69
- screenshot: ReturnType<typeof vi.fn>;
70
- locator: ReturnType<typeof vi.fn>;
71
- locatorClick: ReturnType<typeof vi.fn>;
72
- locatorFill: ReturnType<typeof vi.fn>;
73
- locatorType: ReturnType<typeof vi.fn>;
74
- locatorSetInputFiles: ReturnType<typeof vi.fn>;
75
- keyboardPress: ReturnType<typeof vi.fn>;
76
- route: ReturnType<typeof vi.fn>;
77
- unroute: ReturnType<typeof vi.fn>;
78
- waitForEvent: ReturnType<typeof vi.fn>;
79
- on: ReturnType<typeof vi.fn>;
80
- emitConsole(entry: MockConsoleMessage): Promise<void>;
81
- emitResponse(response: MockNetworkResponse): Promise<void>;
82
- emitDialog(dialog: MockDialog): Promise<void>;
83
- emitDownload(download: MockDownload): Promise<void>;
84
- getUrl(): string;
85
- };
86
-
87
- type RuntimeHarness = {
88
- runtime: {
89
- connectOverCDP(endpointUrl: string): Promise<{
90
- contexts(): Array<{
91
- newPage(): Promise<MockPage>;
92
- }>;
93
- close(): Promise<void>;
94
- }>;
95
- };
96
- connectOverCDP: ReturnType<typeof vi.fn>;
97
- browserClose: ReturnType<typeof vi.fn>;
98
- pages: MockPageRecord[];
99
- };
100
-
101
- type RuntimeHarnessOptions = {
102
- supportsDownloadEvents?: boolean;
103
- };
104
-
105
- function createMockPageRecord(pageNumber: number): MockPageRecord {
106
- let currentUrl = "about:blank";
107
- const listeners = new Map<string, Array<(payload: unknown) => unknown>>();
108
- const waiters = new Map<
109
- string,
110
- Array<{ resolve: (value: unknown) => void; reject: (error: unknown) => void }>
111
- >();
112
-
113
- const goto = vi.fn(async (url: string) => {
114
- currentUrl = url;
115
- });
116
- const bringToFront = vi.fn(async () => {});
117
- const close = vi.fn(async () => {});
118
- const title = vi.fn(async () => `mock-title-${pageNumber}`);
119
- const content = vi.fn(async () => `<html data-page="${pageNumber}" />`);
120
- const screenshot = vi.fn(async () => new Uint8Array([0, 1, 2, pageNumber]));
121
- const locatorClick = vi.fn(async () => {});
122
- const locatorFill = vi.fn(async (_value: string) => {});
123
- const locatorType = vi.fn(async (_value: string) => {});
124
- const locatorSetInputFiles = vi.fn(async (_files: string[]) => {});
125
- const locator = vi.fn((_selector: string) => ({
126
- click: locatorClick,
127
- fill: locatorFill,
128
- type: locatorType,
129
- setInputFiles: locatorSetInputFiles
130
- }));
131
- const keyboardPress = vi.fn(async (_key: string) => {});
132
- const route = vi.fn(async (_url: string, _handler: (...args: unknown[]) => Promise<void>) => {});
133
- const unroute = vi.fn(async (_url: string, _handler?: (...args: unknown[]) => Promise<void>) => {});
134
- const waitForEvent = vi.fn(async (eventName: string) => {
135
- return await new Promise<unknown>((resolve, reject) => {
136
- const current = waiters.get(eventName);
137
- if (current !== undefined) {
138
- current.push({ resolve, reject });
139
- return;
140
- }
141
-
142
- waiters.set(eventName, [{ resolve, reject }]);
143
- });
144
- });
145
- const on = vi.fn((eventName: string, listener: (payload: unknown) => unknown) => {
146
- const existing = listeners.get(eventName);
147
- if (existing !== undefined) {
148
- existing.push(listener);
149
- return;
150
- }
151
-
152
- listeners.set(eventName, [listener]);
153
- });
154
- const emit = async (eventName: string, payload: unknown): Promise<void> => {
155
- const eventListeners = listeners.get(eventName);
156
- if (eventListeners !== undefined) {
157
- for (const listener of eventListeners) {
158
- await listener(payload);
159
- }
160
- }
161
-
162
- const eventWaiters = waiters.get(eventName);
163
- if (eventWaiters === undefined || eventWaiters.length === 0) {
164
- return;
165
- }
166
-
167
- const nextWaiter = eventWaiters.shift();
168
- nextWaiter?.resolve(payload);
169
- };
170
-
171
- const page: MockPage = {
172
- goto,
173
- bringToFront,
174
- close,
175
- url: () => currentUrl,
176
- title,
177
- content,
178
- screenshot,
179
- locator,
180
- route,
181
- unroute,
182
- on,
183
- waitForEvent,
184
- keyboard: {
185
- press: keyboardPress
186
- }
187
- };
188
-
189
- return {
190
- page,
191
- goto,
192
- bringToFront,
193
- close,
194
- title,
195
- content,
196
- screenshot,
197
- locator,
198
- locatorClick,
199
- locatorFill,
200
- locatorType,
201
- locatorSetInputFiles,
202
- keyboardPress,
203
- route,
204
- unroute,
205
- waitForEvent,
206
- on,
207
- emitConsole: async (entry) => {
208
- await emit("console", entry);
209
- },
210
- emitResponse: async (response) => {
211
- await emit("response", response);
212
- },
213
- emitDialog: async (dialog) => {
214
- await emit("dialog", dialog);
215
- },
216
- emitDownload: async (download) => {
217
- await emit("download", download);
218
- },
219
- getUrl: () => currentUrl
220
- };
221
- }
222
-
223
- function createRuntimeHarness(options: RuntimeHarnessOptions = {}): RuntimeHarness {
224
- const supportsDownloadEvents = options.supportsDownloadEvents ?? false;
225
- const pages: MockPageRecord[] = [];
226
- const newPage = vi.fn(async () => {
227
- const pageRecord = createMockPageRecord(pages.length + 1);
228
- if (!supportsDownloadEvents) {
229
- delete (pageRecord.page as Record<string, unknown>).waitForEvent;
230
- }
231
- pages.push(pageRecord);
232
- return pageRecord.page;
233
- });
234
-
235
- const browserClose = vi.fn(async () => {});
236
- const connectOverCDP = vi.fn(async () => ({
237
- contexts: () => [
238
- {
239
- newPage
240
- }
241
- ],
242
- close: browserClose
243
- }));
244
-
245
- return {
246
- runtime: {
247
- connectOverCDP
248
- },
249
- connectOverCDP,
250
- browserClose,
251
- pages
252
- };
253
- }
254
-
255
- describe("createRemoteCdpDriver", () => {
256
- it("throws when cdpUrl is empty", () => {
257
- expect(() => createRemoteCdpDriver({ cdpUrl: "" })).toThrowError(
258
- "Invalid cdpUrl: value must not be empty."
259
- );
260
- });
261
-
262
- it("keeps status lazy and reports parsed endpoint", async () => {
263
- const harness = createRuntimeHarness();
264
- const driver = createRemoteCdpDriver({
265
- cdpUrl: " ws://localhost:9222/devtools/browser/example ",
266
- runtime: harness.runtime
267
- });
268
-
269
- expect(await driver.status()).toEqual({
270
- kind: "remote-cdp",
271
- connected: false,
272
- endpoint: {
273
- url: "ws://localhost:9222/devtools/browser/example",
274
- protocol: "ws:",
275
- host: "localhost:9222"
276
- }
277
- });
278
- expect(await driver.listTabs()).toEqual([]);
279
- expect(harness.connectOverCDP).toHaveBeenCalledTimes(0);
280
-
281
- await driver.openTab("https://example.com/a", "profile:alpha");
282
-
283
- expect(harness.connectOverCDP).toHaveBeenCalledWith(
284
- "ws://localhost:9222/devtools/browser/example"
285
- );
286
- expect(await driver.status()).toMatchObject({
287
- kind: "remote-cdp",
288
- connected: true
289
- });
290
- });
291
-
292
- it("open/list/focus/close keeps deterministic ids per profile", async () => {
293
- const harness = createRuntimeHarness();
294
- const driver = createRemoteCdpDriver({
295
- cdpUrl: "ws://localhost:9222/devtools/browser/example",
296
- runtime: harness.runtime
297
- });
298
- const alpha = "profile:alpha";
299
-
300
- const firstTarget = await driver.openTab("https://example.com/1", alpha);
301
- const secondTarget = await driver.openTab("https://example.com/2", alpha);
302
- const betaTarget = await driver.openTab("https://example.com/3", "profile:beta");
303
-
304
- expect(firstTarget).toBe("target:remote-cdp:profile:alpha:1");
305
- expect(secondTarget).toBe("target:remote-cdp:profile:alpha:2");
306
- expect(betaTarget).toBe("target:remote-cdp:profile:beta:1");
307
- expect(await driver.listTabs(alpha)).toEqual([firstTarget, secondTarget]);
308
-
309
- await driver.focusTab(secondTarget, alpha);
310
- expect(harness.pages[1]?.bringToFront).toHaveBeenCalledTimes(1);
311
-
312
- await driver.closeTab(firstTarget, alpha);
313
- expect(await driver.listTabs(alpha)).toEqual([secondTarget]);
314
- await driver.closeTab(secondTarget, alpha);
315
- expect(await driver.listTabs(alpha)).toEqual([]);
316
- });
317
-
318
- it("snapshot reports hasTarget true for active tab and false after close", async () => {
319
- const harness = createRuntimeHarness();
320
- const driver = createRemoteCdpDriver({
321
- cdpUrl: "ws://localhost:9222/devtools/browser/example",
322
- runtime: harness.runtime
323
- });
324
- const profile = "profile:alpha";
325
- const targetId = await driver.openTab("https://example.com/snapshot", profile);
326
-
327
- await expect(driver.snapshot(targetId, profile)).resolves.toEqual({
328
- kind: "remote-cdp",
329
- profile,
330
- targetId,
331
- endpoint: {
332
- url: "ws://localhost:9222/devtools/browser/example",
333
- protocol: "ws:",
334
- host: "localhost:9222"
335
- },
336
- hasTarget: true,
337
- url: "https://example.com/snapshot",
338
- title: "mock-title-1",
339
- html: '<html data-page="1" />',
340
- requestSummaries: []
341
- });
342
-
343
- await expect(driver.screenshot(targetId, profile)).resolves.toEqual({
344
- kind: "remote-cdp",
345
- profile,
346
- targetId,
347
- endpoint: {
348
- url: "ws://localhost:9222/devtools/browser/example",
349
- protocol: "ws:",
350
- host: "localhost:9222"
351
- },
352
- hasTarget: true,
353
- mimeType: "image/png",
354
- encoding: "base64",
355
- imageBase64: "AAECAQ=="
356
- });
357
- expect(harness.pages[0]?.screenshot).toHaveBeenCalledTimes(1);
358
-
359
- await driver.closeTab(targetId, profile);
360
-
361
- await expect(driver.snapshot(targetId, profile)).resolves.toEqual({
362
- kind: "remote-cdp",
363
- profile,
364
- targetId,
365
- endpoint: {
366
- url: "ws://localhost:9222/devtools/browser/example",
367
- protocol: "ws:",
368
- host: "localhost:9222"
369
- },
370
- hasTarget: false
371
- });
372
- await expect(driver.screenshot(targetId, profile)).resolves.toMatchObject({
373
- hasTarget: false
374
- });
375
- });
376
-
377
- it("act supports goto/click/fill/type/press and unknown action fallback", async () => {
378
- const harness = createRuntimeHarness();
379
- const driver = createRemoteCdpDriver({
380
- cdpUrl: "ws://localhost:9222/devtools/browser/example",
381
- runtime: harness.runtime
382
- });
383
- const profile = "profile:alpha";
384
- const targetId = await driver.openTab("https://example.com/start", profile);
385
- const pageRecord = harness.pages[0];
386
- if (pageRecord === undefined) {
387
- throw new Error("Expected a mock page record.");
388
- }
389
-
390
- await driver.act({ type: "goto", payload: { url: "https://example.com/next" } }, targetId, profile);
391
- await driver.act({ type: "click", payload: { selector: "#go" } }, targetId, profile);
392
- await driver.act(
393
- { type: "fill", payload: { selector: "#name", value: "remote" } },
394
- targetId,
395
- profile
396
- );
397
- await driver.act(
398
- { type: "type", payload: { selector: "#query", text: "remote cdp" } },
399
- targetId,
400
- profile
401
- );
402
- await driver.act({ type: "press", payload: { key: "Enter" } }, targetId, profile);
403
-
404
- await expect(driver.act({ type: "unknown:noop" }, targetId, profile)).resolves.toMatchObject({
405
- actionType: "unknown:noop",
406
- profile,
407
- targetId,
408
- targetKnown: true,
409
- ok: true,
410
- executed: false
411
- });
412
-
413
- expect(pageRecord.getUrl()).toBe("https://example.com/next");
414
- expect(pageRecord.locator).toHaveBeenCalledWith("#go");
415
- expect(pageRecord.locatorClick).toHaveBeenCalledTimes(1);
416
- expect(pageRecord.locatorFill).toHaveBeenCalledWith("remote");
417
- expect(pageRecord.locatorType).toHaveBeenCalledWith("remote cdp");
418
- expect(pageRecord.keyboardPress).toHaveBeenCalledWith("Enter");
419
- });
420
-
421
- it("supports network mock add/clear actions when runtime exposes route controls", async () => {
422
- const harness = createRuntimeHarness();
423
- const driver = createRemoteCdpDriver({
424
- cdpUrl: "ws://localhost:9222/devtools/browser/example",
425
- runtime: harness.runtime
426
- });
427
- const profile = "profile:alpha";
428
- const targetId = await driver.openTab("https://example.com/mock", profile);
429
- const pageRecord = harness.pages[0];
430
- if (pageRecord === undefined) {
431
- throw new Error("Expected a mock page record.");
432
- }
433
-
434
- const addResult = await driver.act(
435
- {
436
- type: "networkMockAdd",
437
- payload: {
438
- urlPattern: "**/api/**",
439
- method: "POST",
440
- status: 201,
441
- body: '{"ok":true}',
442
- contentType: "application/json"
443
- }
444
- },
445
- targetId,
446
- profile
447
- );
448
- expect(addResult).toMatchObject({
449
- ok: true,
450
- executed: true,
451
- data: {
452
- mockId: expect.any(String),
453
- urlPattern: "**/api/**",
454
- method: "POST",
455
- status: 201
456
- }
457
- });
458
- expect(pageRecord.route).toHaveBeenCalledWith("**/api/**", expect.any(Function));
459
-
460
- const mockId = (addResult as { data?: { mockId?: string } }).data?.mockId;
461
- if (typeof mockId !== "string") {
462
- throw new Error("Expected mockId from networkMockAdd action.");
463
- }
464
-
465
- const clearResult = await driver.act(
466
- {
467
- type: "networkMockClear",
468
- payload: {
469
- mockId
470
- }
471
- },
472
- targetId,
473
- profile
474
- );
475
- expect(clearResult).toMatchObject({
476
- ok: true,
477
- executed: true,
478
- data: {
479
- cleared: 1,
480
- mockId
481
- }
482
- });
483
- expect(pageRecord.unroute).toHaveBeenCalledWith("**/api/**", expect.any(Function));
484
- });
485
-
486
- it("captures console + network telemetry and supports request body lookup", async () => {
487
- const harness = createRuntimeHarness();
488
- const driver = createRemoteCdpDriver({
489
- cdpUrl: "ws://localhost:9222/devtools/browser/example",
490
- runtime: harness.runtime
491
- });
492
- const profile = "profile:alpha";
493
- const targetId = await driver.openTab("https://example.com/events", profile);
494
- const pageRecord = harness.pages[0];
495
- if (pageRecord === undefined) {
496
- throw new Error("Expected a mock page record.");
497
- }
498
-
499
- await pageRecord.emitConsole({
500
- type: () => "warning",
501
- text: () => "remote warning",
502
- location: () => ({
503
- url: "https://example.com/app.js",
504
- lineNumber: 9,
505
- columnNumber: 4
506
- })
507
- });
508
-
509
- await pageRecord.emitResponse({
510
- url: () => "https://example.com/api/items",
511
- status: () => 200,
512
- request: () => ({
513
- method: () => "GET",
514
- resourceType: () => "xhr"
515
- }),
516
- text: async () => '{"ok":true}'
517
- });
518
-
519
- await pageRecord.emitResponse({
520
- url: () => "https://example.com/api/binary",
521
- status: () => 201,
522
- request: () => ({
523
- method: () => "POST",
524
- resourceType: () => "fetch"
525
- }),
526
- body: async () => new Uint8Array([0, 1, 2])
527
- });
528
-
529
- const telemetryDriver = driver as typeof driver & {
530
- getConsoleEntries?: (targetId: string, profile?: string) => Array<{
531
- text: string;
532
- type: string;
533
- }>;
534
- getNetworkResponseBody?: (
535
- requestId: string,
536
- targetId: string,
537
- profile?: string
538
- ) => { body: string; encoding: "utf8" | "base64" } | undefined;
539
- };
540
-
541
- const consoleEntries = telemetryDriver.getConsoleEntries?.(targetId, profile) ?? [];
542
- expect(consoleEntries).toEqual([
543
- expect.objectContaining({
544
- type: "warning",
545
- text: "remote warning",
546
- location: {
547
- url: "https://example.com/app.js",
548
- lineNumber: 9,
549
- columnNumber: 4
550
- }
551
- })
552
- ]);
553
- consoleEntries[0].text = "mutated";
554
- expect(telemetryDriver.getConsoleEntries?.(targetId, profile)?.[0]?.text).toBe("remote warning");
555
-
556
- const snapshot = await driver.snapshot(targetId, profile);
557
- expect(snapshot).toMatchObject({
558
- hasTarget: true,
559
- requestSummaries: [
560
- expect.objectContaining({
561
- requestId: expect.any(String),
562
- url: "https://example.com/api/items",
563
- method: "GET",
564
- resourceType: "xhr",
565
- status: 200
566
- }),
567
- expect.objectContaining({
568
- requestId: expect.any(String),
569
- url: "https://example.com/api/binary",
570
- method: "POST",
571
- resourceType: "fetch",
572
- status: 201
573
- })
574
- ]
575
- });
576
-
577
- const requestSummaries = (
578
- snapshot as {
579
- requestSummaries?: Array<{ requestId: string; url: string }>;
580
- }
581
- ).requestSummaries;
582
- const firstRequestId = requestSummaries?.[0]?.requestId;
583
- const secondRequestId = requestSummaries?.[1]?.requestId;
584
- if (typeof firstRequestId !== "string" || typeof secondRequestId !== "string") {
585
- throw new Error("Expected request ids from telemetry snapshot.");
586
- }
587
-
588
- expect(telemetryDriver.getNetworkResponseBody?.(firstRequestId, targetId, profile)).toEqual({
589
- body: '{"ok":true}',
590
- encoding: "utf8"
591
- });
592
- expect(telemetryDriver.getNetworkResponseBody?.(secondRequestId, targetId, profile)).toEqual({
593
- body: "AAEC",
594
- encoding: "base64"
595
- });
596
- expect(telemetryDriver.getNetworkResponseBody?.("request:missing", targetId, profile)).toBeUndefined();
597
- });
598
-
599
- it("keeps upload/dialog/download state scoped to target + profile", async () => {
600
- const harness = createRuntimeHarness();
601
- const driver = createRemoteCdpDriver({
602
- cdpUrl: "ws://localhost:9222/devtools/browser/example",
603
- runtime: harness.runtime
604
- });
605
- const alphaProfile = "profile:alpha";
606
- const betaProfile = "profile:beta";
607
- const alphaTarget = await driver.openTab("https://example.com/alpha", alphaProfile);
608
- const betaTarget = await driver.openTab("https://example.com/beta", betaProfile);
609
-
610
- await expect(driver.armUpload(alphaTarget, ["wrong-profile.txt"], betaProfile)).rejects.toThrowError(
611
- `Unknown targetId: ${alphaTarget} (profile: ${betaProfile})`
612
- );
613
-
614
- await driver.armUpload(alphaTarget, ["alpha.txt"], alphaProfile);
615
- await driver.armUpload(betaTarget, ["beta.txt"], betaProfile);
616
- await driver.armDialog(betaTarget, betaProfile);
617
- await driver.triggerDownload(alphaTarget, alphaProfile);
618
- await driver.triggerDownload(betaTarget, betaProfile);
619
-
620
- await expect(driver.waitDownload(alphaTarget, alphaProfile)).resolves.toEqual({
621
- path: "remote-cdp-profile%3Aalpha-target%3Aremote-cdp%3Aprofile%3Aalpha%3A1-1.bin",
622
- profile: alphaProfile,
623
- targetId: alphaTarget,
624
- endpoint: {
625
- url: "ws://localhost:9222/devtools/browser/example",
626
- protocol: "ws:",
627
- host: "localhost:9222"
628
- },
629
- uploadFiles: ["alpha.txt"],
630
- dialogArmedCount: 0,
631
- triggerCount: 1
632
- });
633
-
634
- await expect(driver.waitDownload(betaTarget, betaProfile)).resolves.toEqual({
635
- path: "remote-cdp-profile%3Abeta-target%3Aremote-cdp%3Aprofile%3Abeta%3A1-1.bin",
636
- profile: betaProfile,
637
- targetId: betaTarget,
638
- endpoint: {
639
- url: "ws://localhost:9222/devtools/browser/example",
640
- protocol: "ws:",
641
- host: "localhost:9222"
642
- },
643
- uploadFiles: ["beta.txt"],
644
- dialogArmedCount: 1,
645
- triggerCount: 1
646
- });
647
- });
648
-
649
- it("supports real upload/dialog/download hooks when runtime exposes events", async () => {
650
- const harness = createRuntimeHarness({
651
- supportsDownloadEvents: true
652
- });
653
- const driver = createRemoteCdpDriver({
654
- cdpUrl: "ws://localhost:9222/devtools/browser/example",
655
- runtime: harness.runtime
656
- });
657
- const profile = "profile:alpha";
658
- const targetId = await driver.openTab("https://example.com/upload", profile);
659
- const pageRecord = harness.pages[0];
660
- if (pageRecord === undefined) {
661
- throw new Error("Expected page record.");
662
- }
663
-
664
- await driver.armUpload(targetId, ["C:\\upload\\alpha.txt"], profile);
665
- await driver.act({ type: "click", payload: { selector: "#file" } }, targetId, profile);
666
- expect(pageRecord.locatorSetInputFiles).toHaveBeenCalledWith(["C:\\upload\\alpha.txt"]);
667
-
668
- const dialogAccept = vi.fn(async () => {});
669
- await driver.armDialog(targetId, profile);
670
- await pageRecord.emitDialog({
671
- accept: dialogAccept
672
- });
673
- expect(dialogAccept).toHaveBeenCalledTimes(1);
674
-
675
- const saveAs = vi.fn(async (_path: string) => {});
676
- await driver.triggerDownload(targetId, profile);
677
- await pageRecord.emitDownload({
678
- path: async () => "C:\\downloads\\alpha.bin",
679
- saveAs,
680
- suggestedFilename: () => "alpha.bin",
681
- url: () => "https://example.com/alpha.bin",
682
- mimeType: () => "application/octet-stream"
683
- });
684
- await expect(
685
- driver.waitDownload(targetId, profile, "C:\\downloads\\saved-alpha.bin")
686
- ).resolves.toMatchObject({
687
- path: "C:\\downloads\\saved-alpha.bin",
688
- profile,
689
- targetId,
690
- suggestedFilename: "alpha.bin",
691
- url: "https://example.com/alpha.bin",
692
- mimeType: "application/octet-stream",
693
- triggerCount: 1
694
- });
695
- expect(saveAs).toHaveBeenCalledWith("C:\\downloads\\saved-alpha.bin");
696
- });
697
-
698
- it("fails waitDownload when requested path cannot be persisted", async () => {
699
- const harness = createRuntimeHarness({
700
- supportsDownloadEvents: true
701
- });
702
- const driver = createRemoteCdpDriver({
703
- cdpUrl: "ws://localhost:9222/devtools/browser/example",
704
- runtime: harness.runtime
705
- });
706
- const profile = "profile:alpha";
707
- const targetId = await driver.openTab("https://example.com/upload", profile);
708
- const pageRecord = harness.pages[0];
709
- if (pageRecord === undefined) {
710
- throw new Error("Expected page record.");
711
- }
712
-
713
- await driver.triggerDownload(targetId, profile);
714
- const saveAs = vi.fn(async () => {
715
- throw new Error("permission denied");
716
- });
717
- await pageRecord.emitDownload({
718
- path: async () => "C:\\downloads\\alpha.bin",
719
- saveAs
720
- });
721
-
722
- await expect(
723
- driver.waitDownload(targetId, profile, "C:\\downloads\\saved-alpha.bin")
724
- ).rejects.toThrow("Failed to persist download to requested path");
725
- expect(saveAs).toHaveBeenCalledWith("C:\\downloads\\saved-alpha.bin");
726
- });
727
- });