@flrande/browserctl 0.1.0-dev.7.1 → 0.1.0-dev.8.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
|
+
});
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import * as a11ySnapshotCommand from "./commands/a11y-snapshot";
|
|
4
|
+
import * as consoleListCommand from "./commands/console-list";
|
|
5
|
+
import * as cookieClearCommand from "./commands/cookie-clear";
|
|
6
|
+
import * as cookieGetCommand from "./commands/cookie-get";
|
|
7
|
+
import * as cookieSetCommand from "./commands/cookie-set";
|
|
8
|
+
import * as daemonClient from "./daemon-client";
|
|
9
|
+
import * as dialogArmCommand from "./commands/dialog-arm";
|
|
10
|
+
import * as domQueryAllCommand from "./commands/dom-query-all";
|
|
11
|
+
import * as domQueryCommand from "./commands/dom-query";
|
|
12
|
+
import * as downloadTriggerCommand from "./commands/download-trigger";
|
|
13
|
+
import * as downloadWaitCommand from "./commands/download-wait";
|
|
14
|
+
import * as elementScreenshotCommand from "./commands/element-screenshot";
|
|
15
|
+
import * as frameListCommand from "./commands/frame-list";
|
|
16
|
+
import * as frameSnapshotCommand from "./commands/frame-snapshot";
|
|
17
|
+
import * as networkWaitForCommand from "./commands/network-wait-for";
|
|
18
|
+
import * as profileListCommand from "./commands/profile-list";
|
|
19
|
+
import * as profileUseCommand from "./commands/profile-use";
|
|
20
|
+
import * as responseBodyCommand from "./commands/response-body";
|
|
21
|
+
import * as snapshotCommand from "./commands/snapshot";
|
|
22
|
+
import * as storageGetCommand from "./commands/storage-get";
|
|
23
|
+
import * as storageSetCommand from "./commands/storage-set";
|
|
24
|
+
import * as tabCloseCommand from "./commands/tab-close";
|
|
25
|
+
import * as tabFocusCommand from "./commands/tab-focus";
|
|
26
|
+
import * as tabOpenCommand from "./commands/tab-open";
|
|
27
|
+
import * as uploadArmCommand from "./commands/upload-arm";
|
|
28
|
+
import { EXIT_CODES, runCli } from "./main";
|
|
29
|
+
|
|
30
|
+
function createIoCapture() {
|
|
31
|
+
const state = {
|
|
32
|
+
stdout: "",
|
|
33
|
+
stderr: ""
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
state,
|
|
38
|
+
io: {
|
|
39
|
+
stdout: {
|
|
40
|
+
write(content: string) {
|
|
41
|
+
state.stdout += content;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
stderr: {
|
|
45
|
+
write(content: string) {
|
|
46
|
+
state.stderr += content;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type DispatchCase = {
|
|
54
|
+
command: string;
|
|
55
|
+
commandArgs: string[];
|
|
56
|
+
moduleRef: Record<string, unknown>;
|
|
57
|
+
handlerName: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const COMMAND_DISPATCH_CASES: DispatchCase[] = [
|
|
61
|
+
{
|
|
62
|
+
command: "a11y-snapshot",
|
|
63
|
+
commandArgs: ["target:1"],
|
|
64
|
+
moduleRef: a11ySnapshotCommand as unknown as Record<string, unknown>,
|
|
65
|
+
handlerName: "runA11ySnapshotCommand"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
command: "console-list",
|
|
69
|
+
commandArgs: ["target:1"],
|
|
70
|
+
moduleRef: consoleListCommand as unknown as Record<string, unknown>,
|
|
71
|
+
handlerName: "runConsoleListCommand"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
command: "cookie-clear",
|
|
75
|
+
commandArgs: ["target:1"],
|
|
76
|
+
moduleRef: cookieClearCommand as unknown as Record<string, unknown>,
|
|
77
|
+
handlerName: "runCookieClearCommand"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
command: "cookie-get",
|
|
81
|
+
commandArgs: ["target:1"],
|
|
82
|
+
moduleRef: cookieGetCommand as unknown as Record<string, unknown>,
|
|
83
|
+
handlerName: "runCookieGetCommand"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
command: "cookie-set",
|
|
87
|
+
commandArgs: ["target:1", "sid", "abc"],
|
|
88
|
+
moduleRef: cookieSetCommand as unknown as Record<string, unknown>,
|
|
89
|
+
handlerName: "runCookieSetCommand"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
command: "dialog-arm",
|
|
93
|
+
commandArgs: ["target:1"],
|
|
94
|
+
moduleRef: dialogArmCommand as unknown as Record<string, unknown>,
|
|
95
|
+
handlerName: "runDialogArmCommand"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
command: "dom-query",
|
|
99
|
+
commandArgs: ["target:1", "#root"],
|
|
100
|
+
moduleRef: domQueryCommand as unknown as Record<string, unknown>,
|
|
101
|
+
handlerName: "runDomQueryCommand"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
command: "dom-query-all",
|
|
105
|
+
commandArgs: ["target:1", ".item"],
|
|
106
|
+
moduleRef: domQueryAllCommand as unknown as Record<string, unknown>,
|
|
107
|
+
handlerName: "runDomQueryAllCommand"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
command: "download-trigger",
|
|
111
|
+
commandArgs: ["target:1"],
|
|
112
|
+
moduleRef: downloadTriggerCommand as unknown as Record<string, unknown>,
|
|
113
|
+
handlerName: "runDownloadTriggerCommand"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
command: "download-wait",
|
|
117
|
+
commandArgs: ["target:1"],
|
|
118
|
+
moduleRef: downloadWaitCommand as unknown as Record<string, unknown>,
|
|
119
|
+
handlerName: "runDownloadWaitCommand"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
command: "element-screenshot",
|
|
123
|
+
commandArgs: ["target:1", "#hero"],
|
|
124
|
+
moduleRef: elementScreenshotCommand as unknown as Record<string, unknown>,
|
|
125
|
+
handlerName: "runElementScreenshotCommand"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
command: "frame-list",
|
|
129
|
+
commandArgs: ["target:1"],
|
|
130
|
+
moduleRef: frameListCommand as unknown as Record<string, unknown>,
|
|
131
|
+
handlerName: "runFrameListCommand"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
command: "frame-snapshot",
|
|
135
|
+
commandArgs: ["target:1", "frame:0"],
|
|
136
|
+
moduleRef: frameSnapshotCommand as unknown as Record<string, unknown>,
|
|
137
|
+
handlerName: "runFrameSnapshotCommand"
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
command: "network-wait-for",
|
|
141
|
+
commandArgs: ["target:1", "/api", "1000"],
|
|
142
|
+
moduleRef: networkWaitForCommand as unknown as Record<string, unknown>,
|
|
143
|
+
handlerName: "runNetworkWaitForCommand"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
command: "profile-list",
|
|
147
|
+
commandArgs: [],
|
|
148
|
+
moduleRef: profileListCommand as unknown as Record<string, unknown>,
|
|
149
|
+
handlerName: "runProfileListCommand"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
command: "profile-use",
|
|
153
|
+
commandArgs: ["managed"],
|
|
154
|
+
moduleRef: profileUseCommand as unknown as Record<string, unknown>,
|
|
155
|
+
handlerName: "runProfileUseCommand"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
command: "response-body",
|
|
159
|
+
commandArgs: ["target:1", "request:1"],
|
|
160
|
+
moduleRef: responseBodyCommand as unknown as Record<string, unknown>,
|
|
161
|
+
handlerName: "runResponseBodyCommand"
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
command: "snapshot",
|
|
165
|
+
commandArgs: ["target:1"],
|
|
166
|
+
moduleRef: snapshotCommand as unknown as Record<string, unknown>,
|
|
167
|
+
handlerName: "runSnapshotCommand"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
command: "storage-get",
|
|
171
|
+
commandArgs: ["target:1", "local", "theme"],
|
|
172
|
+
moduleRef: storageGetCommand as unknown as Record<string, unknown>,
|
|
173
|
+
handlerName: "runStorageGetCommand"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
command: "storage-set",
|
|
177
|
+
commandArgs: ["target:1", "local", "theme", "dark"],
|
|
178
|
+
moduleRef: storageSetCommand as unknown as Record<string, unknown>,
|
|
179
|
+
handlerName: "runStorageSetCommand"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
command: "tab-close",
|
|
183
|
+
commandArgs: ["target:1"],
|
|
184
|
+
moduleRef: tabCloseCommand as unknown as Record<string, unknown>,
|
|
185
|
+
handlerName: "runTabCloseCommand"
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
command: "tab-focus",
|
|
189
|
+
commandArgs: ["target:1"],
|
|
190
|
+
moduleRef: tabFocusCommand as unknown as Record<string, unknown>,
|
|
191
|
+
handlerName: "runTabFocusCommand"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
command: "tab-open",
|
|
195
|
+
commandArgs: ["https://example.com"],
|
|
196
|
+
moduleRef: tabOpenCommand as unknown as Record<string, unknown>,
|
|
197
|
+
handlerName: "runTabOpenCommand"
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
command: "upload-arm",
|
|
201
|
+
commandArgs: ["target:1", "upload.txt"],
|
|
202
|
+
moduleRef: uploadArmCommand as unknown as Record<string, unknown>,
|
|
203
|
+
handlerName: "runUploadArmCommand"
|
|
204
|
+
}
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
afterEach(() => {
|
|
208
|
+
vi.restoreAllMocks();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("cli dispatch matrix", () => {
|
|
212
|
+
it.each(COMMAND_DISPATCH_CASES)(
|
|
213
|
+
"dispatches $command to $handlerName",
|
|
214
|
+
async ({ command, commandArgs, moduleRef, handlerName }) => {
|
|
215
|
+
const handler = moduleRef[handlerName] as ((args: string[]) => Promise<unknown>) | undefined;
|
|
216
|
+
if (typeof handler !== "function") {
|
|
217
|
+
throw new Error(`Expected ${handlerName} to be a function.`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const handlerSpy = vi.spyOn(moduleRef as Record<string, (...args: unknown[]) => unknown>, handlerName)
|
|
221
|
+
.mockResolvedValue({ ok: true });
|
|
222
|
+
const { io, state } = createIoCapture();
|
|
223
|
+
|
|
224
|
+
const exitCode = await runCli([command, ...commandArgs], io);
|
|
225
|
+
|
|
226
|
+
expect(exitCode).toBe(EXIT_CODES.OK);
|
|
227
|
+
expect(state.stderr).toBe("");
|
|
228
|
+
expect(state.stdout).toMatch(new RegExp(`^${command}: `));
|
|
229
|
+
expect(handlerSpy).toHaveBeenCalledTimes(1);
|
|
230
|
+
expect(handlerSpy).toHaveBeenCalledWith(commandArgs);
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
it("dispatches daemon-stop through daemon client", async () => {
|
|
235
|
+
const stopSpy = vi.spyOn(daemonClient, "stopDaemon").mockResolvedValue({
|
|
236
|
+
stopped: true,
|
|
237
|
+
port: 41337,
|
|
238
|
+
pid: 12345
|
|
239
|
+
});
|
|
240
|
+
const { io, state } = createIoCapture();
|
|
241
|
+
|
|
242
|
+
const exitCode = await runCli(["daemon-stop", "--json"], io);
|
|
243
|
+
|
|
244
|
+
expect(exitCode).toBe(EXIT_CODES.OK);
|
|
245
|
+
expect(stopSpy).toHaveBeenCalledTimes(1);
|
|
246
|
+
expect(JSON.parse(state.stdout)).toEqual({
|
|
247
|
+
ok: true,
|
|
248
|
+
command: "daemon-stop",
|
|
249
|
+
data: {
|
|
250
|
+
stopped: true,
|
|
251
|
+
port: 41337,
|
|
252
|
+
pid: 12345
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { PassThrough } from "node:stream";
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { bootstrapBrowserd } from "./bootstrap";
|
|
6
|
+
|
|
7
|
+
function waitForNextJsonLine(stream: PassThrough): Promise<Record<string, unknown>> {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
let buffer = "";
|
|
10
|
+
const timeout = setTimeout(() => {
|
|
11
|
+
stream.off("data", onData);
|
|
12
|
+
reject(new Error("Timed out waiting for JSON line response."));
|
|
13
|
+
}, 1000);
|
|
14
|
+
|
|
15
|
+
const onData = (chunk: string | Buffer) => {
|
|
16
|
+
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
17
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
18
|
+
if (newlineIndex < 0) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
clearTimeout(timeout);
|
|
23
|
+
stream.off("data", onData);
|
|
24
|
+
const line = buffer.slice(0, newlineIndex);
|
|
25
|
+
|
|
26
|
+
resolve(JSON.parse(line) as Record<string, unknown>);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
stream.on("data", onData);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sendToolRequest(
|
|
34
|
+
input: PassThrough,
|
|
35
|
+
output: PassThrough,
|
|
36
|
+
request: Record<string, unknown>
|
|
37
|
+
): Promise<Record<string, unknown>> {
|
|
38
|
+
const responsePromise = waitForNextJsonLine(output);
|
|
39
|
+
input.write(`${JSON.stringify(request)}\n`);
|
|
40
|
+
return responsePromise;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createManagedLegacyRuntime() {
|
|
44
|
+
const input = new PassThrough();
|
|
45
|
+
const output = new PassThrough();
|
|
46
|
+
const runtime = bootstrapBrowserd({
|
|
47
|
+
env: {
|
|
48
|
+
BROWSERD_DEFAULT_DRIVER: "managed"
|
|
49
|
+
},
|
|
50
|
+
input,
|
|
51
|
+
output,
|
|
52
|
+
stdioProtocol: "legacy"
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
input,
|
|
57
|
+
output,
|
|
58
|
+
runtime
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("browserd tool matrix", () => {
|
|
63
|
+
it("returns E_INVALID_ARG for missing required arguments on routed tools", async () => {
|
|
64
|
+
const { input, output, runtime } = createManagedLegacyRuntime();
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const toolNames = [
|
|
68
|
+
"browser.profile.use",
|
|
69
|
+
"browser.tab.focus",
|
|
70
|
+
"browser.tab.close",
|
|
71
|
+
"browser.snapshot",
|
|
72
|
+
"browser.dom.query",
|
|
73
|
+
"browser.dom.queryAll",
|
|
74
|
+
"browser.element.screenshot",
|
|
75
|
+
"browser.a11y.snapshot",
|
|
76
|
+
"browser.cookie.get",
|
|
77
|
+
"browser.cookie.set",
|
|
78
|
+
"browser.cookie.clear",
|
|
79
|
+
"browser.storage.get",
|
|
80
|
+
"browser.storage.set",
|
|
81
|
+
"browser.frame.list",
|
|
82
|
+
"browser.frame.snapshot",
|
|
83
|
+
"browser.act",
|
|
84
|
+
"browser.dialog.arm",
|
|
85
|
+
"browser.download.trigger",
|
|
86
|
+
"browser.network.waitFor"
|
|
87
|
+
] as const;
|
|
88
|
+
|
|
89
|
+
for (const [index, toolName] of toolNames.entries()) {
|
|
90
|
+
const response = await sendToolRequest(input, output, {
|
|
91
|
+
id: `request-missing-arg-${index}`,
|
|
92
|
+
name: toolName,
|
|
93
|
+
traceId: `trace:missing-arg:${index}`,
|
|
94
|
+
arguments: {
|
|
95
|
+
sessionId: "session:missing-arg"
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(response.ok).toBe(false);
|
|
100
|
+
expect(response.error).toMatchObject({
|
|
101
|
+
code: "E_INVALID_ARG"
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
} finally {
|
|
105
|
+
runtime.close();
|
|
106
|
+
input.end();
|
|
107
|
+
output.end();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("returns E_DRIVER_UNAVAILABLE for structured action tools on managed driver", async () => {
|
|
112
|
+
const { input, output, runtime } = createManagedLegacyRuntime();
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const openResponse = await sendToolRequest(input, output, {
|
|
116
|
+
id: "request-unavailable-open",
|
|
117
|
+
name: "browser.tab.open",
|
|
118
|
+
traceId: "trace:unavailable:open",
|
|
119
|
+
arguments: {
|
|
120
|
+
sessionId: "session:unavailable",
|
|
121
|
+
url: "https://example.com/unavailable"
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
const targetId = (openResponse.data as { targetId: string }).targetId;
|
|
125
|
+
|
|
126
|
+
const cases: Array<{ name: string; arguments: Record<string, unknown> }> = [
|
|
127
|
+
{
|
|
128
|
+
name: "browser.dom.query",
|
|
129
|
+
arguments: {
|
|
130
|
+
sessionId: "session:unavailable",
|
|
131
|
+
targetId,
|
|
132
|
+
selector: "#root"
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: "browser.dom.queryAll",
|
|
137
|
+
arguments: {
|
|
138
|
+
sessionId: "session:unavailable",
|
|
139
|
+
targetId,
|
|
140
|
+
selector: ".item"
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "browser.element.screenshot",
|
|
145
|
+
arguments: {
|
|
146
|
+
sessionId: "session:unavailable",
|
|
147
|
+
targetId,
|
|
148
|
+
selector: "#hero"
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: "browser.a11y.snapshot",
|
|
153
|
+
arguments: {
|
|
154
|
+
sessionId: "session:unavailable",
|
|
155
|
+
targetId
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "browser.cookie.get",
|
|
160
|
+
arguments: {
|
|
161
|
+
sessionId: "session:unavailable",
|
|
162
|
+
targetId
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: "browser.cookie.set",
|
|
167
|
+
arguments: {
|
|
168
|
+
sessionId: "session:unavailable",
|
|
169
|
+
targetId,
|
|
170
|
+
name: "sid",
|
|
171
|
+
value: "abc"
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: "browser.cookie.clear",
|
|
176
|
+
arguments: {
|
|
177
|
+
sessionId: "session:unavailable",
|
|
178
|
+
targetId
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "browser.storage.get",
|
|
183
|
+
arguments: {
|
|
184
|
+
sessionId: "session:unavailable",
|
|
185
|
+
targetId,
|
|
186
|
+
scope: "local",
|
|
187
|
+
key: "theme"
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: "browser.storage.set",
|
|
192
|
+
arguments: {
|
|
193
|
+
sessionId: "session:unavailable",
|
|
194
|
+
targetId,
|
|
195
|
+
scope: "local",
|
|
196
|
+
key: "theme",
|
|
197
|
+
value: "dark"
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "browser.frame.list",
|
|
202
|
+
arguments: {
|
|
203
|
+
sessionId: "session:unavailable",
|
|
204
|
+
targetId
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: "browser.frame.snapshot",
|
|
209
|
+
arguments: {
|
|
210
|
+
sessionId: "session:unavailable",
|
|
211
|
+
targetId,
|
|
212
|
+
frameId: "frame:0"
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
for (const [index, testCase] of cases.entries()) {
|
|
218
|
+
const response = await sendToolRequest(input, output, {
|
|
219
|
+
id: `request-driver-unavailable-${index}`,
|
|
220
|
+
name: testCase.name,
|
|
221
|
+
traceId: `trace:driver-unavailable:${index}`,
|
|
222
|
+
arguments: testCase.arguments
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
expect(response.ok).toBe(false);
|
|
226
|
+
expect(response.error).toMatchObject({
|
|
227
|
+
code: "E_DRIVER_UNAVAILABLE"
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
} finally {
|
|
231
|
+
runtime.close();
|
|
232
|
+
input.end();
|
|
233
|
+
output.end();
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("routes managed-driver tools including profile.use, act, focus/close, and waitFor timeout", async () => {
|
|
238
|
+
const { input, output, runtime } = createManagedLegacyRuntime();
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const profileUseResponse = await sendToolRequest(input, output, {
|
|
242
|
+
id: "request-profile-use",
|
|
243
|
+
name: "browser.profile.use",
|
|
244
|
+
traceId: "trace:matrix:profile-use",
|
|
245
|
+
arguments: {
|
|
246
|
+
sessionId: "session:profile-use",
|
|
247
|
+
profile: "managed"
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
expect(profileUseResponse.ok).toBe(true);
|
|
251
|
+
expect(profileUseResponse.data).toEqual({
|
|
252
|
+
profile: "managed"
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const openResponse = await sendToolRequest(input, output, {
|
|
256
|
+
id: "request-flow-open",
|
|
257
|
+
name: "browser.tab.open",
|
|
258
|
+
traceId: "trace:matrix:open",
|
|
259
|
+
arguments: {
|
|
260
|
+
sessionId: "session:flow",
|
|
261
|
+
url: "https://example.com/flow"
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
const targetId = (openResponse.data as { targetId: string }).targetId;
|
|
265
|
+
|
|
266
|
+
const snapshotResponse = await sendToolRequest(input, output, {
|
|
267
|
+
id: "request-flow-snapshot",
|
|
268
|
+
name: "browser.snapshot",
|
|
269
|
+
traceId: "trace:matrix:snapshot",
|
|
270
|
+
arguments: {
|
|
271
|
+
sessionId: "session:flow",
|
|
272
|
+
targetId
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
expect(snapshotResponse.ok).toBe(true);
|
|
276
|
+
expect(snapshotResponse.data).toMatchObject({
|
|
277
|
+
driver: "managed",
|
|
278
|
+
targetId
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const focusResponse = await sendToolRequest(input, output, {
|
|
282
|
+
id: "request-flow-focus",
|
|
283
|
+
name: "browser.tab.focus",
|
|
284
|
+
traceId: "trace:matrix:focus",
|
|
285
|
+
arguments: {
|
|
286
|
+
sessionId: "session:flow",
|
|
287
|
+
targetId
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
expect(focusResponse.ok).toBe(true);
|
|
291
|
+
expect(focusResponse.data).toMatchObject({
|
|
292
|
+
driver: "managed",
|
|
293
|
+
targetId,
|
|
294
|
+
focused: true
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const actResponse = await sendToolRequest(input, output, {
|
|
298
|
+
id: "request-flow-act",
|
|
299
|
+
name: "browser.act",
|
|
300
|
+
traceId: "trace:matrix:act",
|
|
301
|
+
arguments: {
|
|
302
|
+
sessionId: "session:flow",
|
|
303
|
+
targetId,
|
|
304
|
+
action: {
|
|
305
|
+
type: "click",
|
|
306
|
+
payload: {
|
|
307
|
+
selector: "#go"
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
expect(actResponse.ok).toBe(true);
|
|
313
|
+
expect(actResponse.data).toMatchObject({
|
|
314
|
+
driver: "managed",
|
|
315
|
+
targetId,
|
|
316
|
+
result: {
|
|
317
|
+
actionType: "click",
|
|
318
|
+
targetId,
|
|
319
|
+
targetKnown: true,
|
|
320
|
+
ok: true
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const dialogResponse = await sendToolRequest(input, output, {
|
|
325
|
+
id: "request-flow-dialog",
|
|
326
|
+
name: "browser.dialog.arm",
|
|
327
|
+
traceId: "trace:matrix:dialog",
|
|
328
|
+
arguments: {
|
|
329
|
+
sessionId: "session:flow",
|
|
330
|
+
targetId
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
expect(dialogResponse.ok).toBe(true);
|
|
334
|
+
expect(dialogResponse.data).toMatchObject({
|
|
335
|
+
driver: "managed",
|
|
336
|
+
targetId,
|
|
337
|
+
armed: true
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const triggerResponse = await sendToolRequest(input, output, {
|
|
341
|
+
id: "request-flow-trigger",
|
|
342
|
+
name: "browser.download.trigger",
|
|
343
|
+
traceId: "trace:matrix:trigger",
|
|
344
|
+
arguments: {
|
|
345
|
+
sessionId: "session:flow",
|
|
346
|
+
targetId
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
expect(triggerResponse.ok).toBe(true);
|
|
350
|
+
expect(triggerResponse.data).toMatchObject({
|
|
351
|
+
driver: "managed",
|
|
352
|
+
targetId,
|
|
353
|
+
triggered: true
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const waitForResponse = await sendToolRequest(input, output, {
|
|
357
|
+
id: "request-flow-waitfor",
|
|
358
|
+
name: "browser.network.waitFor",
|
|
359
|
+
traceId: "trace:matrix:waitfor",
|
|
360
|
+
arguments: {
|
|
361
|
+
sessionId: "session:flow",
|
|
362
|
+
targetId,
|
|
363
|
+
urlPattern: "/never-match",
|
|
364
|
+
timeoutMs: 5,
|
|
365
|
+
pollMs: 1
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
expect(waitForResponse.ok).toBe(false);
|
|
369
|
+
expect(waitForResponse.error).toMatchObject({
|
|
370
|
+
code: "E_TIMEOUT"
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const closeResponse = await sendToolRequest(input, output, {
|
|
374
|
+
id: "request-flow-close",
|
|
375
|
+
name: "browser.tab.close",
|
|
376
|
+
traceId: "trace:matrix:close",
|
|
377
|
+
arguments: {
|
|
378
|
+
sessionId: "session:flow",
|
|
379
|
+
targetId
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
expect(closeResponse.ok).toBe(true);
|
|
383
|
+
expect(closeResponse.data).toMatchObject({
|
|
384
|
+
driver: "managed",
|
|
385
|
+
targetId,
|
|
386
|
+
closed: true
|
|
387
|
+
});
|
|
388
|
+
} finally {
|
|
389
|
+
runtime.close();
|
|
390
|
+
input.end();
|
|
391
|
+
output.end();
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
});
|