@wdio/nightwatch-devtools 1.8.2 → 2.1.0

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.
@@ -12,10 +12,15 @@ var RUNNER_ENV = {
12
12
  WDIO_CONFIG: "DEVTOOLS_WDIO_CONFIG",
13
13
  NIGHTWATCH_CONFIG: "DEVTOOLS_NIGHTWATCH_CONFIG",
14
14
  WDIO_INITIAL_SPECS: "DEVTOOLS_WDIO_INITIAL_SPECS",
15
+ // The selection the Nightwatch run was launched with (specs, --tag, --env, ...), without --config; see encodeInitialArgs.
16
+ NIGHTWATCH_INITIAL_ARGS: "DEVTOOLS_NIGHTWATCH_INITIAL_ARGS",
15
17
  RUNNER_CWD: "DEVTOOLS_RUNNER_CWD",
16
18
  WDIO_BIN: "DEVTOOLS_WDIO_BIN",
17
19
  NIGHTWATCH_BIN: "DEVTOOLS_NIGHTWATCH_BIN"
18
20
  };
21
+ function encodeInitialArgs(args) {
22
+ return JSON.stringify(args);
23
+ }
19
24
 
20
25
  // ../core/src/error.ts
21
26
  function toError(value) {
@@ -30,7 +35,11 @@ function toError(value) {
30
35
  }
31
36
  return e;
32
37
  }
33
- return new Error(String(value));
38
+ try {
39
+ return new Error(String(value));
40
+ } catch {
41
+ return new Error(Object.prototype.toString.call(value));
42
+ }
34
43
  }
