@flrande/browserctl 0.4.0 → 0.5.0-dev.21.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/apps/browserctl/src/commands/act.test.ts +71 -0
- package/apps/browserctl/src/commands/act.ts +45 -1
- package/apps/browserctl/src/commands/command-wrappers.test.ts +302 -0
- package/apps/browserctl/src/commands/console-list.test.ts +102 -0
- package/apps/browserctl/src/commands/console-list.ts +89 -1
- package/apps/browserctl/src/commands/har-export.test.ts +112 -0
- package/apps/browserctl/src/commands/har-export.ts +120 -0
- package/apps/browserctl/src/commands/memory-delete.ts +20 -0
- package/apps/browserctl/src/commands/memory-inspect.ts +20 -0
- package/apps/browserctl/src/commands/memory-list.ts +90 -0
- package/apps/browserctl/src/commands/memory-mode-set.ts +29 -0
- package/apps/browserctl/src/commands/memory-purge.ts +16 -0
- package/apps/browserctl/src/commands/memory-resolve.ts +56 -0
- package/apps/browserctl/src/commands/memory-status.ts +16 -0
- package/apps/browserctl/src/commands/memory-ttl-set.ts +28 -0
- package/apps/browserctl/src/commands/memory-upsert.ts +142 -0
- package/apps/browserctl/src/commands/network-list.test.ts +110 -0
- package/apps/browserctl/src/commands/network-list.ts +112 -0
- package/apps/browserctl/src/commands/session-drop.test.ts +36 -0
- package/apps/browserctl/src/commands/session-drop.ts +16 -0
- package/apps/browserctl/src/commands/session-list.test.ts +81 -0
- package/apps/browserctl/src/commands/session-list.ts +70 -0
- package/apps/browserctl/src/commands/trace-get.test.ts +61 -0
- package/apps/browserctl/src/commands/trace-get.ts +62 -0
- package/apps/browserctl/src/commands/wait-element.test.ts +80 -0
- package/apps/browserctl/src/commands/wait-element.ts +76 -0
- package/apps/browserctl/src/commands/wait-text.test.ts +110 -0
- package/apps/browserctl/src/commands/wait-text.ts +93 -0
- package/apps/browserctl/src/commands/wait-url.test.ts +80 -0
- package/apps/browserctl/src/commands/wait-url.ts +76 -0
- package/apps/browserctl/src/main.dispatch.test.ts +206 -1
- package/apps/browserctl/src/main.test.ts +30 -0
- package/apps/browserctl/src/main.ts +246 -4
- package/apps/browserd/src/container.ts +1603 -48
- package/apps/browserd/src/main.test.ts +538 -1
- package/apps/browserd/src/tool-matrix.test.ts +492 -3
- package/package.json +5 -1
- package/packages/core/src/driver.ts +1 -1
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/navigation-memory.test.ts +259 -0
- package/packages/core/src/navigation-memory.ts +360 -0
- package/packages/core/src/session-store.test.ts +33 -0
- package/packages/core/src/session-store.ts +111 -6
- package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +112 -2
- package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +233 -10
- package/packages/driver-managed/src/managed-driver.test.ts +124 -0
- package/packages/driver-managed/src/managed-driver.ts +233 -17
- package/packages/driver-managed/src/managed-local-driver.test.ts +104 -2
- package/packages/driver-managed/src/managed-local-driver.ts +232 -10
- package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +112 -2
- package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +232 -10
- package/packages/transport-mcp-stdio/src/tool-map.ts +18 -1
|
@@ -102,6 +102,8 @@ export type RemoteCdpPage = {
|
|
|
102
102
|
content(): Promise<string>;
|
|
103
103
|
screenshot?(options?: Record<string, unknown>): Promise<unknown>;
|
|
104
104
|
locator(selector: string): RemoteCdpLocator;
|
|
105
|
+
route?(url: string, handler: RemoteCdpNetworkMockHandler): Promise<void>;
|
|
106
|
+
unroute?(url: string, handler?: RemoteCdpNetworkMockHandler): Promise<void>;
|
|
105
107
|
keyboard?: RemoteCdpKeyboard;
|
|
106
108
|
on?(eventName: string, listener: (payload: unknown) => unknown): void;
|
|
107
109
|
};
|
|
@@ -140,8 +142,12 @@ type RemoteCdpTargetOperationState = {
|
|
|
140
142
|
uploadFiles: string[];
|
|
141
143
|
dialogArmedCount: number;
|
|
142
144
|
triggerCount: number;
|
|
145
|
+
nextNetworkMockNumber: number;
|
|
146
|
+
networkMocks: Map<string, RemoteCdpNetworkMockBinding>;
|
|
147
|
+
requestedDownloadPath?: string;
|
|
143
148
|
downloadInFlight?: Promise<RemoteCdpDownloadArtifact>;
|
|
144
149
|
latestDownload?: RemoteCdpDownloadArtifact;
|
|
150
|
+
latestRawDownload?: unknown;
|
|
145
151
|
};
|
|
146
152
|
|
|
147
153
|
type RemoteCdpDownloadArtifact = {
|
|
@@ -151,6 +157,12 @@ type RemoteCdpDownloadArtifact = {
|
|
|
151
157
|
mimeType?: string;
|
|
152
158
|
};
|
|
153
159
|
|
|
160
|
+
type RemoteCdpNetworkMockHandler = (...args: unknown[]) => Promise<void>;
|
|
161
|
+
type RemoteCdpNetworkMockBinding = {
|
|
162
|
+
urlPattern: string;
|
|
163
|
+
handler: RemoteCdpNetworkMockHandler;
|
|
164
|
+
};
|
|
165
|
+
|
|
154
166
|
type RemoteCdpTargetTelemetryState = {
|
|
155
167
|
consoleEntries: RemoteCdpConsoleEntry[];
|
|
156
168
|
requestSummaries: RemoteCdpNetworkRequestSummary[];
|
|
@@ -196,7 +208,9 @@ function createTargetOperationState(): RemoteCdpTargetOperationState {
|
|
|
196
208
|
return {
|
|
197
209
|
uploadFiles: [],
|
|
198
210
|
dialogArmedCount: 0,
|
|
199
|
-
triggerCount: 0
|
|
211
|
+
triggerCount: 0,
|
|
212
|
+
nextNetworkMockNumber: 1,
|
|
213
|
+
networkMocks: new Map<string, RemoteCdpNetworkMockBinding>()
|
|
200
214
|
};
|
|
201
215
|
}
|
|
202
216
|
|
|
@@ -894,21 +908,26 @@ export function createRemoteCdpDriver(
|
|
|
894
908
|
profileId: ProfileId,
|
|
895
909
|
targetId: TargetId,
|
|
896
910
|
tab: RemoteCdpTab,
|
|
897
|
-
operationState: RemoteCdpTargetOperationState
|
|
911
|
+
operationState: RemoteCdpTargetOperationState,
|
|
912
|
+
requestedPath?: string
|
|
898
913
|
): Promise<RemoteCdpDownloadArtifact> {
|
|
914
|
+
if (requestedPath !== undefined) {
|
|
915
|
+
operationState.requestedDownloadPath = requestedPath;
|
|
916
|
+
}
|
|
917
|
+
|
|
899
918
|
if (operationState.downloadInFlight !== undefined) {
|
|
900
919
|
return operationState.downloadInFlight;
|
|
901
920
|
}
|
|
902
921
|
|
|
903
|
-
const fallbackPath =
|
|
904
|
-
|
|
905
|
-
targetId,
|
|
906
|
-
Math.max(operationState.triggerCount, 1)
|
|
907
|
-
);
|
|
922
|
+
const fallbackPath =
|
|
923
|
+
operationState.requestedDownloadPath ??
|
|
924
|
+
createDownloadPath(profileId, targetId, Math.max(operationState.triggerCount, 1));
|
|
908
925
|
const waitForEventMethod = getObjectMethod(tab.page, "waitForEvent");
|
|
909
926
|
if (typeof waitForEventMethod !== "function") {
|
|
910
927
|
const artifact: RemoteCdpDownloadArtifact = { path: fallbackPath };
|
|
928
|
+
operationState.requestedDownloadPath = undefined;
|
|
911
929
|
operationState.latestDownload = artifact;
|
|
930
|
+
operationState.latestRawDownload = undefined;
|
|
912
931
|
const resolved = Promise.resolve(artifact);
|
|
913
932
|
operationState.downloadInFlight = resolved;
|
|
914
933
|
return resolved;
|
|
@@ -916,8 +935,13 @@ export function createRemoteCdpDriver(
|
|
|
916
935
|
|
|
917
936
|
const inFlight = (async () => {
|
|
918
937
|
const rawDownload = await waitForEventMethod.call(tab.page, "download");
|
|
919
|
-
|
|
938
|
+
operationState.latestRawDownload = rawDownload;
|
|
939
|
+
const persistedPath =
|
|
940
|
+
operationState.requestedDownloadPath ??
|
|
941
|
+
createDownloadPath(profileId, targetId, Math.max(operationState.triggerCount, 1));
|
|
942
|
+
const artifact = await readDownloadArtifact(rawDownload, persistedPath);
|
|
920
943
|
operationState.latestDownload = artifact;
|
|
944
|
+
operationState.requestedDownloadPath = undefined;
|
|
921
945
|
return artifact;
|
|
922
946
|
})().finally(() => {
|
|
923
947
|
if (operationState.downloadInFlight === inFlight) {
|
|
@@ -929,6 +953,30 @@ export function createRemoteCdpDriver(
|
|
|
929
953
|
return inFlight;
|
|
930
954
|
}
|
|
931
955
|
|
|
956
|
+
async function resolveDownloadArtifactPath(
|
|
957
|
+
operationState: RemoteCdpTargetOperationState,
|
|
958
|
+
artifact: RemoteCdpDownloadArtifact,
|
|
959
|
+
requestedPath?: string
|
|
960
|
+
): Promise<RemoteCdpDownloadArtifact> {
|
|
961
|
+
if (requestedPath === undefined || requestedPath.trim().length === 0 || artifact.path === requestedPath) {
|
|
962
|
+
return artifact;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const failurePrefix = `Failed to persist download to requested path: ${requestedPath}`;
|
|
966
|
+
const saveAsMethod = getObjectMethod(operationState.latestRawDownload, "saveAs");
|
|
967
|
+
if (typeof saveAsMethod !== "function") {
|
|
968
|
+
throw new Error(failurePrefix);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
try {
|
|
972
|
+
await saveAsMethod.call(operationState.latestRawDownload, requestedPath);
|
|
973
|
+
return { ...artifact, path: requestedPath };
|
|
974
|
+
} catch (error) {
|
|
975
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
976
|
+
throw new Error(message.length > 0 ? `${failurePrefix} (${message})` : failurePrefix);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
932
980
|
return {
|
|
933
981
|
status: async () => ({ kind: "remote-cdp", connected: browser !== undefined, endpoint }),
|
|
934
982
|
listProfiles: async () => {
|
|
@@ -1163,6 +1211,141 @@ export function createRemoteCdpDriver(
|
|
|
1163
1211
|
await tab.page.keyboard.press(key);
|
|
1164
1212
|
return { ok: true, executed: true };
|
|
1165
1213
|
}
|
|
1214
|
+
case "networkMockAdd": {
|
|
1215
|
+
const urlPattern = readActionPayloadString(payload, "urlPattern");
|
|
1216
|
+
if (urlPattern === undefined) {
|
|
1217
|
+
return {
|
|
1218
|
+
ok: false,
|
|
1219
|
+
executed: false,
|
|
1220
|
+
error: "action.payload.urlPattern is required for networkMockAdd action."
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const routeMethod = getObjectMethod(tab.page, "route");
|
|
1225
|
+
if (typeof routeMethod !== "function") {
|
|
1226
|
+
return {
|
|
1227
|
+
ok: true,
|
|
1228
|
+
executed: false
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
const methodFilter = readActionPayloadString(payload, "method")?.toUpperCase();
|
|
1233
|
+
const status = readActionPayloadNumber(payload, "status") ?? 200;
|
|
1234
|
+
if (!Number.isInteger(status) || status < 100 || status > 599) {
|
|
1235
|
+
return {
|
|
1236
|
+
ok: false,
|
|
1237
|
+
executed: false,
|
|
1238
|
+
error: "action.payload.status must be an integer between 100 and 599 for networkMockAdd action."
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
const body = readActionPayloadString(payload, "body") ?? "";
|
|
1242
|
+
const contentType = readActionPayloadString(payload, "contentType") ?? "text/plain";
|
|
1243
|
+
const mockId = `mock:${operationState.nextNetworkMockNumber}`;
|
|
1244
|
+
operationState.nextNetworkMockNumber += 1;
|
|
1245
|
+
const handler: RemoteCdpNetworkMockHandler = async (...args) => {
|
|
1246
|
+
const route = args[0];
|
|
1247
|
+
const explicitRequest = args[1];
|
|
1248
|
+
const request = explicitRequest ?? readObjectMethod<unknown>(route, "request");
|
|
1249
|
+
if (methodFilter !== undefined) {
|
|
1250
|
+
const requestMethod = readObjectMethod<string>(request, "method");
|
|
1251
|
+
if (requestMethod?.toUpperCase() !== methodFilter) {
|
|
1252
|
+
const continueMethod = getObjectMethod(route, "continue");
|
|
1253
|
+
if (typeof continueMethod === "function") {
|
|
1254
|
+
await continueMethod.call(route);
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const fallbackMethod = getObjectMethod(route, "fallback");
|
|
1259
|
+
if (typeof fallbackMethod === "function") {
|
|
1260
|
+
await fallbackMethod.call(route);
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
const fulfillMethod = getObjectMethod(route, "fulfill");
|
|
1269
|
+
if (typeof fulfillMethod !== "function") {
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
await fulfillMethod.call(route, {
|
|
1274
|
+
status,
|
|
1275
|
+
body,
|
|
1276
|
+
headers: {
|
|
1277
|
+
"content-type": contentType
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
};
|
|
1281
|
+
|
|
1282
|
+
await routeMethod.call(tab.page, urlPattern, handler);
|
|
1283
|
+
operationState.networkMocks.set(mockId, {
|
|
1284
|
+
urlPattern,
|
|
1285
|
+
handler
|
|
1286
|
+
});
|
|
1287
|
+
return {
|
|
1288
|
+
ok: true,
|
|
1289
|
+
executed: true,
|
|
1290
|
+
data: {
|
|
1291
|
+
mockId,
|
|
1292
|
+
urlPattern,
|
|
1293
|
+
...(methodFilter !== undefined ? { method: methodFilter } : {}),
|
|
1294
|
+
status
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
case "networkMockClear": {
|
|
1299
|
+
const unrouteMethod = getObjectMethod(tab.page, "unroute");
|
|
1300
|
+
if (typeof unrouteMethod !== "function") {
|
|
1301
|
+
return {
|
|
1302
|
+
ok: true,
|
|
1303
|
+
executed: false
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const mockId = readActionPayloadString(payload, "mockId");
|
|
1308
|
+
if (mockId !== undefined) {
|
|
1309
|
+
const binding = operationState.networkMocks.get(mockId);
|
|
1310
|
+
if (binding === undefined) {
|
|
1311
|
+
return {
|
|
1312
|
+
ok: false,
|
|
1313
|
+
executed: false,
|
|
1314
|
+
error: `Unknown network mock id: ${mockId}`
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
await unrouteMethod.call(tab.page, binding.urlPattern, binding.handler);
|
|
1319
|
+
operationState.networkMocks.delete(mockId);
|
|
1320
|
+
return {
|
|
1321
|
+
ok: true,
|
|
1322
|
+
executed: true,
|
|
1323
|
+
data: {
|
|
1324
|
+
cleared: 1,
|
|
1325
|
+
mockId
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
let cleared = 0;
|
|
1331
|
+
for (const [registeredId, binding] of operationState.networkMocks.entries()) {
|
|
1332
|
+
try {
|
|
1333
|
+
await unrouteMethod.call(tab.page, binding.urlPattern, binding.handler);
|
|
1334
|
+
} catch {
|
|
1335
|
+
// Ignore per-route cleanup failures.
|
|
1336
|
+
}
|
|
1337
|
+
operationState.networkMocks.delete(registeredId);
|
|
1338
|
+
cleared += 1;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
return {
|
|
1342
|
+
ok: true,
|
|
1343
|
+
executed: true,
|
|
1344
|
+
data: {
|
|
1345
|
+
cleared
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1166
1349
|
case "domQuery": {
|
|
1167
1350
|
const selector = readActionPayloadString(payload, "selector");
|
|
1168
1351
|
if (selector === undefined) {
|
|
@@ -2002,11 +2185,47 @@ export function createRemoteCdpDriver(
|
|
|
2002
2185
|
const operationState = getOrCreateTargetOperationState(profileId, targetId);
|
|
2003
2186
|
operationState.dialogArmedCount += 1;
|
|
2004
2187
|
},
|
|
2005
|
-
waitDownload: async (targetId, profile) => {
|
|
2188
|
+
waitDownload: async (targetId, profile, path) => {
|
|
2006
2189
|
const profileId = resolveProfileId(profile);
|
|
2007
2190
|
const { tab } = requireTargetInProfile(profileId, targetId);
|
|
2008
2191
|
const operationState = getOrCreateTargetOperationState(profileId, targetId);
|
|
2009
|
-
const
|
|
2192
|
+
const requestedPath = path !== undefined && path.trim().length > 0 ? path : undefined;
|
|
2193
|
+
if (operationState.downloadInFlight === undefined && operationState.latestDownload !== undefined) {
|
|
2194
|
+
const download = await resolveDownloadArtifactPath(
|
|
2195
|
+
operationState,
|
|
2196
|
+
{ ...operationState.latestDownload },
|
|
2197
|
+
requestedPath
|
|
2198
|
+
);
|
|
2199
|
+
operationState.latestDownload = undefined;
|
|
2200
|
+
operationState.latestRawDownload = undefined;
|
|
2201
|
+
operationState.requestedDownloadPath = undefined;
|
|
2202
|
+
return {
|
|
2203
|
+
path: download.path,
|
|
2204
|
+
profile: profileId,
|
|
2205
|
+
targetId,
|
|
2206
|
+
endpoint,
|
|
2207
|
+
uploadFiles: [...operationState.uploadFiles],
|
|
2208
|
+
dialogArmedCount: operationState.dialogArmedCount,
|
|
2209
|
+
triggerCount: operationState.triggerCount,
|
|
2210
|
+
...(download.suggestedFilename !== undefined
|
|
2211
|
+
? { suggestedFilename: download.suggestedFilename }
|
|
2212
|
+
: {}),
|
|
2213
|
+
...(download.url !== undefined ? { url: download.url } : {}),
|
|
2214
|
+
...(download.mimeType !== undefined ? { mimeType: download.mimeType } : {})
|
|
2215
|
+
};
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
const inFlightDownload = await ensureDownloadInFlight(
|
|
2219
|
+
profileId,
|
|
2220
|
+
targetId,
|
|
2221
|
+
tab,
|
|
2222
|
+
operationState,
|
|
2223
|
+
requestedPath
|
|
2224
|
+
);
|
|
2225
|
+
const download = await resolveDownloadArtifactPath(operationState, inFlightDownload, requestedPath);
|
|
2226
|
+
operationState.latestDownload = undefined;
|
|
2227
|
+
operationState.latestRawDownload = undefined;
|
|
2228
|
+
operationState.requestedDownloadPath = undefined;
|
|
2010
2229
|
|
|
2011
2230
|
return {
|
|
2012
2231
|
path: download.path,
|
|
@@ -2028,6 +2247,9 @@ export function createRemoteCdpDriver(
|
|
|
2028
2247
|
const { tab } = requireTargetInProfile(profileId, targetId);
|
|
2029
2248
|
const operationState = getOrCreateTargetOperationState(profileId, targetId);
|
|
2030
2249
|
operationState.triggerCount += 1;
|
|
2250
|
+
operationState.latestDownload = undefined;
|
|
2251
|
+
operationState.latestRawDownload = undefined;
|
|
2252
|
+
operationState.requestedDownloadPath = undefined;
|
|
2031
2253
|
void ensureDownloadInFlight(profileId, targetId, tab, operationState);
|
|
2032
2254
|
},
|
|
2033
2255
|
getConsoleEntries: (targetId, profile) => {
|
|
@@ -28,6 +28,11 @@ export const V1_TOOL_NAMES = [
|
|
|
28
28
|
"browser.dom.queryAll",
|
|
29
29
|
"browser.element.screenshot",
|
|
30
30
|
"browser.a11y.snapshot",
|
|
31
|
+
"browser.wait.element",
|
|
32
|
+
"browser.wait.text",
|
|
33
|
+
"browser.wait.url",
|
|
34
|
+
"browser.network.list",
|
|
35
|
+
"browser.network.harExport",
|
|
31
36
|
"browser.act",
|
|
32
37
|
"browser.upload.arm",
|
|
33
38
|
"browser.dialog.arm",
|
|
@@ -42,7 +47,19 @@ export const V1_TOOL_NAMES = [
|
|
|
42
47
|
"browser.frame.list",
|
|
43
48
|
"browser.frame.snapshot",
|
|
44
49
|
"browser.console.list",
|
|
45
|
-
"browser.network.responseBody"
|
|
50
|
+
"browser.network.responseBody",
|
|
51
|
+
"browser.memory.status",
|
|
52
|
+
"browser.memory.resolve",
|
|
53
|
+
"browser.memory.upsert",
|
|
54
|
+
"browser.memory.list",
|
|
55
|
+
"browser.memory.inspect",
|
|
56
|
+
"browser.memory.delete",
|
|
57
|
+
"browser.memory.purge",
|
|
58
|
+
"browser.memory.mode.set",
|
|
59
|
+
"browser.memory.ttl.set",
|
|
60
|
+
"browser.trace.get",
|
|
61
|
+
"browser.session.list",
|
|
62
|
+
"browser.session.drop"
|
|
46
63
|
] as const;
|
|
47
64
|
|
|
48
65
|
const browserStatusHandler: ToolHandler<{ kind: "stdio"; ready: true }> = async () =>
|