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