35
44
  function errorMessage(value) {
36
45
  if (value instanceof Error) {
@@ -248,6 +257,9 @@ import fs2 from "fs/promises";
248
257
  import path2 from "path";
249
258
 
250
259
  // ../shared/src/trace-actions.ts
260
+ function isWaitAction(action) {
261
+ return action?.method.startsWith("waitFor") ?? false;
262
+ }
251
263
  var ASSERT_ACTION_CLASS = "Assert";
252
264
  var TRACKED_ASSERT_METHODS = [
253
265
  "equal",
@@ -334,6 +346,8 @@ var ACTION_MAP = {
334
346
  waitForElementNotVisible: { class: "Element", method: "waitForHidden" },
335
347
  waitForElementPresent: { class: "Element", method: "waitForExist" },
336
348
  waitForElementNotPresent: { class: "Element", method: "waitForNotExist" },
349
+ // A Selenium wait on an element locator (`until.elementLocated`, `presence_of_element_located`), named by its adapter.
350
+ waitForElementLocated: { class: "Element", method: "waitForExist" },
337
351
  // WDIO page/browser reads
338
352
  getTitle: { class: "Page", method: "getTitle" },
339
353
  getUrl: { class: "Page", method: "getUrl" },
@@ -344,6 +358,8 @@ var ACTION_MAP = {
344
358
  getCssValue: { class: "Element", method: "getCSSProperty" },
345
359
  getRect: { class: "Element", method: "getRect" },
346
360
  getCurrentUrl: { class: "Page", method: "getUrl" },
361
+ // A Selenium wait on any condition other than an element locator.
362
+ wait: { class: "Browser", method: "waitForFunction" },
347
363
  // Raw W3C protocol names. The JS adapters wrap a client library and report
348
364
  // its method names, but an adapter that patches the protocol chokepoint —
349
365
  // the Python one, and any future language binding — reports what WebDriver
@@ -420,6 +436,9 @@ function isInputDispatchingCommand(command) {
420
436
  function mapCommandToAction(command) {
421
437
  return ACTION_MAP[command] ?? mapAssertCommand(command);
422
438
  }
439
+ function isWaitCommand(command) {
440
+ return isWaitAction(mapCommandToAction(command));
441
+ }
423
442
  var ASSERT_TITLE_VALUE_MAX = 40;
424
443
  function formatAssertValue(value) {
425
444
  let text;
@@ -452,6 +471,29 @@ function formatActionTitle(action, args, params, command) {
452
471
  }
453
472
  var FILL_METHODS = /* @__PURE__ */ new Set(["fill", "selectOption"]);
454
473
 
474
+ // ../shared/src/browser-schemes.ts
475
+ var BROWSER_INTERNAL_SCHEMES = /* @__PURE__ */ new Set([
476
+ "about",
477
+ "chrome",
478
+ "chrome-error",
479
+ "chrome-extension",
480
+ "chrome-native",
481
+ "chrome-search",
482
+ "chrome-untrusted",
483
+ "devtools",
484
+ "edge",
485
+ "moz-extension",
486
+ "safari-web-extension"
487
+ ]);
488
+ var SCHEME = /^([a-z][a-z0-9+.-]*):/;
489
+ function schemeOf(url) {
490
+ return SCHEME.exec(url.toLowerCase())?.[1];
491
+ }
492
+ function isBrowserInternalUrl(url) {
493
+ const scheme = schemeOf(url);
494
+ return scheme !== void 0 && (BROWSER_INTERNAL_SCHEMES.has(scheme) || scheme.startsWith("chrome-"));
495
+ }
496
+
455
497
  // ../shared/src/console.ts
456
498
  var CONSOLE_METHODS = ["log", "info", "warn", "error"];
457
499
  var ANSI_REGEX = /\x1b\[[?]?[0-9;]*[A-Za-z]/g;
@@ -503,16 +545,6 @@ function detectLogLevel(text) {
503
545
  function createConsoleLogEntry(type, args, source = LOG_SOURCES.TEST) {
504
546
  return { timestamp: Date.now(), type, args, source };
505
547
  }
506
- function mapChromeBrowserLogs(entries) {
507
- return entries.map((entry) => ({
508
- timestamp: entry.timestamp,
509
- type: chromeLogLevelToLogLevel(
510
- entry.level
511
- ),
512
- args: [entry.message],
513
- source: LOG_SOURCES.BROWSER
514
- }));
515
- }
516
548
  function chromeLogLevelToLogLevel(level) {
517
549
  const levelName = (typeof level === "object" ? level?.name ?? "" : level ?? "").toUpperCase();
518
550
  switch (levelName) {
@@ -561,6 +593,9 @@ function namesAnAutomation(caps) {
561
593
  deepCapString(caps, "appium:automationName") ?? deepCapString(caps, "automationName")
562
594
  );
563
595
  }
596
+ function isCapabilityBag(value) {
597
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
598
+ }
564
599
  function firstCapString(caps, keys, reject = () => false) {
565
600
  for (const key of keys) {
566
601
  const value = deepCapString(caps, key);
@@ -571,7 +606,7 @@ function firstCapString(caps, keys, reject = () => false) {
571
606
  return void 0;
572
607
  }
573
608
  function deviceFromCapabilities(capabilities) {
574
- if (!capabilities || typeof capabilities !== "object") {
609
+ if (!isCapabilityBag(capabilities)) {
575
610
  return void 0;
576
611
  }
577
612
  const caps = capabilities;
@@ -595,7 +630,7 @@ function deviceFromCapabilities(capabilities) {
595
630
  };
596
631
  }
597
632
  function isNativeAppSession(capabilities) {
598
- if (!capabilities || typeof capabilities !== "object") {
633
+ if (!isCapabilityBag(capabilities)) {
599
634
  return false;
600
635
  }
601
636
  const caps = capabilities;
@@ -1045,12 +1080,201 @@ function elementsScript(includeBounds, inViewportOnly, runner) {
1045
1080
  })()`;
1046
1081
  }
1047
1082
 
1083
+ // ../shared/src/page-scripts.ts
1084
+ var COLLECTOR_READY_EXPRESSION = 'typeof window.wdioTraceCollector !== "undefined"';
1085
+ var COLLECTOR_READY_SCRIPT = `return ${COLLECTOR_READY_EXPRESSION};`;
1086
+ var COLLECTOR_INJECT_SCRIPT = `var s=document.createElement('script');s.textContent=arguments[0];document.head.appendChild(s);${COLLECTOR_READY_SCRIPT}`;
1087
+ function collectorDrainExpression(forceAnchor = false) {
1088
+ const call = forceAnchor ? "window.wdioTraceCollector.captureCurrentDom();" : "";
1089
+ return `if (!(${COLLECTOR_READY_EXPRESSION})) { return null; } ${call} return window.wdioTraceCollector.getTraceData();`;
1090
+ }
1091
+ var CAPTURE_PERFORMANCE_SCRIPT = `
1092
+ return (function() {
1093
+ const performance = window.performance;
1094
+ const navigation = performance.getEntriesByType?.('navigation')?.[0];
1095
+ const resources = performance.getEntriesByType?.('resource') || [];
1096
+
1097
+ return {
1098
+ navigation: navigation ? {
1099
+ url: window.location.href,
1100
+ timing: {
1101
+ loadTime: navigation.loadEventEnd - navigation.fetchStart,
1102
+ domReady: navigation.domContentLoadedEventEnd - navigation.fetchStart,
1103
+ responseTime: navigation.responseEnd - navigation.requestStart,
1104
+ dnsLookup: navigation.domainLookupEnd - navigation.domainLookupStart,
1105
+ tcpConnection: navigation.connectEnd - navigation.connectStart,
1106
+ serverResponse: navigation.responseEnd - navigation.responseStart
1107
+ }
1108
+ } : undefined,
1109
+ resources: resources.map(function(resource) {
1110
+ return {
1111
+ url: resource.name,
1112
+ duration: resource.duration,
1113
+ size: resource.transferSize || 0,
1114
+ type: resource.initiatorType,
1115
+ startTime: resource.startTime,
1116
+ responseEnd: resource.responseEnd
1117
+ };
1118
+ }),
1119
+ cookies: (function() {
1120
+ try { return document.cookie; } catch (e) { return ''; }
1121
+ })(),
1122
+ documentInfo: {
1123
+ url: window.location.href,
1124
+ title: document.title,
1125
+ headers: {
1126
+ userAgent: navigator.userAgent,
1127
+ language: navigator.language,
1128
+ platform: navigator.platform
1129
+ },
1130
+ documentInfo: {
1131
+ readyState: document.readyState,
1132
+ referrer: document.referrer,
1133
+ characterSet: document.characterSet
1134
+ }
1135
+ }
1136
+ };
1137
+ })()
1138
+ `;
1139
+
1140
+ // ../shared/src/command-tree.ts
1141
+ function nestCommands(commands) {
1142
+ const nodes = commands.map((command) => ({
1143
+ command,
1144
+ children: []
1145
+ }));
1146
+ const byId = /* @__PURE__ */ new Map();
1147
+ for (const node of nodes) {
1148
+ const { id } = node.command;
1149
+ if (typeof id === "number" && !byId.has(id)) {
1150
+ byId.set(id, node);
1151
+ }
1152
+ }
1153
+ const parentOf = (node) => {
1154
+ const { parentId } = node.command;
1155
+ return typeof parentId === "number" ? byId.get(parentId) : void 0;
1156
+ };
1157
+ const roots = [];
1158
+ for (const node of nodes) {
1159
+ const parent = parentOf(node);
1160
+ if (parent && !closesCycle(node, parentOf)) {
1161
+ parent.children.push(node);
1162
+ } else {
1163
+ roots.push(node);
1164
+ }
1165
+ }
1166
+ return roots;
1167
+ }
1168
+ function closesCycle(node, parentOf) {
1169
+ const seen = /* @__PURE__ */ new Set();
1170
+ for (let at = parentOf(node); at && !seen.has(at); at = parentOf(at)) {
1171
+ if (at === node) {
1172
+ return true;
1173
+ }
1174
+ seen.add(at);
1175
+ }
1176
+ return false;
1177
+ }
1178
+
1179
+ // ../shared/src/command-failure.ts
1180
+ function collapsedAssertResult(result) {
1181
+ if (typeof result === "object" && result !== null && "passed" in result) {
1182
+ return result;
1183
+ }
1184
+ return void 0;
1185
+ }
1186
+ function commandFailure(cmd, action) {
1187
+ if (cmd.error) {
1188
+ return errorFailure(cmd.error);
1189
+ }
1190
+ const collapsed = collapsedAssertResult(cmd.result);
1191
+ if (collapsed && collapsed.passed === false) {
1192
+ const isAssert = action.class === ASSERT_ACTION_CLASS;
1193
+ return {
1194
+ message: String(
1195
+ collapsed.message ?? (isAssert ? "Assertion failed" : `${cmd.command} failed`)
1196
+ )
1197
+ };
1198
+ }
1199
+ if (cmd.result === false && isWaitAction(action)) {
1200
+ const target = cmd.args?.find((a) => typeof a === "string");
1201
+ return {
1202
+ message: target ? `${cmd.command} timed out waiting for ${String(target)}` : `${cmd.command} timed out`
1203
+ };
1204
+ }
1205
+ return void 0;
1206
+ }
1207
+ function errorFailure(error) {
1208
+ return { message: error.message ?? String(error) };
1209
+ }
1210
+ function rowFailure(cmd) {
1211
+ const action = mapCommandToAction(cmd.command);
1212
+ if (action) {
1213
+ return commandFailure(cmd, action);
1214
+ }
1215
+ return cmd.error ? errorFailure(cmd.error) : void 0;
1216
+ }
1217
+ function retriedRows(commands) {
1218
+ const retried = /* @__PURE__ */ new Set();
1219
+ const walk = (nodes, underPassing) => {
1220
+ for (const { command, children } of nodes) {
1221
+ const failed = Boolean(rowFailure(command));
1222
+ if (underPassing && failed) {
1223
+ retried.add(command);
1224
+ }
1225
+ walk(children, underPassing || !failed);
1226
+ }
1227
+ };
1228
+ walk(nestCommands(commands), false);
1229
+ return retried;
1230
+ }
1231
+
1048
1232
  // ../shared/src/image.ts
1049
1233
  var PNG_BASE64_MAGIC = "iVBOR";
1050
1234
  function imageMime(base64) {
1051
1235
  return base64.startsWith(PNG_BASE64_MAGIC) ? "image/png" : "image/jpeg";
1052
1236
  }
1053
1237
 
1238
+ // ../shared/src/request-type.ts
1239
+ function getRequestType(url, mimeType) {
1240
+ const contentType = mimeType?.toLowerCase() ?? "";
1241
+ const urlLower = url.toLowerCase();
1242
+ if (contentType.includes("text/html")) {
1243
+ return "document";
1244
+ }
1245
+ if (contentType.includes("text/css")) {
1246
+ return "stylesheet";
1247
+ }
1248
+ if (contentType.includes("javascript") || contentType.includes("ecmascript")) {
1249
+ return "script";
1250
+ }
1251
+ if (contentType.includes("image/")) {
1252
+ return "image";
1253
+ }
1254
+ if (contentType.includes("font/") || contentType.includes("woff")) {
1255
+ return "font";
1256
+ }
1257
+ if (contentType.includes("application/json")) {
1258
+ return "fetch";
1259
+ }
1260
+ if (urlLower.endsWith(".html") || urlLower.endsWith(".htm")) {
1261
+ return "document";
1262
+ }
1263
+ if (urlLower.endsWith(".css")) {
1264
+ return "stylesheet";
1265
+ }
1266
+ if (urlLower.endsWith(".js") || urlLower.endsWith(".mjs")) {
1267
+ return "script";
1268
+ }
1269
+ if (/\.(png|jpg|jpeg|gif|svg|webp|ico)$/.test(urlLower)) {
1270
+ return "image";
1271
+ }
1272
+ if (/\.(woff|woff2|ttf|eot|otf)$/.test(urlLower)) {
1273
+ return "font";
1274
+ }
1275
+ return "xhr";
1276
+ }
1277
+
1054
1278
  // ../shared/src/routes.ts
1055
1279
  var WS_PATHS = {
1056
1280
  /** Adapter session upgrade endpoint. One socket per running adapter. */
@@ -1147,6 +1371,9 @@ var TEST_STATE = {
1147
1371
  FAILED: "failed",
1148
1372
  SKIPPED: "skipped"
1149
1373
  };
1374
+ var NETWORK_CAPTURE_SOURCE = {
1375
+ RESOURCE_TIMING: "resource-timing"
1376
+ };
1150
1377
  var SCREENCAST_DEFAULTS = {
1151
1378
  enabled: false,
1152
1379
  captureFormat: "jpeg",
@@ -1518,7 +1745,7 @@ function splitNumericSuffix(value) {
1518
1745
  return { rest: value.slice(0, sep), num: Number(digits) };
1519
1746
  }
1520
1747
  function callSourceToStack(callSource) {
1521
- if (!callSource || callSource === "unknown:0") {
1748
+ if (!callSource) {
1522
1749
  return void 0;
1523
1750
  }
1524
1751
  const last = splitNumericSuffix(callSource);
@@ -1551,28 +1778,28 @@ function ownTitle(title, parent) {
1551
1778
  return title.slice(parent.title.length).trim() || title;
1552
1779
  }
1553
1780
  function buildGroupPath(cmd, testMetadata) {
1554
- const path10 = [];
1781
+ const path11 = [];
1555
1782
  if (cmd.testUid) {
1556
1783
  const entry = testMetadata?.get(cmd.testUid);
1557
1784
  for (const ancestor of entry?.ancestry ?? []) {
1558
- path10.push({
1785
+ path11.push({
1559
1786
  uid: ancestor.uid,
1560
- title: ownTitle(ancestor.title, path10[path10.length - 1])
1787
+ title: ownTitle(ancestor.title, path11[path11.length - 1])
1561
1788
  });
1562
1789
  }
1563
- path10.push({
1790
+ path11.push({
1564
1791
  uid: cmd.testUid,
1565
- title: ownTitle(entry?.title ?? cmd.testUid, path10[path10.length - 1])
1792
+ title: ownTitle(entry?.title ?? cmd.testUid, path11[path11.length - 1])
1566
1793
  });
1567
1794
  }
1568
1795
  if (cmd.stepUid) {
1569
1796
  const stepEntry = testMetadata?.get(cmd.stepUid);
1570
- path10.push({
1797
+ path11.push({
1571
1798
  uid: cmd.stepUid,
1572
- title: ownTitle(stepEntry?.title ?? cmd.stepUid, path10[path10.length - 1])
1799
+ title: ownTitle(stepEntry?.title ?? cmd.stepUid, path11[path11.length - 1])
1573
1800
  });
1574
1801
  }
1575
- return path10;
1802
+ return path11;
1576
1803
  }
1577
1804
 
1578
1805
  // ../trace/src/trace-action-events.ts
@@ -1591,12 +1818,6 @@ function serializableResult(result) {
1591
1818
  return void 0;
1592
1819
  }
1593
1820
  }
1594
- function collapsedAssertResult(result) {
1595
- if (typeof result === "object" && result !== null && "passed" in result) {
1596
- return result;
1597
- }
1598
- return void 0;
1599
- }
1600
1821
  function buildAssertParams(cmd) {
1601
1822
  const params = Object.fromEntries(
1602
1823
  cmd.args.map((arg, index) => [String(index), arg])
@@ -1689,28 +1910,6 @@ function buildParamsAndTitle(action, cmd) {
1689
1910
  )
1690
1911
  };
1691
1912
  }
1692
- function actionError(cmd, action) {
1693
- if (cmd.error) {
1694
- const err = cmd.error;
1695
- return { message: err.message ?? String(cmd.error) };
1696
- }
1697
- const isAssert = action.class === ASSERT_ACTION_CLASS;
1698
- const collapsed = collapsedAssertResult(cmd.result);
1699
- if (collapsed && collapsed.passed === false) {
1700
- return {
1701
- message: String(
1702
- collapsed.message ?? (isAssert ? "Assertion failed" : `${cmd.command} failed`)
1703
- )
1704
- };
1705
- }
1706
- if (cmd.result === false && action.method.startsWith("waitFor")) {
1707
- const target = cmd.args?.find((a) => typeof a === "string");
1708
- return {
1709
- message: target ? `${cmd.command} timed out waiting for ${String(target)}` : `${cmd.command} timed out`
1710
- };
1711
- }
1712
- return void 0;
1713
- }
1714
1913
  function resolveActionPoint(action, cmd, snapshotIndex) {
1715
1914
  if (action.class !== "Element" || !POINTABLE_METHODS.has(action.method)) {
1716
1915
  return void 0;
@@ -1729,10 +1928,13 @@ function resolveActionPoint(action, cmd, snapshotIndex) {
1729
1928
  }
1730
1929
  return void 0;
1731
1930
  }
1732
- function buildAfterEvent(cmd, action, callId, endMs, snapshotIndex) {
1931
+ function buildAfterEvent(cmd, action, callId, endMs, ctx) {
1932
+ const { snapshotIndex } = ctx;
1733
1933
  const afterEvent = { type: "after", callId, endTime: endMs };
1734
- const error = actionError(cmd, action);
1735
- if (error) {
1934
+ const error = commandFailure(cmd, action);
1935
+ if (error && ctx.retried.has(cmd)) {
1936
+ afterEvent.retriedError = error;
1937
+ } else if (error) {
1736
1938
  afterEvent.error = error;
1737
1939
  }
1738
1940
  const result = serializableResult(cmd.result);
@@ -1749,13 +1951,11 @@ function buildAfterEvent(cmd, action, callId, endMs, snapshotIndex) {
1749
1951
  }
1750
1952
  return afterEvent;
1751
1953
  }
1752
- function pushActionPair(stream, cmd, action, pageId, wallTime, snapshotIndex) {
1954
+ function pushBefore(stream, cmd, action, pageId, wallTime, parentCallId, snapshotIndex) {
1753
1955
  stream.callCounter++;
1754
1956
  const callId = `call@${stream.callCounter}`;
1755
1957
  const rawStartMs = (cmd.startTime ?? cmd.timestamp) - wallTime;
1756
- const rawEndMs = cmd.timestamp - wallTime;
1757
1958
  const startMs = Math.max(stream.prevEndMs, rawStartMs);
1758
- const endMs = Math.max(startMs + 1, rawEndMs);
1759
1959
  const { params, title } = buildParamsAndTitle(action, cmd);
1760
1960
  const beforeEvent = {
1761
1961
  type: "before",
@@ -1767,7 +1967,7 @@ function pushActionPair(stream, cmd, action, pageId, wallTime, snapshotIndex) {
1767
1967
  params,
1768
1968
  title,
1769
1969
  apiName: `${action.class.toLowerCase()}.${action.method}`,
1770
- parentId: stream.openGroups[stream.openGroups.length - 1]?.callId
1970
+ parentId: parentCallId ?? stream.openGroups[stream.openGroups.length - 1]?.callId
1771
1971
  };
1772
1972
  const stack = callSourceToStack(cmd.callSource);
1773
1973
  if (stack) {
@@ -1778,7 +1978,38 @@ function pushActionPair(stream, cmd, action, pageId, wallTime, snapshotIndex) {
1778
1978
  beforeEvent.beforeSnapshot = beforeName;
1779
1979
  }
1780
1980
  stream.events.push(beforeEvent);
1781
- stream.events.push(buildAfterEvent(cmd, action, callId, endMs, snapshotIndex));
1981
+ return { callId, startMs };
1982
+ }
1983
+ function emitNode(stream, node, ctx, parentCallId) {
1984
+ const cmd = node.command;
1985
+ const action = mapCommandToAction(cmd.command);
1986
+ if (!action) {
1987
+ for (const child of node.children) {
1988
+ emitNode(stream, child, ctx, parentCallId);
1989
+ }
1990
+ return;
1991
+ }
1992
+ const { callId, startMs } = pushBefore(
1993
+ stream,
1994
+ cmd,
1995
+ action,
1996
+ ctx.pageId,
1997
+ ctx.wallTime,
1998
+ parentCallId,
1999
+ ctx.snapshotIndex
2000
+ );
2001
+ if (node.children.length) {
2002
+ stream.prevEndMs = startMs;
2003
+ for (const child of node.children) {
2004
+ emitNode(stream, child, ctx, callId);
2005
+ }
2006
+ }
2007
+ const endMs = Math.max(
2008
+ startMs + 1,
2009
+ cmd.timestamp - ctx.wallTime,
2010
+ node.children.length ? stream.prevEndMs : 0
2011
+ );
2012
+ stream.events.push(buildAfterEvent(cmd, action, callId, endMs, ctx));
1782
2013
  stream.prevEndMs = endMs;
1783
2014
  }
1784
2015
  function buildActionEvents(commands, pageId, wallTime, testMetadata, snapshotIndex) {
@@ -1791,20 +2022,42 @@ function buildActionEvents(commands, pageId, wallTime, testMetadata, snapshotInd
1791
2022
  const ordered = [...commands].sort(
1792
2023
  (a, b) => (a.startTime ?? a.timestamp) - (b.startTime ?? b.timestamp) || (a.sequence ?? 0) - (b.sequence ?? 0)
1793
2024
  );
1794
- for (const cmd of ordered) {
1795
- const action = mapCommandToAction(cmd.command);
1796
- if (!action) {
1797
- continue;
2025
+ const ctx = {
2026
+ pageId,
2027
+ wallTime,
2028
+ snapshotIndex,
2029
+ retried: retriedRows(ordered)
2030
+ };
2031
+ for (const node of nestCommands(ordered)) {
2032
+ if (hasAction(node)) {
2033
+ syncGroups(stream, node.command, pageId, wallTime, testMetadata);
1798
2034
  }
1799
- syncGroups(stream, cmd, pageId, wallTime, testMetadata);
1800
- pushActionPair(stream, cmd, action, pageId, wallTime, snapshotIndex);
2035
+ emitNode(stream, node, ctx);
1801
2036
  }
1802
2037
  closeGroupsFrom(stream, 0);
1803
2038
  return stream.events;
1804
2039
  }
2040
+ function hasAction(node) {
2041
+ return Boolean(mapCommandToAction(node.command.command)) || node.children.some(hasAction);
2042
+ }
1805
2043
 
1806
2044
  // ../trace/src/trace-har.ts
1807
2045
  var INLINE_BODY_TEXT_MAX_BYTES = 8 * 1024;
2046
+ var HAR_PHASE_NOT_APPLICABLE = -1;
2047
+ function toHarTimings(timings, duration) {
2048
+ if (!timings) {
2049
+ return { send: 0, wait: Math.max(0, duration), receive: 0 };
2050
+ }
2051
+ return {
2052
+ blocked: timings.blocked ?? HAR_PHASE_NOT_APPLICABLE,
2053
+ dns: timings.dns ?? HAR_PHASE_NOT_APPLICABLE,
2054
+ connect: timings.connect ?? HAR_PHASE_NOT_APPLICABLE,
2055
+ ssl: timings.ssl ?? HAR_PHASE_NOT_APPLICABLE,
2056
+ send: timings.send ?? 0,
2057
+ wait: timings.wait ?? 0,
2058
+ receive: timings.receive ?? 0
2059
+ };
2060
+ }
1808
2061
  function toHeaderArray(h) {
1809
2062
  if (!h) {
1810
2063
  return [];
@@ -1868,11 +2121,9 @@ function networkRequestToHar(entry, opts = {}) {
1868
2121
  bodySize: entry.size ?? -1
1869
2122
  },
1870
2123
  cache: {},
1871
- timings: {
1872
- send: 0,
1873
- wait: Math.max(0, duration),
1874
- receive: 0
1875
- }
2124
+ timings: toHarTimings(entry.timings, duration),
2125
+ _resourceType: entry.type,
2126
+ ...entry.source ? { _source: entry.source } : {}
1876
2127
  }
1877
2128
  };
1878
2129
  }
@@ -1989,6 +2240,7 @@ function capturedSteps(commands) {
1989
2240
  function generateTranscript(commands, startWallTime, title) {
1990
2241
  const wallTimeISO = new Date(startWallTime).toISOString();
1991
2242
  const lines = [`# ${title ?? "Session"} \u2014 ${wallTimeISO}`, ""];
2243
+ const retried = retriedRows(commands);
1992
2244
  capturedSteps(commands).forEach(({ entry, action }, idx) => {
1993
2245
  const rawArgs = entry.args;
1994
2246
  const parts = [stripAnsi(formatActionTitle(action, rawArgs))];
@@ -1999,7 +2251,8 @@ function generateTranscript(commands, startWallTime, title) {
1999
2251
  }
2000
2252
  }
2001
2253
  if (entry.error) {
2002
- parts.push(`ERROR: ${errorMessage2(entry.error)}`);
2254
+ const label = retried.has(entry) ? "RETRIED" : "ERROR";
2255
+ parts.push(`${label}: ${errorMessage2(entry.error)}`);
2003
2256
  }
2004
2257
  lines.push(asNumberedItem(`${idx + 1}. `, parts.join(" ")));
2005
2258
  });
@@ -2741,7 +2994,7 @@ async function maybeWriteManifest(ctx, fanOutArtifacts) {
2741
2994
  }
2742
2995
  const artifacts = ctx.collectedArtifacts ?? fanOutArtifacts;
2743
2996
  try {
2744
- const path10 = await writeArtifactsManifest(
2997
+ const path11 = await writeArtifactsManifest(
2745
2998
  ctx.resolveOutputDir(),
2746
2999
  buildArtifactsManifest({
2747
3000
  sessionId: ctx.sessionId,
@@ -2750,7 +3003,7 @@ async function maybeWriteManifest(ctx, fanOutArtifacts) {
2750
3003
  testMetadata: ctx.testMetadata
2751
3004
  })
2752
3005
  );
2753
- ctx.log?.("info", `Artifacts manifest written to ${path10}`);
3006
+ ctx.log?.("info", `Artifacts manifest written to ${path11}`);
2754
3007
  } catch (err) {
2755
3008
  ctx.log?.(
2756
3009
  "warn",
@@ -2764,48 +3017,6 @@ import { createRequire } from "module";
2764
3017
 
2765
3018
  // ../core/src/net.ts
2766
3019
  import * as net from "net";
2767
-
2768
- // ../core/src/request-type.ts
2769
- function getRequestType(url, mimeType) {
2770
- const contentType = mimeType?.toLowerCase() ?? "";
2771
- const urlLower = url.toLowerCase();
2772
- if (contentType.includes("text/html")) {
2773
- return "document";
2774
- }
2775
- if (contentType.includes("text/css")) {
2776
- return "stylesheet";
2777
- }
2778
- if (contentType.includes("javascript") || contentType.includes("ecmascript")) {
2779
- return "script";
2780
- }
2781
- if (contentType.includes("image/")) {
2782
- return "image";
2783
- }
2784
- if (contentType.includes("font/") || contentType.includes("woff")) {
2785
- return "font";
2786
- }
2787
- if (contentType.includes("application/json")) {
2788
- return "fetch";
2789
- }
2790
- if (urlLower.endsWith(".html") || urlLower.endsWith(".htm")) {
2791
- return "document";
2792
- }
2793
- if (urlLower.endsWith(".css")) {
2794
- return "stylesheet";
2795
- }
2796
- if (urlLower.endsWith(".js") || urlLower.endsWith(".mjs")) {
2797
- return "script";
2798
- }
2799
- if (/\.(png|jpg|jpeg|gif|svg|webp|ico)$/.test(urlLower)) {
2800
- return "image";
2801
- }
2802
- if (/\.(woff|woff2|ttf|eot|otf)$/.test(urlLower)) {
2803
- return "font";
2804
- }
2805
- return "xhr";
2806
- }
2807
-
2808
- // ../core/src/net.ts
2809
3020
  function isPortInUse(port, hostname) {
2810
3021
  return new Promise((resolve) => {
2811
3022
  const server = net.createServer();
@@ -2893,34 +3104,56 @@ function bidiResponseTiming(timings, startTime, timestamp) {
2893
3104
  const endTime = Number(timestamp ?? Date.now());
2894
3105
  return { endTime, time: Math.max(0, endTime - startTime) };
2895
3106
  }
2896
- function handleBidiRequestSent(rawEvent, pending, sinks, log) {
3107
+ var MAX_TRACKED_REQUESTS = 5e3;
3108
+ function remember(rows, id, entry) {
3109
+ rows.set(id, entry);
3110
+ if (rows.size <= MAX_TRACKED_REQUESTS) {
3111
+ return;
3112
+ }
3113
+ for (const [key, row] of rows) {
3114
+ if (row.status !== void 0) {
3115
+ rows.delete(key);
3116
+ return;
3117
+ }
3118
+ }
3119
+ rows.delete(rows.keys().next().value);
3120
+ }
3121
+ function handleBidiRequestSent(rawEvent, rows, sinks, log) {
2897
3122
  const event = rawEvent;
2898
3123
  try {
2899
3124
  const requestId = String(event?.request?.request ?? event?.id ?? "");
2900
- if (!requestId) {
3125
+ const shape = rawEvent;
3126
+ if (!requestId || shape?.response || shape?.errorText) {
2901
3127
  return;
2902
3128
  }
3129
+ const previous = rows.get(requestId);
2903
3130
  const entry = {
2904
3131
  id: requestId,
2905
3132
  url: event?.request?.url ?? "",
2906
3133
  method: event?.request?.method ?? "GET",
2907
3134
  requestHeaders: arrayHeadersToObject(event?.request?.headers),
2908
- timestamp: Date.now(),
2909
- startTime: Number(event?.timestamp ?? Date.now()),
3135
+ // A redirect hop re-sends the request under the same id: one row, which
3136
+ // keeps the clock of the hop it follows and reopens for the next answer.
3137
+ timestamp: previous?.timestamp ?? Date.now(),
3138
+ startTime: previous?.startTime ?? Number(event?.timestamp ?? Date.now()),
2910
3139
  type: getRequestType(event?.request?.url ?? "")
2911
3140
  };
2912
- pending.set(requestId, entry);
2913
- sinks.pushNetworkRequest(entry);
3141
+ remember(rows, requestId, entry);
3142
+ if (previous) {
3143
+ sinks.replaceNetworkRequest(requestId, entry);
3144
+ } else {
3145
+ sinks.pushNetworkRequest(entry);
3146
+ }
2914
3147
  } catch (err) {
2915
3148
  log("warn", `beforeRequestSent threw: ${errorMessage(err)}`);
2916
3149
  }
2917
3150
  }
2918
- function handleBidiResponseCompleted(rawEvent, pending, sinks, log) {
3151
+ function handleBidiResponseCompleted(rawEvent, rows, sinks, log) {
2919
3152
  const event = rawEvent;
2920
3153
  try {
2921
3154
  const requestId = String(event?.request?.request ?? event?.id ?? "");
2922
- const previous = pending.get(requestId);
2923
- if (!previous) {
3155
+ const previous = rows.get(requestId);
3156
+ if (!previous || !event?.response) {
2924
3157
  return;
2925
3158
  }
2926
3159
  const { endTime, time } = bidiResponseTiming(
@@ -2935,25 +3168,78 @@ function handleBidiResponseCompleted(rawEvent, pending, sinks, log) {
2935
3168
  responseHeaders: arrayHeadersToObject(event?.response?.headers),
2936
3169
  type: getRequestType(previous.url, event?.response?.mimeType),
2937
3170
  endTime,
2938
- time,
2939
- size: Number(event?.response?.bytesReceived) || void 0
3171
+ time,
3172
+ size: Number(event?.response?.bytesReceived) || void 0
3173
+ };
3174
+ remember(rows, requestId, finalized);
3175
+ sinks.replaceNetworkRequest(requestId, finalized);
3176
+ } catch (err) {
3177
+ log("warn", `responseCompleted threw: ${errorMessage(err)}`);
3178
+ }
3179
+ }
3180
+ function handleBidiFetchError(rawEvent, rows, sinks, log) {
3181
+ const event = rawEvent;
3182
+ try {
3183
+ const requestId = String(event?.request?.request ?? event?.id ?? "");
3184
+ const previous = rows.get(requestId);
3185
+ if (!previous || typeof event?.errorText !== "string") {
3186
+ return;
3187
+ }
3188
+ const { endTime, time } = bidiResponseTiming(
3189
+ event?.request?.timings,
3190
+ previous.startTime,
3191
+ event?.timestamp
3192
+ );
3193
+ const errorText = event?.errorText ?? "Failed";
3194
+ const finalized = {
3195
+ ...previous,
3196
+ status: 0,
3197
+ statusText: errorText,
3198
+ error: errorText,
3199
+ endTime,
3200
+ time
2940
3201
  };
2941
- pending.delete(requestId);
3202
+ remember(rows, requestId, finalized);
2942
3203
  sinks.replaceNetworkRequest(requestId, finalized);
2943
3204
  } catch (err) {
2944
- log("warn", `responseCompleted threw: ${errorMessage(err)}`);
3205
+ log("warn", `fetchError threw: ${errorMessage(err)}`);
3206
+ }
3207
+ }
3208
+ async function subscribeFetchErrors(inspector, rows, sinks, log) {
3209
+ if (typeof inspector.fetchError === "function") {
3210
+ await inspector.fetchError((e) => handleBidiFetchError(e, rows, sinks, log));
3211
+ return;
3212
+ }
3213
+ const bidi = inspector.bidi;
3214
+ if (typeof bidi?.subscribe !== "function" || !bidi.socket?.on) {
3215
+ return;
2945
3216
  }
3217
+ await bidi.subscribe("network.fetchError");
3218
+ bidi.socket.on("message", (raw) => {
3219
+ try {
3220
+ const { method, params } = JSON.parse(String(raw));
3221
+ if (method === "network.fetchError") {
3222
+ handleBidiFetchError(params, rows, sinks, log);
3223
+ }
3224
+ } catch {
3225
+ }
3226
+ });
2946
3227
  }
2947
3228
  async function attachNetworkInspector(driver, factory, sinks, log) {
2948
3229
  try {
2949
3230
  const inspector = await factory(driver);
2950
- const pending = /* @__PURE__ */ new Map();
3231
+ const rows = /* @__PURE__ */ new Map();
2951
3232
  await inspector.beforeRequestSent(
2952
- (e) => handleBidiRequestSent(e, pending, sinks, log)
3233
+ (e) => handleBidiRequestSent(e, rows, sinks, log)
2953
3234
  );
2954
3235
  await inspector.responseCompleted(
2955
- (e) => handleBidiResponseCompleted(e, pending, sinks, log)
3236
+ (e) => handleBidiResponseCompleted(e, rows, sinks, log)
2956
3237
  );
3238
+ try {
3239
+ await subscribeFetchErrors(inspector, rows, sinks, log);
3240
+ } catch (err) {
3241
+ log("warn", `BiDi network.fetchError unavailable: ${errorMessage(err)}`);
3242
+ }
2957
3243
  log("info", "\u2713 BiDi NetworkInspector attached (request + response)");
2958
3244
  return true;
2959
3245
  } catch (err) {
@@ -3036,7 +3322,7 @@ function resolveBaseDir(input) {
3036
3322
  if (!dir) {
3037
3323
  continue;
3038
3324
  }
3039
- if (dir.includes(NODE_MODULES_SEGMENT)) {
3325
+ if (`${dir}${path4.sep}`.includes(NODE_MODULES_SEGMENT)) {
3040
3326
  continue;
3041
3327
  }
3042
3328
  if (isWritable(dir)) {
@@ -3111,6 +3397,144 @@ function serializeWebSnapshot(nodes, context, options = {}) {
3111
3397
  // ../core/src/locators/xml-parsing.ts
3112
3398
  import { DOMParser } from "@xmldom/xmldom";
3113
3399
  import xpath from "xpath";
3400
+
3401
+ // ../core/src/locators/xpath-index.ts
3402
+ var NAME = /[A-Za-z_][\w.-]*/y;
3403
+ var active = null;
3404
+ function parseSimpleXPath(expr) {
3405
+ if (!expr.startsWith("//")) {
3406
+ return null;
3407
+ }
3408
+ let pos = 2;
3409
+ let tag;
3410
+ if (expr[pos] === "*") {
3411
+ tag = "*";
3412
+ pos++;
3413
+ } else {
3414
+ NAME.lastIndex = pos;
3415
+ const m = NAME.exec(expr);
3416
+ if (!m) {
3417
+ return null;
3418
+ }
3419
+ tag = m[0];
3420
+ pos = NAME.lastIndex;
3421
+ }
3422
+ if (pos === expr.length) {
3423
+ return { tag, conditions: [] };
3424
+ }
3425
+ if (expr[pos] !== "[" || !expr.endsWith("]")) {
3426
+ return null;
3427
+ }
3428
+ const conditions = parseConditions(expr.slice(pos + 1, -1));
3429
+ return conditions ? { tag, conditions } : null;
3430
+ }
3431
+ function parseConditions(body) {
3432
+ const conditions = [];
3433
+ let pos = 0;
3434
+ for (; ; ) {
3435
+ if (body[pos] !== "@") {
3436
+ return null;
3437
+ }
3438
+ NAME.lastIndex = pos + 1;
3439
+ const m = NAME.exec(body);
3440
+ if (!m || body[NAME.lastIndex] !== "=") {
3441
+ return null;
3442
+ }
3443
+ const quote = body[NAME.lastIndex + 1];
3444
+ if (quote !== '"' && quote !== "'") {
3445
+ return null;
3446
+ }
3447
+ const start = NAME.lastIndex + 2;
3448
+ const end = body.indexOf(quote, start);
3449
+ if (end === -1) {
3450
+ return null;
3451
+ }
3452
+ conditions.push({ name: m[0], value: body.slice(start, end) });
3453
+ pos = end + 1;
3454
+ if (pos === body.length) {
3455
+ return conditions;
3456
+ }
3457
+ if (!body.startsWith(" and ", pos)) {
3458
+ return null;
3459
+ }
3460
+ pos += 5;
3461
+ }
3462
+ }
3463
+ function append(map, key, value) {
3464
+ const list = map.get(key);
3465
+ if (list) {
3466
+ list.push(value);
3467
+ } else {
3468
+ map.set(key, [value]);
3469
+ }
3470
+ }
3471
+ function buildIndex(doc) {
3472
+ const index = {
3473
+ doc,
3474
+ all: [],
3475
+ byTag: /* @__PURE__ */ new Map(),
3476
+ byAttr: /* @__PURE__ */ new Map()
3477
+ };
3478
+ const stack = doc.documentElement ? [doc.documentElement] : [];
3479
+ const ordered = [];
3480
+ while (stack.length > 0) {
3481
+ const el = stack.pop();
3482
+ if (el.namespaceURI || el.nodeName.includes(":")) {
3483
+ return null;
3484
+ }
3485
+ ordered.push(el);
3486
+ for (let i = el.childNodes.length - 1; i >= 0; i--) {
3487
+ const child = el.childNodes.item(i);
3488
+ if (child?.nodeType === 1) {
3489
+ stack.push(child);
3490
+ }
3491
+ }
3492
+ }
3493
+ for (const el of ordered) {
3494
+ index.all.push(el);
3495
+ append(index.byTag, el.nodeName, el);
3496
+ for (let i = 0; i < el.attributes.length; i++) {
3497
+ const { name, value } = el.attributes.item(i);
3498
+ if (name === "xmlns" || name.includes(":")) {
3499
+ return null;
3500
+ }
3501
+ let byValue = index.byAttr.get(name);
3502
+ if (!byValue) {
3503
+ byValue = /* @__PURE__ */ new Map();
3504
+ index.byAttr.set(name, byValue);
3505
+ }
3506
+ append(byValue, value, el);
3507
+ }
3508
+ }
3509
+ return index;
3510
+ }
3511
+ function withXPathIndex(doc, walk) {
3512
+ const previous = active;
3513
+ active = doc ? buildIndex(doc) : null;
3514
+ try {
3515
+ return walk();
3516
+ } finally {
3517
+ active = previous;
3518
+ }
3519
+ }
3520
+ function selectIndexed(doc, expr) {
3521
+ if (active?.doc !== doc) {
3522
+ return null;
3523
+ }
3524
+ const query = parseSimpleXPath(expr);
3525
+ if (!query) {
3526
+ return null;
3527
+ }
3528
+ const [first, ...rest] = query.conditions;
3529
+ const candidates = first ? active.byAttr.get(first.name)?.get(first.value) ?? [] : query.tag === "*" ? active.all : active.byTag.get(query.tag) ?? [];
3530
+ return candidates.filter(
3531
+ (el) => (query.tag === "*" || el.nodeName === query.tag) && rest.every(
3532
+ (c) => el.hasAttribute(c.name) && el.getAttribute(c.name) === c.value
3533
+ )
3534
+ );
3535
+ }
3536
+
3537
+ // ../core/src/locators/xml-parsing.ts
3114
3538
  function childNodesOf(node) {
3115
3539
  const children = [];
3116
3540
  if (node.childNodes) {
@@ -3134,14 +3558,14 @@ function translateRecursively(domNode, parentPath = "", index = null) {
3134
3558
  }
3135
3559
  }
3136
3560
  }
3137
- const path10 = index === null ? "" : `${parentPath ? parentPath + "." : ""}${index}`;
3561
+ const path11 = index === null ? "" : `${parentPath ? parentPath + "." : ""}${index}`;
3138
3562
  return {
3139
3563
  children: childNodesOf(domNode).map(
3140
- (childNode, childIndex) => translateRecursively(childNode, path10, childIndex)
3564
+ (childNode, childIndex) => translateRecursively(childNode, path11, childIndex)
3141
3565
  ),
3142
3566
  tagName: domNode.nodeName,
3143
3567
  attributes,
3144
- path: path10
3568
+ path: path11
3145
3569
  };
3146
3570
  }
3147
3571
  function isSameElement(node1, node2) {
@@ -3240,6 +3664,10 @@ function xmlToDOM(sourceXML) {
3240
3664
  }
3241
3665
  function evaluateXPath(doc, xpathExpr) {
3242
3666
  try {
3667
+ const indexed = selectIndexed(doc, xpathExpr);
3668
+ if (indexed) {
3669
+ return indexed;
3670
+ }
3243
3671
  const nodes = xpath.select(xpathExpr, doc);
3244
3672
  if (Array.isArray(nodes)) {
3245
3673
  return nodes;
@@ -3278,11 +3706,11 @@ function checkXPathUniqueness(doc, xpathExpr, targetNode) {
3278
3706
  return { isUnique: false };
3279
3707
  }
3280
3708
  }
3281
- function findDOMNodeByPath(doc, path10) {
3282
- if (!path10) {
3709
+ function findDOMNodeByPath(doc, path11) {
3710
+ if (!path11) {
3283
3711
  return doc.documentElement;
3284
3712
  }
3285
- const indices = path10.split(".").map(Number);
3713
+ const indices = path11.split(".").map(Number);
3286
3714
  let current = doc.documentElement;
3287
3715
  for (const index of indices) {
3288
3716
  if (!current) {
@@ -4488,7 +4916,7 @@ function generateAllElementLocators(sourceXML, options) {
4488
4916
  results: [],
4489
4917
  parsedDOM
4490
4918
  };
4491
- traverseTree(sourceJSON, ctx);
4919
+ withXPathIndex(parsedDOM, () => traverseTree(sourceJSON, ctx));
4492
4920
  return ctx.results;
4493
4921
  }
4494
4922
 
@@ -4832,21 +5260,21 @@ async function captureAndAttachVideo(input) {
4832
5260
  import http from "http";
4833
5261
  import https from "https";
4834
5262
  var WEBDRIVER_REQUEST_TIMEOUT_MS = 5e3;
4835
- function basePrefix(path10) {
4836
- if (!path10 || path10 === "/") {
5263
+ function basePrefix(path11) {
5264
+ if (!path11 || path11 === "/") {
4837
5265
  return "";
4838
5266
  }
4839
- return path10.endsWith("/") ? path10.slice(0, -1) : path10;
5267
+ return path11.endsWith("/") ? path11.slice(0, -1) : path11;
4840
5268
  }
4841
5269
  function formatHost(hostname) {
4842
5270
  const isIpv6Literal = hostname.includes(":") && !hostname.startsWith("[");
4843
5271
  return isIpv6Literal ? `[${hostname}]` : hostname;
4844
5272
  }
4845
- function sessionEndpoint(address, sessionId, path10) {
5273
+ function sessionEndpoint(address, sessionId, path11) {
4846
5274
  const protocol = address.protocol ?? "http";
4847
5275
  const prefix = basePrefix(address.path);
4848
5276
  const host = formatHost(address.hostname);
4849
- return `${protocol}://${host}:${address.port}${prefix}/session/${sessionId}/${path10}`;
5277
+ return `${protocol}://${host}:${address.port}${prefix}/session/${sessionId}/${path11}`;
4850
5278
  }
4851
5279
  function authHeaders(address) {
4852
5280
  if (!address.user || !address.key) {
@@ -4930,21 +5358,222 @@ function webdriverRequest(address, endpoint, method, body) {
4930
5358
  req.end();
4931
5359
  });
4932
5360
  }
4933
- function webdriverGet(address, sessionId, path10) {
5361
+ function webdriverGet(address, sessionId, path11) {
4934
5362
  return webdriverRequest(
4935
5363
  address,
4936
- sessionEndpoint(address, sessionId, path10),
5364
+ sessionEndpoint(address, sessionId, path11),
4937
5365
  "GET"
4938
5366
  );
4939
5367
  }
4940
- function webdriverPost(address, sessionId, path10, body) {
5368
+ function webdriverPost(address, sessionId, path11, body) {
4941
5369
  return webdriverRequest(
4942
5370
  address,
4943
- sessionEndpoint(address, sessionId, path10),
5371
+ sessionEndpoint(address, sessionId, path11),
4944
5372
  "POST",
4945
5373
  body
4946
5374
  );
4947
5375
  }
5376
+ function webdriverExecute(address, sessionId, body, args = []) {
5377
+ return webdriverPost(address, sessionId, "execute/sync", {
5378
+ script: body,
5379
+ args
5380
+ });
5381
+ }
5382
+
5383
+ // ../core/src/appium-interceptor.ts
5384
+ var INTERCEPT_CAPABILITY = "appium:intercept";
5385
+ var APPIUM_OPTIONS = "appium:options";
5386
+ var NATIVE_NETWORK_DRAIN_INTERVAL_MS = 2e3;
5387
+ function flattenInterceptedHeaders(headers) {
5388
+ const flat = {};
5389
+ for (const [name, value] of Object.entries(headers ?? {})) {
5390
+ if (/\s/.test(name)) {
5391
+ continue;
5392
+ }
5393
+ flat[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
5394
+ }
5395
+ return flat;
5396
+ }
5397
+ function interceptedBodyText(body) {
5398
+ if (body === void 0 || body === null || body === "") {
5399
+ return void 0;
5400
+ }
5401
+ return typeof body === "string" ? body : JSON.stringify(body);
5402
+ }
5403
+ function interceptedToNetworkRequest(entry, id, observedAt) {
5404
+ const responseHeaders = flattenInterceptedHeaders(entry.responseHeaders);
5405
+ const responseBody = interceptedBodyText(entry.responseBody);
5406
+ return {
5407
+ id,
5408
+ url: entry.url,
5409
+ method: entry.method,
5410
+ status: entry.statusCode,
5411
+ type: getRequestType(entry.url, responseHeaders["content-type"]),
5412
+ timestamp: observedAt.timestamp,
5413
+ startTime: observedAt.startTime,
5414
+ endTime: observedAt.startTime,
5415
+ requestHeaders: flattenInterceptedHeaders(entry.requestHeaders),
5416
+ responseHeaders,
5417
+ response: {
5418
+ fromCache: false,
5419
+ headers: responseHeaders,
5420
+ mimeType: responseHeaders["content-type"]?.split(";")[0]?.trim() ?? "",
5421
+ status: entry.statusCode
5422
+ },
5423
+ requestBody: interceptedBodyText(entry.requestBody),
5424
+ responseBody,
5425
+ size: responseBody === void 0 ? void 0 : Buffer.byteLength(responseBody)
5426
+ };
5427
+ }
5428
+ var InterceptorNetworkPoller = class {
5429
+ #execute;
5430
+ #listenerId;
5431
+ #seen = 0;
5432
+ // Drains are chained: two overlapping reads would both list the entries past the same `#seen`.
5433
+ #tail = Promise.resolve([]);
5434
+ constructor(execute) {
5435
+ this.#execute = execute;
5436
+ }
5437
+ async start() {
5438
+ const id = await this.#execute("interceptor: startListening", [{}]);
5439
+ this.#listenerId = typeof id === "string" ? id : void 0;
5440
+ return this.#listenerId !== void 0;
5441
+ }
5442
+ drain() {
5443
+ this.#tail = this.#tail.catch(() => []).then(() => this.#readFresh());
5444
+ return this.#tail;
5445
+ }
5446
+ async #readFresh() {
5447
+ const listenerId = this.#listenerId;
5448
+ if (!listenerId) {
5449
+ return [];
5450
+ }
5451
+ const data = await this.#execute("interceptor: getInterceptedData", [
5452
+ { id: listenerId }
5453
+ ]);
5454
+ if (!Array.isArray(data)) {
5455
+ throw new Error("the plugin returned no intercepted data");
5456
+ }
5457
+ if (data.length <= this.#seen) {
5458
+ return [];
5459
+ }
5460
+ const observedAt = { timestamp: Date.now(), startTime: performance.now() };
5461
+ const fresh = data.slice(this.#seen).map(
5462
+ (entry, offset) => interceptedToNetworkRequest(
5463
+ entry,
5464
+ `interceptor-${listenerId}-${this.#seen + offset}`,
5465
+ observedAt
5466
+ )
5467
+ );
5468
+ this.#seen = data.length;
5469
+ return fresh;
5470
+ }
5471
+ };
5472
+ function asksForIntercept(caps) {
5473
+ if (!caps || typeof caps !== "object") {
5474
+ return false;
5475
+ }
5476
+ const map = caps;
5477
+ const read = (key) => typeof map.get === "function" ? map.get(key) : map[key];
5478
+ if (read(INTERCEPT_CAPABILITY) === true) {
5479
+ return true;
5480
+ }
5481
+ const grouped = read(APPIUM_OPTIONS);
5482
+ return !!grouped && typeof grouped === "object" && grouped.intercept === true;
5483
+ }
5484
+ function requestsInterceptedNetwork(capabilities) {
5485
+ if (asksForIntercept(capabilities)) {
5486
+ return true;
5487
+ }
5488
+ if (!capabilities || typeof capabilities !== "object") {
5489
+ return false;
5490
+ }
5491
+ const { alwaysMatch, firstMatch } = capabilities;
5492
+ return [alwaysMatch, ...Array.isArray(firstMatch) ? firstMatch : []].some(
5493
+ asksForIntercept
5494
+ );
5495
+ }
5496
+ var NativeNetworkCapture = class {
5497
+ #sink;
5498
+ #onLog;
5499
+ #now;
5500
+ #lastDrainAt = Number.NEGATIVE_INFINITY;
5501
+ #poller;
5502
+ // A start superseded by a later one (session replaced mid-handshake) must not install its poller.
5503
+ #generation = 0;
5504
+ constructor(sink, onLog, now = Date.now) {
5505
+ this.#sink = sink;
5506
+ this.#onLog = onLog;
5507
+ this.#now = now;
5508
+ }
5509
+ /** Called per session: a listener dies with its session. */
5510
+ async start(target) {
5511
+ const generation = ++this.#generation;
5512
+ this.#poller = void 0;
5513
+ if (!target?.requested || !isNativeAppSession(target.capabilities)) {
5514
+ return;
5515
+ }
5516
+ if (!target.execute) {
5517
+ this.#onLog(
5518
+ "warn",
5519
+ `${INTERCEPT_CAPABILITY} is set but ${target.unavailableReason ?? "the driver cannot be reached outside the command path"}; native network is not captured`
5520
+ );
5521
+ return;
5522
+ }
5523
+ const poller = new InterceptorNetworkPoller(target.execute);
5524
+ const started = await poller.start().catch((err) => {
5525
+ this.#onLog(
5526
+ "warn",
5527
+ `appium-interceptor listener failed to start: ${errorMessage(err)}`
5528
+ );
5529
+ return false;
5530
+ });
5531
+ if (generation !== this.#generation) {
5532
+ return;
5533
+ }
5534
+ if (!started) {
5535
+ this.#onLog(
5536
+ "warn",
5537
+ `${INTERCEPT_CAPABILITY} is set but the interceptor listener did not start; is Appium running with --use-plugins=appium-interceptor?`
5538
+ );
5539
+ return;
5540
+ }
5541
+ this.#onLog("info", "Native network capture active via appium-interceptor");
5542
+ this.#poller = poller;
5543
+ this.#lastDrainAt = Number.NEGATIVE_INFINITY;
5544
+ }
5545
+ /** Whether a listener is running, so a caller can skip a drain that cannot find anything. */
5546
+ get active() {
5547
+ return this.#poller !== void 0;
5548
+ }
5549
+ /** At session end, so a drain after the driver quit does not reach a dead session. */
5550
+ stop() {
5551
+ this.#generation++;
5552
+ this.#poller = void 0;
5553
+ }
5554
+ /** Lists what the proxy recorded since the last call. A test or session boundary must not pass `throttle`. */
5555
+ async forward(options = {}) {
5556
+ const poller = this.#poller;
5557
+ if (!poller) {
5558
+ return;
5559
+ }
5560
+ const now = this.#now();
5561
+ if (options.throttle && now - this.#lastDrainAt < NATIVE_NETWORK_DRAIN_INTERVAL_MS) {
5562
+ return;
5563
+ }
5564
+ this.#lastDrainAt = now;
5565
+ const requests = await poller.drain().catch((err) => {
5566
+ this.#onLog(
5567
+ "warn",
5568
+ `Reading appium-interceptor data failed: ${errorMessage(err)}`
5569
+ );
5570
+ return [];
5571
+ });
5572
+ if (requests.length > 0) {
5573
+ this.#sink.recordNetworkRequests(requests);
5574
+ }
5575
+ }
5576
+ };
4948
5577
 
4949
5578
  // ../core/src/viewport.ts
4950
5579
  var VISUAL_VIEWPORT_SCRIPT = `
@@ -4999,10 +5628,60 @@ async function resolveViewport(capabilities, probes) {
4999
5628
  import { createRequire as createRequire3 } from "module";
5000
5629
 
5001
5630
  // ../core/src/stack.ts
5631
+ import { existsSync, readFileSync } from "fs";
5632
+ import path8 from "path";
5633
+ import { fileURLToPath } from "url";
5002
5634
  import { parse as parseStackTrace } from "stacktrace-parser";
5635
+ var WORKSPACE_ROOT_NAME = "devtools-monorepo";
5636
+ function findOwnPackagesDir(fromDir) {
5637
+ for (let dir = fromDir; ; dir = path8.dirname(dir)) {
5638
+ if (existsSync(path8.join(dir, "pnpm-workspace.yaml"))) {
5639
+ return readPackageName(dir) === WORKSPACE_ROOT_NAME ? path8.join(dir, "packages") : void 0;
5640
+ }
5641
+ if (path8.dirname(dir) === dir) {
5642
+ return void 0;
5643
+ }
5644
+ }
5645
+ }
5646
+ function readPackageName(dir) {
5647
+ try {
5648
+ const pkg = JSON.parse(
5649
+ readFileSync(path8.join(dir, "package.json"), "utf8")
5650
+ );
5651
+ return typeof pkg.name === "string" ? pkg.name : void 0;
5652
+ } catch {
5653
+ return void 0;
5654
+ }
5655
+ }
5656
+ var ownPackagesDir;
5657
+ function resolveOwnPackagesDir() {
5658
+ if (ownPackagesDir === void 0) {
5659
+ try {
5660
+ ownPackagesDir = findOwnPackagesDir(path8.dirname(fileURLToPath(import.meta.url))) ?? null;
5661
+ } catch {
5662
+ ownPackagesDir = null;
5663
+ }
5664
+ }
5665
+ return ownPackagesDir ?? void 0;
5666
+ }
5667
+ function isPackageSource(file, packagesDir) {
5668
+ if (!packagesDir) {
5669
+ return false;
5670
+ }
5671
+ try {
5672
+ const filePath = file.startsWith("file://") ? fileURLToPath(file) : file;
5673
+ const relative = path8.relative(packagesDir, filePath);
5674
+ if (relative.startsWith("..") || path8.isAbsolute(relative)) {
5675
+ return false;
5676
+ }
5677
+ return relative.split(path8.sep)[1] === "src";
5678
+ } catch {
5679
+ return false;
5680
+ }
5681
+ }
5003
5682
  function isUserCodeFrame(frame) {
5004
5683
  const { file } = frame;
5005
- return !!(file && !file.includes("/node_modules/") && !file.includes("<anonymous>") && !file.includes("node:internal") && !file.includes("/dist/") && !file.endsWith("/index.js"));
5684
+ return !!(file && !file.includes("/node_modules/") && !file.includes("<anonymous>") && !file.includes("node:internal") && !file.includes("/dist/") && !file.endsWith("/index.js") && !isPackageSource(file, resolveOwnPackagesDir()));
5006
5685
  }
5007
5686
  function normalizeFilePath(filePath) {
5008
5687
  const stripped = filePath.replace(/^file:\/\//, "").split(":")[0];
@@ -5019,16 +5698,24 @@ function isAssertFromUserCode(stack) {
5019
5698
  const caller = parseStackTrace(stack)[1];
5020
5699
  return !!caller && isUserCodeFrame(caller);
5021
5700
  }
5701
+ var NO_CALL_SOURCE = {
5702
+ filePath: void 0,
5703
+ callSource: void 0
5704
+ };
5022
5705
  function getCallSourceFromStack(stack = new Error().stack) {
5023
5706
  if (!stack) {
5024
- return { filePath: void 0, callSource: "unknown:0" };
5707
+ return NO_CALL_SOURCE;
5025
5708
  }
5026
- const frame = parseStackTrace(stack).find(isUserCodeFrame);
5027
- if (!frame?.file) {
5028
- return { filePath: void 0, callSource: "unknown:0" };
5709
+ try {
5710
+ const frame = parseStackTrace(stack).find(isUserCodeFrame);
5711
+ if (!frame?.file) {
5712
+ return NO_CALL_SOURCE;
5713
+ }
5714
+ const filePath = normalizeFilePath(frame.file);
5715
+ return { filePath, callSource: `${filePath}:${frame.lineNumber ?? 0}` };
5716
+ } catch {
5717
+ return NO_CALL_SOURCE;
5029
5718
  }
5030
- const filePath = normalizeFilePath(frame.file);
5031
- return { filePath, callSource: `${filePath}:${frame.lineNumber ?? 0}` };
5032
5719
  }
5033
5720
 
5034
5721
  // ../core/src/assert-patcher.ts
@@ -5250,19 +5937,14 @@ function patchNodeAssert(onCommand, onLog, resolveAssertTarget) {
5250
5937
 
5251
5938
  // ../core/src/script-loader.ts
5252
5939
  import fs8 from "fs/promises";
5253
- import path8 from "path";
5940
+ import path9 from "path";
5254
5941
  import { createRequire as createRequire4 } from "module";
5255
5942
  var require4 = createRequire4(import.meta.url);
5256
- var COLLECTOR_READY_EXPRESSION = 'typeof window.wdioTraceCollector !== "undefined"';
5257
- function collectorDrainExpression(forceAnchor = false) {
5258
- const call = forceAnchor ? "window.wdioTraceCollector.captureCurrentDom();" : "";
5259
- return `if (!(${COLLECTOR_READY_EXPRESSION})) { return null; } ${call} return window.wdioTraceCollector.getTraceData();`;
5260
- }
5261
5943
  function collectorSourceCandidates(entry) {
5262
- const dir = path8.dirname(entry);
5944
+ const dir = path9.dirname(entry);
5263
5945
  return [
5264
- path8.join(dir, "script.js"),
5265
- path8.join(dir, "..", "dist", "script.js")
5946
+ path9.join(dir, "script.js"),
5947
+ path9.join(dir, "..", "dist", "script.js")
5266
5948
  ];
5267
5949
  }
5268
5950
  async function loadCollectorSource() {
@@ -5284,20 +5966,8 @@ async function loadCollectorSource() {
5284
5966
  async function loadInjectableScript() {
5285
5967
  return `(async function() { ${await loadCollectorSource()} })()`;
5286
5968
  }
5287
- var NON_INSTRUMENTABLE_SCHEMES = [
5288
- "about:",
5289
- "data:",
5290
- "chrome:",
5291
- "chrome-error:",
5292
- "edge:",
5293
- "moz-extension:",
5294
- "chrome-extension:"
5295
- ];
5296
5969
  function isInstrumentableDocument(url) {
5297
- if (!url) {
5298
- return false;
5299
- }
5300
- return !NON_INSTRUMENTABLE_SCHEMES.some((scheme) => url.startsWith(scheme));
5970
+ return Boolean(url) && !isBrowserInternalUrl(url) && schemeOf(url) !== "data";
5301
5971
  }
5302
5972
  async function drainCollectorWithRecovery(opts) {
5303
5973
  const payload = await opts.drain();
@@ -5329,6 +5999,9 @@ async function drainCollectorWithRecovery(opts) {
5329
5999
  async function pollUntilReady(check, opts = {}) {
5330
6000
  const attempts = opts.attempts ?? 5;
5331
6001
  const intervalMs = opts.intervalMs ?? 200;
6002
+ if (opts.checkFirst && await check()) {
6003
+ return true;
6004
+ }
5332
6005
  for (let i = 0; i < attempts; i++) {
5333
6006
  await new Promise((resolve) => setTimeout(resolve, intervalMs));
5334
6007
  if (await check()) {
@@ -5402,7 +6075,7 @@ var TerminalLineThrottle = class {
5402
6075
  // ../core/src/finalize-screencast.ts
5403
6076
  import fs9 from "fs";
5404
6077
  import os2 from "os";
5405
- import path9 from "path";
6078
+ import path10 from "path";
5406
6079
  async function finalizeScreencast({
5407
6080
  recorder,
5408
6081
  sessionId,
@@ -5426,12 +6099,12 @@ async function finalizeScreencast({
5426
6099
  }
5427
6100
  const fileName = `${filenamePrefix}-${sessionId}.webm`;
5428
6101
  const candidate = outputDir || process.cwd();
5429
- let videoPath = path9.join(candidate, fileName);
6102
+ let videoPath = path10.join(candidate, fileName);
5430
6103
  try {
5431
6104
  fs9.mkdirSync(candidate, { recursive: true });
5432
6105
  fs9.accessSync(candidate, fs9.constants.W_OK);
5433
6106
  } catch {
5434
- videoPath = path9.join(os2.tmpdir(), fileName);
6107
+ videoPath = path10.join(os2.tmpdir(), fileName);
5435
6108
  }
5436
6109
  try {
5437
6110
  await encodeToVideo(frames, videoPath, { captureFormat });
@@ -5499,54 +6172,9 @@ function isInputDispatchInFlight() {
5499
6172
  }
5500
6173
 
5501
6174
  // ../core/src/performance-capture.ts
5502
- var CAPTURE_PERFORMANCE_SCRIPT = `
5503
- (function() {
5504
- const performance = window.performance;
5505
- const navigation = performance.getEntriesByType?.('navigation')?.[0];
5506
- const resources = performance.getEntriesByType?.('resource') || [];
5507
-
5508
- return {
5509
- navigation: navigation ? {
5510
- url: window.location.href,
5511
- timing: {
5512
- loadTime: navigation.loadEventEnd - navigation.fetchStart,
5513
- domReady: navigation.domContentLoadedEventEnd - navigation.fetchStart,
5514
- responseTime: navigation.responseEnd - navigation.requestStart,
5515
- dnsLookup: navigation.domainLookupEnd - navigation.domainLookupStart,
5516
- tcpConnection: navigation.connectEnd - navigation.connectStart,
5517
- serverResponse: navigation.responseEnd - navigation.responseStart
5518
- }
5519
- } : undefined,
5520
- resources: resources.map(function(resource) {
5521
- return {
5522
- url: resource.name,
5523
- duration: resource.duration,
5524
- size: resource.transferSize || 0,
5525
- type: resource.initiatorType,
5526
- startTime: resource.startTime,
5527
- responseEnd: resource.responseEnd
5528
- };
5529
- }),
5530
- cookies: (function() {
5531
- try { return document.cookie; } catch (e) { return ''; }
5532
- })(),
5533
- documentInfo: {
5534
- url: window.location.href,
5535
- title: document.title,
5536
- headers: {
5537
- userAgent: navigator.userAgent,
5538
- language: navigator.language,
5539
- platform: navigator.platform
5540
- },
5541
- documentInfo: {
5542
- readyState: document.readyState,
5543
- referrer: document.referrer,
5544
- characterSet: document.characterSet
5545
- }
5546
- }
5547
- };
5548
- })()
5549
- `;
6175
+ function formatMilliseconds(ms) {
6176
+ return typeof ms === "number" && Number.isFinite(ms) ? `${ms.toFixed(2)} ms` : void 0;
6177
+ }
5550
6178
  function applyPerformanceData(command, payload, navigatedUrl) {
5551
6179
  if (!payload || !payload.navigation) {
5552
6180
  return false;
@@ -5559,7 +6187,7 @@ function applyPerformanceData(command, payload, navigatedUrl) {
5559
6187
  command.documentInfo = payload.documentInfo;
5560
6188
  command.result = {
5561
6189
  url: navigatedUrl,
5562
- loadTime: payload.navigation?.timing?.loadTime,
6190
+ loadTime: formatMilliseconds(payload.navigation?.timing?.loadTime),
5563
6191
  resources: payload.resources,
5564
6192
  resourceCount: payload.resources?.length,
5565
6193
  cookies: payload.cookies,
@@ -5591,14 +6219,17 @@ function rememberReadValue(value, selector) {
5591
6219
  }
5592
6220
  }
5593
6221
  }
5594
- function selectorForReadValue(value) {
5595
- return selectorByReadValue.get(value) ?? void 0;
6222
+ function clearReadValues() {
6223
+ selectorByReadValue.clear();
5596
6224
  }
5597
6225
  function resolveAssertTargetFromArgs(args, fromHandle) {
5598
6226
  for (const arg of args) {
5599
- const selector = fromHandle?.(arg) ?? selectorForReadValue(arg);
5600
- if (selector) {
5601
- return selector;
6227
+ const handle = fromHandle?.(arg);
6228
+ if (handle) {
6229
+ return handle;
6230
+ }
6231
+ if (selectorByReadValue.has(arg)) {
6232
+ return selectorByReadValue.get(arg) ?? void 0;
5602
6233
  }
5603
6234
  }
5604
6235
  return void 0;
@@ -5919,7 +6550,33 @@ var ScreencastRecorderBase = class {
5919
6550
  }
5920
6551
  };
5921
6552
 
6553
+ // ../core/src/browser-focus.ts
6554
+ import { spawn } from "child_process";
6555
+ var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
6556
+ function restoreBrowserFocus(target, platform = process.platform, launch = spawn) {
6557
+ const local = !target.host || LOCAL_HOSTS.has(target.host);
6558
+ if (platform !== "darwin" || !local || !/safari/i.test(target.browserName ?? "") || /ios/i.test(target.platformName ?? "")) {
6559
+ return false;
6560
+ }
6561
+ try {
6562
+ const child = launch("open", ["-a", "Safari"], {
6563
+ detached: true,
6564
+ stdio: "ignore"
6565
+ });
6566
+ child.on?.(
6567
+ "error",
6568
+ (err) => target.onError?.(`Could not bring Safari to the front: ${err.message}`)
6569
+ );
6570
+ child.unref?.();
6571
+ return true;
6572
+ } catch (err) {
6573
+ target.onError?.(`Could not bring Safari to the front: ${String(err)}`);
6574
+ return false;
6575
+ }
6576
+ }
6577
+
5922
6578
  // ../core/src/session-capturer.ts
6579
+ import { randomInt } from "crypto";
5923
6580
  import fs10 from "fs/promises";
5924
6581
  import { WebSocket } from "ws";
5925
6582
  var SessionCapturerBase = class _SessionCapturerBase {
@@ -5952,7 +6609,8 @@ var SessionCapturerBase = class _SessionCapturerBase {
5952
6609
  // (WDIO owns the command lifecycle), so this state is harmless overhead.
5953
6610
  // `protected` (not `#`) so subclasses can override the send/replace flow
5954
6611
  // while still sharing the counter and de-dup set with base helpers.
5955
- commandCounter = 0;
6612
+ // A row's public `id` must not repeat across processes: a rerun child's rows join its parent's in the backend's accumulator, and test workers share one dashboard.
6613
+ commandCounter = randomInt(2 ** 32) * 2 ** 20;
5956
6614
  sentCommandIds = /* @__PURE__ */ new Set();
5957
6615
  // Map of file path → source text. Populated by `captureSource` (also
5958
6616
  // accessed by adapter-specific source-discovery flows, e.g. service's
@@ -6267,26 +6925,24 @@ var SessionCapturerBase = class _SessionCapturerBase {
6267
6925
  *
6268
6926
  * `skipConsoleLogs` / `skipNetworkRequests` opt out when an out-of-band
6269
6927
  * channel (BiDi) is already delivering those streams — without the gate
6270
- * the dashboard would see each entry twice.
6928
+ * the dashboard would see each entry twice. `skipResourceTimingRequests`
6929
+ * drops only the page's Resource Timing entries: a driver-side source
6930
+ * reports the same loads with their headers, while the collector's patched
6931
+ * fetch/XHR rows still carry the bodies nothing else captures.
6271
6932
  */
6272
6933
  processTracePayload(payload, opts = {}) {
6273
6934
  const { mutations, traceLogs, consoleLogs, networkRequests, metadata } = payload;
6274
6935
  if (metadata && typeof metadata === "object") {
6275
6936
  this.mergeMetadata(metadata);
6276
6937
  }
6277
- if (!opts.skipConsoleLogs && Array.isArray(consoleLogs) && consoleLogs.length > 0) {
6278
- const tagged = consoleLogs.map((entry) => ({
6279
- ...entry,
6280
- source: LOG_SOURCES.BROWSER
6281
- }));
6938
+ const tagged = opts.skipConsoleLogs ? [] : this.#tagBrowserConsoleLogs(consoleLogs);
6939
+ if (tagged.length > 0) {
6282
6940
  this.consoleLogs.push(...tagged);
6283
6941
  this.sendUpstream("consoleLogs", tagged);
6284
6942
  }
6285
- if (!opts.skipNetworkRequests && Array.isArray(networkRequests) && networkRequests.length > 0) {
6286
- const reqs = networkRequests;
6287
- this.networkRequests.push(...reqs);
6288
- this.sendUpstream("networkRequests", reqs);
6289
- }
6943
+ this.recordNetworkRequests(
6944
+ this.#acceptedNetworkRequests(networkRequests, opts)
6945
+ );
6290
6946
  if (Array.isArray(mutations) && mutations.length > 0) {
6291
6947
  const batch = mutations;
6292
6948
  reattributeDomAnchors(
@@ -6303,6 +6959,50 @@ var SessionCapturerBase = class _SessionCapturerBase {
6303
6959
  this.sendUpstream("logs", logs);
6304
6960
  }
6305
6961
  }
6962
+ #tagBrowserConsoleLogs(consoleLogs) {
6963
+ if (!Array.isArray(consoleLogs)) {
6964
+ return [];
6965
+ }
6966
+ return consoleLogs.map((entry) => ({
6967
+ ...entry,
6968
+ source: LOG_SOURCES.BROWSER
6969
+ }));
6970
+ }
6971
+ /** Lists network rows and sends them in one message; a re-sent id replaces its row, as the dashboard does. */
6972
+ recordNetworkRequests(reqs) {
6973
+ if (reqs.length === 0) {
6974
+ return;
6975
+ }
6976
+ for (const req of reqs) {
6977
+ this.#upsertNetworkRequest(req);
6978
+ }
6979
+ this.sendUpstream("networkRequests", reqs);
6980
+ }
6981
+ /** Row index by request id; checked against the row before use, since adapters also write rows by index. */
6982
+ #networkRequestIndex = /* @__PURE__ */ new Map();
6983
+ #upsertNetworkRequest(req) {
6984
+ let at = req.id ? this.#networkRequestIndex.get(req.id) : void 0;
6985
+ if (at !== void 0 && this.networkRequests[at]?.id !== req.id) {
6986
+ at = this.networkRequests.findIndex((r) => r.id === req.id);
6987
+ }
6988
+ if (at !== void 0 && at !== -1) {
6989
+ this.networkRequests[at] = req;
6990
+ this.#networkRequestIndex.set(req.id, at);
6991
+ return;
6992
+ }
6993
+ if (req.id) {
6994
+ this.#networkRequestIndex.set(req.id, this.networkRequests.length);
6995
+ }
6996
+ this.networkRequests.push(req);
6997
+ }
6998
+ #acceptedNetworkRequests(networkRequests, opts) {
6999
+ if (opts.skipNetworkRequests || !Array.isArray(networkRequests)) {
7000
+ return [];
7001
+ }
7002
+ return networkRequests.filter(
7003
+ (req) => !opts.skipResourceTimingRequests || req.source !== NETWORK_CAPTURE_SOURCE.RESOURCE_TIMING
7004
+ );
7005
+ }
6306
7006
  /**
6307
7007
  * Resolve when the WS reaches OPEN state, or `false` on timeout / error.
6308
7008
  * Returns immediately if already open. Used by adapters that need a
@@ -6711,11 +7411,20 @@ var TestReporterBase = class {
6711
7411
 
6712
7412
  export {
6713
7413
  mapCommandToAction,
7414
+ isWaitCommand,
6714
7415
  stripAnsi,
6715
- mapChromeBrowserLogs,
7416
+ COLLECTOR_READY_SCRIPT,
7417
+ COLLECTOR_INJECT_SCRIPT,
7418
+ collectorDrainExpression,
7419
+ CAPTURE_PERFORMANCE_SCRIPT,
7420
+ commandFailure,
7421
+ getRequestType,
6716
7422
  WS_SCOPE,
6717
7423
  REUSE_ENV,
7424
+ RUNNER_ENV,
7425
+ encodeInitialArgs,
6718
7426
  TEST_STATE,
7427
+ NETWORK_CAPTURE_SOURCE,
6719
7428
  SCREENCAST_DEFAULTS,
6720
7429
  captureActionSnapshot,
6721
7430
  toError,
@@ -6728,6 +7437,9 @@ export {
6728
7437
  TestAttemptTracker,
6729
7438
  webdriverGet,
6730
7439
  webdriverPost,
7440
+ webdriverExecute,
7441
+ requestsInterceptedNetwork,
7442
+ NativeNetworkCapture,
6731
7443
  resolveViewport,
6732
7444
  isUserCodeFrame,
6733
7445
  normalizeFilePath,
@@ -6744,11 +7456,8 @@ export {
6744
7456
  recordSpecBoundary,
6745
7457
  flushRangeLogged,
6746
7458
  finalizeTraceExport,
6747
- getRequestType,
6748
7459
  findFreePort,
6749
7460
  attachBidiHandlers,
6750
- COLLECTOR_READY_EXPRESSION,
6751
- collectorDrainExpression,
6752
7461
  loadInjectableScript,
6753
7462
  drainCollectorWithRecovery,
6754
7463
  pollUntilReady,
@@ -6756,12 +7465,13 @@ export {
6756
7465
  finalizeScreencast,
6757
7466
  beginInputDispatch,
6758
7467
  resolveAdapterOutputDir,
6759
- CAPTURE_PERFORMANCE_SCRIPT,
6760
7468
  applyPerformanceData,
6761
7469
  rememberReadValue,
7470
+ clearReadValues,
6762
7471
  resolveAssertTargetFromArgs,
6763
7472
  RetryTracker,
6764
7473
  ScreencastRecorderBase,
7474
+ restoreBrowserFocus,
6765
7475
  SessionCapturerBase,
6766
7476
  createSuiteStats,
6767
7477
  createTestStats,
@@ -6773,4 +7483,4 @@ export {
6773
7483
  findTestDefinitions,
6774
7484
  TestReporterBase
6775
7485
  };
6776
- //# sourceMappingURL=chunk-DM4FCAIJ.js.map
7486
+ //# sourceMappingURL=chunk-DRZC45YG.js.map