@roxybrowser/playwright 2.0.2-beta.2 → 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.
@@ -449,6 +449,7 @@ class CdpConnectedBrowserSession {
449
449
  pageConsoleStates = new Map();
450
450
  pageNetworkStates = new Map();
451
451
  pageDialogStates = new Map();
452
+ dialogWaiters = new Map();
452
453
  activeTabId;
453
454
  versionString = "Chromium/unknown";
454
455
  constructor(browserClient, connection) {
@@ -469,10 +470,7 @@ class CdpConnectedBrowserSession {
469
470
  });
470
471
  const session = new CdpConnectedBrowserSession(browserClient, connection);
471
472
  session.versionString = version.Browser;
472
- const tabs = await session.refreshTabs();
473
- if (tabs.length === 0) {
474
- await session.newTab();
475
- }
473
+ await session.refreshTabs();
476
474
  await session.getActivePageClient().catch(() => undefined);
477
475
  return session;
478
476
  }
@@ -536,6 +534,7 @@ class CdpConnectedBrowserSession {
536
534
  async click(target, options) {
537
535
  const pageClient = await this.getActivePageClient();
538
536
  const contextId = await this.getActiveUtilityContextId(pageClient);
537
+ const tabId = await this.getActiveTabId();
539
538
  const source = "nodeToken" in target ? ACTION_POINT_EVALUATE_SOURCE : ACTION_POINT_BY_SELECTOR_SOURCE;
540
539
  const arg = "nodeToken" in target ? { nodeToken: target.nodeToken } : { selector: target.selector };
541
540
  const point = await evaluateCdp(pageClient, source, arg, contextId);
@@ -569,7 +568,7 @@ class CdpConnectedBrowserSession {
569
568
  modifiers: modifiersMask
570
569
  });
571
570
  await delay(options.clickHoldMs);
572
- await pageClient.Input.dispatchMouseEvent({
571
+ const releasePromise = pageClient.Input.dispatchMouseEvent({
573
572
  type: "mouseReleased",
574
573
  x: point.x,
575
574
  y: point.y,
@@ -577,6 +576,10 @@ class CdpConnectedBrowserSession {
577
576
  clickCount,
578
577
  modifiers: modifiersMask
579
578
  });
579
+ await Promise.race([
580
+ releasePromise,
581
+ this.waitForDialog(tabId, options.clickHoldMs + 1000)
582
+ ]);
580
583
  }
581
584
  }
582
585
  async drag(start, end, options) {
@@ -833,26 +836,105 @@ class CdpConnectedBrowserSession {
833
836
  }
834
837
  }
835
838
  async handleDialog(accept, promptText) {
836
- const tabId = await this.getActiveTabId();
839
+ const tabId = this.dialogTabId();
837
840
  if (!this.pageDialogStates.has(tabId)) {
838
841
  throw new McpToolError("no_dialog", "No dialog visible.");
839
842
  }
840
- const pageClient = await this.getActivePageClient();
843
+ const pageClient = this.pageClients.get(tabId) ?? await this.getActivePageClient();
841
844
  this.pageDialogStates.delete(tabId);
842
845
  await pageClient.Page.handleJavaScriptDialog({
843
846
  accept,
844
847
  ...(promptText !== undefined ? { promptText } : {})
845
848
  });
846
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
+ }
847
893
  async networkRequests() {
848
894
  const tabId = await this.getActiveTabId();
895
+ await this.hydratePerformanceResourceRequests(tabId);
849
896
  return this.ensureNetworkState(tabId).requests.map(cloneNetworkRequest);
850
897
  }
851
898
  async networkRequest(index) {
852
899
  const tabId = await this.getActiveTabId();
900
+ await this.hydratePerformanceResourceRequests(tabId);
853
901
  const request = this.ensureNetworkState(tabId).requests[index - 1];
854
902
  return request ? cloneNetworkRequest(request) : undefined;
855
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
+ }
856
938
  async runCodeUnsafe(code) {
857
939
  const pageClient = await this.getActivePageClient();
858
940
  const contextId = await this.getActiveUtilityContextId(pageClient);
@@ -922,6 +1004,16 @@ class CdpConnectedBrowserSession {
922
1004
  }
923
1005
  return activeTab.id;
924
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
+ }
925
1017
  async getActivePageClient() {
926
1018
  const tabs = await this.refreshTabs();
927
1019
  const activeTab = tabs.find((tab) => tab.active);
@@ -1016,6 +1108,7 @@ class CdpConnectedBrowserSession {
1016
1108
  ...(event.defaultPrompt !== undefined ? { defaultPrompt: event.defaultPrompt } : {}),
1017
1109
  ...(event.url !== undefined ? { url: event.url } : {})
1018
1110
  });
1111
+ this.resolveDialogWaiters(tabId);
1019
1112
  });
