@flrande/browserctl 0.1.0-dev.7.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/LICENSE +21 -0
- package/README-CN.md +66 -0
- package/README.md +66 -0
- package/apps/browserctl/src/commands/a11y-snapshot.ts +20 -0
- package/apps/browserctl/src/commands/act.ts +20 -0
- package/apps/browserctl/src/commands/common.test.ts +87 -0
- package/apps/browserctl/src/commands/common.ts +191 -0
- package/apps/browserctl/src/commands/console-list.ts +20 -0
- package/apps/browserctl/src/commands/cookie-clear.ts +18 -0
- package/apps/browserctl/src/commands/cookie-get.ts +18 -0
- package/apps/browserctl/src/commands/cookie-set.ts +22 -0
- package/apps/browserctl/src/commands/dialog-arm.ts +20 -0
- package/apps/browserctl/src/commands/dom-query-all.ts +18 -0
- package/apps/browserctl/src/commands/dom-query.ts +18 -0
- package/apps/browserctl/src/commands/download-trigger.ts +22 -0
- package/apps/browserctl/src/commands/download-wait.test.ts +67 -0
- package/apps/browserctl/src/commands/download-wait.ts +27 -0
- package/apps/browserctl/src/commands/element-screenshot.ts +20 -0
- package/apps/browserctl/src/commands/frame-list.ts +16 -0
- package/apps/browserctl/src/commands/frame-snapshot.ts +18 -0
- package/apps/browserctl/src/commands/network-wait-for.ts +100 -0
- package/apps/browserctl/src/commands/profile-list.ts +16 -0
- package/apps/browserctl/src/commands/profile-use.ts +18 -0
- package/apps/browserctl/src/commands/response-body.ts +24 -0
- package/apps/browserctl/src/commands/screenshot.ts +16 -0
- package/apps/browserctl/src/commands/snapshot.ts +16 -0
- package/apps/browserctl/src/commands/status.ts +10 -0
- package/apps/browserctl/src/commands/storage-get.ts +20 -0
- package/apps/browserctl/src/commands/storage-set.ts +22 -0
- package/apps/browserctl/src/commands/tab-close.ts +20 -0
- package/apps/browserctl/src/commands/tab-focus.ts +20 -0
- package/apps/browserctl/src/commands/tab-open.ts +19 -0
- package/apps/browserctl/src/commands/tabs.ts +13 -0
- package/apps/browserctl/src/commands/upload-arm.ts +26 -0
- package/apps/browserctl/src/daemon-client.test.ts +253 -0
- package/apps/browserctl/src/daemon-client.ts +632 -0
- package/apps/browserctl/src/e2e.test.ts +99 -0
- package/apps/browserctl/src/main.test.ts +215 -0
- package/apps/browserctl/src/main.ts +372 -0
- package/apps/browserctl/src/smoke.test.ts +16 -0
- package/apps/browserctl/src/smoke.ts +5 -0
- package/apps/browserd/src/bootstrap.ts +432 -0
- package/apps/browserd/src/chrome-relay-extension-bridge.test.ts +275 -0
- package/apps/browserd/src/chrome-relay-extension-bridge.ts +506 -0
- package/apps/browserd/src/container.ts +1531 -0
- package/apps/browserd/src/main.test.ts +864 -0
- package/apps/browserd/src/main.ts +7 -0
- package/bin/browserctl.cjs +21 -0
- package/bin/browserd.cjs +21 -0
- package/extensions/chrome-relay/README-CN.md +38 -0
- package/extensions/chrome-relay/README.md +38 -0
- package/extensions/chrome-relay/background.js +1687 -0
- package/extensions/chrome-relay/manifest.json +15 -0
- package/extensions/chrome-relay/popup.html +369 -0
- package/extensions/chrome-relay/popup.js +972 -0
- package/package.json +51 -0
- package/packages/core/src/bootstrap.test.ts +10 -0
- package/packages/core/src/driver-registry.test.ts +45 -0
- package/packages/core/src/driver-registry.ts +22 -0
- package/packages/core/src/driver.ts +47 -0
- package/packages/core/src/index.ts +5 -0
- package/packages/core/src/ref-cache.test.ts +61 -0
- package/packages/core/src/ref-cache.ts +28 -0
- package/packages/core/src/session-store.test.ts +49 -0
- package/packages/core/src/session-store.ts +33 -0
- package/packages/core/src/types.ts +9 -0
- package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +634 -0
- package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +2206 -0
- package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.test.ts +264 -0
- package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.ts +521 -0
- package/packages/driver-chrome-relay/src/index.ts +26 -0
- package/packages/driver-managed/src/index.ts +22 -0
- package/packages/driver-managed/src/managed-driver.test.ts +59 -0
- package/packages/driver-managed/src/managed-driver.ts +125 -0
- package/packages/driver-managed/src/managed-local-driver.test.ts +506 -0
- package/packages/driver-managed/src/managed-local-driver.ts +2021 -0
- package/packages/driver-remote-cdp/src/index.ts +19 -0
- package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +617 -0
- package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +2042 -0
- package/packages/protocol/src/envelope.test.ts +25 -0
- package/packages/protocol/src/envelope.ts +31 -0
- package/packages/protocol/src/errors.test.ts +17 -0
- package/packages/protocol/src/errors.ts +11 -0
- package/packages/protocol/src/index.ts +3 -0
- package/packages/protocol/src/tools.ts +3 -0
- package/packages/transport-mcp-stdio/src/index.ts +3 -0
- package/packages/transport-mcp-stdio/src/sdk-server.ts +139 -0
- package/packages/transport-mcp-stdio/src/server.test.ts +281 -0
- package/packages/transport-mcp-stdio/src/server.ts +183 -0
- package/packages/transport-mcp-stdio/src/tool-map.ts +67 -0
- package/scripts/smoke.ps1 +127 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { createChromeRelayExtensionRuntime } from "./chrome-relay-extension-runtime";
|
|
4
|
+
|
|
5
|
+
type InvokeResult = Record<string, unknown> | undefined;
|
|
6
|
+
|
|
7
|
+
type InvokeStub = ReturnType<typeof vi.fn<[string, Record<string, unknown> | undefined], Promise<InvokeResult>>>;
|
|
8
|
+
|
|
9
|
+
function createInvokeStub(): InvokeStub {
|
|
10
|
+
return vi.fn(async (_method, _params) => ({}));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("createChromeRelayExtensionRuntime", () => {
|
|
14
|
+
it("throws when extension bridge is not connected", async () => {
|
|
15
|
+
const invoke = createInvokeStub();
|
|
16
|
+
const runtime = createChromeRelayExtensionRuntime({
|
|
17
|
+
transport: {
|
|
18
|
+
invoke,
|
|
19
|
+
isConnected: () => false
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await expect(runtime.connectOverCDP("http://127.0.0.1:9223")).rejects.toThrowError(
|
|
24
|
+
"Chrome relay extension is not connected."
|
|
25
|
+
);
|
|
26
|
+
expect(invoke).toHaveBeenCalledTimes(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("routes page operations to extension transport methods", async () => {
|
|
30
|
+
const invoke = vi.fn(async (method: string, params?: Record<string, unknown>) => {
|
|
31
|
+
if (method === "tab.open") {
|
|
32
|
+
return {
|
|
33
|
+
tabId: 11,
|
|
34
|
+
url: params?.url,
|
|
35
|
+
title: "Opened"
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (method === "tab.info") {
|
|
40
|
+
return {
|
|
41
|
+
tabId: 11,
|
|
42
|
+
url: "https://example.com/next",
|
|
43
|
+
title: "Info title"
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (method === "tab.snapshot") {
|
|
48
|
+
return {
|
|
49
|
+
tabId: 11,
|
|
50
|
+
url: "https://example.com/next",
|
|
51
|
+
title: "Snapshot title",
|
|
52
|
+
html: "<html data-from=\"extension\" />"
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (method === "tab.screenshot") {
|
|
57
|
+
return {
|
|
58
|
+
tabId: 11,
|
|
59
|
+
mimeType: "image/png",
|
|
60
|
+
encoding: "base64",
|
|
61
|
+
imageBase64: "AAEC",
|
|
62
|
+
width: 1200,
|
|
63
|
+
height: 800
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
ok: true
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
const runtime = createChromeRelayExtensionRuntime({
|
|
72
|
+
transport: {
|
|
73
|
+
invoke,
|
|
74
|
+
isConnected: () => true
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const browser = await runtime.connectOverCDP("http://127.0.0.1:9223");
|
|
79
|
+
const context = browser.contexts()[0];
|
|
80
|
+
if (context === undefined) {
|
|
81
|
+
throw new Error("Expected browser context.");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const page = await context.newPage();
|
|
85
|
+
await page.goto("https://example.com/start");
|
|
86
|
+
await page.bringToFront();
|
|
87
|
+
await page.locator("#submit").click();
|
|
88
|
+
await page.locator("#name").fill("BrowserCtl");
|
|
89
|
+
await page.locator("#query").type("hello");
|
|
90
|
+
await page.keyboard?.press("Enter");
|
|
91
|
+
|
|
92
|
+
expect(page.url()).toBe("https://example.com/start");
|
|
93
|
+
await expect(page.title()).resolves.toBe("Info title");
|
|
94
|
+
await expect(page.content()).resolves.toBe("<html data-from=\"extension\" />");
|
|
95
|
+
await expect(page.screenshot?.()).resolves.toEqual({
|
|
96
|
+
mimeType: "image/png",
|
|
97
|
+
encoding: "base64",
|
|
98
|
+
imageBase64: "AAEC",
|
|
99
|
+
width: 1200,
|
|
100
|
+
height: 800
|
|
101
|
+
});
|
|
102
|
+
await page.close();
|
|
103
|
+
|
|
104
|
+
expect(invoke).toHaveBeenCalledWith("tab.open", { url: "https://example.com/start" });
|
|
105
|
+
expect(invoke).toHaveBeenCalledWith("tab.focus", { tabId: 11 });
|
|
106
|
+
expect(invoke).toHaveBeenCalledWith("tab.act", {
|
|
107
|
+
tabId: 11,
|
|
108
|
+
action: { type: "click", payload: { selector: "#submit" } }
|
|
109
|
+
});
|
|
110
|
+
expect(invoke).toHaveBeenCalledWith("tab.act", {
|
|
111
|
+
tabId: 11,
|
|
112
|
+
action: { type: "fill", payload: { selector: "#name", value: "BrowserCtl" } }
|
|
113
|
+
});
|
|
114
|
+
expect(invoke).toHaveBeenCalledWith("tab.act", {
|
|
115
|
+
tabId: 11,
|
|
116
|
+
action: { type: "type", payload: { selector: "#query", text: "hello" } }
|
|
117
|
+
});
|
|
118
|
+
expect(invoke).toHaveBeenCalledWith("tab.act", {
|
|
119
|
+
tabId: 11,
|
|
120
|
+
action: { type: "press", payload: { key: "Enter" } }
|
|
121
|
+
});
|
|
122
|
+
expect(invoke).toHaveBeenCalledWith("tab.info", { tabId: 11 });
|
|
123
|
+
expect(invoke).toHaveBeenCalledWith("tab.snapshot", { tabId: 11 });
|
|
124
|
+
expect(invoke).toHaveBeenCalledWith("tab.screenshot", { tabId: 11 });
|
|
125
|
+
expect(invoke).toHaveBeenCalledWith("tab.close", { tabId: 11 });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("throws when extension does not return a numeric tab id", async () => {
|
|
129
|
+
const invoke = vi.fn(async (method: string) => {
|
|
130
|
+
if (method === "tab.open") {
|
|
131
|
+
return {
|
|
132
|
+
tabId: "bad"
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {};
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const runtime = createChromeRelayExtensionRuntime({
|
|
140
|
+
transport: {
|
|
141
|
+
invoke,
|
|
142
|
+
isConnected: () => true
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const browser = await runtime.connectOverCDP("http://127.0.0.1:9223");
|
|
147
|
+
const context = browser.contexts()[0];
|
|
148
|
+
if (context === undefined) {
|
|
149
|
+
throw new Error("Expected browser context.");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const page = await context.newPage();
|
|
153
|
+
await expect(page.goto("https://example.com/fail")).rejects.toThrowError(
|
|
154
|
+
"Extension relay response is missing numeric tabId for tab.open."
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("maps transport events to page console/response listeners", async () => {
|
|
159
|
+
let onEventListener:
|
|
160
|
+
| ((event: {
|
|
161
|
+
kind: "console" | "response";
|
|
162
|
+
tabId: number;
|
|
163
|
+
entry?: { type: string; text: string };
|
|
164
|
+
response?: {
|
|
165
|
+
requestId: string;
|
|
166
|
+
url: string;
|
|
167
|
+
status: number;
|
|
168
|
+
method: string;
|
|
169
|
+
resourceType: string;
|
|
170
|
+
body: string;
|
|
171
|
+
encoding: "utf8" | "base64";
|
|
172
|
+
};
|
|
173
|
+
}) => void)
|
|
174
|
+
| undefined;
|
|
175
|
+
const invoke = vi.fn(async (method: string, params?: Record<string, unknown>) => {
|
|
176
|
+
if (method === "tab.open") {
|
|
177
|
+
return {
|
|
178
|
+
tabId: 22,
|
|
179
|
+
url: params?.url,
|
|
180
|
+
title: "Opened"
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
ok: true
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
const runtime = createChromeRelayExtensionRuntime({
|
|
189
|
+
transport: {
|
|
190
|
+
invoke,
|
|
191
|
+
isConnected: () => true,
|
|
192
|
+
onEvent: (listener) => {
|
|
193
|
+
onEventListener = listener;
|
|
194
|
+
return () => {
|
|
195
|
+
onEventListener = undefined;
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const browser = await runtime.connectOverCDP("http://127.0.0.1:9223");
|
|
202
|
+
const context = browser.contexts()[0];
|
|
203
|
+
if (context === undefined) {
|
|
204
|
+
throw new Error("Expected browser context.");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const page = await context.newPage();
|
|
208
|
+
await page.goto("https://example.com/events");
|
|
209
|
+
|
|
210
|
+
const consoleListener = vi.fn();
|
|
211
|
+
const responseListener = vi.fn();
|
|
212
|
+
page.on?.("console", consoleListener);
|
|
213
|
+
page.on?.("response", responseListener);
|
|
214
|
+
|
|
215
|
+
onEventListener?.({
|
|
216
|
+
kind: "console",
|
|
217
|
+
tabId: 22,
|
|
218
|
+
entry: {
|
|
219
|
+
type: "warning",
|
|
220
|
+
text: "extension console event"
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
onEventListener?.({
|
|
224
|
+
kind: "response",
|
|
225
|
+
tabId: 22,
|
|
226
|
+
response: {
|
|
227
|
+
requestId: "request:extension:22:1",
|
|
228
|
+
url: "https://example.com/api/items",
|
|
229
|
+
status: 200,
|
|
230
|
+
method: "GET",
|
|
231
|
+
resourceType: "xhr",
|
|
232
|
+
body: "{\"ok\":true}",
|
|
233
|
+
encoding: "utf8"
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(consoleListener).toHaveBeenCalledTimes(1);
|
|
238
|
+
const consoleMessage = consoleListener.mock.calls[0]?.[0] as {
|
|
239
|
+
type(): string;
|
|
240
|
+
text(): string;
|
|
241
|
+
};
|
|
242
|
+
expect(consoleMessage.type()).toBe("warning");
|
|
243
|
+
expect(consoleMessage.text()).toBe("extension console event");
|
|
244
|
+
|
|
245
|
+
expect(responseListener).toHaveBeenCalledTimes(1);
|
|
246
|
+
const responseMessage = responseListener.mock.calls[0]?.[0] as {
|
|
247
|
+
url(): string;
|
|
248
|
+
status(): number;
|
|
249
|
+
request(): {
|
|
250
|
+
method(): string;
|
|
251
|
+
resourceType(): string | undefined;
|
|
252
|
+
};
|
|
253
|
+
text(): Promise<string>;
|
|
254
|
+
};
|
|
255
|
+
expect(responseMessage.url()).toBe("https://example.com/api/items");
|
|
256
|
+
expect(responseMessage.status()).toBe(200);
|
|
257
|
+
expect(responseMessage.request().method()).toBe("GET");
|
|
258
|
+
expect(responseMessage.request().resourceType()).toBe("xhr");
|
|
259
|
+
await expect(responseMessage.text()).resolves.toBe("{\"ok\":true}");
|
|
260
|
+
|
|
261
|
+
await browser.close();
|
|
262
|
+
expect(onEventListener).toBeUndefined();
|
|
263
|
+
});
|
|
264
|
+
});
|