@flrande/browserctl 0.4.0-dev.15.1 → 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.
- 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
|
@@ -115,6 +115,8 @@ export type ManagedLocalPage = {
|
|
|
115
115
|
content(): Promise<string>;
|
|
116
116
|
screenshot?(options?: Record<string, unknown>): Promise<unknown>;
|
|
117
117
|
locator(selector: string): ManagedLocalLocator;
|
|
118
|
+
route?(url: string, handler: ManagedLocalNetworkMockHandler): Promise<void>;
|
|
119
|
+
unroute?(url: string, handler?: ManagedLocalNetworkMockHandler): Promise<void>;
|
|
118
120
|
keyboard?: ManagedLocalKeyboard;
|
|
119
121
|
on?(eventName: string, listener: (payload: unknown) => unknown): void;
|
|
120
122
|
};
|
|
@@ -154,8 +156,12 @@ type ManagedLocalTargetOperationState = {
|
|
|
154
156
|
uploadFiles: string[];
|
|
155
157
|
dialogArmedCount: number;
|
|
156
158
|
triggerCount: number;
|
|
159
|
+
nextNetworkMockNumber: number;
|
|
160
|
+
networkMocks: Map<string, ManagedLocalNetworkMockBinding>;
|
|
161
|
+
requestedDownloadPath?: string;
|
|
157
162
|
downloadInFlight?: Promise<ManagedLocalDownloadArtifact>;
|
|
158
163
|
latestDownload?: ManagedLocalDownloadArtifact;
|
|
164
|
+
latestRawDownload?: unknown;
|
|
159
165
|
};
|
|
160
166
|
|
|
161
167
|
type ManagedLocalDownloadArtifact = {
|
|
@@ -165,6 +171,12 @@ type ManagedLocalDownloadArtifact = {
|
|
|
165
171
|
mimeType?: string;
|
|
166
172
|
};
|
|
167
173
|
|
|
174
|
+
type ManagedLocalNetworkMockHandler = (...args: unknown[]) => Promise<void>;
|
|
175
|
+
type ManagedLocalNetworkMockBinding = {
|
|
176
|
+
urlPattern: string;
|
|
177
|
+
handler: ManagedLocalNetworkMockHandler;
|
|
178
|
+
};
|
|
179
|
+
|
|
168
180
|
type ManagedLocalTargetTelemetryState = {
|
|
169
181
|
consoleEntries: ManagedLocalConsoleEntry[];
|
|
170
182
|
requestSummaries: ManagedLocalNetworkRequestSummary[];
|
|
@@ -192,7 +204,9 @@ function createTargetOperationState(): ManagedLocalTargetOperationState {
|
|
|
192
204
|
return {
|
|
193
205
|
uploadFiles: [],
|
|
194
206
|
dialogArmedCount: 0,
|
|
195
|
-
triggerCount: 0
|
|
207
|
+
triggerCount: 0,
|
|
208
|
+
nextNetworkMockNumber: 1,
|
|
209
|
+
networkMocks: new Map<string, ManagedLocalNetworkMockBinding>()
|
|
196
210
|
};
|
|
197
211
|
}
|
|
198
212
|
|
|
@@ -889,21 +903,26 @@ export function createManagedLocalDriver(
|
|
|
889
903
|
profileId: ProfileId,
|
|
890
904
|
targetId: TargetId,
|
|
891
905
|
tab: ManagedLocalTab,
|
|
892
|
-
operationState: ManagedLocalTargetOperationState
|
|
906
|
+
operationState: ManagedLocalTargetOperationState,
|
|
907
|
+
requestedPath?: string
|
|
893
908
|
): Promise<ManagedLocalDownloadArtifact> {
|
|
909
|
+
if (requestedPath !== undefined) {
|
|
910
|
+
operationState.requestedDownloadPath = requestedPath;
|
|
911
|
+
}
|
|
912
|
+
|
|
894
913
|
if (operationState.downloadInFlight !== undefined) {
|
|
895
914
|
return operationState.downloadInFlight;
|
|
896
915
|
}
|
|
897
916
|
|
|
898
|
-
const fallbackPath =
|
|
899
|
-
|
|
900
|
-
targetId,
|
|
901
|
-
Math.max(operationState.triggerCount, 1)
|
|
902
|
-
);
|
|
917
|
+
const fallbackPath =
|
|
918
|
+
operationState.requestedDownloadPath ??
|
|
919
|
+
createDownloadPath(profileId, targetId, Math.max(operationState.triggerCount, 1));
|
|
903
920
|
const waitForEventMethod = getObjectMethod(tab.page, "waitForEvent");
|
|
904
921
|
if (typeof waitForEventMethod !== "function") {
|
|
905
922
|
const artifact: ManagedLocalDownloadArtifact = { path: fallbackPath };
|
|
923
|
+
operationState.requestedDownloadPath = undefined;
|
|
906
924
|
operationState.latestDownload = artifact;
|
|
925
|
+
operationState.latestRawDownload = undefined;
|
|
907
926
|
const resolved = Promise.resolve(artifact);
|
|
908
927
|
operationState.downloadInFlight = resolved;
|
|
909
928
|
return resolved;
|
|
@@ -911,8 +930,13 @@ export function createManagedLocalDriver(
|
|
|
911
930
|
|
|
912
931
|
const inFlight = (async () => {
|
|
913
932
|
const rawDownload = await waitForEventMethod.call(tab.page, "download");
|
|
914
|
-
|
|
933
|
+
operationState.latestRawDownload = rawDownload;
|
|
934
|
+
const persistedPath =
|
|
935
|
+
operationState.requestedDownloadPath ??
|
|
936
|
+
createDownloadPath(profileId, targetId, Math.max(operationState.triggerCount, 1));
|
|
937
|
+
const artifact = await readDownloadArtifact(rawDownload, persistedPath);
|
|
915
938
|
operationState.latestDownload = artifact;
|
|
939
|
+
operationState.requestedDownloadPath = undefined;
|
|
916
940
|
return artifact;
|
|
917
941
|
})().finally(() => {
|
|
918
942
|
if (operationState.downloadInFlight === inFlight) {
|
|
@@ -924,6 +948,30 @@ export function createManagedLocalDriver(
|
|
|
924
948
|
return inFlight;
|
|
925
949
|
}
|
|
926
950
|
|
|
951
|
+
async function resolveDownloadArtifactPath(
|
|
952
|
+
operationState: ManagedLocalTargetOperationState,
|
|
953
|
+
artifact: ManagedLocalDownloadArtifact,
|
|
954
|
+
requestedPath?: string
|
|
955
|
+
): Promise<ManagedLocalDownloadArtifact> {
|
|
956
|
+
if (requestedPath === undefined || requestedPath.trim().length === 0 || artifact.path === requestedPath) {
|
|
957
|
+
return artifact;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const failurePrefix = `Failed to persist download to requested path: ${requestedPath}`;
|
|
961
|
+
const saveAsMethod = getObjectMethod(operationState.latestRawDownload, "saveAs");
|
|
962
|
+
if (typeof saveAsMethod !== "function") {
|
|
963
|
+
throw new Error(failurePrefix);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
try {
|
|
967
|
+
await saveAsMethod.call(operationState.latestRawDownload, requestedPath);
|
|
968
|
+
return { ...artifact, path: requestedPath };
|
|
969
|
+
} catch (error) {
|
|
970
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
971
|
+
throw new Error(message.length > 0 ? `${failurePrefix} (${message})` : failurePrefix);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
927
975
|
return {
|
|
928
976
|
status: async () => ({
|
|
929
977
|
kind: "managed-local",
|
|
@@ -1167,6 +1215,141 @@ export function createManagedLocalDriver(
|
|
|
1167
1215
|
await tab.page.keyboard.press(key);
|
|
1168
1216
|
return { ok: true, executed: true };
|
|
1169
1217
|
}
|
|
1218
|
+
case "networkMockAdd": {
|
|
1219
|
+
const urlPattern = readActionPayloadString(payload, "urlPattern");
|
|
1220
|
+
if (urlPattern === undefined) {
|
|
1221
|
+
return {
|
|
1222
|
+
ok: false,
|
|
1223
|
+
executed: false,
|
|
1224
|
+
error: "action.payload.urlPattern is required for networkMockAdd action."
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const routeMethod = getObjectMethod(tab.page, "route");
|
|
1229
|
+
if (typeof routeMethod !== "function") {
|
|
1230
|
+
return {
|
|
1231
|
+
ok: true,
|
|
1232
|
+
executed: false
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const methodFilter = readActionPayloadString(payload, "method")?.toUpperCase();
|
|
1237
|
+
const status = readActionPayloadNumber(payload, "status") ?? 200;
|
|
1238
|
+
if (!Number.isInteger(status) || status < 100 || status > 599) {
|
|
1239
|
+
return {
|
|
1240
|
+
ok: false,
|
|
1241
|
+
executed: false,
|
|
1242
|
+
error: "action.payload.status must be an integer between 100 and 599 for networkMockAdd action."
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
const body = readActionPayloadString(payload, "body") ?? "";
|
|
1246
|
+
const contentType = readActionPayloadString(payload, "contentType") ?? "text/plain";
|
|
1247
|
+
const mockId = `mock:${operationState.nextNetworkMockNumber}`;
|
|
1248
|
+
operationState.nextNetworkMockNumber += 1;
|
|
1249
|
+
const handler: ManagedLocalNetworkMockHandler = async (...args) => {
|
|
1250
|
+
const route = args[0];
|
|
1251
|
+
const explicitRequest = args[1];
|
|
1252
|
+
const request = explicitRequest ?? readObjectMethod<unknown>(route, "request");
|
|
1253
|
+
if (methodFilter !== undefined) {
|
|
1254
|
+
const requestMethod = readObjectMethod<string>(request, "method");
|
|
1255
|
+
if (requestMethod?.toUpperCase() !== methodFilter) {
|
|
1256
|
+
const continueMethod = getObjectMethod(route, "continue");
|
|
1257
|
+
if (typeof continueMethod === "function") {
|
|
1258
|
+
await continueMethod.call(route);
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const fallbackMethod = getObjectMethod(route, "fallback");
|
|
1263
|
+
if (typeof fallbackMethod === "function") {
|
|
1264
|
+
await fallbackMethod.call(route);
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
const fulfillMethod = getObjectMethod(route, "fulfill");
|
|
1273
|
+
if (typeof fulfillMethod !== "function") {
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
await fulfillMethod.call(route, {
|
|
1278
|
+
status,
|
|
1279
|
+
body,
|
|
1280
|
+
headers: {
|
|
1281
|
+
"content-type": contentType
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
};
|
|
1285
|
+
|
|
1286
|
+
await routeMethod.call(tab.page, urlPattern, handler);
|
|
1287
|
+
operationState.networkMocks.set(mockId, {
|
|
1288
|
+
urlPattern,
|
|
1289
|
+
handler
|
|
1290
|
+
});
|
|
1291
|
+
return {
|
|
1292
|
+
ok: true,
|
|
1293
|
+
executed: true,
|
|
1294
|
+
data: {
|
|
1295
|
+
mockId,
|
|
1296
|
+
urlPattern,
|
|
1297
|
+
...(methodFilter !== undefined ? { method: methodFilter } : {}),
|
|
1298
|
+
status
|
|
1299
|
+
}
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
case "networkMockClear": {
|
|
1303
|
+
const unrouteMethod = getObjectMethod(tab.page, "unroute");
|
|
1304
|
+
if (typeof unrouteMethod !== "function") {
|
|
1305
|
+
return {
|
|
1306
|
+
ok: true,
|
|
1307
|
+
executed: false
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
const mockId = readActionPayloadString(payload, "mockId");
|
|
1312
|
+
if (mockId !== undefined) {
|
|
1313
|
+
const binding = operationState.networkMocks.get(mockId);
|
|
1314
|
+
if (binding === undefined) {
|
|
1315
|
+
return {
|
|
1316
|
+
ok: false,
|
|
1317
|
+
executed: false,
|
|
1318
|
+
error: `Unknown network mock id: ${mockId}`
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
await unrouteMethod.call(tab.page, binding.urlPattern, binding.handler);
|
|
1323
|
+
operationState.networkMocks.delete(mockId);
|
|
1324
|
+
return {
|
|
1325
|
+
ok: true,
|
|
1326
|
+
executed: true,
|
|
1327
|
+
data: {
|
|
1328
|
+
cleared: 1,
|
|
1329
|
+
mockId
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
let cleared = 0;
|
|
1335
|
+
for (const [registeredId, binding] of operationState.networkMocks.entries()) {
|
|
1336
|
+
try {
|
|
1337
|
+
await unrouteMethod.call(tab.page, binding.urlPattern, binding.handler);
|
|
1338
|
+
} catch {
|
|
1339
|
+
// Ignore per-route cleanup failures.
|
|
1340
|
+
}
|
|
1341
|
+
operationState.networkMocks.delete(registeredId);
|
|
1342
|
+
cleared += 1;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
return {
|
|
1346
|
+
ok: true,
|
|
1347
|
+
executed: true,
|
|
1348
|
+
data: {
|
|
1349
|
+
cleared
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1170
1353
|
case "domQuery": {
|
|
1171
1354
|
const selector = readActionPayloadString(payload, "selector");
|
|
1172
1355
|
if (selector === undefined) {
|
|
@@ -1982,11 +2165,47 @@ export function createManagedLocalDriver(
|
|
|
1982
2165
|
const operationState = getOrCreateTargetOperationState(profileId, targetId);
|
|
1983
2166
|
operationState.dialogArmedCount += 1;
|
|
1984
2167
|
},
|
|
1985
|
-
waitDownload: async (targetId, profile) => {
|
|
2168
|
+
waitDownload: async (targetId, profile, path) => {
|
|
1986
2169
|
const profileId = resolveProfileId(profile);
|
|
1987
2170
|
const { tab } = requireTargetInProfile(profileId, targetId);
|
|
1988
2171
|
const operationState = getOrCreateTargetOperationState(profileId, targetId);
|
|
1989
|
-
const
|
|
2172
|
+
const requestedPath = path !== undefined && path.trim().length > 0 ? path : undefined;
|
|
2173
|
+
if (operationState.downloadInFlight === undefined && operationState.latestDownload !== undefined) {
|
|
2174
|
+
const download = await resolveDownloadArtifactPath(
|
|
2175
|
+
operationState,
|
|
2176
|
+
{ ...operationState.latestDownload },
|
|
2177
|
+
requestedPath
|
|
2178
|
+
);
|
|
2179
|
+
|
|
2180
|
+
operationState.latestDownload = undefined;
|
|
2181
|
+
operationState.latestRawDownload = undefined;
|
|
2182
|
+
operationState.requestedDownloadPath = undefined;
|
|
2183
|
+
return {
|
|
2184
|
+
path: download.path,
|
|
2185
|
+
profile: profileId,
|
|
2186
|
+
targetId,
|
|
2187
|
+
uploadFiles: [...operationState.uploadFiles],
|
|
2188
|
+
dialogArmedCount: operationState.dialogArmedCount,
|
|
2189
|
+
triggerCount: operationState.triggerCount,
|
|
2190
|
+
...(download.suggestedFilename !== undefined
|
|
2191
|
+
? { suggestedFilename: download.suggestedFilename }
|
|
2192
|
+
: {}),
|
|
2193
|
+
...(download.url !== undefined ? { url: download.url } : {}),
|
|
2194
|
+
...(download.mimeType !== undefined ? { mimeType: download.mimeType } : {})
|
|
2195
|
+
};
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
const inFlightDownload = await ensureDownloadInFlight(
|
|
2199
|
+
profileId,
|
|
2200
|
+
targetId,
|
|
2201
|
+
tab,
|
|
2202
|
+
operationState,
|
|
2203
|
+
requestedPath
|
|
2204
|
+
);
|
|
2205
|
+
const download = await resolveDownloadArtifactPath(operationState, inFlightDownload, requestedPath);
|
|
2206
|
+
operationState.latestDownload = undefined;
|
|
2207
|
+
operationState.latestRawDownload = undefined;
|
|
2208
|
+
operationState.requestedDownloadPath = undefined;
|
|
1990
2209
|
|
|
1991
2210
|
return {
|
|
1992
2211
|
path: download.path,
|
|
@@ -2007,6 +2226,9 @@ export function createManagedLocalDriver(
|
|
|
2007
2226
|
const { tab } = requireTargetInProfile(profileId, targetId);
|
|
2008
2227
|
const operationState = getOrCreateTargetOperationState(profileId, targetId);
|
|
2009
2228
|
operationState.triggerCount += 1;
|
|
2229
|
+
operationState.latestDownload = undefined;
|
|
2230
|
+
operationState.latestRawDownload = undefined;
|
|
2231
|
+
operationState.requestedDownloadPath = undefined;
|
|
2010
2232
|
void ensureDownloadInFlight(profileId, targetId, tab, operationState);
|
|
2011
2233
|
},
|
|
2012
2234
|
getConsoleEntries: (targetId, profile) => {
|
|
@@ -50,6 +50,8 @@ type MockPage = {
|
|
|
50
50
|
type(value: string): Promise<void>;
|
|
51
51
|
setInputFiles?(files: string[]): Promise<void>;
|
|
52
52
|
};
|
|
53
|
+
route?(url: string, handler: (...args: unknown[]) => Promise<void>): Promise<void>;
|
|
54
|
+
unroute?(url: string, handler?: (...args: unknown[]) => Promise<void>): Promise<void>;
|
|
53
55
|
keyboard?: {
|
|
54
56
|
press(key: string): Promise<void>;
|
|
55
57
|
};
|
|
@@ -71,6 +73,8 @@ type MockPageRecord = {
|
|
|
71
73
|
locatorType: ReturnType<typeof vi.fn>;
|
|
72
74
|
locatorSetInputFiles: ReturnType<typeof vi.fn>;
|
|
73
75
|
keyboardPress: ReturnType<typeof vi.fn>;
|
|
76
|
+
route: ReturnType<typeof vi.fn>;
|
|
77
|
+
unroute: ReturnType<typeof vi.fn>;
|
|
74
78
|
waitForEvent: ReturnType<typeof vi.fn>;
|
|
75
79
|
on: ReturnType<typeof vi.fn>;
|
|
76
80
|
emitConsole(entry: MockConsoleMessage): Promise<void>;
|
|
@@ -125,6 +129,8 @@ function createMockPageRecord(pageNumber: number): MockPageRecord {
|
|
|
125
129
|
setInputFiles: locatorSetInputFiles
|
|
126
130
|
}));
|
|
127
131
|
const keyboardPress = vi.fn(async (_key: string) => {});
|
|
132
|
+
const route = vi.fn(async (_url: string, _handler: (...args: unknown[]) => Promise<void>) => {});
|
|
133
|
+
const unroute = vi.fn(async (_url: string, _handler?: (...args: unknown[]) => Promise<void>) => {});
|
|
128
134
|
const waitForEvent = vi.fn(async (eventName: string) => {
|
|
129
135
|
return await new Promise<unknown>((resolve, reject) => {
|
|
130
136
|
const current = waiters.get(eventName);
|
|
@@ -171,6 +177,8 @@ function createMockPageRecord(pageNumber: number): MockPageRecord {
|
|
|
171
177
|
content,
|
|
172
178
|
screenshot,
|
|
173
179
|
locator,
|
|
180
|
+
route,
|
|
181
|
+
unroute,
|
|
174
182
|
on,
|
|
175
183
|
waitForEvent,
|
|
176
184
|
keyboard: {
|
|
@@ -192,6 +200,8 @@ function createMockPageRecord(pageNumber: number): MockPageRecord {
|
|
|
192
200
|
locatorType,
|
|
193
201
|
locatorSetInputFiles,
|
|
194
202
|
keyboardPress,
|
|
203
|
+
route,
|
|
204
|
+
unroute,
|
|
195
205
|
waitForEvent,
|
|
196
206
|
on,
|
|
197
207
|
emitConsole: async (entry) => {
|
|
@@ -408,6 +418,71 @@ describe("createRemoteCdpDriver", () => {
|
|
|
408
418
|
expect(pageRecord.keyboardPress).toHaveBeenCalledWith("Enter");
|
|
409
419
|
});
|
|
410
420
|
|
|
421
|
+
it("supports network mock add/clear actions when runtime exposes route controls", async () => {
|
|
422
|
+
const harness = createRuntimeHarness();
|
|
423
|
+
const driver = createRemoteCdpDriver({
|
|
424
|
+
cdpUrl: "ws://localhost:9222/devtools/browser/example",
|
|
425
|
+
runtime: harness.runtime
|
|
426
|
+
});
|
|
427
|
+
const profile = "profile:alpha";
|
|
428
|
+
const targetId = await driver.openTab("https://example.com/mock", profile);
|
|
429
|
+
const pageRecord = harness.pages[0];
|
|
430
|
+
if (pageRecord === undefined) {
|
|
431
|
+
throw new Error("Expected a mock page record.");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const addResult = await driver.act(
|
|
435
|
+
{
|
|
436
|
+
type: "networkMockAdd",
|
|
437
|
+
payload: {
|
|
438
|
+
urlPattern: "**/api/**",
|
|
439
|
+
method: "POST",
|
|
440
|
+
status: 201,
|
|
441
|
+
body: '{"ok":true}',
|
|
442
|
+
contentType: "application/json"
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
targetId,
|
|
446
|
+
profile
|
|
447
|
+
);
|
|
448
|
+
expect(addResult).toMatchObject({
|
|
449
|
+
ok: true,
|
|
450
|
+
executed: true,
|
|
451
|
+
data: {
|
|
452
|
+
mockId: expect.any(String),
|
|
453
|
+
urlPattern: "**/api/**",
|
|
454
|
+
method: "POST",
|
|
455
|
+
status: 201
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
expect(pageRecord.route).toHaveBeenCalledWith("**/api/**", expect.any(Function));
|
|
459
|
+
|
|
460
|
+
const mockId = (addResult as { data?: { mockId?: string } }).data?.mockId;
|
|
461
|
+
if (typeof mockId !== "string") {
|
|
462
|
+
throw new Error("Expected mockId from networkMockAdd action.");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const clearResult = await driver.act(
|
|
466
|
+
{
|
|
467
|
+
type: "networkMockClear",
|
|
468
|
+
payload: {
|
|
469
|
+
mockId
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
targetId,
|
|
473
|
+
profile
|
|
474
|
+
);
|
|
475
|
+
expect(clearResult).toMatchObject({
|
|
476
|
+
ok: true,
|
|
477
|
+
executed: true,
|
|
478
|
+
data: {
|
|
479
|
+
cleared: 1,
|
|
480
|
+
mockId
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
expect(pageRecord.unroute).toHaveBeenCalledWith("**/api/**", expect.any(Function));
|
|
484
|
+
});
|
|
485
|
+
|
|
411
486
|
it("captures console + network telemetry and supports request body lookup", async () => {
|
|
412
487
|
const harness = createRuntimeHarness();
|
|
413
488
|
const driver = createRemoteCdpDriver({
|
|
@@ -597,15 +672,19 @@ describe("createRemoteCdpDriver", () => {
|
|
|
597
672
|
});
|
|
598
673
|
expect(dialogAccept).toHaveBeenCalledTimes(1);
|
|
599
674
|
|
|
675
|
+
const saveAs = vi.fn(async (_path: string) => {});
|
|
600
676
|
await driver.triggerDownload(targetId, profile);
|
|
601
677
|
await pageRecord.emitDownload({
|
|
602
678
|
path: async () => "C:\\downloads\\alpha.bin",
|
|
679
|
+
saveAs,
|
|
603
680
|
suggestedFilename: () => "alpha.bin",
|
|
604
681
|
url: () => "https://example.com/alpha.bin",
|
|
605
682
|
mimeType: () => "application/octet-stream"
|
|
606
683
|
});
|
|
607
|
-
await expect(
|
|
608
|
-
|
|
684
|
+
await expect(
|
|
685
|
+
driver.waitDownload(targetId, profile, "C:\\downloads\\saved-alpha.bin")
|
|
686
|
+
).resolves.toMatchObject({
|
|
687
|
+
path: "C:\\downloads\\saved-alpha.bin",
|
|
609
688
|
profile,
|
|
610
689
|
targetId,
|
|
611
690
|
suggestedFilename: "alpha.bin",
|
|
@@ -613,5 +692,36 @@ describe("createRemoteCdpDriver", () => {
|
|
|
613
692
|
mimeType: "application/octet-stream",
|
|
614
693
|
triggerCount: 1
|
|
615
694
|
});
|
|
695
|
+
expect(saveAs).toHaveBeenCalledWith("C:\\downloads\\saved-alpha.bin");
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it("fails waitDownload when requested path cannot be persisted", async () => {
|
|
699
|
+
const harness = createRuntimeHarness({
|
|
700
|
+
supportsDownloadEvents: true
|
|
701
|
+
});
|
|
702
|
+
const driver = createRemoteCdpDriver({
|
|
703
|
+
cdpUrl: "ws://localhost:9222/devtools/browser/example",
|
|
704
|
+
runtime: harness.runtime
|
|
705
|
+
});
|
|
706
|
+
const profile = "profile:alpha";
|
|
707
|
+
const targetId = await driver.openTab("https://example.com/upload", profile);
|
|
708
|
+
const pageRecord = harness.pages[0];
|
|
709
|
+
if (pageRecord === undefined) {
|
|
710
|
+
throw new Error("Expected page record.");
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
await driver.triggerDownload(targetId, profile);
|
|
714
|
+
const saveAs = vi.fn(async () => {
|
|
715
|
+
throw new Error("permission denied");
|
|
716
|
+
});
|
|
717
|
+
await pageRecord.emitDownload({
|
|
718
|
+
path: async () => "C:\\downloads\\alpha.bin",
|
|
719
|
+
saveAs
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
await expect(
|
|
723
|
+
driver.waitDownload(targetId, profile, "C:\\downloads\\saved-alpha.bin")
|
|
724
|
+
).rejects.toThrow("Failed to persist download to requested path");
|
|
725
|
+
expect(saveAs).toHaveBeenCalledWith("C:\\downloads\\saved-alpha.bin");
|
|
616
726
|
});
|
|
617
727
|
});
|