@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,608 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
-
3
- import {
4
- createManagedLocalDriver,
5
- type ManagedLocalBrowser,
6
- type ManagedLocalBrowserContext,
7
- type ManagedLocalDriverRuntime,
8
- type ManagedLocalPage
9
- } from "./index";
10
-
11
- type MockPageRecord = {
12
- page: ManagedLocalPage;
13
- goto: ReturnType<typeof vi.fn>;
14
- bringToFront: ReturnType<typeof vi.fn>;
15
- close: ReturnType<typeof vi.fn>;
16
- title: ReturnType<typeof vi.fn>;
17
- content: ReturnType<typeof vi.fn>;
18
- screenshot: ReturnType<typeof vi.fn>;
19
- locator: ReturnType<typeof vi.fn>;
20
- locatorClick: ReturnType<typeof vi.fn>;
21
- locatorFill: ReturnType<typeof vi.fn>;
22
- locatorType: ReturnType<typeof vi.fn>;
23
- locatorSetInputFiles: ReturnType<typeof vi.fn>;
24
- keyboardPress: ReturnType<typeof vi.fn>;
25
- route: ReturnType<typeof vi.fn>;
26
- unroute: ReturnType<typeof vi.fn>;
27
- waitForEvent: ReturnType<typeof vi.fn>;
28
- on: ReturnType<typeof vi.fn>;
29
- emitConsole(entry: MockConsoleMessage): Promise<void>;
30
- emitResponse(response: MockNetworkResponse): Promise<void>;
31
- emitDialog(dialog: MockDialog): Promise<void>;
32
- emitDownload(download: MockDownload): Promise<void>;
33
- getUrl(): string;
34
- };
35
-
36
- type RuntimeHarness = {
37
- runtime: ManagedLocalDriverRuntime;
38
- launch: ReturnType<typeof vi.fn>;
39
- newContext: ReturnType<typeof vi.fn>;
40
- browserClose: ReturnType<typeof vi.fn>;
41
- contextClose: ReturnType<typeof vi.fn>;
42
- pages: MockPageRecord[];
43
- };
44
-
45
- type RuntimeHarnessOptions = {
46
- newContextDelayMs?: number;
47
- supportsDownloadEvents?: boolean;
48
- };
49
-
50
- type MockConsoleMessage = {
51
- type(): string;
52
- text(): string;
53
- location?(): {
54
- url?: string;
55
- lineNumber?: number;
56
- columnNumber?: number;
57
- };
58
- };
59
-
60
- type MockNetworkResponse = {
61
- url(): string;
62
- status(): number;
63
- request(): {
64
- method(): string;
65
- resourceType?(): string;
66
- };
67
- text?(): Promise<string>;
68
- body?(): Promise<Uint8Array | string>;
69
- };
70
-
71
- type MockDialog = {
72
- accept?(): Promise<void>;
73
- dismiss?(): Promise<void>;
74
- };
75
-
76
- type MockDownload = {
77
- path?(): Promise<string>;
78
- saveAs?(path: string): Promise<void>;
79
- suggestedFilename?(): string;
80
- url?(): string;
81
- mimeType?(): string;
82
- };
83
-
84
- function createMockPageRecord(pageNumber: number): MockPageRecord {
85
- let currentUrl = "about:blank";
86
- const listeners = new Map<string, Array<(payload: unknown) => unknown>>();
87
- const waiters = new Map<
88
- string,
89
- Array<{ resolve: (value: unknown) => void; reject: (error: unknown) => void }>
90
- >();
91
-
92
- const goto = vi.fn(async (url: string) => {
93
- currentUrl = url;
94
- });
95
- const bringToFront = vi.fn(async () => {});
96
- const close = vi.fn(async () => {});
97
- const title = vi.fn(async () => `mock-title-${pageNumber}`);
98
- const content = vi.fn(async () => `<html data-page="${pageNumber}" />`);
99
- const screenshot = vi.fn(async () => new Uint8Array([0, 1, 2, pageNumber]));
100
- const locatorClick = vi.fn(async () => {});
101
- const locatorFill = vi.fn(async (_value: string) => {});
102
- const locatorType = vi.fn(async (_value: string) => {});
103
- const locatorSetInputFiles = vi.fn(async (_files: string[]) => {});
104
- const locator = vi.fn((_selector: string) => ({
105
- click: locatorClick,
106
- fill: locatorFill,
107
- type: locatorType,
108
- setInputFiles: locatorSetInputFiles
109
- }));
110
- const keyboardPress = vi.fn(async (_key: string) => {});
111
- const route = vi.fn(async (_url: string, _handler: (...args: unknown[]) => Promise<void>) => {});
112
- const unroute = vi.fn(async (_url: string, _handler?: (...args: unknown[]) => Promise<void>) => {});
113
- const waitForEvent = vi.fn(async (eventName: string) => {
114
- return await new Promise<unknown>((resolve, reject) => {
115
- const current = waiters.get(eventName);
116
- if (current !== undefined) {
117
- current.push({ resolve, reject });
118
- return;
119
- }
120
-
121
- waiters.set(eventName, [{ resolve, reject }]);
122
- });
123
- });
124
- const on = vi.fn((eventName: string, listener: (payload: unknown) => unknown) => {
125
- const eventListeners = listeners.get(eventName);
126
- if (eventListeners !== undefined) {
127
- eventListeners.push(listener);
128
- return;
129
- }
130
-
131
- listeners.set(eventName, [listener]);
132
- });
133
- const emit = async (eventName: string, payload: unknown): Promise<void> => {
134
- const eventListeners = listeners.get(eventName);
135
- if (eventListeners !== undefined) {
136
- for (const listener of eventListeners) {
137
- await listener(payload);
138
- }
139
- }
140
-
141
- const eventWaiters = waiters.get(eventName);
142
- if (eventWaiters === undefined || eventWaiters.length === 0) {
143
- return;
144
- }
145
-
146
- const nextWaiter = eventWaiters.shift();
147
- nextWaiter?.resolve(payload);
148
- };
149
- const page: ManagedLocalPage & { on: typeof on; waitForEvent: typeof waitForEvent } = {
150
- goto,
151
- bringToFront,
152
- close,
153
- url: () => currentUrl,
154
- title,
155
- content,
156
- screenshot,
157
- locator,
158
- route,
159
- unroute,
160
- on,
161
- waitForEvent,
162
- keyboard: {
163
- press: keyboardPress
164
- }
165
- };
166
-
167
- return {
168
- page,
169
- goto,
170
- bringToFront,
171
- close,
172
- title,
173
- content,
174
- screenshot,
175
- locator,
176
- locatorClick,
177
- locatorFill,
178
- locatorType,
179
- locatorSetInputFiles,
180
- keyboardPress,
181
- route,
182
- unroute,
183
- waitForEvent,
184
- on,
185
- emitConsole: async (entry) => {
186
- await emit("console", entry);
187
- },
188
- emitResponse: async (response) => {
189
- await emit("response", response);
190
- },
191
- emitDialog: async (dialog) => {
192
- await emit("dialog", dialog);
193
- },
194
- emitDownload: async (download) => {
195
- await emit("download", download);
196
- },
197
- getUrl: () => currentUrl
198
- };
199
- }
200
-
201
- function createRuntimeHarness(options: RuntimeHarnessOptions = {}): RuntimeHarness {
202
- const newContextDelayMs = options.newContextDelayMs ?? 0;
203
- const supportsDownloadEvents = options.supportsDownloadEvents ?? false;
204
- const pages: MockPageRecord[] = [];
205
- const contextClose = vi.fn(async () => {});
206
- const newPage = vi.fn(async () => {
207
- const pageRecord = createMockPageRecord(pages.length + 1);
208
- if (!supportsDownloadEvents) {
209
- delete (pageRecord.page as Record<string, unknown>).waitForEvent;
210
- }
211
- pages.push(pageRecord);
212
- return pageRecord.page;
213
- });
214
-
215
- const newContext = vi.fn(async (): Promise<ManagedLocalBrowserContext> => {
216
- if (newContextDelayMs > 0) {
217
- await new Promise<void>((resolve) => {
218
- setTimeout(resolve, newContextDelayMs);
219
- });
220
- }
221
-
222
- return {
223
- newPage,
224
- close: contextClose
225
- };
226
- });
227
-
228
- const browserClose = vi.fn(async () => {});
229
- const launch = vi.fn(async (): Promise<ManagedLocalBrowser> => ({
230
- newContext,
231
- close: browserClose
232
- }));
233
-
234
- return {
235
- runtime: { launch },
236
- launch,
237
- newContext,
238
- browserClose,
239
- contextClose,
240
- pages
241
- };
242
- }
243
-
244
- describe("createManagedLocalDriver", () => {
245
- it("does not launch a browser during status/list-only calls", async () => {
246
- const harness = createRuntimeHarness();
247
- const driver = createManagedLocalDriver({
248
- runtime: harness.runtime,
249
- browserName: "firefox",
250
- headless: false,
251
- channel: "firefox",
252
- launchTimeoutMs: 4_000,
253
- args: ["--private"]
254
- });
255
-
256
- expect(await driver.status()).toMatchObject({
257
- kind: "managed-local",
258
- launched: false,
259
- browserName: "firefox",
260
- headless: false
261
- });
262
- expect(await driver.listTabs()).toEqual([]);
263
- expect(harness.launch).toHaveBeenCalledTimes(0);
264
- });
265
-
266
- it("lazily launches and supports tab lifecycle, snapshot, and actions", async () => {
267
- const harness = createRuntimeHarness();
268
- const driver = createManagedLocalDriver({
269
- runtime: harness.runtime,
270
- browserName: "webkit",
271
- headless: true
272
- });
273
-
274
- const targetId = await driver.openTab("https://example.com/a");
275
-
276
- expect(harness.launch).toHaveBeenCalledTimes(1);
277
- expect(harness.launch).toHaveBeenCalledWith({
278
- browserName: "webkit",
279
- headless: true,
280
- channel: undefined,
281
- executablePath: undefined,
282
- launchTimeoutMs: undefined,
283
- args: []
284
- });
285
- expect(await driver.listTabs()).toEqual([targetId]);
286
-
287
- await driver.focusTab(targetId);
288
- expect(harness.pages[0]?.bringToFront).toHaveBeenCalledTimes(1);
289
-
290
- const clickResult = await driver.act(
291
- { type: "click", payload: { selector: "#go" } },
292
- targetId
293
- );
294
- expect(clickResult).toMatchObject({
295
- actionType: "click",
296
- targetId,
297
- targetKnown: true,
298
- ok: true
299
- });
300
- expect(harness.pages[0]?.locator).toHaveBeenCalledWith("#go");
301
- expect(harness.pages[0]?.locatorClick).toHaveBeenCalledTimes(1);
302
-
303
- const snapshot = await driver.snapshot(targetId);
304
- expect(snapshot).toEqual({
305
- kind: "managed-local",
306
- profile: "profile:managed-local:default",
307
- targetId,
308
- hasTarget: true,
309
- url: "https://example.com/a",
310
- title: "mock-title-1",
311
- html: '<html data-page="1" />',
312
- requestSummaries: []
313
- });
314
-
315
- await expect(driver.screenshot(targetId)).resolves.toEqual({
316
- kind: "managed-local",
317
- profile: "profile:managed-local:default",
318
- targetId,
319
- hasTarget: true,
320
- mimeType: "image/png",
321
- encoding: "base64",
322
- imageBase64: "AAECAQ=="
323
- });
324
- expect(harness.pages[0]?.screenshot).toHaveBeenCalledTimes(1);
325
-
326
- await driver.closeTab(targetId);
327
- expect(harness.pages[0]?.close).toHaveBeenCalledTimes(1);
328
- expect(await driver.listTabs()).toEqual([]);
329
- await expect(driver.snapshot(targetId)).resolves.toMatchObject({
330
- hasTarget: false
331
- });
332
- await expect(driver.screenshot(targetId)).resolves.toMatchObject({
333
- hasTarget: false
334
- });
335
- });
336
-
337
- it("collects console and network events and exposes response-body lookup", async () => {
338
- const harness = createRuntimeHarness();
339
- const driver = createManagedLocalDriver({ runtime: harness.runtime });
340
- const profile = "profile:alpha";
341
- const targetId = await driver.openTab("https://example.com/events", profile);
342
- const pageRecord = harness.pages[0];
343
- if (pageRecord === undefined) {
344
- throw new Error("Expected a mock page to be created.");
345
- }
346
-
347
- await pageRecord.emitConsole({
348
- type: () => "warning",
349
- text: () => "managed-local warning",
350
- location: () => ({
351
- url: "https://example.com/app.js",
352
- lineNumber: 7,
353
- columnNumber: 2
354
- })
355
- });
356
- await pageRecord.emitResponse({
357
- url: () => "https://example.com/api/items",
358
- status: () => 200,
359
- request: () => ({
360
- method: () => "GET",
361
- resourceType: () => "xhr"
362
- }),
363
- text: async () => '{"ok":true}'
364
- });
365
-
366
- expect(pageRecord.on).toHaveBeenCalledWith("console", expect.any(Function));
367
- expect(pageRecord.on).toHaveBeenCalledWith("response", expect.any(Function));
368
-
369
- const telemetryDriver = driver as typeof driver & {
370
- getConsoleEntries?: (targetId: string, profile?: string) => unknown[];
371
- getNetworkResponseBody?: (
372
- requestId: string,
373
- targetId: string,
374
- profile?: string
375
- ) => { body: string; encoding: string } | undefined;
376
- };
377
- expect(telemetryDriver.getConsoleEntries).toBeTypeOf("function");
378
- expect(telemetryDriver.getNetworkResponseBody).toBeTypeOf("function");
379
-
380
- const consoleEntries = telemetryDriver.getConsoleEntries?.(targetId) ?? [];
381
- expect(consoleEntries).toEqual([
382
- expect.objectContaining({
383
- type: "warning",
384
- text: "managed-local warning",
385
- location: {
386
- url: "https://example.com/app.js",
387
- lineNumber: 7,
388
- columnNumber: 2
389
- }
390
- })
391
- ]);
392
-
393
- const snapshot = await driver.snapshot(targetId, profile);
394
- expect(snapshot).toMatchObject({
395
- hasTarget: true,
396
- requestSummaries: [
397
- expect.objectContaining({
398
- requestId: expect.any(String),
399
- url: "https://example.com/api/items",
400
- method: "GET",
401
- resourceType: "xhr",
402
- status: 200
403
- })
404
- ]
405
- });
406
-
407
- const requestId = (
408
- snapshot as {
409
- requestSummaries?: Array<{ requestId?: string }>;
410
- }
411
- ).requestSummaries?.[0]?.requestId;
412
- if (typeof requestId !== "string") {
413
- throw new Error("Expected requestId in snapshot metadata.");
414
- }
415
-
416
- expect(telemetryDriver.getNetworkResponseBody?.(requestId, targetId)).toEqual({
417
- body: '{"ok":true}',
418
- encoding: "utf8"
419
- });
420
- });
421
-
422
- it("supports network mock add/clear actions when runtime exposes route controls", async () => {
423
- const harness = createRuntimeHarness();
424
- const driver = createManagedLocalDriver({ runtime: harness.runtime });
425
- const targetId = await driver.openTab("https://example.com/mock");
426
- const pageRecord = harness.pages[0];
427
- if (pageRecord === undefined) {
428
- throw new Error("Expected a mock page to be created.");
429
- }
430
-
431
- const addResult = await driver.act(
432
- {
433
- type: "networkMockAdd",
434
- payload: {
435
- urlPattern: "**/api/**",
436
- method: "POST",
437
- status: 201,
438
- body: '{"ok":true}',
439
- contentType: "application/json"
440
- }
441
- },
442
- targetId
443
- );
444
-
445
- expect(addResult).toMatchObject({
446
- ok: true,
447
- executed: true,
448
- data: {
449
- mockId: expect.any(String),
450
- urlPattern: "**/api/**",
451
- method: "POST",
452
- status: 201
453
- }
454
- });
455
- expect(pageRecord.route).toHaveBeenCalledWith("**/api/**", expect.any(Function));
456
-
457
- const mockId = (addResult as { data?: { mockId?: string } }).data?.mockId;
458
- if (typeof mockId !== "string") {
459
- throw new Error("Expected mockId from networkMockAdd action.");
460
- }
461
-
462
- const clearResult = await driver.act(
463
- {
464
- type: "networkMockClear",
465
- payload: {
466
- mockId
467
- }
468
- },
469
- targetId
470
- );
471
- expect(clearResult).toMatchObject({
472
- ok: true,
473
- executed: true,
474
- data: {
475
- cleared: 1,
476
- mockId
477
- }
478
- });
479
- expect(pageRecord.unroute).toHaveBeenCalledWith("**/api/**", expect.any(Function));
480
- });
481
-
482
- it("keeps upload/dialog/download state scoped to profile + target", async () => {
483
- const harness = createRuntimeHarness();
484
- const driver = createManagedLocalDriver({ runtime: harness.runtime });
485
- const profile = "profile:alpha";
486
- const targetId = await driver.openTab("https://example.com/download", profile);
487
-
488
- await expect(driver.armUpload(targetId, ["alpha.txt"], "profile:beta")).rejects.toThrowError(
489
- `Unknown targetId: ${targetId} (profile: profile:beta)`
490
- );
491
-
492
- await driver.armUpload(targetId, ["alpha.txt"], profile);
493
- await driver.armDialog(targetId, profile);
494
- await driver.triggerDownload(targetId, profile);
495
-
496
- await expect(driver.waitDownload(targetId, profile)).resolves.toMatchObject({
497
- profile,
498
- targetId,
499
- uploadFiles: ["alpha.txt"],
500
- dialogArmedCount: 1,
501
- triggerCount: 1
502
- });
503
- });
504
-
505
- it("supports real upload/dialog/download hooks when runtime exposes events", async () => {
506
- const harness = createRuntimeHarness({
507
- supportsDownloadEvents: true
508
- });
509
- const driver = createManagedLocalDriver({
510
- runtime: harness.runtime
511
- });
512
- const profile = "profile:alpha";
513
- const targetId = await driver.openTab("https://example.com/upload", profile);
514
- const pageRecord = harness.pages[0];
515
- if (pageRecord === undefined) {
516
- throw new Error("Expected page record.");
517
- }
518
-
519
- await driver.armUpload(targetId, ["C:\\upload\\alpha.txt"], profile);
520
- await driver.act({ type: "click", payload: { selector: "#file" } }, targetId, profile);
521
- expect(pageRecord.locatorSetInputFiles).toHaveBeenCalledWith(["C:\\upload\\alpha.txt"]);
522
-
523
- const dialogAccept = vi.fn(async () => {});
524
- await driver.armDialog(targetId, profile);
525
- await pageRecord.emitDialog({
526
- accept: dialogAccept
527
- });
528
- expect(dialogAccept).toHaveBeenCalledTimes(1);
529
-
530
- const saveAs = vi.fn(async (_path: string) => {});
531
- await driver.triggerDownload(targetId, profile);
532
- await pageRecord.emitDownload({
533
- path: async () => "C:\\downloads\\alpha.bin",
534
- saveAs,
535
- suggestedFilename: () => "alpha.bin",
536
- url: () => "https://example.com/alpha.bin",
537
- mimeType: () => "application/octet-stream"
538
- });
539
- await expect(
540
- driver.waitDownload(targetId, profile, "C:\\downloads\\saved-alpha.bin")
541
- ).resolves.toMatchObject({
542
- path: "C:\\downloads\\saved-alpha.bin",
543
- profile,
544
- targetId,
545
- suggestedFilename: "alpha.bin",
546
- url: "https://example.com/alpha.bin",
547
- mimeType: "application/octet-stream",
548
- triggerCount: 1
549
- });
550
- expect(saveAs).toHaveBeenCalledWith("C:\\downloads\\saved-alpha.bin");
551
- });
552
-
553
- it("fails waitDownload when requested path cannot be persisted", async () => {
554
- const harness = createRuntimeHarness({
555
- supportsDownloadEvents: true
556
- });
557
- const driver = createManagedLocalDriver({
558
- runtime: harness.runtime
559
- });
560
- const profile = "profile:alpha";
561
- const targetId = await driver.openTab("https://example.com/upload", profile);
562
- const pageRecord = harness.pages[0];
563
- if (pageRecord === undefined) {
564
- throw new Error("Expected page record.");
565
- }
566
-
567
- await driver.triggerDownload(targetId, profile);
568
- const saveAs = vi.fn(async () => {
569
- throw new Error("permission denied");
570
- });
571
- await pageRecord.emitDownload({
572
- path: async () => "C:\\downloads\\alpha.bin",
573
- saveAs
574
- });
575
-
576
- await expect(
577
- driver.waitDownload(targetId, profile, "C:\\downloads\\saved-alpha.bin")
578
- ).rejects.toThrow("Failed to persist download to requested path");
579
- expect(saveAs).toHaveBeenCalledWith("C:\\downloads\\saved-alpha.bin");
580
- });
581
-
582
- it("reuses one browser launch while creating separate profile contexts", async () => {
583
- const harness = createRuntimeHarness();
584
- const driver = createManagedLocalDriver({ runtime: harness.runtime });
585
-
586
- await driver.openTab("https://example.com/alpha", "profile:alpha");
587
- await driver.openTab("https://example.com/beta", "profile:beta");
588
-
589
- expect(harness.launch).toHaveBeenCalledTimes(1);
590
- expect(harness.newContext).toHaveBeenCalledTimes(2);
591
- });
592
-
593
- it("serializes same-profile context init during concurrent openTab calls", async () => {
594
- const harness = createRuntimeHarness({ newContextDelayMs: 15 });
595
- const driver = createManagedLocalDriver({ runtime: harness.runtime });
596
-
597
- const [firstTarget, secondTarget] = await Promise.all([
598
- driver.openTab("https://example.com/concurrent/a", "profile:alpha"),
599
- driver.openTab("https://example.com/concurrent/b", "profile:alpha")
600
- ]);
601
-
602
- const tabs = await driver.listTabs("profile:alpha");
603
- expect(tabs).toHaveLength(2);
604
- expect(new Set(tabs)).toEqual(new Set([firstTarget, secondTarget]));
605
- expect(harness.launch).toHaveBeenCalledTimes(1);
606
- expect(harness.newContext).toHaveBeenCalledTimes(1);
607
- });
608
- });