1020
1113
  this.installNetworkCollection(tabId, client);
1021
1114
  }
@@ -1063,13 +1156,15 @@ class CdpConnectedBrowserSession {
1063
1156
  client.Network.loadingFinished(async (event) => {
1064
1157
  const request = state.byRequestId.get(event.requestId);
1065
1158
  if (!request) {
1159
+ resolveLoadingDone(state, event.requestId, true);
1066
1160
  return;
1067
1161
  }
1068
1162
  const startedAt = state.startedAt.get(event.requestId);
1069
1163
  if (startedAt !== undefined && event.timestamp !== undefined) {
1070
1164
  request.durationMs = Math.round(event.timestamp * 1000 - startedAt);
1071
1165
  }
1072
- if (canReadResponseBody(request)) {
1166
+ if (canReadResponseBody(request) && !state.bodyRead.has(event.requestId)) {
1167
+ state.bodyRead.add(event.requestId);
1073
1168
  const clientNetwork = client.Network;
1074
1169
  const body = await clientNetwork?.getResponseBody({ requestId: event.requestId }).catch(() => undefined);
1075
1170
  if (body) {
@@ -1078,10 +1173,12 @@ class CdpConnectedBrowserSession {
1078
1173
  : body.body;
1079
1174
  }
1080
1175
  }
1176
+ resolveLoadingDone(state, event.requestId, true);
1081
1177
  });
1082
1178
  client.Network.loadingFailed((event) => {
1083
1179
  const request = state.byRequestId.get(event.requestId);
1084
1180
  if (!request) {
1181
+ resolveLoadingDone(state, event.requestId, false);
1085
1182
  return;
1086
1183
  }
1087
1184
  request.failureText = event.errorText ?? "Unknown error";
@@ -1089,6 +1186,7 @@ class CdpConnectedBrowserSession {
1089
1186
  if (startedAt !== undefined && event.timestamp !== undefined) {
1090
1187
  request.durationMs = Math.round(event.timestamp * 1000 - startedAt);
1091
1188
  }
1189
+ resolveLoadingDone(state, event.requestId, false);
1092
1190
  });
1093
1191
  }
1094
1192
  ensureConsoleState(tabId) {
@@ -1110,7 +1208,10 @@ class CdpConnectedBrowserSession {
1110
1208
  state = {
1111
1209
  requests: [],
1112
1210
  byRequestId: new Map(),
1113
- startedAt: new Map()
1211
+ startedAt: new Map(),
1212
+ hydratedPerformanceResources: false,
1213
+ loadingDone: new Map(),
1214
+ bodyRead: new Set()
1114
1215
  };
1115
1216
  this.pageNetworkStates.set(tabId, state);
1116
1217
  }
@@ -1126,10 +1227,82 @@ class CdpConnectedBrowserSession {
1126
1227
  this.pageNetworkStates.set(tabId, {
1127
1228
  requests: [],
1128
1229
  byRequestId: new Map(),
1129
- startedAt: new Map()
1230
+ startedAt: new Map(),
1231
+ hydratedPerformanceResources: false,
1232
+ loadingDone: new Map(),
1233
+ bodyRead: new Set()
1130
1234
  });
1131
1235
  this.pageDialogStates.delete(tabId);
1132
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
+ }
1133
1306
  addConsoleMessage(tabId, message) {
1134
1307
  const state = this.ensureConsoleState(tabId);
1135
1308
  if (!shouldIncludeConsoleMessage(message.type)) {
@@ -1299,6 +1472,59 @@ function canReadResponseBody(request) {
1299
1472
  }
1300
1473
  return request.status !== 204 && request.status !== 304 && !(request.status >= 100 && request.status < 200);
1301
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
+ }
1302
1528
  function cloneNetworkRequest(request) {
1303
1529
  return {
1304
1530
  ...request,
@@ -1398,6 +1624,7 @@ class BidiConnectedBrowserSession {
1398
1624
  pageConsoleStates = new Map();
1399
1625
  pageNetworkStates = new Map();
1400
1626
  pageDialogStates = new Map();
1627
+ dialogWaiters = new Map();
1401
1628
  bidiListeners = new Map();
1402
1629
  responseDataCollector;
1403
1630
  activeTabId;
@@ -1420,10 +1647,7 @@ class BidiConnectedBrowserSession {
1420
1647
  const session = new BidiConnectedBrowserSession(client);
1421
1648
  session.ownsSession = await ensureMcpBiDiSession(client, args.endpoint, args.sessionId);
1422
1649
  await session.initialize();
1423
- const tabs = await session.refreshTabs();
1424
- if (tabs.length === 0) {
1425
- await session.newTab();
1426
- }
1650
+ await session.refreshTabs();
1427
1651
  return session;
1428
1652
  }
1429
1653
  async version() {
@@ -1482,10 +1706,14 @@ class BidiConnectedBrowserSession {
1482
1706
  async snapshot(request = {}) {
1483
1707
  const tabId = await this.getActiveTabId();
1484
1708
  const result = await retryUntilReady(() => evaluateBiDi(this.client, tabId, ARIA_SNAPSHOT_EVALUATE_SOURCE, toAriaSnapshotPayload(request)));
1485
- return toBrowserSnapshot(result, request, {
1709
+ const snapshot = toBrowserSnapshot(result, request, {
1486
1710
  console: this.consoleSummary(tabId),
1487
1711
  consoleLink: await this.takeConsoleLink(tabId)
1488
1712
  });
1713
+ return {
1714
+ ...snapshot,
1715
+ retryable: true
1716
+ };
1489
1717
  }
1490
1718
  async consoleMessages(level = "info", all = false) {
1491
1719
  const activeTabId = await this.getActiveTabId();
@@ -1829,7 +2057,7 @@ class BidiConnectedBrowserSession {
1829
2057
  }
1830
2058
  }
1831
2059
  async handleDialog(accept, promptText) {
1832
- const tabId = await this.getActiveTabId();
2060
+ const tabId = this.dialogTabId();
1833
2061
  if (!this.pageDialogStates.has(tabId)) {
1834
2062
  throw new McpToolError("no_dialog", "No dialog visible.");
1835
2063
  }
@@ -1840,6 +2068,9 @@ class BidiConnectedBrowserSession {
1840
2068
  ...(promptText !== undefined ? { userText: promptText } : {})
1841
2069
  });
1842
2070
  }
2071
+ async hasDialog() {
2072
+ return this.pageDialogStates.size > 0;
2073
+ }
1843
2074
  async networkRequests() {
1844
2075
  const tabId = await this.getActiveTabId();
1845
2076
  return this.ensureNetworkState(tabId).requests.map(cloneNetworkRequest);
@@ -1849,6 +2080,21 @@ class BidiConnectedBrowserSession {
1849
2080
  const request = this.ensureNetworkState(tabId).requests[index - 1];
1850
2081
  return request ? cloneNetworkRequest(request) : undefined;
1851
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
+ }
1852
2098
  async runCodeUnsafe(code) {
1853
2099
  return this.evaluate(`async () => {
1854
2100
  const fn = eval(${JSON.stringify(`(${code})`)});
@@ -1921,6 +2167,63 @@ class BidiConnectedBrowserSession {
1921
2167
  targetArg(target) {
1922
2168
  return "nodeToken" in target ? { nodeToken: target.nodeToken } : { selector: target.selector };
1923
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
+ }
1924
2227
  async actionPoint(tabId, target) {
1925
2228
  const source = "nodeToken" in target ? ACTION_POINT_EVALUATE_SOURCE : ACTION_POINT_BY_SELECTOR_SOURCE;
1926
2229
  const point = await evaluateBiDi(this.client, tabId, source, this.targetArg(target));
@@ -2103,7 +2406,10 @@ class BidiConnectedBrowserSession {
2103
2406
  state = {
2104
2407
  requests: [],
2105
2408
  byRequestId: new Map(),
2106
- startedAt: new Map()
2409
+ startedAt: new Map(),
2410
+ hydratedPerformanceResources: false,
2411
+ loadingDone: new Map(),
2412
+ bodyRead: new Set()
2107
2413
  };
2108
2414
  this.pageNetworkStates.set(tabId, state);
2109
2415
  }
@@ -2119,7 +2425,10 @@ class BidiConnectedBrowserSession {
2119
2425
  this.pageNetworkStates.set(tabId, {
2120
2426
  requests: [],
2121
2427
  byRequestId: new Map(),
2122
- startedAt: new Map()
2428
+ startedAt: new Map(),
2429
+ hydratedPerformanceResources: false,
2430
+ loadingDone: new Map(),
2431
+ bodyRead: new Set()
2123
2432
  });
2124
2433
  this.pageDialogStates.delete(tabId);
2125
2434
  }