@flrande/browserctl 0.4.0 → 0.5.0-dev.19.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.
Files changed (52) hide show
  1. package/apps/browserctl/src/commands/act.test.ts +71 -0
  2. package/apps/browserctl/src/commands/act.ts +45 -1
  3. package/apps/browserctl/src/commands/command-wrappers.test.ts +302 -0
  4. package/apps/browserctl/src/commands/console-list.test.ts +102 -0
  5. package/apps/browserctl/src/commands/console-list.ts +89 -1
  6. package/apps/browserctl/src/commands/har-export.test.ts +112 -0
  7. package/apps/browserctl/src/commands/har-export.ts +120 -0
  8. package/apps/browserctl/src/commands/memory-delete.ts +20 -0
  9. package/apps/browserctl/src/commands/memory-inspect.ts +20 -0
  10. package/apps/browserctl/src/commands/memory-list.ts +90 -0
  11. package/apps/browserctl/src/commands/memory-mode-set.ts +29 -0
  12. package/apps/browserctl/src/commands/memory-purge.ts +16 -0
  13. package/apps/browserctl/src/commands/memory-resolve.ts +56 -0
  14. package/apps/browserctl/src/commands/memory-status.ts +16 -0
  15. package/apps/browserctl/src/commands/memory-ttl-set.ts +28 -0
  16. package/apps/browserctl/src/commands/memory-upsert.ts +142 -0
  17. package/apps/browserctl/src/commands/network-list.test.ts +110 -0
  18. package/apps/browserctl/src/commands/network-list.ts +112 -0
  19. package/apps/browserctl/src/commands/session-drop.test.ts +36 -0
  20. package/apps/browserctl/src/commands/session-drop.ts +16 -0
  21. package/apps/browserctl/src/commands/session-list.test.ts +81 -0
  22. package/apps/browserctl/src/commands/session-list.ts +70 -0
  23. package/apps/browserctl/src/commands/trace-get.test.ts +61 -0
  24. package/apps/browserctl/src/commands/trace-get.ts +62 -0
  25. package/apps/browserctl/src/commands/wait-element.test.ts +80 -0
  26. package/apps/browserctl/src/commands/wait-element.ts +76 -0
  27. package/apps/browserctl/src/commands/wait-text.test.ts +110 -0
  28. package/apps/browserctl/src/commands/wait-text.ts +93 -0
  29. package/apps/browserctl/src/commands/wait-url.test.ts +80 -0
  30. package/apps/browserctl/src/commands/wait-url.ts +76 -0
  31. package/apps/browserctl/src/main.dispatch.test.ts +206 -1
  32. package/apps/browserctl/src/main.test.ts +30 -0
  33. package/apps/browserctl/src/main.ts +246 -4
  34. package/apps/browserd/src/container.ts +1603 -48
  35. package/apps/browserd/src/main.test.ts +538 -1
  36. package/apps/browserd/src/tool-matrix.test.ts +492 -3
  37. package/package.json +5 -1
  38. package/packages/core/src/driver.ts +1 -1
  39. package/packages/core/src/index.ts +1 -0
  40. package/packages/core/src/navigation-memory.test.ts +259 -0
  41. package/packages/core/src/navigation-memory.ts +360 -0
  42. package/packages/core/src/session-store.test.ts +33 -0
  43. package/packages/core/src/session-store.ts +111 -6
  44. package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +112 -2
  45. package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +233 -10
  46. package/packages/driver-managed/src/managed-driver.test.ts +124 -0
  47. package/packages/driver-managed/src/managed-driver.ts +233 -17
  48. package/packages/driver-managed/src/managed-local-driver.test.ts +104 -2
  49. package/packages/driver-managed/src/managed-local-driver.ts +232 -10
  50. package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +112 -2
  51. package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +232 -10
  52. 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 = createDownloadPath(
904
- profileId,
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
- const artifact = await readDownloadArtifact(rawDownload, fallbackPath);
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 download = await ensureDownloadInFlight(profileId, targetId, tab, operationState);
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 () =>