@flrande/browserctl 0.1.0 → 0.2.0-dev.12.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.
- package/INSTALL-CN.md +28 -0
- package/INSTALL.md +28 -0
- package/README-CN.md +27 -1131
- package/README.md +27 -1131
- package/apps/browserctl/src/commands/command-wrappers.test.ts +386 -0
- package/apps/browserctl/src/commands/network-wait-for.test.ts +90 -0
- package/apps/browserctl/src/e2e.test.ts +9 -5
- package/apps/browserctl/src/main.dispatch.test.ts +256 -0
- package/apps/browserctl/src/smoke.e2e.test.ts +97 -0
- package/apps/browserctl/src/test-port.ts +26 -0
- package/apps/browserd/src/chrome-relay-extension-bridge.test.ts +6 -31
- package/apps/browserd/src/container.ts +12 -10
- package/apps/browserd/src/main.test.ts +81 -46
- package/apps/browserd/src/test-port.ts +26 -0
- package/apps/browserd/src/tool-matrix.test.ts +398 -0
- package/extensions/chrome-relay/README-CN.md +39 -0
- package/extensions/chrome-relay/README.md +3 -0
- package/package.json +6 -4
- package/apps/browserctl/src/smoke.test.ts +0 -16
- package/apps/browserctl/src/smoke.ts +0 -5
- package/scripts/smoke.ps1 +0 -127
|
@@ -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,97 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { stopDaemon } from "./daemon-client";
|
|
4
|
+
import { EXIT_CODES, runCli } from "./main";
|
|
5
|
+
import { reserveLoopbackPort } from "./test-port";
|
|
6
|
+
|
|
7
|
+
let testDaemonPort = 0;
|
|
8
|
+
let activeRelayPort = 0;
|
|
9
|
+
|
|
10
|
+
function createIoCapture() {
|
|
11
|
+
const state = {
|
|
12
|
+
stdout: "",
|
|
13
|
+
stderr: ""
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
state,
|
|
18
|
+
io: {
|
|
19
|
+
stdout: {
|
|
20
|
+
write(content: string) {
|
|
21
|
+
state.stdout += content;
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
stderr: {
|
|
25
|
+
write(content: string) {
|
|
26
|
+
state.stderr += content;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseJsonLine(state: { stdout: string }): Record<string, unknown> {
|
|
34
|
+
return JSON.parse(state.stdout.trim()) as Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
beforeEach(async () => {
|
|
38
|
+
testDaemonPort = await reserveLoopbackPort();
|
|
39
|
+
activeRelayPort = await reserveLoopbackPort();
|
|
40
|
+
while (activeRelayPort === testDaemonPort) {
|
|
41
|
+
activeRelayPort = await reserveLoopbackPort();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
process.env.BROWSERCTL_DAEMON_PORT = String(testDaemonPort);
|
|
45
|
+
process.env.BROWSERD_CHROME_RELAY_URL = `http://127.0.0.1:${activeRelayPort}`;
|
|
46
|
+
delete process.env.BROWSERD_MANAGED_LOCAL_ENABLED;
|
|
47
|
+
delete process.env.BROWSERD_DEFAULT_DRIVER;
|
|
48
|
+
delete process.env.BROWSERD_CHROME_RELAY_MODE;
|
|
49
|
+
delete process.env.BROWSERD_CHROME_RELAY_EXTENSION_TOKEN;
|
|
50
|
+
await stopDaemon(testDaemonPort);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(async () => {
|
|
54
|
+
await stopDaemon(testDaemonPort);
|
|
55
|
+
delete process.env.BROWSERCTL_DAEMON_PORT;
|
|
56
|
+
delete process.env.BROWSERD_CHROME_RELAY_URL;
|
|
57
|
+
delete process.env.BROWSERD_MANAGED_LOCAL_ENABLED;
|
|
58
|
+
delete process.env.BROWSERD_DEFAULT_DRIVER;
|
|
59
|
+
delete process.env.BROWSERD_CHROME_RELAY_MODE;
|
|
60
|
+
delete process.env.BROWSERD_CHROME_RELAY_EXTENSION_TOKEN;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("browserctl smoke e2e", () => {
|
|
64
|
+
it("starts with extension-first defaults and reports chrome-relay status", async () => {
|
|
65
|
+
const startCapture = createIoCapture();
|
|
66
|
+
const startExitCode = await runCli(["daemon-start", "--json"], startCapture.io);
|
|
67
|
+
|
|
68
|
+
expect(startExitCode).toBe(EXIT_CODES.OK);
|
|
69
|
+
const startPayload = parseJsonLine(startCapture.state);
|
|
70
|
+
expect(startPayload.ok).toBe(true);
|
|
71
|
+
|
|
72
|
+
const statusCapture = createIoCapture();
|
|
73
|
+
const statusExitCode = await runCli(["status", "--json"], statusCapture.io);
|
|
74
|
+
|
|
75
|
+
expect(statusExitCode).toBe(EXIT_CODES.OK);
|
|
76
|
+
const statusPayload = parseJsonLine(statusCapture.state);
|
|
77
|
+
expect(statusPayload.ok).toBe(true);
|
|
78
|
+
const statusData = statusPayload.data as Record<string, unknown>;
|
|
79
|
+
expect(statusData.driver).toBe("chrome-relay");
|
|
80
|
+
expect(statusData.status).toMatchObject({
|
|
81
|
+
kind: "chrome-relay",
|
|
82
|
+
connected: false
|
|
83
|
+
});
|
|
84
|
+
const relayStatus = statusData.status as Record<string, unknown>;
|
|
85
|
+
expect(String(relayStatus.relayUrl ?? "")).toContain(`:${activeRelayPort}`);
|
|
86
|
+
|
|
87
|
+
const stopCapture = createIoCapture();
|
|
88
|
+
const stopExitCode = await runCli(["daemon-stop", "--json"], stopCapture.io);
|
|
89
|
+
expect(stopExitCode).toBe(EXIT_CODES.OK);
|
|
90
|
+
const stopPayload = parseJsonLine(stopCapture.state);
|
|
91
|
+
expect(stopPayload.ok).toBe(true);
|
|
92
|
+
expect(stopPayload.data).toMatchObject({
|
|
93
|
+
stopped: true,
|
|
94
|
+
port: testDaemonPort
|
|
95
|
+
});
|
|
96
|
+
}, 20_000);
|
|
97
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createServer } from "node:net";
|
|
2
|
+
|
|
3
|
+
export async function reserveLoopbackPort(): Promise<number> {
|
|
4
|
+
return await new Promise<number>((resolve, reject) => {
|
|
5
|
+
const server = createServer();
|
|
6
|
+
|
|
7
|
+
server.once("error", reject);
|
|
8
|
+
server.listen(0, "127.0.0.1", () => {
|
|
9
|
+
const address = server.address();
|
|
10
|
+
if (typeof address !== "object" || address === null) {
|
|
11
|
+
server.close(() => reject(new Error("Failed to reserve loopback port.")));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const port = address.port;
|
|
16
|
+
server.close((error) => {
|
|
17
|
+
if (error !== undefined) {
|
|
18
|
+
reject(error);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
resolve(port);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -1,33 +1,8 @@
|
|
|
1
|
-
import { createServer } from "node:net";
|
|
2
|
-
|
|
3
1
|
import { afterEach, describe, expect, it } from "vitest";
|
|
4
2
|
import { WebSocket } from "ws";
|
|
5
3
|
|
|
6
4
|
import { createChromeRelayExtensionBridge } from "./chrome-relay-extension-bridge";
|
|
7
|
-
|
|
8
|
-
async function reservePort(): Promise<number> {
|
|
9
|
-
return await new Promise<number>((resolve, reject) => {
|
|
10
|
-
const server = createServer();
|
|
11
|
-
server.once("error", reject);
|
|
12
|
-
server.listen(0, "127.0.0.1", () => {
|
|
13
|
-
const address = server.address();
|
|
14
|
-
if (typeof address !== "object" || address === null) {
|
|
15
|
-
server.close(() => resolve(0));
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const port = address.port;
|
|
20
|
-
server.close((closeError) => {
|
|
21
|
-
if (closeError !== undefined) {
|
|
22
|
-
reject(closeError);
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
resolve(port);
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
}
|
|
5
|
+
import { reserveLoopbackPort } from "./test-port";
|
|
31
6
|
|
|
32
7
|
async function waitForCondition(predicate: () => boolean, timeoutMs = 1000): Promise<void> {
|
|
33
8
|
const start = Date.now();
|
|
@@ -61,7 +36,7 @@ afterEach(async () => {
|
|
|
61
36
|
|
|
62
37
|
describe("createChromeRelayExtensionBridge", () => {
|
|
63
38
|
it("requires token configuration for extension bridge startup", async () => {
|
|
64
|
-
const port = await
|
|
39
|
+
const port = await reserveLoopbackPort();
|
|
65
40
|
|
|
66
41
|
expect(() =>
|
|
67
42
|
createChromeRelayExtensionBridge({
|
|
@@ -71,7 +46,7 @@ describe("createChromeRelayExtensionBridge", () => {
|
|
|
71
46
|
});
|
|
72
47
|
|
|
73
48
|
it("sends requests to extension client and receives responses", async () => {
|
|
74
|
-
const port = await
|
|
49
|
+
const port = await reserveLoopbackPort();
|
|
75
50
|
const bridge = createChromeRelayExtensionBridge({
|
|
76
51
|
relayUrl: `http://127.0.0.1:${port}`,
|
|
77
52
|
token: "secret-token",
|
|
@@ -134,7 +109,7 @@ describe("createChromeRelayExtensionBridge", () => {
|
|
|
134
109
|
});
|
|
135
110
|
|
|
136
111
|
it("rejects invoke when extension is not connected", async () => {
|
|
137
|
-
const port = await
|
|
112
|
+
const port = await reserveLoopbackPort();
|
|
138
113
|
const bridge = createChromeRelayExtensionBridge({
|
|
139
114
|
relayUrl: `http://127.0.0.1:${port}`,
|
|
140
115
|
token: "secret-token",
|
|
@@ -148,7 +123,7 @@ describe("createChromeRelayExtensionBridge", () => {
|
|
|
148
123
|
});
|
|
149
124
|
|
|
150
125
|
it("enforces token validation for extension websocket connection", async () => {
|
|
151
|
-
const port = await
|
|
126
|
+
const port = await reserveLoopbackPort();
|
|
152
127
|
const bridge = createChromeRelayExtensionBridge({
|
|
153
128
|
relayUrl: `http://127.0.0.1:${port}`,
|
|
154
129
|
token: "secret-token",
|
|
@@ -188,7 +163,7 @@ describe("createChromeRelayExtensionBridge", () => {
|
|
|
188
163
|
});
|
|
189
164
|
|
|
190
165
|
it("forwards extension event envelopes to bridge listeners", async () => {
|
|
191
|
-
const port = await
|
|
166
|
+
const port = await reserveLoopbackPort();
|
|
192
167
|
const bridge = createChromeRelayExtensionBridge({
|
|
193
168
|
relayUrl: `http://127.0.0.1:${port}`,
|
|
194
169
|
token: "secret-token",
|
|
@@ -53,9 +53,11 @@ export type BrowserdConfig = {
|
|
|
53
53
|
|
|
54
54
|
const DEFAULT_DRIVER_KEY = "managed";
|
|
55
55
|
const MANAGED_LOCAL_DRIVER_KEY = "managed-local";
|
|
56
|
+
const DEFAULT_CONFIG_DRIVER_KEY = "chrome-relay";
|
|
56
57
|
const DEFAULT_MANAGED_LOCAL_BROWSER_NAME: ManagedLocalBrowserName = "chromium";
|
|
57
58
|
const DEFAULT_MANAGED_LOCAL_HEADLESS = true;
|
|
58
|
-
const DEFAULT_CHROME_RELAY_MODE: ChromeRelayMode = "
|
|
59
|
+
const DEFAULT_CHROME_RELAY_MODE: ChromeRelayMode = "extension";
|
|
60
|
+
const DEFAULT_CHROME_RELAY_EXTENSION_TOKEN = "browserctl-relay";
|
|
59
61
|
const DEFAULT_CHROME_RELAY_EXTENSION_REQUEST_TIMEOUT_MS = 5_000;
|
|
60
62
|
const DEFAULT_NETWORK_WAIT_TIMEOUT_MS = 10_000;
|
|
61
63
|
const DEFAULT_NETWORK_WAIT_POLL_MS = 200;
|
|
@@ -181,7 +183,11 @@ function parseManagedLocalBrowserName(value: string | undefined): ManagedLocalBr
|
|
|
181
183
|
|
|
182
184
|
function parseChromeRelayMode(value: string | undefined): ChromeRelayMode {
|
|
183
185
|
const normalizedValue = value?.trim().toLowerCase();
|
|
184
|
-
|
|
186
|
+
if (normalizedValue === "extension" || normalizedValue === "cdp") {
|
|
187
|
+
return normalizedValue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return DEFAULT_CHROME_RELAY_MODE;
|
|
185
191
|
}
|
|
186
192
|
|
|
187
193
|
export type BrowserdContainer = {
|
|
@@ -198,16 +204,12 @@ export function loadBrowserdConfig(
|
|
|
198
204
|
): BrowserdConfig {
|
|
199
205
|
const managedLocalEnabled = parseBooleanFlag(env.BROWSERD_MANAGED_LOCAL_ENABLED, true);
|
|
200
206
|
const chromeRelayMode = parseChromeRelayMode(env.BROWSERD_CHROME_RELAY_MODE);
|
|
201
|
-
const chromeRelayExtensionToken =
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
"BROWSERD_CHROME_RELAY_EXTENSION_TOKEN is required when BROWSERD_CHROME_RELAY_MODE=extension."
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
+
const chromeRelayExtensionToken =
|
|
208
|
+
resolveNonEmptyString(env.BROWSERD_CHROME_RELAY_EXTENSION_TOKEN) ??
|
|
209
|
+
(chromeRelayMode === "extension" ? DEFAULT_CHROME_RELAY_EXTENSION_TOKEN : undefined);
|
|
207
210
|
|
|
208
211
|
const defaultDriver =
|
|
209
|
-
resolveNonEmptyString(env.BROWSERD_DEFAULT_DRIVER) ??
|
|
210
|
-
(managedLocalEnabled ? MANAGED_LOCAL_DRIVER_KEY : DEFAULT_DRIVER_KEY);
|
|
212
|
+
resolveNonEmptyString(env.BROWSERD_DEFAULT_DRIVER) ?? DEFAULT_CONFIG_DRIVER_KEY;
|
|
211
213
|
|
|
212
214
|
return {
|
|
213
215
|
chromeRelayUrl: env.BROWSERD_CHROME_RELAY_URL ?? "http://127.0.0.1:9223",
|