@roxybrowser/playwright 2.0.2-beta.1 → 2.0.2-beta.4
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/dist/browser.d.ts +33 -5
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +127 -15
- package/dist/browser.js.map +1 -1
- package/dist/browserType.d.ts.map +1 -1
- package/dist/browserType.js +2 -1
- package/dist/browserType.js.map +1 -1
- package/dist/mcp/backend/connect.d.ts +1 -0
- package/dist/mcp/backend/connect.d.ts.map +1 -1
- package/dist/mcp/backend/connect.js +4 -2
- package/dist/mcp/backend/connect.js.map +1 -1
- package/dist/mcp/backend/network.d.ts.map +1 -1
- package/dist/mcp/backend/network.js +30 -4
- package/dist/mcp/backend/network.js.map +1 -1
- package/dist/mcp/backend/response.d.ts +2 -0
- package/dist/mcp/backend/response.d.ts.map +1 -1
- package/dist/mcp/backend/response.js +52 -6
- package/dist/mcp/backend/response.js.map +1 -1
- package/dist/mcp/connectedBrowser.d.ts.map +1 -1
- package/dist/mcp/connectedBrowser.js +355 -25
- package/dist/mcp/connectedBrowser.js.map +1 -1
- package/dist/mcp/runtime.d.ts +3 -0
- package/dist/mcp/runtime.d.ts.map +1 -1
- package/dist/mcp/runtime.js +62 -8
- package/dist/mcp/runtime.js.map +1 -1
- package/dist/mcp/transports/inMemory.d.ts.map +1 -1
- package/dist/mcp/transports/inMemory.js +1 -0
- package/dist/mcp/transports/inMemory.js.map +1 -1
- package/dist/mcp/types.d.ts +5 -0
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/page.d.ts +2 -0
- package/dist/page.d.ts.map +1 -1
- package/dist/page.js +12 -0
- package/dist/page.js.map +1 -1
- package/dist/protocol/bidi/backend.d.ts +1 -1
- package/dist/protocol/bidi/backend.d.ts.map +1 -1
- package/dist/protocol/bidi/backend.js +6 -2
- package/dist/protocol/bidi/backend.js.map +1 -1
- package/dist/roxybrowser.bundle.js +516 -60
- package/dist/roxybrowser.bundle.js.map +1 -1
- package/dist/types/api.d.ts +21 -2
- package/dist/types/api.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import * as cdpModule from "chrome-remote-interface";
|
|
|
4
4
|
import { normalizeAriaSnapshotOptions, retryUntilReady } from "../ariaSnapshot.js";
|
|
5
5
|
import { PLAYWRIGHT_ARIA_SNAPSHOT_EVALUATE_SOURCE as ARIA_SNAPSHOT_EVALUATE_SOURCE } from "../vendor/playwright/ariaSnapshotEvaluate.js";
|
|
6
6
|
import { getBidiClientFactory } from "../protocol/bidi/client.js";
|
|
7
|
+
import { parseSerializedEvaluationResult, wrapWithSerializedEvaluationResult } from "../protocol/evaluationSerializer.js";
|
|
7
8
|
import { McpToolError } from "./errors.js";
|
|
8
9
|
import { ACTION_POINT_EVALUATE_SOURCE, ACTION_POINT_BY_SELECTOR_SOURCE } from "./snapshot.js";
|
|
9
10
|
function delay(ms) {
|
|
@@ -351,7 +352,7 @@ async function evaluateBiDi(client, contextId, functionSource, arg) {
|
|
|
351
352
|
? `(${functionSource})()`
|
|
352
353
|
: `(${functionSource})(${JSON.stringify(arg)})`;
|
|
353
354
|
const response = (await client.scriptEvaluate({
|
|
354
|
-
expression,
|
|
355
|
+
expression: wrapWithSerializedEvaluationResult(expression),
|
|
355
356
|
target: {
|
|
356
357
|
context: contextId
|
|
357
358
|
},
|
|
@@ -361,7 +362,7 @@ async function evaluateBiDi(client, contextId, functionSource, arg) {
|
|
|
361
362
|
if (response.type === "exception") {
|
|
362
363
|
throw new Error(response.exceptionDetails?.text || "BiDi runtime evaluation failed.");
|
|
363
364
|
}
|
|
364
|
-
return response.result
|
|
365
|
+
return parseSerializedEvaluationResult(extractBiDiValue(response.result));
|
|
365
366
|
}
|
|
366
367
|
async function evaluateBiDiRef(client, contextId, functionSource, arg) {
|
|
367
368
|
const expression = arg === undefined
|
|
@@ -389,6 +390,22 @@ async function evaluateBiDiRef(client, contextId, functionSource, arg) {
|
|
|
389
390
|
...(response.result?.value?.handle !== undefined ? { handle: response.result.value.handle } : {})
|
|
390
391
|
};
|
|
391
392
|
}
|
|
393
|
+
function extractBiDiValue(value) {
|
|
394
|
+
if (!value) {
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
if (value.type === "array" && Array.isArray(value.value)) {
|
|
398
|
+
return value.value.map((entry) => extractBiDiValue(entry));
|
|
399
|
+
}
|
|
400
|
+
if (value.type === "object" && Array.isArray(value.value)) {
|
|
401
|
+
const obj = {};
|
|
402
|
+
for (const [key, val] of value.value) {
|
|
403
|
+
obj[key] = extractBiDiValue(val);
|
|
404
|
+
}
|
|
405
|
+
return obj;
|
|
406
|
+
}
|
|
407
|
+
return value.value;
|
|
408
|
+
}
|
|
392
409
|
function toAriaSnapshotPayload(request = {}) {
|
|
393
410
|
return {
|
|
394
411
|
options: normalizeAriaSnapshotOptions({
|
|
@@ -432,6 +449,7 @@ class CdpConnectedBrowserSession {
|
|
|
432
449
|
pageConsoleStates = new Map();
|
|
433
450
|
pageNetworkStates = new Map();
|
|
434
451
|
pageDialogStates = new Map();
|
|
452
|
+
dialogWaiters = new Map();
|
|
435
453
|
activeTabId;
|
|
436
454
|
versionString = "Chromium/unknown";
|
|
437
455
|
constructor(browserClient, connection) {
|
|
@@ -452,10 +470,7 @@ class CdpConnectedBrowserSession {
|
|
|
452
470
|
});
|
|
453
471
|
const session = new CdpConnectedBrowserSession(browserClient, connection);
|
|
454
472
|
session.versionString = version.Browser;
|
|
455
|
-
|
|
456
|
-
if (tabs.length === 0) {
|
|
457
|
-
await session.newTab();
|
|
458
|
-
}
|
|
473
|
+
await session.refreshTabs();
|
|
459
474
|
await session.getActivePageClient().catch(() => undefined);
|
|
460
475
|
return session;
|
|
461
476
|
}
|
|
@@ -519,6 +534,7 @@ class CdpConnectedBrowserSession {
|
|
|
519
534
|
async click(target, options) {
|
|
520
535
|
const pageClient = await this.getActivePageClient();
|
|
521
536
|
const contextId = await this.getActiveUtilityContextId(pageClient);
|
|
537
|
+
const tabId = await this.getActiveTabId();
|
|
522
538
|
const source = "nodeToken" in target ? ACTION_POINT_EVALUATE_SOURCE : ACTION_POINT_BY_SELECTOR_SOURCE;
|
|
523
539
|
const arg = "nodeToken" in target ? { nodeToken: target.nodeToken } : { selector: target.selector };
|
|
524
540
|
const point = await evaluateCdp(pageClient, source, arg, contextId);
|
|
@@ -552,7 +568,7 @@ class CdpConnectedBrowserSession {
|
|
|
552
568
|
modifiers: modifiersMask
|
|
553
569
|
});
|
|
554
570
|
await delay(options.clickHoldMs);
|
|
555
|
-
|
|
571
|
+
const releasePromise = pageClient.Input.dispatchMouseEvent({
|
|
556
572
|
type: "mouseReleased",
|
|
557
573
|
x: point.x,
|
|
558
574
|
y: point.y,
|
|
@@ -560,6 +576,10 @@ class CdpConnectedBrowserSession {
|
|
|
560
576
|
clickCount,
|
|
561
577
|
modifiers: modifiersMask
|
|
562
578
|
});
|
|
579
|
+
await Promise.race([
|
|
580
|
+
releasePromise,
|
|
581
|
+
this.waitForDialog(tabId, options.clickHoldMs + 1000)
|
|
582
|
+
]);
|
|
563
583
|
}
|
|
564
584
|
}
|
|
565
585
|
async drag(start, end, options) {
|
|
@@ -816,26 +836,105 @@ class CdpConnectedBrowserSession {
|
|
|
816
836
|
}
|
|
817
837
|
}
|
|
818
838
|
async handleDialog(accept, promptText) {
|
|
819
|
-
const tabId =
|
|
839
|
+
const tabId = this.dialogTabId();
|
|
820
840
|
if (!this.pageDialogStates.has(tabId)) {
|
|
821
841
|
throw new McpToolError("no_dialog", "No dialog visible.");
|
|
822
842
|
}
|
|
823
|
-
const pageClient = await this.getActivePageClient();
|
|
843
|
+
const pageClient = this.pageClients.get(tabId) ?? await this.getActivePageClient();
|
|
824
844
|
this.pageDialogStates.delete(tabId);
|
|
825
845
|
await pageClient.Page.handleJavaScriptDialog({
|
|
826
846
|
accept,
|
|
827
847
|
...(promptText !== undefined ? { promptText } : {})
|
|
828
848
|
});
|
|
829
849
|
}
|
|
850
|
+
async hasDialog() {
|
|
851
|
+
return this.pageDialogStates.size > 0;
|
|
852
|
+
}
|
|
853
|
+
waitForDialog(tabId, timeoutMs) {
|
|
854
|
+
if (this.pageDialogStates.has(tabId)) {
|
|
855
|
+
return Promise.resolve();
|
|
856
|
+
}
|
|
857
|
+
return new Promise((resolve) => {
|
|
858
|
+
const waiter = {
|
|
859
|
+
resolve: () => {
|
|
860
|
+
if (waiter.timer) {
|
|
861
|
+
clearTimeout(waiter.timer);
|
|
862
|
+
}
|
|
863
|
+
this.removeDialogWaiter(tabId, waiter);
|
|
864
|
+
resolve();
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
waiter.timer = setTimeout(() => waiter.resolve(), timeoutMs);
|
|
868
|
+
const waiters = this.dialogWaiters.get(tabId) ?? new Set();
|
|
869
|
+
waiters.add(waiter);
|
|
870
|
+
this.dialogWaiters.set(tabId, waiters);
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
resolveDialogWaiters(tabId) {
|
|
874
|
+
const waiters = this.dialogWaiters.get(tabId);
|
|
875
|
+
if (!waiters) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
this.dialogWaiters.delete(tabId);
|
|
879
|
+
for (const waiter of waiters) {
|
|
880
|
+
waiter.resolve();
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
removeDialogWaiter(tabId, waiter) {
|
|
884
|
+
const waiters = this.dialogWaiters.get(tabId);
|
|
885
|
+
if (!waiters) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
waiters.delete(waiter);
|
|
889
|
+
if (waiters.size === 0) {
|
|
890
|
+
this.dialogWaiters.delete(tabId);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
830
893
|
async networkRequests() {
|
|
831
894
|
const tabId = await this.getActiveTabId();
|
|
895
|
+
await this.hydratePerformanceResourceRequests(tabId);
|
|
832
896
|
return this.ensureNetworkState(tabId).requests.map(cloneNetworkRequest);
|
|
833
897
|
}
|
|
834
898
|
async networkRequest(index) {
|
|
835
899
|
const tabId = await this.getActiveTabId();
|
|
900
|
+
await this.hydratePerformanceResourceRequests(tabId);
|
|
836
901
|
const request = this.ensureNetworkState(tabId).requests[index - 1];
|
|
837
902
|
return request ? cloneNetworkRequest(request) : undefined;
|
|
838
903
|
}
|
|
904
|
+
async fetchResponseBody(index) {
|
|
905
|
+
const tabId = await this.getActiveTabId();
|
|
906
|
+
const state = this.ensureNetworkState(tabId);
|
|
907
|
+
const request = state.requests[index - 1];
|
|
908
|
+
if (!request || !request.requestId) {
|
|
909
|
+
return request?.responseBody;
|
|
910
|
+
}
|
|
911
|
+
if (!canReadResponseBody(request)) {
|
|
912
|
+
return undefined;
|
|
913
|
+
}
|
|
914
|
+
if (request.responseBody !== undefined) {
|
|
915
|
+
return request.responseBody;
|
|
916
|
+
}
|
|
917
|
+
await waitForLoadingDone(state, request.requestId, 5_000).catch(() => undefined);
|
|
918
|
+
if (request.responseBody !== undefined) {
|
|
919
|
+
return request.responseBody;
|
|
920
|
+
}
|
|
921
|
+
if (state.bodyRead.has(request.requestId)) {
|
|
922
|
+
return undefined;
|
|
923
|
+
}
|
|
924
|
+
state.bodyRead.add(request.requestId);
|
|
925
|
+
const pageClient = this.pageClients.get(tabId) ?? await this.getActivePageClient();
|
|
926
|
+
const clientNetwork = pageClient.Network;
|
|
927
|
+
if (!clientNetwork) {
|
|
928
|
+
return undefined;
|
|
929
|
+
}
|
|
930
|
+
const body = await clientNetwork.getResponseBody({ requestId: request.requestId }).catch(() => undefined);
|
|
931
|
+
if (body) {
|
|
932
|
+
request.responseBody = body.base64Encoded
|
|
933
|
+
? Buffer.from(body.body, "base64").toString("utf8")
|
|
934
|
+
: body.body;
|
|
935
|
+
}
|
|
936
|
+
return request.responseBody;
|
|
937
|
+
}
|
|
839
938
|
async runCodeUnsafe(code) {
|
|
840
939
|
const pageClient = await this.getActivePageClient();
|
|
841
940
|
const contextId = await this.getActiveUtilityContextId(pageClient);
|
|
@@ -905,6 +1004,16 @@ class CdpConnectedBrowserSession {
|
|
|
905
1004
|
}
|
|
906
1005
|
return activeTab.id;
|
|
907
1006
|
}
|
|
1007
|
+
dialogTabId() {
|
|
1008
|
+
if (this.activeTabId && this.pageDialogStates.has(this.activeTabId)) {
|
|
1009
|
+
return this.activeTabId;
|
|
1010
|
+
}
|
|
1011
|
+
const tabId = this.pageDialogStates.keys().next().value;
|
|
1012
|
+
if (!tabId) {
|
|
1013
|
+
throw new McpToolError("no_dialog", "No dialog visible.");
|
|
1014
|
+
}
|
|
1015
|
+
return tabId;
|
|
1016
|
+
}
|
|
908
1017
|
async getActivePageClient() {
|
|
909
1018
|
const tabs = await this.refreshTabs();
|
|
910
1019
|
const activeTab = tabs.find((tab) => tab.active);
|
|
@@ -999,6 +1108,7 @@ class CdpConnectedBrowserSession {
|
|
|
999
1108
|
...(event.defaultPrompt !== undefined ? { defaultPrompt: event.defaultPrompt } : {}),
|
|
1000
1109
|
...(event.url !== undefined ? { url: event.url } : {})
|
|
1001
1110
|
});
|
|
1111
|
+
this.resolveDialogWaiters(tabId);
|
|
1002
1112
|
});
|
|
1003
1113
|
this.installNetworkCollection(tabId, client);
|
|
1004
1114
|
}
|
|
@@ -1046,13 +1156,15 @@ class CdpConnectedBrowserSession {
|
|
|
1046
1156
|
client.Network.loadingFinished(async (event) => {
|
|
1047
1157
|
const request = state.byRequestId.get(event.requestId);
|
|
1048
1158
|
if (!request) {
|
|
1159
|
+
resolveLoadingDone(state, event.requestId, true);
|
|
1049
1160
|
return;
|
|
1050
1161
|
}
|
|
1051
1162
|
const startedAt = state.startedAt.get(event.requestId);
|
|
1052
1163
|
if (startedAt !== undefined && event.timestamp !== undefined) {
|
|
1053
1164
|
request.durationMs = Math.round(event.timestamp * 1000 - startedAt);
|
|
1054
1165
|
}
|
|
1055
|
-
if (canReadResponseBody(request)) {
|
|
1166
|
+
if (canReadResponseBody(request) && !state.bodyRead.has(event.requestId)) {
|
|
1167
|
+
state.bodyRead.add(event.requestId);
|
|
1056
1168
|
const clientNetwork = client.Network;
|
|
1057
1169
|
const body = await clientNetwork?.getResponseBody({ requestId: event.requestId }).catch(() => undefined);
|
|
1058
1170
|
if (body) {
|
|
@@ -1061,10 +1173,12 @@ class CdpConnectedBrowserSession {
|
|
|
1061
1173
|
: body.body;
|
|
1062
1174
|
}
|
|
1063
1175
|
}
|
|
1176
|
+
resolveLoadingDone(state, event.requestId, true);
|
|
1064
1177
|
});
|
|
1065
1178
|
client.Network.loadingFailed((event) => {
|
|
1066
1179
|
const request = state.byRequestId.get(event.requestId);
|
|
1067
1180
|
if (!request) {
|
|
1181
|
+
resolveLoadingDone(state, event.requestId, false);
|
|
1068
1182
|
return;
|
|
1069
1183
|
}
|
|
1070
1184
|
request.failureText = event.errorText ?? "Unknown error";
|
|
@@ -1072,6 +1186,7 @@ class CdpConnectedBrowserSession {
|
|
|
1072
1186
|
if (startedAt !== undefined && event.timestamp !== undefined) {
|
|
1073
1187
|
request.durationMs = Math.round(event.timestamp * 1000 - startedAt);
|
|
1074
1188
|
}
|
|
1189
|
+
resolveLoadingDone(state, event.requestId, false);
|
|
1075
1190
|
});
|
|
1076
1191
|
}
|
|
1077
1192
|
ensureConsoleState(tabId) {
|
|
@@ -1093,7 +1208,10 @@ class CdpConnectedBrowserSession {
|
|
|
1093
1208
|
state = {
|
|
1094
1209
|
requests: [],
|
|
1095
1210
|
byRequestId: new Map(),
|
|
1096
|
-
startedAt: new Map()
|
|
1211
|
+
startedAt: new Map(),
|
|
1212
|
+
hydratedPerformanceResources: false,
|
|
1213
|
+
loadingDone: new Map(),
|
|
1214
|
+
bodyRead: new Set()
|
|
1097
1215
|
};
|
|
1098
1216
|
this.pageNetworkStates.set(tabId, state);
|
|
1099
1217
|
}
|
|
@@ -1109,10 +1227,82 @@ class CdpConnectedBrowserSession {
|
|
|
1109
1227
|
this.pageNetworkStates.set(tabId, {
|
|
1110
1228
|
requests: [],
|
|
1111
1229
|
byRequestId: new Map(),
|
|
1112
|
-
startedAt: new Map()
|
|
1230
|
+
startedAt: new Map(),
|
|
1231
|
+
hydratedPerformanceResources: false,
|
|
1232
|
+
loadingDone: new Map(),
|
|
1233
|
+
bodyRead: new Set()
|
|
1113
1234
|
});
|
|
1114
1235
|
this.pageDialogStates.delete(tabId);
|
|
1115
1236
|
}
|
|
1237
|
+
async hydratePerformanceResourceRequests(tabId) {
|
|
1238
|
+
const state = this.ensureNetworkState(tabId);
|
|
1239
|
+
if (state.hydratedPerformanceResources) {
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
state.hydratedPerformanceResources = true;
|
|
1243
|
+
const pageClient = await this.getActivePageClient();
|
|
1244
|
+
const contextId = await this.getActiveUtilityContextId(pageClient);
|
|
1245
|
+
const documentRequest = await evaluateCdp(pageClient, String.raw `() => {
|
|
1246
|
+
const navigation = performance.getEntriesByType("navigation")[0];
|
|
1247
|
+
return {
|
|
1248
|
+
url: String(location.href || ""),
|
|
1249
|
+
duration: navigation ? Math.round(Number(navigation.duration || 0)) : undefined
|
|
1250
|
+
};
|
|
1251
|
+
}`, undefined, contextId).catch(() => undefined);
|
|
1252
|
+
if (documentRequest?.url && !Array.from(state.byRequestId.values()).some((request) => request.url === documentRequest.url)) {
|
|
1253
|
+
const requestId = `performance:navigation:${documentRequest.url}`;
|
|
1254
|
+
const request = {
|
|
1255
|
+
index: state.requests.length + 1,
|
|
1256
|
+
requestId,
|
|
1257
|
+
method: "GET",
|
|
1258
|
+
url: documentRequest.url,
|
|
1259
|
+
resourceType: "document",
|
|
1260
|
+
requestHeaders: {},
|
|
1261
|
+
status: 200,
|
|
1262
|
+
statusText: "OK",
|
|
1263
|
+
...(documentRequest.duration !== undefined ? { durationMs: documentRequest.duration } : {})
|
|
1264
|
+
};
|
|
1265
|
+
state.requests.push(request);
|
|
1266
|
+
state.byRequestId.set(requestId, request);
|
|
1267
|
+
}
|
|
1268
|
+
const resources = await evaluateCdp(pageClient, String.raw `() => performance.getEntriesByType("resource").map((entry) => ({
|
|
1269
|
+
name: String(entry.name || ""),
|
|
1270
|
+
initiatorType: String(entry.initiatorType || "other"),
|
|
1271
|
+
duration: Math.round(Number(entry.duration || 0)),
|
|
1272
|
+
responseStatus: typeof entry.responseStatus === "number" ? entry.responseStatus : undefined
|
|
1273
|
+
}))`, undefined, contextId).catch(() => []);
|
|
1274
|
+
for (const resource of resources) {
|
|
1275
|
+
if (!resource.name || Array.from(state.byRequestId.values()).some((request) => request.url === resource.name)) {
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
const status = resource.responseStatus && resource.responseStatus > 0
|
|
1279
|
+
? resource.responseStatus
|
|
1280
|
+
: await this.probeResourceStatus(pageClient, contextId, resource.name);
|
|
1281
|
+
const requestId = `performance:${resource.name}`;
|
|
1282
|
+
const request = {
|
|
1283
|
+
index: state.requests.length + 1,
|
|
1284
|
+
requestId,
|
|
1285
|
+
method: "GET",
|
|
1286
|
+
url: resource.name,
|
|
1287
|
+
resourceType: normalizeResourceType(resource.initiatorType),
|
|
1288
|
+
requestHeaders: {},
|
|
1289
|
+
...(status !== undefined ? { status, statusText: statusTextForStatus(status) } : {}),
|
|
1290
|
+
...(resource.duration !== undefined ? { durationMs: resource.duration } : {})
|
|
1291
|
+
};
|
|
1292
|
+
state.requests.push(request);
|
|
1293
|
+
state.byRequestId.set(requestId, request);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
async probeResourceStatus(pageClient, contextId, url) {
|
|
1297
|
+
return evaluateCdp(pageClient, String.raw `async (url) => {
|
|
1298
|
+
try {
|
|
1299
|
+
const response = await fetch(url, { method: "HEAD", cache: "no-store" });
|
|
1300
|
+
return response.status;
|
|
1301
|
+
} catch {
|
|
1302
|
+
return undefined;
|
|
1303
|
+
}
|
|
1304
|
+
}`, url, contextId).catch(() => undefined);
|
|
1305
|
+
}
|
|
1116
1306
|
addConsoleMessage(tabId, message) {
|
|
1117
1307
|
const state = this.ensureConsoleState(tabId);
|
|
1118
1308
|
if (!shouldIncludeConsoleMessage(message.type)) {
|
|
@@ -1282,6 +1472,59 @@ function canReadResponseBody(request) {
|
|
|
1282
1472
|
}
|
|
1283
1473
|
return request.status !== 204 && request.status !== 304 && !(request.status >= 100 && request.status < 200);
|
|
1284
1474
|
}
|
|
1475
|
+
function loadingDoneEntry(state, requestId) {
|
|
1476
|
+
let entry = state.loadingDone.get(requestId);
|
|
1477
|
+
if (!entry) {
|
|
1478
|
+
let resolve;
|
|
1479
|
+
let reject;
|
|
1480
|
+
const promise = new Promise((res, rej) => {
|
|
1481
|
+
resolve = res;
|
|
1482
|
+
reject = rej;
|
|
1483
|
+
});
|
|
1484
|
+
entry = { promise, resolve, reject };
|
|
1485
|
+
state.loadingDone.set(requestId, entry);
|
|
1486
|
+
}
|
|
1487
|
+
return entry;
|
|
1488
|
+
}
|
|
1489
|
+
function resolveLoadingDone(state, requestId, success) {
|
|
1490
|
+
const entry = state.loadingDone.get(requestId);
|
|
1491
|
+
if (!entry) {
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
state.loadingDone.delete(requestId);
|
|
1495
|
+
if (success) {
|
|
1496
|
+
entry.resolve();
|
|
1497
|
+
}
|
|
1498
|
+
else {
|
|
1499
|
+
entry.reject(new Error("Request failed before the response body was available."));
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
async function waitForLoadingDone(state, requestId, timeoutMs) {
|
|
1503
|
+
const entry = loadingDoneEntry(state, requestId);
|
|
1504
|
+
await Promise.race([
|
|
1505
|
+
entry.promise,
|
|
1506
|
+
new Promise((resolve) => setTimeout(resolve, timeoutMs))
|
|
1507
|
+
]);
|
|
1508
|
+
}
|
|
1509
|
+
function statusTextForStatus(status) {
|
|
1510
|
+
if (status === 200)
|
|
1511
|
+
return "OK";
|
|
1512
|
+
if (status === 204)
|
|
1513
|
+
return "No Content";
|
|
1514
|
+
if (status === 304)
|
|
1515
|
+
return "Not Modified";
|
|
1516
|
+
if (status === 400)
|
|
1517
|
+
return "Bad Request";
|
|
1518
|
+
if (status === 401)
|
|
1519
|
+
return "Unauthorized";
|
|
1520
|
+
if (status === 403)
|
|
1521
|
+
return "Forbidden";
|
|
1522
|
+
if (status === 404)
|
|
1523
|
+
return "Not Found";
|
|
1524
|
+
if (status === 500)
|
|
1525
|
+
return "Internal Server Error";
|
|
1526
|
+
return "";
|
|
1527
|
+
}
|
|
1285
1528
|
function cloneNetworkRequest(request) {
|
|
1286
1529
|
return {
|
|
1287
1530
|
...request,
|
|
@@ -1381,6 +1624,7 @@ class BidiConnectedBrowserSession {
|
|
|
1381
1624
|
pageConsoleStates = new Map();
|
|
1382
1625
|
pageNetworkStates = new Map();
|
|
1383
1626
|
pageDialogStates = new Map();
|
|
1627
|
+
dialogWaiters = new Map();
|
|
1384
1628
|
bidiListeners = new Map();
|
|
1385
1629
|
responseDataCollector;
|
|
1386
1630
|
activeTabId;
|
|
@@ -1398,15 +1642,12 @@ class BidiConnectedBrowserSession {
|
|
|
1398
1642
|
}
|
|
1399
1643
|
const client = await getBidiClientFactory()({
|
|
1400
1644
|
browserName: "firefox",
|
|
1401
|
-
webSocketUrl: normalizeFirefoxBidiEndpoint(args.endpoint)
|
|
1645
|
+
webSocketUrl: normalizeFirefoxBidiEndpoint(args.endpoint, args.sessionId)
|
|
1402
1646
|
});
|
|
1403
1647
|
const session = new BidiConnectedBrowserSession(client);
|
|
1404
|
-
session.ownsSession = await ensureMcpBiDiSession(client, args.endpoint);
|
|
1648
|
+
session.ownsSession = await ensureMcpBiDiSession(client, args.endpoint, args.sessionId);
|
|
1405
1649
|
await session.initialize();
|
|
1406
|
-
|
|
1407
|
-
if (tabs.length === 0) {
|
|
1408
|
-
await session.newTab();
|
|
1409
|
-
}
|
|
1650
|
+
await session.refreshTabs();
|
|
1410
1651
|
return session;
|
|
1411
1652
|
}
|
|
1412
1653
|
async version() {
|
|
@@ -1465,10 +1706,14 @@ class BidiConnectedBrowserSession {
|
|
|
1465
1706
|
async snapshot(request = {}) {
|
|
1466
1707
|
const tabId = await this.getActiveTabId();
|
|
1467
1708
|
const result = await retryUntilReady(() => evaluateBiDi(this.client, tabId, ARIA_SNAPSHOT_EVALUATE_SOURCE, toAriaSnapshotPayload(request)));
|
|
1468
|
-
|
|
1709
|
+
const snapshot = toBrowserSnapshot(result, request, {
|
|
1469
1710
|
console: this.consoleSummary(tabId),
|
|
1470
1711
|
consoleLink: await this.takeConsoleLink(tabId)
|
|
1471
1712
|
});
|
|
1713
|
+
return {
|
|
1714
|
+
...snapshot,
|
|
1715
|
+
retryable: true
|
|
1716
|
+
};
|
|
1472
1717
|
}
|
|
1473
1718
|
async consoleMessages(level = "info", all = false) {
|
|
1474
1719
|
const activeTabId = await this.getActiveTabId();
|
|
@@ -1812,7 +2057,7 @@ class BidiConnectedBrowserSession {
|
|
|
1812
2057
|
}
|
|
1813
2058
|
}
|
|
1814
2059
|
async handleDialog(accept, promptText) {
|
|
1815
|
-
const tabId =
|
|
2060
|
+
const tabId = this.dialogTabId();
|
|
1816
2061
|
if (!this.pageDialogStates.has(tabId)) {
|
|
1817
2062
|
throw new McpToolError("no_dialog", "No dialog visible.");
|
|
1818
2063
|
}
|
|
@@ -1823,6 +2068,9 @@ class BidiConnectedBrowserSession {
|
|
|
1823
2068
|
...(promptText !== undefined ? { userText: promptText } : {})
|
|
1824
2069
|
});
|
|
1825
2070
|
}
|
|
2071
|
+
async hasDialog() {
|
|
2072
|
+
return this.pageDialogStates.size > 0;
|
|
2073
|
+
}
|
|
1826
2074
|
async networkRequests() {
|
|
1827
2075
|
const tabId = await this.getActiveTabId();
|
|
1828
2076
|
return this.ensureNetworkState(tabId).requests.map(cloneNetworkRequest);
|
|
@@ -1832,6 +2080,21 @@ class BidiConnectedBrowserSession {
|
|
|
1832
2080
|
const request = this.ensureNetworkState(tabId).requests[index - 1];
|
|
1833
2081
|
return request ? cloneNetworkRequest(request) : undefined;
|
|
1834
2082
|
}
|
|
2083
|
+
async fetchResponseBody(index) {
|
|
2084
|
+
const tabId = await this.getActiveTabId();
|
|
2085
|
+
const request = this.ensureNetworkState(tabId).requests[index - 1];
|
|
2086
|
+
if (!request || !request.requestId) {
|
|
2087
|
+
return request?.responseBody;
|
|
2088
|
+
}
|
|
2089
|
+
if (request.responseBody !== undefined) {
|
|
2090
|
+
return request.responseBody;
|
|
2091
|
+
}
|
|
2092
|
+
const body = await this.getResponseBody(request.requestId).catch(() => undefined);
|
|
2093
|
+
if (body !== undefined) {
|
|
2094
|
+
request.responseBody = body;
|
|
2095
|
+
}
|
|
2096
|
+
return request.responseBody;
|
|
2097
|
+
}
|
|
1835
2098
|
async runCodeUnsafe(code) {
|
|
1836
2099
|
return this.evaluate(`async () => {
|
|
1837
2100
|
const fn = eval(${JSON.stringify(`(${code})`)});
|
|
@@ -1904,6 +2167,63 @@ class BidiConnectedBrowserSession {
|
|
|
1904
2167
|
targetArg(target) {
|
|
1905
2168
|
return "nodeToken" in target ? { nodeToken: target.nodeToken } : { selector: target.selector };
|
|
1906
2169
|
}
|
|
2170
|
+
dialogTabId() {
|
|
2171
|
+
if (this.activeTabId && this.pageDialogStates.has(this.activeTabId)) {
|
|
2172
|
+
return this.activeTabId;
|
|
2173
|
+
}
|
|
2174
|
+
const tabId = this.pageDialogStates.keys().next().value;
|
|
2175
|
+
if (!tabId) {
|
|
2176
|
+
throw new McpToolError("no_dialog", "No dialog visible.");
|
|
2177
|
+
}
|
|
2178
|
+
return tabId;
|
|
2179
|
+
}
|
|
2180
|
+
waitForDialog(tabId, timeoutMs) {
|
|
2181
|
+
if (this.pageDialogStates.has(tabId)) {
|
|
2182
|
+
return Promise.resolve();
|
|
2183
|
+
}
|
|
2184
|
+
return new Promise((resolve, reject) => {
|
|
2185
|
+
const waiter = {
|
|
2186
|
+
resolve: () => {
|
|
2187
|
+
if (waiter.timer) {
|
|
2188
|
+
clearTimeout(waiter.timer);
|
|
2189
|
+
}
|
|
2190
|
+
this.removeDialogWaiter(tabId, waiter);
|
|
2191
|
+
resolve();
|
|
2192
|
+
},
|
|
2193
|
+
reject: (error) => {
|
|
2194
|
+
if (waiter.timer) {
|
|
2195
|
+
clearTimeout(waiter.timer);
|
|
2196
|
+
}
|
|
2197
|
+
this.removeDialogWaiter(tabId, waiter);
|
|
2198
|
+
reject(error);
|
|
2199
|
+
}
|
|
2200
|
+
};
|
|
2201
|
+
waiter.timer = setTimeout(() => waiter.reject?.(new Error("Timed out waiting for dialog.")), timeoutMs);
|
|
2202
|
+
const waiters = this.dialogWaiters.get(tabId) ?? new Set();
|
|
2203
|
+
waiters.add(waiter);
|
|
2204
|
+
this.dialogWaiters.set(tabId, waiters);
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
resolveDialogWaiters(tabId) {
|
|
2208
|
+
const waiters = this.dialogWaiters.get(tabId);
|
|
2209
|
+
if (!waiters) {
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
this.dialogWaiters.delete(tabId);
|
|
2213
|
+
for (const waiter of waiters) {
|
|
2214
|
+
waiter.resolve();
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
removeDialogWaiter(tabId, waiter) {
|
|
2218
|
+
const waiters = this.dialogWaiters.get(tabId);
|
|
2219
|
+
if (!waiters) {
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
waiters.delete(waiter);
|
|
2223
|
+
if (waiters.size === 0) {
|
|
2224
|
+
this.dialogWaiters.delete(tabId);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
1907
2227
|
async actionPoint(tabId, target) {
|
|
1908
2228
|
const source = "nodeToken" in target ? ACTION_POINT_EVALUATE_SOURCE : ACTION_POINT_BY_SELECTOR_SOURCE;
|
|
1909
2229
|
const point = await evaluateBiDi(this.client, tabId, source, this.targetArg(target));
|
|
@@ -2086,7 +2406,10 @@ class BidiConnectedBrowserSession {
|
|
|
2086
2406
|
state = {
|
|
2087
2407
|
requests: [],
|
|
2088
2408
|
byRequestId: new Map(),
|
|
2089
|
-
startedAt: new Map()
|
|
2409
|
+
startedAt: new Map(),
|
|
2410
|
+
hydratedPerformanceResources: false,
|
|
2411
|
+
loadingDone: new Map(),
|
|
2412
|
+
bodyRead: new Set()
|
|
2090
2413
|
};
|
|
2091
2414
|
this.pageNetworkStates.set(tabId, state);
|
|
2092
2415
|
}
|
|
@@ -2102,7 +2425,10 @@ class BidiConnectedBrowserSession {
|
|
|
2102
2425
|
this.pageNetworkStates.set(tabId, {
|
|
2103
2426
|
requests: [],
|
|
2104
2427
|
byRequestId: new Map(),
|
|
2105
|
-
startedAt: new Map()
|
|
2428
|
+
startedAt: new Map(),
|
|
2429
|
+
hydratedPerformanceResources: false,
|
|
2430
|
+
loadingDone: new Map(),
|
|
2431
|
+
bodyRead: new Set()
|
|
2106
2432
|
});
|
|
2107
2433
|
this.pageDialogStates.delete(tabId);
|
|
2108
2434
|
}
|
|
@@ -2148,16 +2474,20 @@ class BidiConnectedBrowserSession {
|
|
|
2148
2474
|
return `${relativePath}${lineRange}`;
|
|
2149
2475
|
}
|
|
2150
2476
|
}
|
|
2151
|
-
function normalizeFirefoxBidiEndpoint(endpoint) {
|
|
2477
|
+
function normalizeFirefoxBidiEndpoint(endpoint, sessionId) {
|
|
2152
2478
|
const url = new URL(endpoint);
|
|
2479
|
+
if (sessionId) {
|
|
2480
|
+
url.pathname = `/session/${sessionId}`;
|
|
2481
|
+
return url.toString();
|
|
2482
|
+
}
|
|
2153
2483
|
if (url.pathname === "/" || url.pathname === "") {
|
|
2154
2484
|
url.pathname = "/session";
|
|
2155
2485
|
}
|
|
2156
2486
|
return url.toString();
|
|
2157
2487
|
}
|
|
2158
|
-
async function ensureMcpBiDiSession(client, endpoint) {
|
|
2488
|
+
async function ensureMcpBiDiSession(client, endpoint, sessionId) {
|
|
2159
2489
|
await client.sessionStatus({});
|
|
2160
|
-
if (isSessionSpecificFirefoxBidiEndpoint(endpoint)) {
|
|
2490
|
+
if (sessionId || isSessionSpecificFirefoxBidiEndpoint(endpoint)) {
|
|
2161
2491
|
return false;
|
|
2162
2492
|
}
|
|
2163
2493
|
try {
|