@flrande/browserctl 0.1.0 → 0.2.0-dev.9.1

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.
@@ -0,0 +1,386 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const { callDaemonToolMock } = vi.hoisted(() => ({
4
+ callDaemonToolMock: vi.fn()
5
+ }));
6
+
7
+ vi.mock("../daemon-client", () => ({
8
+ callDaemonTool: callDaemonToolMock
9
+ }));
10
+
11
+ import { DAEMON_STARTUP_ARGUMENT } from "./common";
12
+ import { runA11ySnapshotCommand } from "./a11y-snapshot";
13
+ import { runActCommand } from "./act";
14
+ import { runConsoleListCommand } from "./console-list";
15
+ import { runCookieClearCommand } from "./cookie-clear";
16
+ import { runCookieGetCommand } from "./cookie-get";
17
+ import { runCookieSetCommand } from "./cookie-set";
18
+ import { runDialogArmCommand } from "./dialog-arm";
19
+ import { runDomQueryAllCommand } from "./dom-query-all";
20
+ import { runDomQueryCommand } from "./dom-query";
21
+ import { runDownloadTriggerCommand } from "./download-trigger";
22
+ import { runDownloadWaitCommand } from "./download-wait";
23
+ import { runElementScreenshotCommand } from "./element-screenshot";
24
+ import { runFrameListCommand } from "./frame-list";
25
+ import { runFrameSnapshotCommand } from "./frame-snapshot";
26
+ import { runNetworkWaitForCommand } from "./network-wait-for";
27
+ import { runProfileListCommand } from "./profile-list";
28
+ import { runProfileUseCommand } from "./profile-use";
29
+ import { runResponseBodyCommand } from "./response-body";
30
+ import { runScreenshotCommand } from "./screenshot";
31
+ import { runSnapshotCommand } from "./snapshot";
32
+ import { runStatusCommand } from "./status";
33
+ import { runStorageGetCommand } from "./storage-get";
34
+ import { runStorageSetCommand } from "./storage-set";
35
+ import { runTabCloseCommand } from "./tab-close";
36
+ import { runTabFocusCommand } from "./tab-focus";
37
+ import { runTabOpenCommand } from "./tab-open";
38
+ import { runTabsCommand } from "./tabs";
39
+ import { runUploadArmCommand } from "./upload-arm";
40
+
41
+ type WrapperCase = {
42
+ name: string;
43
+ run: () => Promise<unknown>;
44
+ expectedTool: string;
45
+ expectedArgs: Record<string, unknown>;
46
+ };
47
+
48
+ const BASE_ARGS = {
49
+ sessionId: "cli:local"
50
+ };
51
+
52
+ const CASES: WrapperCase[] = [
53
+ {
54
+ name: "status",
55
+ run: async () => await runStatusCommand(),
56
+ expectedTool: "browser.status",
57
+ expectedArgs: { ...BASE_ARGS }
58
+ },
59
+ {
60
+ name: "tabs",
61
+ run: async () => await runTabsCommand(),
62
+ expectedTool: "browser.tab.list",
63
+ expectedArgs: { ...BASE_ARGS }
64
+ },
65
+ {
66
+ name: "profile-list",
67
+ run: async () => await runProfileListCommand(),
68
+ expectedTool: "browser.profile.list",
69
+ expectedArgs: { ...BASE_ARGS }
70
+ },
71
+ {
72
+ name: "profile-use",
73
+ run: async () => await runProfileUseCommand(["managed"]),
74
+ expectedTool: "browser.profile.use",
75
+ expectedArgs: {
76
+ ...BASE_ARGS,
77
+ profile: "managed"
78
+ }
79
+ },
80
+ {
81
+ name: "tab-open",
82
+ run: async () => await runTabOpenCommand(["https://example.com"]),
83
+ expectedTool: "browser.tab.open",
84
+ expectedArgs: {
85
+ ...BASE_ARGS,
86
+ url: "https://example.com"
87
+ }
88
+ },
89
+ {
90
+ name: "tab-focus",
91
+ run: async () => await runTabFocusCommand(["target:1"]),
92
+ expectedTool: "browser.tab.focus",
93
+ expectedArgs: {
94
+ ...BASE_ARGS,
95
+ targetId: "target:1"
96
+ }
97
+ },
98
+ {
99
+ name: "tab-close",
100
+ run: async () => await runTabCloseCommand(["target:1"]),
101
+ expectedTool: "browser.tab.close",
102
+ expectedArgs: {
103
+ ...BASE_ARGS,
104
+ targetId: "target:1"
105
+ }
106
+ },
107
+ {
108
+ name: "snapshot",
109
+ run: async () => await runSnapshotCommand(["target:1"]),
110
+ expectedTool: "browser.snapshot",
111
+ expectedArgs: {
112
+ ...BASE_ARGS,
113
+ targetId: "target:1"
114
+ }
115
+ },
116
+ {
117
+ name: "screenshot",
118
+ run: async () => await runScreenshotCommand(["target:1"]),
119
+ expectedTool: "browser.screenshot",
120
+ expectedArgs: {
121
+ ...BASE_ARGS,
122
+ targetId: "target:1"
123
+ }
124
+ },
125
+ {
126
+ name: "dom-query",
127
+ run: async () => await runDomQueryCommand(["target:1", "#root"]),
128
+ expectedTool: "browser.dom.query",
129
+ expectedArgs: {
130
+ ...BASE_ARGS,
131
+ targetId: "target:1",
132
+ selector: "#root"
133
+ }
134
+ },
135
+ {
136
+ name: "dom-query-all",
137
+ run: async () => await runDomQueryAllCommand(["target:1", ".item"]),
138
+ expectedTool: "browser.dom.queryAll",
139
+ expectedArgs: {
140
+ ...BASE_ARGS,
141
+ targetId: "target:1",
142
+ selector: ".item"
143
+ }
144
+ },
145
+ {
146
+ name: "element-screenshot",
147
+ run: async () => await runElementScreenshotCommand(["target:1", "#hero"]),
148
+ expectedTool: "browser.element.screenshot",
149
+ expectedArgs: {
150
+ ...BASE_ARGS,
151
+ targetId: "target:1",
152
+ selector: "#hero"
153
+ }
154
+ },
155
+ {
156
+ name: "a11y-snapshot",
157
+ run: async () => await runA11ySnapshotCommand(["target:1", " #main "]),
158
+ expectedTool: "browser.a11y.snapshot",
159
+ expectedArgs: {
160
+ ...BASE_ARGS,
161
+ targetId: "target:1",
162
+ selector: "#main"
163
+ }
164
+ },
165
+ {
166
+ name: "network-wait-for",
167
+ run: async () =>
168
+ await runNetworkWaitForCommand([
169
+ "target:1",
170
+ "/api/items",
171
+ "--method",
172
+ "post",
173
+ "--status",
174
+ "201",
175
+ "--timeout-ms",
176
+ "3000",
177
+ "--poll-ms",
178
+ "50"
179
+ ]),
180
+ expectedTool: "browser.network.waitFor",
181
+ expectedArgs: {
182
+ ...BASE_ARGS,
183
+ targetId: "target:1",
184
+ urlPattern: "/api/items",
185
+ method: "POST",
186
+ status: 201,
187
+ timeoutMs: 3000,
188
+ pollMs: 50
189
+ }
190
+ },
191
+ {
192
+ name: "cookie-get",
193
+ run: async () => await runCookieGetCommand(["target:1", " sid "]),
194
+ expectedTool: "browser.cookie.get",
195
+ expectedArgs: {
196
+ ...BASE_ARGS,
197
+ targetId: "target:1",
198
+ name: "sid"
199
+ }
200
+ },
201
+ {
202
+ name: "cookie-set",
203
+ run: async () => await runCookieSetCommand(["target:1", "sid", "abc", " https://example.com "]),
204
+ expectedTool: "browser.cookie.set",
205
+ expectedArgs: {
206
+ ...BASE_ARGS,
207
+ targetId: "target:1",
208
+ name: "sid",
209
+ value: "abc",
210
+ url: "https://example.com"
211
+ }
212
+ },
213
+ {
214
+ name: "cookie-clear",
215
+ run: async () => await runCookieClearCommand(["target:1", " sid "]),
216
+ expectedTool: "browser.cookie.clear",
217
+ expectedArgs: {
218
+ ...BASE_ARGS,
219
+ targetId: "target:1",
220
+ name: "sid"
221
+ }
222
+ },
223
+ {
224
+ name: "storage-get",
225
+ run: async () => await runStorageGetCommand(["target:1", "local", "theme"]),
226
+ expectedTool: "browser.storage.get",
227
+ expectedArgs: {
228
+ ...BASE_ARGS,
229
+ targetId: "target:1",
230
+ scope: "local",
231
+ key: "theme"
232
+ }
233
+ },
234
+ {
235
+ name: "storage-set",
236
+ run: async () => await runStorageSetCommand(["target:1", "session", "token", "abc"]),
237
+ expectedTool: "browser.storage.set",
238
+ expectedArgs: {
239
+ ...BASE_ARGS,
240
+ targetId: "target:1",
241
+ scope: "session",
242
+ key: "token",
243
+ value: "abc"
244
+ }
245
+ },
246
+ {
247
+ name: "frame-list",
248
+ run: async () => await runFrameListCommand(["target:1"]),
249
+ expectedTool: "browser.frame.list",
250
+ expectedArgs: {
251
+ ...BASE_ARGS,
252
+ targetId: "target:1"
253
+ }
254
+ },
255
+ {
256
+ name: "frame-snapshot",
257
+ run: async () => await runFrameSnapshotCommand(["target:1", "frame:0"]),
258
+ expectedTool: "browser.frame.snapshot",
259
+ expectedArgs: {
260
+ ...BASE_ARGS,
261
+ targetId: "target:1",
262
+ frameId: "frame:0"
263
+ }
264
+ },
265
+ {
266
+ name: "act",
267
+ run: async () => await runActCommand(["click", "target:1"]),
268
+ expectedTool: "browser.act",
269
+ expectedArgs: {
270
+ ...BASE_ARGS,
271
+ targetId: "target:1",
272
+ action: {
273
+ type: "click"
274
+ }
275
+ }
276
+ },
277
+ {
278
+ name: "upload-arm",
279
+ run: async () => await runUploadArmCommand(["target:1", "a.txt", "b.txt"]),
280
+ expectedTool: "browser.upload.arm",
281
+ expectedArgs: {
282
+ ...BASE_ARGS,
283
+ targetId: "target:1",
284
+ files: ["a.txt", "b.txt"]
285
+ }
286
+ },
287
+ {
288
+ name: "dialog-arm",
289
+ run: async () => await runDialogArmCommand(["target:1"]),
290
+ expectedTool: "browser.dialog.arm",
291
+ expectedArgs: {
292
+ ...BASE_ARGS,
293
+ targetId: "target:1"
294
+ }
295
+ },
296
+ {
297
+ name: "download-trigger",
298
+ run: async () => await runDownloadTriggerCommand(["target:1"]),
299
+ expectedTool: "browser.download.trigger",
300
+ expectedArgs: {
301
+ ...BASE_ARGS,
302
+ targetId: "target:1"
303
+ }
304
+ },
305
+ {
306
+ name: "download-wait",
307
+ run: async () => await runDownloadWaitCommand(["target:1", "downloads/file.bin"]),
308
+ expectedTool: "browser.download.wait",
309
+ expectedArgs: {
310
+ ...BASE_ARGS,
311
+ targetId: "target:1",
312
+ path: "downloads/file.bin"
313
+ }
314
+ },
315
+ {
316
+ name: "console-list",
317
+ run: async () => await runConsoleListCommand(["target:1"]),
318
+ expectedTool: "browser.console.list",
319
+ expectedArgs: {
320
+ ...BASE_ARGS,
321
+ targetId: "target:1"
322
+ }
323
+ },
324
+ {
325
+ name: "response-body",
326
+ run: async () => await runResponseBodyCommand(["target:1", "request:1"]),
327
+ expectedTool: "browser.network.responseBody",
328
+ expectedArgs: {
329
+ ...BASE_ARGS,
330
+ targetId: "target:1",
331
+ requestId: "request:1"
332
+ }
333
+ }
334
+ ];
335
+
336
+ afterEach(() => {
337
+ callDaemonToolMock.mockReset();
338
+ delete process.env.BROWSERCTL_AUTH_TOKEN;
339
+ delete process.env.BROWSERCTL_BROWSER;
340
+ });
341
+
342
+ describe("command wrappers", () => {
343
+ it.each(CASES)("forwards $name to daemon tool", async ({ run, expectedTool, expectedArgs }) => {
344
+ callDaemonToolMock.mockResolvedValue({ ok: true });
345
+
346
+ await run();
347
+
348
+ expect(callDaemonToolMock).toHaveBeenCalledTimes(1);
349
+ expect(callDaemonToolMock).toHaveBeenCalledWith(expectedTool, expectedArgs);
350
+ });
351
+
352
+ it("forwards session/profile/token/browser context switches", async () => {
353
+ callDaemonToolMock.mockResolvedValue({ ok: true });
354
+
355
+ await runStatusCommand([
356
+ "--session",
357
+ "session:matrix",
358
+ "--profile",
359
+ "chrome-relay",
360
+ "--token",
361
+ "cli-token",
362
+ "--browser",
363
+ "edge"
364
+ ]);
365
+
366
+ expect(callDaemonToolMock).toHaveBeenCalledTimes(1);
367
+ expect(callDaemonToolMock).toHaveBeenCalledWith("browser.status", {
368
+ sessionId: "session:matrix",
369
+ profile: "chrome-relay",
370
+ authToken: "cli-token",
371
+ [DAEMON_STARTUP_ARGUMENT]: {
372
+ managedLocal: {
373
+ browserName: "chromium",
374
+ channel: "msedge"
375
+ }
376
+ }
377
+ });
378
+ });
379
+
380
+ it("throws for upload-arm when files are missing", async () => {
381
+ await expect(runUploadArmCommand(["target:1"]))
382
+ .rejects.toThrow("Missing required argument: files.");
383
+
384
+ expect(callDaemonToolMock).not.toHaveBeenCalled();
385
+ });
386
+ });
@@ -0,0 +1,90 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const { callDaemonToolMock } = vi.hoisted(() => ({
4
+ callDaemonToolMock: vi.fn()
5
+ }));
6
+
7
+ vi.mock("../daemon-client", () => ({
8
+ callDaemonTool: callDaemonToolMock
9
+ }));
10
+
11
+ import { runNetworkWaitForCommand } from "./network-wait-for";
12
+
13
+ afterEach(() => {
14
+ callDaemonToolMock.mockReset();
15
+ });
16
+
17
+ describe("runNetworkWaitForCommand", () => {
18
+ it("supports positional timeout shorthand", async () => {
19
+ callDaemonToolMock.mockResolvedValue({ ok: true });
20
+
21
+ await runNetworkWaitForCommand(["target:1", "/api", "2500"]);
22
+
23
+ expect(callDaemonToolMock).toHaveBeenCalledTimes(1);
24
+ expect(callDaemonToolMock).toHaveBeenCalledWith("browser.network.waitFor", {
25
+ sessionId: "cli:local",
26
+ targetId: "target:1",
27
+ urlPattern: "/api",
28
+ timeoutMs: 2500
29
+ });
30
+ });
31
+
32
+ it.each([
33
+ {
34
+ label: "--method",
35
+ args: ["target:1", "/api", "--method"],
36
+ message: "Missing value for --method."
37
+ },
38
+ {
39
+ label: "--status",
40
+ args: ["target:1", "/api", "--status"],
41
+ message: "Missing value for --status."
42
+ },
43
+ {
44
+ label: "--timeout-ms",
45
+ args: ["target:1", "/api", "--timeout-ms"],
46
+ message: "Missing value for --timeout-ms."
47
+ },
48
+ {
49
+ label: "--poll-ms",
50
+ args: ["target:1", "/api", "--poll-ms"],
51
+ message: "Missing value for --poll-ms."
52
+ }
53
+ ])("throws when $label is missing a value", async ({ args, message }) => {
54
+ await expect(runNetworkWaitForCommand(args)).rejects.toThrow(message);
55
+ expect(callDaemonToolMock).not.toHaveBeenCalled();
56
+ });
57
+
58
+ it.each([
59
+ {
60
+ label: "status <= 0",
61
+ args: ["target:1", "/api", "--status", "0"],
62
+ message: "--status must be a positive integer."
63
+ },
64
+ {
65
+ label: "timeout <= 0",
66
+ args: ["target:1", "/api", "--timeout-ms", "-10"],
67
+ message: "--timeout-ms must be a positive integer."
68
+ },
69
+ {
70
+ label: "poll <= 0",
71
+ args: ["target:1", "/api", "--poll-ms", "0"],
72
+ message: "--poll-ms must be a positive integer."
73
+ },
74
+ {
75
+ label: "positional timeout <= 0",
76
+ args: ["target:1", "/api", "0"],
77
+ message: "timeoutMs must be a positive integer."
78
+ }
79
+ ])("throws on non-positive numeric option: $label", async ({ args, message }) => {
80
+ await expect(runNetworkWaitForCommand(args)).rejects.toThrow(message);
81
+ expect(callDaemonToolMock).not.toHaveBeenCalled();
82
+ });
83
+
84
+ it("throws on unknown option token", async () => {
85
+ await expect(runNetworkWaitForCommand(["target:1", "/api", "1200", "--unknown"]))
86
+ .rejects.toThrow("Unknown network-wait-for option: --unknown");
87
+
88
+ expect(callDaemonToolMock).not.toHaveBeenCalled();
89
+ });
90
+ });