@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.
package/dist/index.js CHANGED
@@ -1,7 +1,11 @@
1
1
  import {
2
2
  CAPTURE_PERFORMANCE_SCRIPT,
3
- COLLECTOR_READY_EXPRESSION,
3
+ COLLECTOR_INJECT_SCRIPT,
4
+ COLLECTOR_READY_SCRIPT,
5
+ NETWORK_CAPTURE_SOURCE,
6
+ NativeNetworkCapture,
4
7
  REUSE_ENV,
8
+ RUNNER_ENV,
5
9
  RetryTracker,
6
10
  SCREENCAST_DEFAULTS,
7
11
  ScreencastRecorderBase,
@@ -16,14 +20,17 @@ import {
16
20
  captureActionSnapshot,
17
21
  captureAndAttachScreenshot,
18
22
  captureAndAttachVideo,
23
+ clearReadValues,
19
24
  collectSuiteTestMetadata,
20
25
  collectorDrainExpression,
26
+ commandFailure,
21
27
  computeSuiteFinalStateStrict,
22
28
  computeSuiteRunningState,
23
29
  createSuiteStats,
24
30
  createTestStats,
25
31
  deterministicUid,
26
32
  drainCollectorWithRecovery,
33
+ encodeInitialArgs,
27
34
  errorMessage,
28
35
  finalizeScreencast,
29
36
  finalizeTraceExport,
@@ -35,9 +42,9 @@ import {
35
42
  getCallSourceFromStack,
36
43
  getRequestType,
37
44
  isUserCodeFrame,
45
+ isWaitCommand,
38
46
  lastRenderedScreenshot,
39
47
  loadInjectableScript,
40
- mapChromeBrowserLogs,
41
48
  mapCommandToAction,
42
49
  matcherAssertionToCommandLog,
43
50
  normalizeFilePath,
@@ -47,9 +54,11 @@ import {
47
54
  recordSpecBoundary,
48
55
  registerCollectorPreload,
49
56
  rememberReadValue,
57
+ requestsInterceptedNetwork,
50
58
  resolveAdapterOutputDir,
51
59
  resolveAssertTargetFromArgs,
52
60
  resolveViewport,
61
+ restoreBrowserFocus,
53
62
  safeSerializeAssertArg,
54
63
  serializeError,
55
64
  stampSuiteEnd,
@@ -58,9 +67,10 @@ import {
58
67
  toError,
59
68
  tracePolicyModeWarning,
60
69
  upsertRichestSnapshot,
70
+ webdriverExecute,
61
71
  webdriverGet,
62
72
  webdriverPost
63
- } from "./chunk-DM4FCAIJ.js";
73
+ } from "./chunk-DRZC45YG.js";
64
74
 
65
75
  // src/index.ts
66
76
  import { fileURLToPath } from "url";
@@ -164,14 +174,18 @@ async function emitTestArtifacts(input) {
164
174
  // src/helpers/assertCapture.ts
165
175
  import logger from "@wdio/logger";
166
176
  var log = logger("@wdio/nightwatch-devtools:assertCapture");
167
- function wireAssertCapture(getCapturer, getTestUid) {
177
+ function wireAssertCapture(getCapturer, getTestUid, scopeReadValues = () => {
178
+ }) {
168
179
  patchNodeAssert(
169
180
  (cmd) => captureAssert(getCapturer(), getTestUid(), cmd),
170
181
  (level, message) => log[level](message),
171
182
  // No handle resolver: Nightwatch commands take selector strings, so an
172
183
  // element object never reaches an assert — only the value's provenance,
173
184
  // recorded by the command path (BrowserProxy), can name the target.
174
- (args) => resolveAssertTargetFromArgs(args)
185
+ (args) => {
186
+ scopeReadValues();
187
+ return resolveAssertTargetFromArgs(args);
188
+ }
175
189
  );
176
190
  }
177
191
  function captureAssert(capturer, testUid, cmd) {
@@ -196,7 +210,7 @@ function captureAssert(capturer, testUid, cmd) {
196
210
 
197
211
  // src/index.ts
198
212
  import { stop as stopBackend } from "@wdio/devtools-backend";
199
- import logger15 from "@wdio/logger";
213
+ import logger16 from "@wdio/logger";
200
214
 
201
215
  // src/run-lifecycle.ts
202
216
  import * as fs2 from "fs";
@@ -253,6 +267,7 @@ var TIMING = {
253
267
  };
254
268
  var BOOLEAN_COMMAND_PATTERN = /^waitFor|^is[A-Z]|^has[A-Z]|(Visible|Present|Enabled|Selected|NotVisible|NotPresent)$/;
255
269
  var NAVIGATION_COMMANDS = ["url", "navigate", "navigateTo"];
270
+ var NIGHTWATCH_CONFIG_FLAGS = ["--config", "-c"];
256
271
  var CONFIG_FILENAMES = [
257
272
  "nightwatch.conf.cjs",
258
273
  "nightwatch.conf.js",
@@ -266,10 +281,13 @@ import * as fs from "fs";
266
281
  import * as path from "path";
267
282
  import { parse as parseStackTrace } from "stacktrace-parser";
268
283
  function determineTestState(testcase) {
269
- if (testcase.passed === 0 && testcase.failed === 0) {
284
+ if (testcase.failed > 0 || testcase.errors > 0) {
285
+ return "failed";
286
+ }
287
+ if (testcase.passed === 0 && testcase.skipped > 0) {
270
288
  return "skipped";
271
289
  }
272
- return testcase.passed > 0 && testcase.failed === 0 ? "passed" : "failed";
290
+ return "passed";
273
291
  }
274
292
  function getTestIcon(state) {
275
293
  return state === "passed" ? "\u2705" : state === "skipped" ? "\u23ED" : "\u274C";
@@ -290,13 +308,53 @@ function buildPluginMetadataOptions(input) {
290
308
  return {
291
309
  framework: nightwatchRunnerId(input.isCucumberRunner),
292
310
  configFile: input.configPath,
293
- baseDir: process.cwd(),
294
- runCapabilities: {
295
- canRunSuites: true,
296
- canRunTests: !input.isCucumberRunner,
297
- canRunAll: false
298
- }
311
+ baseDir: process.cwd()
312
+ };
313
+ }
314
+ function isNightwatchCli(entry) {
315
+ if (!entry) {
316
+ return false;
317
+ }
318
+ try {
319
+ const script = fs.realpathSync(entry);
320
+ const pkgDir = path.dirname(path.dirname(script));
321
+ const pkg = JSON.parse(
322
+ fs.readFileSync(path.join(pkgDir, "package.json"), "utf8")
323
+ );
324
+ return path.basename(path.dirname(script)) === "bin" && pkg.name === "nightwatch";
325
+ } catch {
326
+ return false;
327
+ }
328
+ }
329
+ function captureNightwatchSelection(argv = process.argv, cwd = process.cwd()) {
330
+ if (!isNightwatchCli(argv[1])) {
331
+ return void 0;
332
+ }
333
+ const spec = (arg) => {
334
+ const local = path.resolve(cwd, arg);
335
+ return fs.existsSync(local) ? local : arg;
299
336
  };
337
+ const args = [];
338
+ const rest = argv.slice(2);
339
+ for (let i = 0; i < rest.length; i++) {
340
+ const arg = rest[i];
341
+ if (arg === "--") {
342
+ args.push(arg, ...rest.slice(i + 1).map(spec));
343
+ break;
344
+ }
345
+ if (!arg.startsWith("-")) {
346
+ args.push(spec(arg));
347
+ continue;
348
+ }
349
+ const next = rest[i + 1];
350
+ const takesNext = !arg.includes("=") && !arg.startsWith("--no-") && next !== void 0 && !next.startsWith("-");
351
+ const value = takesNext ? rest[++i] : void 0;
352
+ if (NIGHTWATCH_CONFIG_FLAGS.includes(arg.split("=")[0])) {
353
+ continue;
354
+ }
355
+ args.push(arg, ...value === void 0 ? [] : [value]);
356
+ }
357
+ return args.length ? args : void 0;
300
358
  }
301
359
  function generateStableUid2(itemOrFile, name) {
302
360
  let file, testName;
@@ -517,6 +575,13 @@ async function runPluginBefore(ctx) {
517
575
  ctx.options.port,
518
576
  ctx.options.hostname
519
577
  );
578
+ const selection = captureNightwatchSelection();
579
+ if (selection) {
580
+ process.env[RUNNER_ENV.NIGHTWATCH_INITIAL_ARGS] = encodeInitialArgs(selection);
581
+ } else {
582
+ delete process.env[RUNNER_ENV.NIGHTWATCH_INITIAL_ARGS];
583
+ }
584
+ process.env[RUNNER_ENV.RUNNER_CWD] ??= process.cwd();
520
585
  log2.info("\u{1F680} Starting DevTools backend...");
521
586
  const { port } = await start(ctx.options);
522
587
  ctx.options.port = port;
@@ -634,7 +699,7 @@ async function waitForDevtoolsBrowserClose(ctx) {
634
699
  }
635
700
 
636
701
  // src/session-init.ts
637
- import logger10 from "@wdio/logger";
702
+ import logger11 from "@wdio/logger";
638
703
 
639
704
  // src/helpers/webdriverHttp.ts
640
705
  import logger3 from "@wdio/logger";
@@ -654,7 +719,7 @@ var firstDefined = (obj, ...keys) => {
654
719
  }
655
720
  return void 0;
656
721
  };
657
- function resolveWebDriverAddress(browser) {
722
+ function configuredWebDriver(browser) {
658
723
  const transportSettings = getPath(browser, "transport", "settings", "webdriver") || getPath(browser, "queue", "transport", "settings", "webdriver") || getPath(
659
724
  browser,
660
725
  "nightwatchInstance",
@@ -664,22 +729,47 @@ function resolveWebDriverAddress(browser) {
664
729
  ) || {};
665
730
  const opts = getProp(browser, "options") ?? {};
666
731
  const nightwatchSettings = getPath(browser, "nightwatchInstance", "settings") || getPath(browser, "globals", "nightwatchInstance", "settings") || {};
667
- const driverHost = String(
668
- firstDefined(transportSettings, "host", "server_address") || firstDefined(getProp(opts, "webdriver"), "host") || firstDefined(getProp(nightwatchSettings, "webdriver"), "host") || "localhost"
669
- );
670
- const driverPort = Number(
671
- firstDefined(transportSettings, "port") || firstDefined(getProp(opts, "webdriver"), "port") || firstDefined(getProp(nightwatchSettings, "webdriver"), "port") || 9515
672
- );
673
- return { driverHost, driverPort };
732
+ const sources = [
733
+ transportSettings,
734
+ getProp(opts, "webdriver"),
735
+ getProp(nightwatchSettings, "webdriver")
736
+ ];
737
+ const values = (...keys) => sources.map((source) => firstDefined(source, ...keys));
738
+ const pick = (...keys) => values(...keys).find(Boolean);
739
+ return {
740
+ host: pick("host", "server_address"),
741
+ port: pick("port"),
742
+ pathPrefix: pick("default_path_prefix"),
743
+ // `ssl: false` is a setting, not an absence, so it must not fall through to a later source.
744
+ ssl: values("ssl").find((v) => v !== void 0),
745
+ username: pick("username"),
746
+ accessKey: values("access_key").find((v) => v !== void 0)
747
+ };
674
748
  }
675
- function address(browser) {
676
- const { driverHost, driverPort } = resolveWebDriverAddress(browser);
749
+ function connection(configured, hostname, port) {
750
+ const { pathPrefix, ssl, username, accessKey } = configured;
751
+ const secure = ssl === void 0 ? port === 443 : Boolean(ssl);
752
+ const hasCredentials = Boolean(username) && accessKey !== void 0;
677
753
  return {
678
- hostname: driverHost,
679
- port: driverPort,
754
+ hostname,
755
+ port,
756
+ protocol: secure ? "https" : "http",
757
+ path: typeof pathPrefix === "string" ? pathPrefix : void 0,
758
+ ...hasCredentials ? { user: String(username), key: String(accessKey) } : {},
680
759
  onWarn: (message) => log3.warn(message)
681
760
  };
682
761
  }
762
+ function resolveWebDriverAddress(browser) {
763
+ const { host, port } = configuredWebDriver(browser);
764
+ return {
765
+ driverHost: String(host || "localhost"),
766
+ driverPort: Number(port || 9515)
767
+ };
768
+ }
769
+ function address(browser) {
770
+ const { driverHost, driverPort } = resolveWebDriverAddress(browser);
771
+ return connection(configuredWebDriver(browser), driverHost, driverPort);
772
+ }
683
773
  function sessionId(browser) {
684
774
  return browser.sessionId;
685
775
  }
@@ -691,8 +781,21 @@ function webdriverPost2(browser, path5, body) {
691
781
  const id = sessionId(browser);
692
782
  return id ? webdriverPost(address(browser), id, path5, body) : Promise.resolve(null);
693
783
  }
694
- function webdriverExecute(browser, body) {
695
- return webdriverPost2(browser, "execute/sync", { script: body, args: [] });
784
+ function sessionExecutor(browser) {
785
+ const id = sessionId(browser);
786
+ const configured = configuredWebDriver(browser);
787
+ if (!id || !configured.port) {
788
+ return void 0;
789
+ }
790
+ const at = connection(
791
+ configured,
792
+ String(configured.host || "localhost"),
793
+ Number(configured.port)
794
+ );
795
+ return (script, args) => webdriverExecute(at, id, script, args);
796
+ }
797
+ function webdriverExecute2(browser, body, args = []) {
798
+ return webdriverPost2(browser, "execute/sync", { script: body, args });
696
799
  }
697
800
 
698
801
  // src/session.ts
@@ -706,7 +809,7 @@ function captureActionSnapshot2(browser, command, timestamp, runner, native = fa
706
809
  runner,
707
810
  takeScreenshot: () => webdriverGet2(browser, "screenshot"),
708
811
  ...native ? {} : {
709
- runScript: (src) => webdriverExecute(browser, `return (${src})`),
812
+ runScript: (src) => webdriverExecute2(browser, `return (${src})`),
710
813
  getUrl: () => webdriverGet2(browser, "url"),
711
814
  getTitle: () => webdriverGet2(browser, "title")
712
815
  }
@@ -714,6 +817,7 @@ function captureActionSnapshot2(browser, command, timestamp, runner, native = fa
714
817
  }
715
818
 
716
819
  // src/helpers/perfLogs.ts
820
+ var RESOURCE_TIMING_MATCH_MS = 2e3;
717
821
  function applyResponseReceived(p, response) {
718
822
  const responseHeaders = {};
719
823
  for (const [k, v] of Object.entries(response.headers || {})) {
@@ -739,7 +843,9 @@ function startPendingRequest(params, entry, pending) {
739
843
  url: req.url,
740
844
  method: req.method,
741
845
  requestHeaders: req.headers ?? {},
742
- timestamp: startTime,
846
+ // Epoch, from the event's own wall clock: every consumer dates a row by
847
+ // `timestamp`, and the monotonic `startTime` only diffs within a session.
848
+ timestamp: typeof params.wallTime === "number" ? Math.round(params.wallTime * 1e3) : entry.timestamp,
743
849
  startTime
744
850
  });
745
851
  }
@@ -797,6 +903,95 @@ function parseNetworkFromPerfLogs(logs) {
797
903
  }
798
904
  return completed;
799
905
  }
906
+ function resourceTimingRowsByUrl(rows) {
907
+ const byUrl = /* @__PURE__ */ new Map();
908
+ rows.forEach((row, index) => {
909
+ if (row.source !== NETWORK_CAPTURE_SOURCE.RESOURCE_TIMING) {
910
+ return;
911
+ }
912
+ const indexes = byUrl.get(row.url);
913
+ if (indexes) {
914
+ indexes.push(index);
915
+ } else {
916
+ byUrl.set(row.url, [index]);
917
+ }
918
+ });
919
+ return byUrl;
920
+ }
921
+ function takeNearestResourceTimingRow(indexes, rows, at) {
922
+ let best;
923
+ let bestDelta = RESOURCE_TIMING_MATCH_MS;
924
+ for (const index of indexes) {
925
+ const delta = Math.abs(rows[index].timestamp - at);
926
+ if (delta <= bestDelta) {
927
+ best = index;
928
+ bestDelta = delta;
929
+ }
930
+ }
931
+ if (best !== void 0) {
932
+ indexes.splice(indexes.indexOf(best), 1);
933
+ }
934
+ return best;
935
+ }
936
+ function overlay(row, entry) {
937
+ const merged = {
938
+ ...row,
939
+ method: entry.method,
940
+ requestHeaders: entry.requestHeaders,
941
+ type: entry.type ?? row.type,
942
+ ...entry.status !== void 0 ? { status: entry.status } : {},
943
+ ...entry.statusText !== void 0 ? { statusText: entry.statusText } : {},
944
+ ...entry.responseHeaders ? { responseHeaders: entry.responseHeaders } : {},
945
+ ...entry.size !== void 0 ? { size: entry.size } : {},
946
+ ...entry.error !== void 0 ? { error: entry.error } : {}
947
+ };
948
+ delete merged.source;
949
+ return merged;
950
+ }
951
+ function withoutLoadsTheLogListed(payload, listed) {
952
+ const requests = payload.networkRequests;
953
+ if (!Array.isArray(requests) || listed.length === 0) {
954
+ return payload;
955
+ }
956
+ const rows = requests;
957
+ const rowsByUrl = resourceTimingRowsByUrl(rows);
958
+ const taken = /* @__PURE__ */ new Set();
959
+ for (const load of listed) {
960
+ const index = takeNearestResourceTimingRow(
961
+ rowsByUrl.get(load.url) ?? [],
962
+ rows,
963
+ load.timestamp
964
+ );
965
+ if (index !== void 0) {
966
+ taken.add(index);
967
+ }
968
+ }
969
+ if (taken.size === 0) {
970
+ return payload;
971
+ }
972
+ return {
973
+ ...payload,
974
+ networkRequests: rows.filter((_, index) => !taken.has(index))
975
+ };
976
+ }
977
+ function upgradeResourceTimingRows(incoming, existing) {
978
+ const rowsByUrl = resourceTimingRowsByUrl(existing);
979
+ const upgrades = [];
980
+ const fresh = [];
981
+ for (const entry of incoming) {
982
+ const index = takeNearestResourceTimingRow(
983
+ rowsByUrl.get(entry.url) ?? [],
984
+ existing,
985
+ entry.timestamp
986
+ );
987
+ if (index === void 0) {
988
+ fresh.push(entry);
989
+ continue;
990
+ }
991
+ upgrades.push({ index, request: overlay(existing[index], entry) });
992
+ }
993
+ return { upgrades, fresh };
994
+ }
800
995
  function dedupeNetworkRequests(incoming, existing) {
801
996
  const failedKey = (entry) => {
802
997
  try {
@@ -838,17 +1033,14 @@ function dedupeNetworkRequests(incoming, existing) {
838
1033
 
839
1034
  // src/session.ts
840
1035
  var log4 = logger4("@wdio/nightwatch-devtools:SessionCapturer");
841
- function unwrapDriverValue(result) {
842
- if (result && typeof result === "object" && "value" in result) {
843
- return result.value;
844
- }
845
- return result;
846
- }
847
1036
  var SessionCapturer = class extends SessionCapturerBase {
848
1037
  #browser;
849
1038
  // True once BiDi inspectors are attached — the per-command perf-log network
850
1039
  // capture path skips when set, so we don't double-emit network requests.
851
1040
  bidiActive = false;
1041
+ /** Whether BiDi has delivered at least one network event; a subscription
1042
+ * that delivers nothing is not a source. */
1043
+ bidiNetworkObserved = false;
852
1044
  /** Whether the session was created with `goog:loggingPrefs.performance`, set
853
1045
  * from the capabilities at session init. A perf log exists only because a
854
1046
  * session asked for one, so without it every fetch is a round trip that can
@@ -857,6 +1049,10 @@ var SessionCapturer = class extends SessionCapturerBase {
857
1049
  * 21 of them in a two-test run, where XCUITest lists `performance` among its
858
1050
  * supported types and then serves none of it. */
859
1051
  perfLogsRequested = false;
1052
+ /** Loads the performance log listed before the page's drain could, spent
1053
+ * at the next drain that reaches the driver, whatever it finds, and
1054
+ * emptied with the session that listed them. */
1055
+ loadsListedByLog = [];
860
1056
  /** True once the collector is registered to run at document-start. Every
861
1057
  * document then instruments and anchors itself, so the paths that exist to
862
1058
  * notice a navigation after the fact — re-injection and the settle poll — have
@@ -869,6 +1065,11 @@ var SessionCapturer = class extends SessionCapturerBase {
869
1065
  runner = nightwatchRunnerId(false);
870
1066
  actionSnapshots = [];
871
1067
  snapshotCaptures = [];
1068
+ /** Native app network from appium-interceptor's proxy; started per session. */
1069
+ nativeNetwork = new NativeNetworkCapture(
1070
+ this,
1071
+ (level, message) => log4[level](message)
1072
+ );
872
1073
  constructor(devtoolsOptions = {}, browser) {
873
1074
  super(devtoolsOptions);
874
1075
  this.#browser = browser;
@@ -895,6 +1096,7 @@ var SessionCapturer = class extends SessionCapturerBase {
895
1096
  const commandId = this.commandCounter++;
896
1097
  const commandLogEntry = {
897
1098
  _id: commandId,
1099
+ id: commandId,
898
1100
  command,
899
1101
  args,
900
1102
  result,
@@ -904,7 +1106,8 @@ var SessionCapturer = class extends SessionCapturerBase {
904
1106
  sequence: meta.sequence,
905
1107
  callSource: meta.callSource,
906
1108
  testUid: meta.testUid,
907
- selector: meta.selector
1109
+ selector: meta.selector,
1110
+ parentId: meta.parentId
908
1111
  };
909
1112
  this.commandsLog.push(commandLogEntry);
910
1113
  const isNavigationCommand = NAVIGATION_COMMANDS.some(
@@ -947,15 +1150,18 @@ var SessionCapturer = class extends SessionCapturerBase {
947
1150
  return;
948
1151
  }
949
1152
  await new Promise((resolve2) => setTimeout(resolve2, 500));
950
- const raw = await this.#browser.execute(CAPTURE_PERFORMANCE_SCRIPT);
951
- const payload = unwrapDriverValue(
952
- raw
1153
+ const payload = await webdriverExecute2(
1154
+ this.#browser,
1155
+ CAPTURE_PERFORMANCE_SCRIPT
953
1156
  );
954
- applyPerformanceData(
1157
+ if (applyPerformanceData(
955
1158
  commandLogEntry,
956
- payload,
1159
+ payload ?? void 0,
957
1160
  typeof args[0] === "string" ? args[0] : void 0
958
- );
1161
+ ) && // A retry may have replaced the row meanwhile; replacing it again would bring the old row back.
1162
+ this.commandsLog.includes(commandLogEntry)) {
1163
+ this.sendReplaceCommand(commandLogEntry.timestamp, commandLogEntry);
1164
+ }
959
1165
  }
960
1166
  /**
961
1167
  * Ingest a pre-built assertion entry (native `browser.assert`/`verify`
@@ -985,7 +1191,8 @@ var SessionCapturer = class extends SessionCapturerBase {
985
1191
  const idx = this.commandsLog.findIndex(
986
1192
  (c) => c._id === oldId
987
1193
  );
988
- const oldTimestamp = idx !== -1 ? this.commandsLog[idx]?.timestamp ?? 0 : 0;
1194
+ const previous = idx !== -1 ? this.commandsLog[idx] : void 0;
1195
+ const oldTimestamp = previous?.timestamp ?? 0;
989
1196
  if (idx !== -1) {
990
1197
  this.commandsLog.splice(idx, 1);
991
1198
  }
@@ -994,6 +1201,7 @@ var SessionCapturer = class extends SessionCapturerBase {
994
1201
  const commandId = this.commandCounter++;
995
1202
  const entry = {
996
1203
  _id: commandId,
1204
+ id: previous?.id ?? commandId,
997
1205
  command,
998
1206
  args,
999
1207
  result,
@@ -1003,18 +1211,68 @@ var SessionCapturer = class extends SessionCapturerBase {
1003
1211
  sequence: meta.sequence,
1004
1212
  callSource: meta.callSource,
1005
1213
  testUid: meta.testUid,
1006
- selector: meta.selector
1214
+ selector: meta.selector,
1215
+ parentId: meta.parentId
1007
1216
  };
1008
1217
  this.commandsLog.push(entry);
1009
1218
  return { entry, oldTimestamp };
1010
1219
  }
1220
+ /** Rows {@link openCommand} sent that have not ended yet. */
1221
+ #openRows = /* @__PURE__ */ new Set();
1222
+ /** The newest row that has ended: a row still running has caused nothing yet. */
1223
+ #newestEndedTimestamp() {
1224
+ for (let i = this.commandsLog.length - 1; i >= 0; i--) {
1225
+ if (!this.#openRows.has(this.commandsLog[i])) {
1226
+ return this.commandsLog[i].timestamp;
1227
+ }
1228
+ }
1229
+ return void 0;
1230
+ }
1231
+ /** Send a row that has started and not ended (a wait), so the rows it issues have a row to nest under while it runs. */
1232
+ openCommand(command, args, meta) {
1233
+ const id = this.commandCounter++;
1234
+ const entry = {
1235
+ _id: id,
1236
+ id,
1237
+ command,
1238
+ args,
1239
+ timestamp: meta.startTime ?? Date.now(),
1240
+ startTime: meta.startTime,
1241
+ sequence: meta.sequence,
1242
+ callSource: meta.callSource,
1243
+ testUid: meta.testUid,
1244
+ selector: meta.selector,
1245
+ parentId: meta.parentId
1246
+ };
1247
+ this.commandsLog.push(entry);
1248
+ this.#openRows.add(entry);
1249
+ this.sendCommand(entry);
1250
+ return entry;
1251
+ }
1252
+ /** End a row {@link openCommand} sent, stamped at completion, replaced by its `id` and moved to where it completed; a row that never ran keeps its place. */
1253
+ closeCommand(entry, result, error, timestamp, ran = true) {
1254
+ this.#openRows.delete(entry);
1255
+ const at = this.commandsLog.lastIndexOf(entry);
1256
+ if (ran && at !== -1) {
1257
+ this.commandsLog.splice(at, 1);
1258
+ this.commandsLog.push(entry);
1259
+ }
1260
+ const oldTimestamp = entry.timestamp;
1261
+ entry.result = result;
1262
+ entry.error = serializeError(error);
1263
+ entry.timestamp = timestamp;
1264
+ if (!error) {
1265
+ this.#queueActionSnapshot(entry.command, timestamp);
1266
+ }
1267
+ this.sendReplaceCommand(oldTimestamp, entry);
1268
+ }
1011
1269
  /** Birth time of the document this capturer last observed, so a replacement
1012
1270
  * is still visible to a poll that starts after the navigation committed. */
1013
1271
  lastDocumentOrigin;
1014
1272
  /** The current document's own birth time, which is what a DOM anchor is
1015
1273
  * stamped with. Undefined when the session cannot answer. */
1016
1274
  async documentOrigin(browser) {
1017
- return webdriverExecute(browser, "return performance.timeOrigin;").then((origin) => origin ?? void 0).catch(() => void 0);
1275
+ return webdriverExecute2(browser, "return performance.timeOrigin;").then((origin) => origin ?? void 0).catch(() => void 0);
1018
1276
  }
1019
1277
  /**
1020
1278
  * Wait for a navigation to actually replace the document, then drain with a
@@ -1063,11 +1321,7 @@ var SessionCapturer = class extends SessionCapturerBase {
1063
1321
  if (!replaced) {
1064
1322
  return;
1065
1323
  }
1066
- await this.captureTrace(
1067
- browser,
1068
- true,
1069
- this.commandsLog[this.commandsLog.length - 1]?.timestamp
1070
- );
1324
+ await this.captureTrace(browser, true, this.#newestEndedTimestamp());
1071
1325
  }
1072
1326
  /**
1073
1327
  * Take a screenshot by calling the WebDriver HTTP endpoint directly, bypassing
@@ -1097,26 +1351,25 @@ var SessionCapturer = class extends SessionCapturerBase {
1097
1351
  return;
1098
1352
  }
1099
1353
  try {
1100
- const alreadyPresent = await webdriverExecute(
1354
+ const alreadyPresent = await webdriverExecute2(
1101
1355
  browser,
1102
- `return ${COLLECTOR_READY_EXPRESSION};`
1356
+ COLLECTOR_READY_SCRIPT
1103
1357
  );
1104
1358
  if (alreadyPresent === true) {
1105
1359
  return;
1106
1360
  }
1107
1361
  const scriptContent = await loadInjectableScript();
1108
- const injectionScript = `
1109
- const script = document.createElement('script');
1110
- script.textContent = ${JSON.stringify(scriptContent)};
1111
- document.head.appendChild(script);
1112
- return true;
1113
- `;
1114
- await webdriverExecute(browser, injectionScript);
1115
- const hasCollector = await pollUntilReady(
1116
- async () => await webdriverExecute(
1362
+ const injected = await webdriverExecute2(
1363
+ browser,
1364
+ COLLECTOR_INJECT_SCRIPT,
1365
+ [scriptContent]
1366
+ );
1367
+ const hasCollector = injected === true || await pollUntilReady(
1368
+ async () => await webdriverExecute2(
1117
1369
  browser,
1118
- `return ${COLLECTOR_READY_EXPRESSION};`
1119
- ) === true
1370
+ COLLECTOR_READY_SCRIPT
1371
+ ) === true,
1372
+ { checkFirst: true }
1120
1373
  );
1121
1374
  if (hasCollector) {
1122
1375
  log4.info("\u2713 Script injected and collector ready");
@@ -1128,23 +1381,6 @@ var SessionCapturer = class extends SessionCapturerBase {
1128
1381
  throw err;
1129
1382
  }
1130
1383
  }
1131
- /**
1132
- * Capture Chrome DevTools browser console logs via WebDriver log API.
1133
- * Requires loggingPrefs: { browser: 'ALL' } in Chrome capabilities.
1134
- */
1135
- async captureBrowserLogs(browser) {
1136
- try {
1137
- const rawLogs = await browser.getLog("browser");
1138
- const logs = unwrapDriverValue(rawLogs);
1139
- if (!Array.isArray(logs) || logs.length === 0) {
1140
- return;
1141
- }
1142
- const entries = mapChromeBrowserLogs(logs);
1143
- this.consoleLogs.push(...entries);
1144
- this.sendUpstream("consoleLogs", entries);
1145
- } catch {
1146
- }
1147
- }
1148
1384
  /**
1149
1385
  * Parse Chrome performance logs to extract network request entries.
1150
1386
  * Requires loggingPrefs: { performance: 'ALL' } in Chrome capabilities.
@@ -1153,33 +1389,39 @@ var SessionCapturer = class extends SessionCapturerBase {
1153
1389
  if (this.bidiActive || !this.perfLogsRequested) {
1154
1390
  return;
1155
1391
  }
1156
- try {
1157
- const rawLogs = await browser.getLog("performance");
1158
- const logs = unwrapDriverValue(rawLogs);
1159
- if (!Array.isArray(logs) || logs.length === 0) {
1160
- return;
1161
- }
1162
- const networkEntries = parseNetworkFromPerfLogs(logs);
1163
- if (networkEntries.length === 0) {
1164
- return;
1165
- }
1166
- const deduped = dedupeNetworkRequests(
1167
- networkEntries,
1168
- this.networkRequests
1169
- );
1170
- if (deduped.length > 0) {
1171
- const normalized = deduped.map((d) => ({
1172
- ...d,
1173
- type: d.type ?? "other"
1174
- }));
1175
- this.networkRequests.push(...normalized);
1176
- this.sendUpstream("networkRequests", normalized);
1177
- }
1178
- } catch (err) {
1179
- const msg = errorMessage(err) ?? "";
1180
- if (!msg.includes("log type") && !msg.includes("performance")) {
1181
- log4.warn(`Performance log capture failed: ${msg}`);
1182
- }
1392
+ const logs = await webdriverPost2(browser, "se/log", {
1393
+ type: "performance"
1394
+ });
1395
+ if (!Array.isArray(logs) || logs.length === 0) {
1396
+ return;
1397
+ }
1398
+ const networkEntries = parseNetworkFromPerfLogs(logs);
1399
+ if (networkEntries.length > 0) {
1400
+ this.#ingestPerfLogEntries(networkEntries);
1401
+ }
1402
+ }
1403
+ /** A log entry for a load the page already reported replaces that row in
1404
+ * place, under the row's own id; the rest are appended once. */
1405
+ #ingestPerfLogEntries(entries) {
1406
+ const { upgrades, fresh } = upgradeResourceTimingRows(
1407
+ entries,
1408
+ this.networkRequests
1409
+ );
1410
+ for (const { index, request } of upgrades) {
1411
+ this.networkRequests[index] = request;
1412
+ }
1413
+ const deduped = dedupeNetworkRequests(
1414
+ fresh,
1415
+ this.networkRequests
1416
+ );
1417
+ const appended = deduped.map((d) => ({ ...d, type: d.type ?? "other" }));
1418
+ this.networkRequests.push(...appended);
1419
+ this.loadsListedByLog.push(
1420
+ ...appended.map(({ url, timestamp }) => ({ url, timestamp }))
1421
+ );
1422
+ const sent = [...upgrades.map((upgrade) => upgrade.request), ...appended];
1423
+ if (sent.length > 0) {
1424
+ this.sendUpstream("networkRequests", sent);
1183
1425
  }
1184
1426
  }
1185
1427
  /** `forceAnchor` forces a full-DOM anchor of the current document before
@@ -1189,28 +1431,45 @@ var SessionCapturer = class extends SessionCapturerBase {
1189
1431
  * that command, since it overrides the inference at ingest. */
1190
1432
  async captureTrace(browser, forceAnchor = false, anchorTimestamp) {
1191
1433
  if (this.isNativeAppSession) {
1434
+ await this.nativeNetwork.forward({ throttle: true });
1192
1435
  return;
1193
1436
  }
1194
1437
  await this.drainCollector(browser, forceAnchor, anchorTimestamp);
1195
1438
  await this.captureNetworkFromPerformanceLogs(browser);
1196
1439
  }
1440
+ /** At a test or scenario end, before its slice is written: the proxy drain there is never throttled. */
1441
+ async captureTraceAtTestEnd(browser) {
1442
+ if (this.isNativeAppSession) {
1443
+ await this.nativeNetwork.forward();
1444
+ return;
1445
+ }
1446
+ await this.captureTrace(browser, true);
1447
+ }
1197
1448
  async drainCollector(browser, forceAnchor, anchorTimestamp) {
1198
1449
  try {
1199
1450
  const traceData = await drainCollectorWithRecovery({
1200
- drain: () => webdriverExecute(
1451
+ drain: () => webdriverExecute2(
1201
1452
  browser,
1202
1453
  collectorDrainExpression(forceAnchor)
1203
1454
  ),
1204
1455
  injectIntoCurrentDocument: () => this.injectScript(browser),
1205
- currentUrl: () => webdriverExecute(browser, "return location.href;").then(
1456
+ currentUrl: () => webdriverExecute2(browser, "return location.href;").then(
1206
1457
  (u) => u ?? void 0
1207
1458
  ),
1208
1459
  log: (level, message) => log4[level](message)
1209
1460
  });
1461
+ const listedByLog = this.loadsListedByLog;
1462
+ this.loadsListedByLog = [];
1210
1463
  if (!traceData) {
1211
1464
  return;
1212
1465
  }
1213
- this.processTracePayload(traceData, { anchorTimestamp });
1466
+ this.processTracePayload(
1467
+ withoutLoadsTheLogListed(traceData, listedByLog),
1468
+ {
1469
+ anchorTimestamp,
1470
+ skipResourceTimingRequests: this.bidiNetworkObserved
1471
+ }
1472
+ );
1214
1473
  const mutationCount = Array.isArray(
1215
1474
  traceData.mutations
1216
1475
  ) ? traceData.mutations.length : 0;
@@ -1613,7 +1872,7 @@ var SuiteManager = class {
1613
1872
  };
1614
1873
 
1615
1874
  // src/helpers/browserProxy.ts
1616
- import logger7 from "@wdio/logger";
1875
+ import logger8 from "@wdio/logger";
1617
1876
 
1618
1877
  // src/helpers/callbackError.ts
1619
1878
  function callbackError(result, depth = 0) {
@@ -1624,17 +1883,23 @@ function callbackError(result, depth = 0) {
1624
1883
  return void 0;
1625
1884
  }
1626
1885
  const r = result;
1627
- if ("value" in r) {
1628
- return depth < 2 ? callbackError(r.value, depth + 1) : void 0;
1629
- }
1630
- if ("passed" in r || !r.error) {
1886
+ if ("passed" in r) {
1631
1887
  return void 0;
1632
1888
  }
1633
- const err = new Error(String(r.message ?? r.error));
1634
- if (typeof r.stack === "string") {
1635
- err.stack = r.stack;
1889
+ if (r.error instanceof Error) {
1890
+ return r.error;
1891
+ }
1892
+ if (r.error) {
1893
+ const err = new Error(String(r.message ?? r.error));
1894
+ if (typeof r.stack === "string") {
1895
+ err.stack = r.stack;
1896
+ }
1897
+ return err;
1898
+ }
1899
+ if ("value" in r) {
1900
+ return depth < 2 ? callbackError(r.value, depth + 1) : void 0;
1636
1901
  }
1637
- return err;
1902
+ return void 0;
1638
1903
  }
1639
1904
 
1640
1905
  // src/helpers/assertTarget.ts
@@ -2109,8 +2374,126 @@ var NativeAssertRecorder = class {
2109
2374
  }
2110
2375
  };
2111
2376
 
2377
+ // src/helpers/waitRows.ts
2378
+ import { AsyncLocalStorage } from "async_hooks";
2379
+ var NOT_RUN_ERROR = "NotRunError";
2380
+ var WaitRows = class {
2381
+ constructor(capturer) {
2382
+ this.capturer = capturer;
2383
+ }
2384
+ capturer;
2385
+ #scope = new AsyncLocalStorage();
2386
+ #open = /* @__PURE__ */ new Set();
2387
+ /** The innermost open wait making this call; a scope outlives its wait, so a closed one defers to its own parent. */
2388
+ enclosing() {
2389
+ let wait = this.#scope.getStore();
2390
+ while (wait?.closed) {
2391
+ wait = wait.parent;
2392
+ }
2393
+ return wait;
2394
+ }
2395
+ /** Send the wait's row running, before the queue runs it, so its condition's commands have a row to name. */
2396
+ open(cmd) {
2397
+ const row = this.capturer.openCommand(cmd.methodName, cmd.logArgs, {
2398
+ testUid: cmd.testUid,
2399
+ callSource: cmd.callSource,
2400
+ startTime: cmd.invokedAt,
2401
+ sequence: cmd.sequence,
2402
+ selector: commandRowSelector(cmd.methodName, cmd.logArgs),
2403
+ parentId: cmd.parent?.row.id
2404
+ });
2405
+ const wait = { row, parent: cmd.parent, children: /* @__PURE__ */ new Map(), closed: false };
2406
+ this.#open.add(wait);
2407
+ return wait;
2408
+ }
2409
+ /** The args a wait is called with: a `waitUntil` condition runs every poll inside the wait's scope. */
2410
+ args(cmd) {
2411
+ const [condition, ...rest] = cmd.logArgs;
2412
+ const wait = cmd.wait;
2413
+ if (!wait || typeof condition !== "function") {
2414
+ return cmd.logArgs;
2415
+ }
2416
+ const scope = this.#scope;
2417
+ const scoped = function(...args) {
2418
+ return scope.run(wait, () => condition.apply(this, args));
2419
+ };
2420
+ return [scoped, ...rest];
2421
+ }
2422
+ /** End the wait's row by `id`, stamped at completion; a wait Nightwatch reports as `false` timed out. */
2423
+ close(wait, result, error, completedAt) {
2424
+ wait.closed = true;
2425
+ this.#open.delete(wait);
2426
+ const { row } = wait;
2427
+ const action = mapCommandToAction(row.command);
2428
+ const timeout = !error && action ? commandFailure({ ...row, result }, action) : void 0;
2429
+ this.capturer.closeCommand(
2430
+ row,
2431
+ result,
2432
+ error ?? (timeout && new Error(timeout.message)),
2433
+ completedAt
2434
+ );
2435
+ }
2436
+ /** Close every wait still open as not run: Nightwatch drops a queued wait, callback and all, when an earlier assert aborts the queue. */
2437
+ closeUnrun() {
2438
+ const unrun = [...this.#open];
2439
+ for (const wait of unrun) {
2440
+ wait.closed = true;
2441
+ this.#open.delete(wait);
2442
+ const error = new Error(
2443
+ `${wait.row.command} did not run: the test ended before Nightwatch reached it`
2444
+ );
2445
+ error.name = NOT_RUN_ERROR;
2446
+ this.capturer.closeCommand(
2447
+ wait.row,
2448
+ void 0,
2449
+ error,
2450
+ wait.row.startTime ?? wait.row.timestamp,
2451
+ false
2452
+ );
2453
+ }
2454
+ return unrun;
2455
+ }
2456
+ };
2457
+
2458
+ // src/helpers/navigationWrap.ts
2459
+ import logger7 from "@wdio/logger";
2460
+ var log7 = logger7("@wdio/nightwatch-devtools:navigation");
2461
+ function wrapNavigation(browser, sessionCapturer) {
2462
+ const b = browser;
2463
+ const wrapNav = (methodName) => {
2464
+ if (typeof b[methodName] !== "function") {
2465
+ return;
2466
+ }
2467
+ const original = b[methodName].bind(browser);
2468
+ b[methodName] = function(...args) {
2469
+ if (typeof args[0] !== "string") {
2470
+ return original(...args);
2471
+ }
2472
+ const result = original(...args);
2473
+ const injectAndCapture = () => {
2474
+ log7.info(`[nav] ${methodName}(${args[0] ?? ""}) \u2014 injecting script`);
2475
+ return sessionCapturer.injectScript(browser).then(() => sessionCapturer.captureTrace(browser, true)).catch(
2476
+ (err) => log7.error(`Failed to inject script: ${err.message}`)
2477
+ );
2478
+ };
2479
+ const chainable = result;
2480
+ if (chainable && typeof chainable.perform === "function") {
2481
+ void Promise.resolve(result).then(injectAndCapture).catch(() => {
2482
+ });
2483
+ return result;
2484
+ }
2485
+ return Promise.resolve(result).then(injectAndCapture).catch(() => {
2486
+ });
2487
+ };
2488
+ };
2489
+ wrapNav("url");
2490
+ wrapNav("navigate");
2491
+ wrapNav("navigateTo");
2492
+ log7.info("\u2713 Script injection wrapped");
2493
+ }
2494
+
2112
2495
  // src/helpers/browserProxy.ts
2113
- var log7 = logger7("@wdio/nightwatch-devtools:browserProxy");
2496
+ var log8 = logger8("@wdio/nightwatch-devtools:browserProxy");
2114
2497
  var BrowserProxy = class {
2115
2498
  constructor(sessionCapturer, testManager, getCurrentTest, captureAssertions = true, observeSession) {
2116
2499
  this.sessionCapturer = sessionCapturer;
@@ -2118,6 +2501,7 @@ var BrowserProxy = class {
2118
2501
  this.getCurrentTest = getCurrentTest;
2119
2502
  this.captureAssertions = captureAssertions;
2120
2503
  this.observeSession = observeSession;
2504
+ this.waits = new WaitRows(sessionCapturer);
2121
2505
  this.assertRecorder = new NativeAssertRecorder(
2122
2506
  sessionCapturer,
2123
2507
  () => this.rowTest(),
@@ -2140,6 +2524,7 @@ var BrowserProxy = class {
2140
2524
  * rather than appending, showing only the final execution result.
2141
2525
  */
2142
2526
  retryTracker = new RetryTracker();
2527
+ waits;
2143
2528
  /**
2144
2529
  * Monotonic issue-order counter. Stamped when the test ISSUES a row — a
2145
2530
  * driver command at invocation, an assert at enqueue — which is source order.
@@ -2157,6 +2542,8 @@ var BrowserProxy = class {
2157
2542
  * the assert path, which runs outside any command callback.
2158
2543
  */
2159
2544
  browser = null;
2545
+ /** The test core's value registry currently holds reads for; see {@link scopeReadValuesToRowTest}. */
2546
+ readValueTestUid;
2160
2547
  /**
2161
2548
  * The test a captured row belongs to: the `it` Nightwatch reports as running
2162
2549
  * right now, falling back to the lifecycle test. On the BDD `describe/it`
@@ -2175,6 +2562,19 @@ var BrowserProxy = class {
2175
2562
  rowTestUid() {
2176
2563
  return this.rowTest()?.uid;
2177
2564
  }
2565
+ /**
2566
+ * Forget the values an earlier test read once the running test changes. The
2567
+ * lifecycle reset below fires once per MODULE on the BDD interface, so this
2568
+ * per-`it` live read is what keeps one `it`'s reads from naming another's
2569
+ * assertion target. Called before every record and every lookup.
2570
+ */
2571
+ scopeReadValuesToRowTest() {
2572
+ const uid = this.rowTestUid();
2573
+ if (uid !== this.readValueTestUid) {
2574
+ clearReadValues();
2575
+ this.readValueTestUid = uid;
2576
+ }
2577
+ }
2178
2578
  /** Per-command-scope dedup state only. Cucumber calls this per STEP, so it
2179
2579
  * must not touch the native-assert buffer — that buffer spans the whole
2180
2580
  * scenario and is drained once, at the scenario's pre-quit hook. */
@@ -2188,6 +2588,8 @@ var BrowserProxy = class {
2188
2588
  resetTestTracking() {
2189
2589
  this.resetCommandTracking();
2190
2590
  this.assertRecorder.clear();
2591
+ clearReadValues();
2592
+ this.readValueTestUid = void 0;
2191
2593
  }
2192
2594
  /** Hand off this test's recorded native-assertion calls and clear the
2193
2595
  * buffer so the next test starts fresh. */
@@ -2200,47 +2602,9 @@ var BrowserProxy = class {
2200
2602
  setCurrentTestFullPath(path5) {
2201
2603
  this.currentTestFullPath = path5;
2202
2604
  }
2203
- /**
2204
- * Wrap browser navigation methods (url / navigate / navigateTo) to inject
2205
- * the DevTools script after every navigation.
2206
- *
2207
- * Uses `browser` from the closure (not `this` inside perform) so it works
2208
- * for both standard Nightwatch (chainable API) and Cucumber async/await mode
2209
- * where `this` inside a perform callback is not the browser.
2210
- */
2605
+ /** Inject the DevTools script after every navigation; see {@link wrapNavigation}. */
2211
2606
  wrapUrlMethod(browser) {
2212
- const sessionCapturer = this.sessionCapturer;
2213
- const b = browser;
2214
- const wrapNav = (methodName) => {
2215
- if (typeof b[methodName] !== "function") {
2216
- return;
2217
- }
2218
- const original = b[methodName].bind(browser);
2219
- b[methodName] = function(...args) {
2220
- if (typeof args[0] !== "string") {
2221
- return original(...args);
2222
- }
2223
- const result = original(...args);
2224
- const injectAndCapture = () => {
2225
- log7.info(`[nav] ${methodName}(${args[0] ?? ""}) \u2014 injecting script`);
2226
- return sessionCapturer.injectScript(browser).then(() => sessionCapturer.captureTrace(browser, true)).catch(
2227
- (err) => log7.error(`Failed to inject script: ${err.message}`)
2228
- );
2229
- };
2230
- const chainable = result;
2231
- if (chainable && typeof chainable.perform === "function") {
2232
- void Promise.resolve(result).then(injectAndCapture).catch(() => {
2233
- });
2234
- return result;
2235
- }
2236
- return Promise.resolve(result).then(injectAndCapture).catch(() => {
2237
- });
2238
- };
2239
- };
2240
- wrapNav("url");
2241
- wrapNav("navigate");
2242
- wrapNav("navigateTo");
2243
- log7.info("\u2713 Script injection wrapped");
2607
+ wrapNavigation(browser, this.sessionCapturer);
2244
2608
  }
2245
2609
  wrapBrowserCommands(browser) {
2246
2610
  this.browser = browser;
@@ -2278,36 +2642,81 @@ var BrowserProxy = class {
2278
2642
  this.assertRecorder.wrapNamespaces(browser);
2279
2643
  }
2280
2644
  this.proxiedBrowsers.add(browser);
2281
- log7.info(`\u2713 Wrapped ${wrappedMethods.length} browser methods`);
2645
+ log8.info(`\u2713 Wrapped ${wrappedMethods.length} browser methods`);
2282
2646
  }
2283
- handleRetryReplacement(browser, methodName, logArgs, serializedResult, meta, commandError) {
2647
+ handleRetryReplacement(browser, oldId, methodName, logArgs, serializedResult, meta, commandError) {
2284
2648
  const { entry, oldTimestamp } = this.sessionCapturer.replaceCommand(
2285
- this.retryTracker.lastId,
2649
+ oldId,
2286
2650
  methodName,
2287
2651
  logArgs,
2288
2652
  serializedResult,
2289
2653
  commandError,
2290
2654
  meta
2291
2655
  );
2292
- this.retryTracker.setLastId(entry._id ?? null);
2293
2656
  this.sessionCapturer.sendReplaceCommand(oldTimestamp, entry);
2294
2657
  this.attachScreenshot(browser, entry, methodName, " (retry)");
2658
+ return entry._id ?? null;
2295
2659
  }
2296
- captureFreshCommand(browser, methodName, logArgs, serializedResult, meta, cmdSig, commandError) {
2297
- this.retryTracker.setLastSig(cmdSig);
2298
- this.retryTracker.setLastId(null);
2660
+ captureFreshCommand(browser, methodName, logArgs, serializedResult, meta, commandError) {
2299
2661
  this.drainOutgoingPage(browser, methodName);
2300
2662
  this.sessionCapturer.captureCommand(methodName, logArgs, serializedResult, commandError, meta).catch(
2301
- (err) => log7.error(`Failed to capture ${methodName}: ${err.message}`)
2663
+ (err) => log8.error(`Failed to capture ${methodName}: ${err.message}`)
2302
2664
  );
2303
2665
  const lastCommand = this.sessionCapturer.commandsLog[this.sessionCapturer.commandsLog.length - 1];
2304
2666
  if (lastCommand) {
2305
- this.retryTracker.setLastId(lastCommand._id ?? null);
2306
2667
  this.sessionCapturer.sendCommand(lastCommand);
2307
- log7.info(`[command] ${methodName}`);
2668
+ log8.info(`[command] ${methodName}`);
2308
2669
  this.attachScreenshot(browser, lastCommand, methodName);
2309
2670
  }
2310
2671
  this.anchorDestination(browser, methodName);
2672
+ if (!this.isDomMutating(methodName)) {
2673
+ void this.sessionCapturer.nativeNetwork.forward({ throttle: true });
2674
+ }
2675
+ return lastCommand?._id ?? null;
2676
+ }
2677
+ /**
2678
+ * Record a completed command. A poll folds into the row of the previous
2679
+ * identical call: the last row captured, or, inside a wait, the row of the
2680
+ * same call its condition already issued, since a condition with two reads
2681
+ * alternates them and never repeats the last one.
2682
+ */
2683
+ recordCompleted(browser, cmd, meta, serializedResult, commandError) {
2684
+ const { methodName, logArgs, cmdSig, parent } = cmd;
2685
+ const foldInto = parent ? parent.children.get(cmdSig) : this.retryTracker.isRetry(cmdSig) ? this.retryTracker.lastId : void 0;
2686
+ const id = foldInto === void 0 ? this.captureFreshCommand(
2687
+ browser,
2688
+ methodName,
2689
+ logArgs,
2690
+ serializedResult,
2691
+ meta,
2692
+ commandError
2693
+ ) : this.handleRetryReplacement(
2694
+ browser,
2695
+ foldInto,
2696
+ methodName,
2697
+ logArgs,
2698
+ serializedResult,
2699
+ meta,
2700
+ commandError
2701
+ );
2702
+ if (!parent) {
2703
+ this.retryTracker.recordCapture(cmdSig, id);
2704
+ } else if (id !== null) {
2705
+ parent.children.set(cmdSig, id);
2706
+ }
2707
+ }
2708
+ /** Close the waits an aborted queue never ran, before the test unit's rows are read. */
2709
+ closeUnrunWaits() {
2710
+ if (this.waits.closeUnrun().length > 0) {
2711
+ this.retryTracker.reset();
2712
+ }
2713
+ }
2714
+ /** End a wait's row; whatever is captured next is not a poll of what ran before it. */
2715
+ closeWait(browser, wait, result, error, completedAt) {
2716
+ this.retryTracker.reset();
2717
+ this.waits.close(wait, result, error, completedAt);
2718
+ this.attachScreenshot(browser, wait.row, wait.row.command);
2719
+ void this.sessionCapturer.nativeNetwork.forward({ throttle: true });
2311
2720
  }
2312
2721
  isDomMutating(methodName) {
2313
2722
  return NAVIGATION_COMMANDS.includes(methodName) || [
@@ -2377,47 +2786,44 @@ var BrowserProxy = class {
2377
2786
  * into one call because `makeCaptureCallback` is already over the line cap. */
2378
2787
  serializeAndAttributeResult(methodName, logArgs, callbackResult) {
2379
2788
  const result = serializeCommandResult(callbackResult, methodName);
2789
+ this.scopeReadValuesToRowTest();
2380
2790
  rememberReadValue(result, commandTargetSelector(methodName, logArgs));
2381
2791
  return result;
2382
2792
  }
2383
2793
  // Result-capturing callback factory — called by Nightwatch's async queue
2384
2794
  // when the command completes. This is where we get the *actual* result.
2385
- makeCaptureCallback(browser, methodName, logArgs, cmdSig, callSource, hasUserSource, invokedAt, sequence, testUid, userCallback) {
2795
+ makeCaptureCallback(browser, cmd) {
2796
+ const { methodName, logArgs, cmdSig, userCallback } = cmd;
2386
2797
  return (callbackResult) => {
2387
2798
  const commandTimestamp = Date.now();
2388
2799
  this.popCommandStackIfMatches(methodName, cmdSig);
2389
2800
  const commandError = callbackError(callbackResult);
2390
2801
  const serializedResult = commandError ? void 0 : this.serializeAndAttributeResult(methodName, logArgs, callbackResult);
2391
- const effectiveUid = this.rowTestUid() ?? testUid;
2392
- if (effectiveUid && hasUserSource) {
2393
- const meta = {
2394
- testUid: effectiveUid,
2395
- callSource,
2396
- timestamp: commandTimestamp,
2397
- startTime: invokedAt,
2398
- sequence,
2399
- selector: commandRowSelector(methodName, logArgs)
2400
- };
2401
- if (this.retryTracker.isRetry(cmdSig)) {
2402
- this.handleRetryReplacement(
2403
- browser,
2404
- methodName,
2405
- logArgs,
2406
- serializedResult,
2407
- meta,
2408
- commandError
2409
- );
2410
- } else {
2411
- this.captureFreshCommand(
2412
- browser,
2413
- methodName,
2414
- logArgs,
2415
- serializedResult,
2416
- meta,
2417
- cmdSig,
2418
- commandError
2419
- );
2420
- }
2802
+ const effectiveUid = this.rowTestUid() ?? cmd.testUid;
2803
+ if (cmd.wait) {
2804
+ this.closeWait(
2805
+ browser,
2806
+ cmd.wait,
2807
+ serializedResult,
2808
+ commandError,
2809
+ commandTimestamp
2810
+ );
2811
+ } else if (effectiveUid && cmd.hasUserSource) {
2812
+ this.recordCompleted(
2813
+ browser,
2814
+ cmd,
2815
+ {
2816
+ testUid: effectiveUid,
2817
+ callSource: cmd.callSource,
2818
+ timestamp: commandTimestamp,
2819
+ startTime: cmd.invokedAt,
2820
+ sequence: cmd.sequence,
2821
+ selector: commandRowSelector(methodName, logArgs),
2822
+ parentId: cmd.parent?.row.id
2823
+ },
2824
+ serializedResult,
2825
+ commandError
2826
+ );
2421
2827
  }
2422
2828
  if (userCallback) {
2423
2829
  return userCallback(callbackResult);
@@ -2441,7 +2847,7 @@ var BrowserProxy = class {
2441
2847
  this.currentTestFullPath = callInfo.filePath;
2442
2848
  }
2443
2849
  const lastArg = args[args.length - 1];
2444
- const hasUserCallback = typeof lastArg === "function";
2850
+ const hasUserCallback = typeof lastArg === "function" && !(methodName === "waitUntil" && args.length === 1);
2445
2851
  const logArgs = hasUserCallback ? args.slice(0, -1) : args;
2446
2852
  const cmdSig = JSON.stringify({
2447
2853
  command: methodName,
@@ -2464,49 +2870,59 @@ var BrowserProxy = class {
2464
2870
  browserAny.currentTest
2465
2871
  )
2466
2872
  );
2467
- const { userCallback, logArgs, cmdSig, callSource, hasUserSource } = this.describeInvocation(methodName, args);
2468
- const invokedAt = Date.now();
2469
- const sequence = this.issueCounter++;
2470
- const captureCallback = this.makeCaptureCallback(
2471
- browser,
2472
- methodName,
2473
- logArgs,
2474
- cmdSig,
2475
- callSource,
2476
- hasUserSource,
2477
- invokedAt,
2478
- sequence,
2479
- this.rowTestUid(),
2480
- userCallback
2481
- );
2873
+ const cmd = this.pendingCommand(methodName, args);
2874
+ const captureCallback = this.makeCaptureCallback(browser, cmd);
2482
2875
  const closeInputWindow = beginInputDispatch(methodName);
2483
2876
  const gatedCallback = (callbackResult) => {
2484
2877
  closeInputWindow();
2485
2878
  return captureCallback(callbackResult);
2486
2879
  };
2487
- const modifiedArgs = [...logArgs, gatedCallback];
2880
+ const modifiedArgs = [...this.waits.args(cmd), gatedCallback];
2488
2881
  try {
2489
2882
  return originalMethod(...modifiedArgs);
2490
2883
  } catch (error) {
2491
2884
  closeInputWindow();
2492
- this.popCommandStackIfMatches(methodName, cmdSig);
2493
- this.captureCommandError(methodName, logArgs, error, callSource);
2885
+ this.popCommandStackIfMatches(methodName, cmd.cmdSig);
2886
+ if (cmd.wait) {
2887
+ this.closeWait(browser, cmd.wait, void 0, toError(error), Date.now());
2888
+ } else {
2889
+ this.captureCommandError(methodName, cmd.logArgs, error, cmd.callSource);
2890
+ }
2494
2891
  throw error;
2495
2892
  }
2496
2893
  }
2894
+ /** Describe the call, and send its row now when it is a wait. */
2895
+ pendingCommand(methodName, args) {
2896
+ const cmd = {
2897
+ ...this.describeInvocation(methodName, args),
2898
+ methodName,
2899
+ // Invocation time only: the row's real completion stamp is taken inside
2900
+ // the capture callback (see makeCaptureCallback).
2901
+ invokedAt: Date.now(),
2902
+ sequence: this.issueCounter++,
2903
+ testUid: this.rowTestUid(),
2904
+ parent: this.waits.enclosing()
2905
+ };
2906
+ if (isWaitCommand(methodName) && cmd.testUid && cmd.hasUserSource) {
2907
+ this.retryTracker.reset();
2908
+ cmd.wait = this.waits.open(cmd);
2909
+ log8.info(`[command] ${methodName} (running)`);
2910
+ }
2911
+ return cmd;
2912
+ }
2497
2913
  captureCommandError(methodName, args, error, callSource) {
2498
2914
  const currentTest = this.rowTest();
2499
2915
  if (!currentTest) {
2500
2916
  return;
2501
2917
  }
2502
2918
  const normalizedError = toError(error);
2503
- log7.error(`[command error] ${methodName}: ${normalizedError.message}`);
2919
+ log8.error(`[command error] ${methodName}: ${normalizedError.message}`);
2504
2920
  this.sessionCapturer.captureCommand(methodName, args, void 0, normalizedError, {
2505
2921
  testUid: currentTest.uid,
2506
2922
  callSource,
2507
2923
  selector: commandRowSelector(methodName, args)
2508
2924
  }).catch(
2509
- (err) => log7.error(`Failed to capture ${methodName}: ${err.message}`)
2925
+ (err) => log8.error(`Failed to capture ${methodName}: ${err.message}`)
2510
2926
  );
2511
2927
  const lastCommand = this.sessionCapturer.commandsLog[this.sessionCapturer.commandsLog.length - 1];
2512
2928
  if (lastCommand) {
@@ -2526,10 +2942,10 @@ var BrowserProxy = class {
2526
2942
  attachScreenshot(browser, entry, methodName, suffix = "") {
2527
2943
  const ts = entry.timestamp ?? 0;
2528
2944
  this.sessionCapturer.takeScreenshotViaHttp(browser).then((screenshot) => {
2529
- if (screenshot) {
2945
+ if (screenshot && this.sessionCapturer.commandsLog.includes(entry)) {
2530
2946
  entry.screenshot = screenshot;
2531
2947
  this.sessionCapturer.sendReplaceCommand(ts, entry);
2532
- log7.info(`[screenshot] Attached to ${methodName}${suffix}`);
2948
+ log8.info(`[screenshot] Attached to ${methodName}${suffix}`);
2533
2949
  }
2534
2950
  }).catch(() => {
2535
2951
  });
@@ -2537,11 +2953,11 @@ var BrowserProxy = class {
2537
2953
  };
2538
2954
 
2539
2955
  // src/screencast-session.ts
2540
- import logger9 from "@wdio/logger";
2956
+ import logger10 from "@wdio/logger";
2541
2957
 
2542
2958
  // src/screencast.ts
2543
- import logger8 from "@wdio/logger";
2544
- var log8 = logger8("@wdio/nightwatch-devtools:ScreencastRecorder");
2959
+ import logger9 from "@wdio/logger";
2960
+ var log9 = logger9("@wdio/nightwatch-devtools:ScreencastRecorder");
2545
2961
  var ScreencastRecorder = class extends ScreencastRecorderBase {
2546
2962
  #sessionCapturer;
2547
2963
  constructor(sessionCapturer, options) {
@@ -2549,15 +2965,15 @@ var ScreencastRecorder = class extends ScreencastRecorderBase {
2549
2965
  this.#sessionCapturer = sessionCapturer;
2550
2966
  }
2551
2967
  onPollingStarted(intervalMs) {
2552
- log8.info(
2968
+ log9.info(
2553
2969
  `\u2713 Screencast recording started (polling mode, ${intervalMs} ms interval)`
2554
2970
  );
2555
2971
  }
2556
2972
  onPollingStopped(frameCount) {
2557
- log8.info(`\u2713 Screencast stopped \u2014 ${frameCount} frame(s) collected`);
2973
+ log9.info(`\u2713 Screencast stopped \u2014 ${frameCount} frame(s) collected`);
2558
2974
  }
2559
2975
  onUnavailable(err) {
2560
- log8.warn(
2976
+ log9.warn(
2561
2977
  `Screencast unavailable (${errorMessage(err)}). Recording skipped.`
2562
2978
  );
2563
2979
  }
@@ -2571,7 +2987,7 @@ var ScreencastRecorder = class extends ScreencastRecorderBase {
2571
2987
  };
2572
2988
 
2573
2989
  // src/screencast-session.ts
2574
- var log9 = logger9("@wdio/nightwatch-devtools:screencast-session");
2990
+ var log10 = logger10("@wdio/nightwatch-devtools:screencast-session");
2575
2991
  async function startScreencast(ctx, browser, sessionId2) {
2576
2992
  if (!ctx.screencastOptions.enabled || ctx.screencastRecorder || !sessionId2) {
2577
2993
  return;
@@ -2581,7 +2997,7 @@ async function startScreencast(ctx, browser, sessionId2) {
2581
2997
  ctx.screencastOptions
2582
2998
  );
2583
2999
  ctx.screencastSessionId = sessionId2;
2584
- log9.info(`\u{1F3AC} Starting screencast for session ${sessionId2}`);
3000
+ log10.info(`\u{1F3AC} Starting screencast for session ${sessionId2}`);
2585
3001
  await ctx.screencastRecorder.start(browser);
2586
3002
  }
2587
3003
  function needsScreencastRotation(ctx, sessionId2) {
@@ -2595,16 +3011,16 @@ function rotateScreencastForSession(ctx, browser) {
2595
3011
  if (!needsScreencastRotation(ctx, sessionId2)) {
2596
3012
  return;
2597
3013
  }
2598
- log9.info(`\u{1F3AC} Session replaced \u2014 rotating screencast to ${sessionId2}`);
3014
+ log10.info(`\u{1F3AC} Session replaced \u2014 rotating screencast to ${sessionId2}`);
2599
3015
  ctx.screencastRotation = ctx.finalizeCurrentScreencast().then(() => startScreencast(ctx, browser, sessionId2)).catch(
2600
- (err) => log9.warn(`Screencast rotation failed: ${errorMessage(err)}`)
3016
+ (err) => log10.warn(`Screencast rotation failed: ${errorMessage(err)}`)
2601
3017
  ).finally(() => {
2602
3018
  ctx.screencastRotation = void 0;
2603
3019
  });
2604
3020
  }
2605
3021
 
2606
3022
  // src/session-init.ts
2607
- var log10 = logger10("@wdio/nightwatch-devtools:session-init");
3023
+ var log11 = logger11("@wdio/nightwatch-devtools:session-init");
2608
3024
  function initReporterChain(ctx) {
2609
3025
  ctx.testReporter = new TestReporter(
2610
3026
  (suitesData) => {
@@ -2636,9 +3052,9 @@ function initReporterChain(ctx) {
2636
3052
  }
2637
3053
  function readViewport(browser) {
2638
3054
  return resolveViewport(browser.capabilities || {}, {
2639
- runScript: (body) => webdriverExecute(browser, body),
3055
+ runScript: (body) => webdriverExecute2(browser, body),
2640
3056
  getWindowSize: () => webdriverGet2(browser, "window/rect"),
2641
- onWarn: (message) => log10.warn(message)
3057
+ onWarn: (message) => log11.warn(message)
2642
3058
  });
2643
3059
  }
2644
3060
  function wantsPerformanceLog(capabilities, desiredCapabilities) {
@@ -2671,17 +3087,29 @@ async function broadcastSessionMetadata(ctx, browser) {
2671
3087
  ctx.sessionCapturer.sendUpstream("metadata", metadata);
2672
3088
  const browserName = capabilities.browserName || desiredCapabilities.browserName || "unknown";
2673
3089
  const browserVersion = capabilities.browserVersion || capabilities.version || "";
2674
- log10.info(
3090
+ log11.info(
2675
3091
  `\u2713 Browser: ${browserName}${browserVersion ? " " + browserVersion : ""} (session: ${sessionId2})`
2676
3092
  );
3093
+ if (ctx.mode === "live" && restoreBrowserFocus({
3094
+ browserName: String(browserName),
3095
+ platformName: String(
3096
+ capabilities.platformName || desiredCapabilities.platformName || ""
3097
+ ),
3098
+ host: resolveWebDriverAddress(browser).driverHost,
3099
+ onError: (message) => log11.warn(message)
3100
+ })) {
3101
+ log11.info(
3102
+ "\u2713 Safari brought back to the front (it delivers clicks only there)"
3103
+ );
3104
+ }
2677
3105
  const wantsPerfLog = wantsPerformanceLog(
2678
3106
  capabilities,
2679
3107
  desiredCapabilities
2680
3108
  );
2681
3109
  ctx.sessionCapturer.perfLogsRequested = wantsPerfLog;
2682
3110
  if (!wantsPerfLog && !ctx.bidiEnabled) {
2683
- log10.warn(
2684
- "\u26A0 Network tab will be empty \u2014 add 'goog:loggingPrefs': { performance: 'ALL' } to your capabilities (or enable bidi:true)"
3111
+ log11.warn(
3112
+ "\u26A0 Network tab will carry the page's own Resource Timing only (no headers, no bodies). Add 'goog:loggingPrefs': { performance: 'ALL' } to your capabilities (or enable bidi:true) for full requests"
2685
3113
  );
2686
3114
  }
2687
3115
  }
@@ -2691,17 +3119,17 @@ async function tryAttachBidi(ctx, browser) {
2691
3119
  }
2692
3120
  const driver = browser.driver;
2693
3121
  if (!driver) {
2694
- log10.warn("bidi:true set but browser.driver unavailable \u2014 skipping");
3122
+ log11.warn("bidi:true set but browser.driver unavailable \u2014 skipping");
2695
3123
  return;
2696
3124
  }
2697
- const { attachBidiHandlers, buildBidiSinks } = await import("./bidi-WNZOXX5X.js");
3125
+ const { attachBidiHandlers, buildBidiSinks } = await import("./bidi-C67DYUCA.js");
2698
3126
  const ok = await attachBidiHandlers(
2699
3127
  driver,
2700
3128
  buildBidiSinks(ctx.sessionCapturer)
2701
3129
  );
2702
3130
  if (ok) {
2703
3131
  ctx.sessionCapturer.bidiActive = true;
2704
- log10.info("\u2713 BiDi attached \u2014 perf-log network capture disabled");
3132
+ log11.info("\u2713 BiDi attached \u2014 perf-log network capture disabled");
2705
3133
  }
2706
3134
  }
2707
3135
  async function tryRegisterPreload(ctx, browser) {
@@ -2711,13 +3139,19 @@ async function tryRegisterPreload(ctx, browser) {
2711
3139
  }
2712
3140
  ctx.sessionCapturer.preloadRegistered = await registerCollectorPreload(
2713
3141
  driver,
2714
- (level, message) => log10[level](message)
3142
+ (level, message) => log11[level](message)
2715
3143
  );
2716
3144
  }
2717
3145
  async function armCaptureForSession(ctx, browser) {
2718
3146
  ctx.armedSessionId = browser.sessionId ?? void 0;
2719
3147
  await tryRegisterPreload(ctx, browser);
2720
3148
  await tryAttachBidi(ctx, browser);
3149
+ await ctx.sessionCapturer.nativeNetwork.start({
3150
+ requested: requestsInterceptedNetwork(browser.desiredCapabilities),
3151
+ capabilities: browser.capabilities,
3152
+ execute: sessionExecutor(browser),
3153
+ unavailableReason: "the session id or the configured WebDriver port is unknown"
3154
+ });
2721
3155
  }
2722
3156
  function needsCaptureRearm(ctx, sessionId2) {
2723
3157
  return Boolean(ctx.sessionCapturer) && sessionId2 !== void 0 && ctx.armedSessionId !== void 0 && ctx.armedSessionId !== sessionId2;
@@ -2729,16 +3163,19 @@ async function armReplacedSession(ctx, browser) {
2729
3163
  }
2730
3164
  ctx.sessionCapturer.preloadRegistered = false;
2731
3165
  ctx.sessionCapturer.bidiActive = false;
2732
- log10.info(`Session replaced \u2014 re-arming capture for ${sessionId2}`);
3166
+ ctx.sessionCapturer.bidiNetworkObserved = false;
3167
+ ctx.sessionCapturer.loadsListedByLog = [];
3168
+ ctx.sessionCapturer.nativeNetwork.stop();
3169
+ log11.info(`Session replaced \u2014 re-arming capture for ${sessionId2}`);
2733
3170
  await armCaptureForSession(ctx, browser);
2734
3171
  }
2735
3172
  function rearmCaptureForSession(ctx, browser) {
2736
3173
  void armReplacedSession(ctx, browser).catch(
2737
- (err) => log10.warn(`Capture re-arm failed: ${errorMessage(err)}`)
3174
+ (err) => log11.warn(`Capture re-arm failed: ${errorMessage(err)}`)
2738
3175
  );
2739
3176
  }
2740
3177
  async function rebindSessionToBrowser(ctx, browser) {
2741
- log10.info("Browser session changed \u2014 re-targeting the run capturer");
3178
+ log11.info("Browser session changed \u2014 re-targeting the run capturer");
2742
3179
  ctx.isScriptInjected = false;
2743
3180
  await broadcastSessionMetadata(ctx, browser);
2744
3181
  await armReplacedSession(ctx, browser);
@@ -2768,7 +3205,7 @@ async function ensureSessionInitialized(ctx, browser) {
2768
3205
  if (ctx.mode !== "trace") {
2769
3206
  const connected = await ctx.sessionCapturer.waitForConnection(3e3);
2770
3207
  if (!connected) {
2771
- log10.error("\u274C Worker WebSocket failed to connect!");
3208
+ log11.error("\u274C Worker WebSocket failed to connect!");
2772
3209
  }
2773
3210
  }
2774
3211
  initReporterChain(ctx);
@@ -2793,7 +3230,7 @@ async function finalizeCurrentScreencast(ctx) {
2793
3230
  }),
2794
3231
  captureFormat: ctx.screencastOptions.captureFormat,
2795
3232
  sendUpstream: (scope, data) => ctx.sessionCapturer?.sendUpstream(scope, data),
2796
- onLog: (level, message) => log10[level](message)
3233
+ onLog: (level, message) => log11[level](message)
2797
3234
  });
2798
3235
  }
2799
3236
  ctx.screencastRecorder = void 0;
@@ -2851,8 +3288,8 @@ var PluginScreencast = class {
2851
3288
  };
2852
3289
 
2853
3290
  // src/event-hub.ts
2854
- import logger11 from "@wdio/logger";
2855
- var log11 = logger11("@wdio/nightwatch-devtools:event-hub");
3291
+ import logger12 from "@wdio/logger";
3292
+ var log12 = logger12("@wdio/nightwatch-devtools:event-hub");
2856
3293
  function makeSessionMetadataHandler(bindings) {
2857
3294
  return (data) => {
2858
3295
  try {
@@ -2873,23 +3310,23 @@ function makeSessionMetadataHandler(bindings) {
2873
3310
  options: bindings.buildMetadataOptions()
2874
3311
  });
2875
3312
  } catch (err) {
2876
- log11.error(`Error in event handler: ${errorMessage(err)}`);
3313
+ log12.error(`Error in event handler: ${errorMessage(err)}`);
2877
3314
  }
2878
3315
  };
2879
3316
  }
2880
3317
  function registerEventHandlers(eventHub, bindings) {
2881
3318
  bindings.setCucumberRunner(eventHub.runner === "cucumber");
2882
3319
  if (eventHub.runner === "cucumber") {
2883
- log11.info("\u2713 Cucumber runner detected via NightwatchEventHub");
3320
+ log12.info("\u2713 Cucumber runner detected via NightwatchEventHub");
2884
3321
  }
2885
- log11.info("\u2713 NightwatchEventHub registered \u2014 enriched metadata enabled");
3322
+ log12.info("\u2713 NightwatchEventHub registered \u2014 enriched metadata enabled");
2886
3323
  const handler = makeSessionMetadataHandler(bindings);
2887
3324
  eventHub.on("TestSuiteStarted", handler);
2888
3325
  eventHub.on("TestRunStarted", handler);
2889
3326
  }
2890
3327
 
2891
3328
  // src/cucumber-lifecycle.ts
2892
- import logger12 from "@wdio/logger";
3329
+ import logger13 from "@wdio/logger";
2893
3330
 
2894
3331
  // src/helpers/cucumberResult.ts
2895
3332
  function cucumberResultToTestState(result) {
@@ -3084,7 +3521,7 @@ function flushTestSlice(ctx) {
3084
3521
  }
3085
3522
 
3086
3523
  // src/cucumber-lifecycle.ts
3087
- var log12 = logger12("@wdio/nightwatch-devtools:cucumber");
3524
+ var log13 = logger13("@wdio/nightwatch-devtools:cucumber");
3088
3525
  function attachScenarioToFeature(ctx, featureSuite, scenarioSuite) {
3089
3526
  const existingIdx = featureSuite.suites.findIndex(
3090
3527
  (s) => s.uid === scenarioSuite.uid
@@ -3168,7 +3605,7 @@ async function initCucumberScenario(ctx, browser, pickle) {
3168
3605
  ctx.setCurrentTest(null);
3169
3606
  ctx.testReporter.updateSuites();
3170
3607
  ctx.wrapBrowserOnce(browser);
3171
- log12.info(`\u{1F952} Scenario: ${scenarioName}`);
3608
+ log13.info(`\u{1F952} Scenario: ${scenarioName}`);
3172
3609
  }
3173
3610
  async function finalizeCucumberScenario(ctx, _browser, result, pickle) {
3174
3611
  try {
@@ -3190,7 +3627,7 @@ async function finalizeCucumberScenario(ctx, _browser, result, pickle) {
3190
3627
  }
3191
3628
  ctx.incrementCount(scenarioState);
3192
3629
  const icon = ctx.testIcon(scenarioState);
3193
- log12.info(
3630
+ log13.info(
3194
3631
  ` ${icon} ${pickle?.name ?? "Unknown"} (${(duration / 1e3).toFixed(2)}s)`
3195
3632
  );
3196
3633
  ctx.testReporter.updateSuites();
@@ -3199,12 +3636,13 @@ async function finalizeCucumberScenario(ctx, _browser, result, pickle) {
3199
3636
  ctx.setCurrentTest(null);
3200
3637
  }
3201
3638
  } catch (err) {
3202
- log12.error(`Failed to finalize Cucumber scenario: ${errorMessage(err)}`);
3639
+ log13.error(`Failed to finalize Cucumber scenario: ${errorMessage(err)}`);
3203
3640
  }
3204
3641
  }
3205
3642
  async function captureCucumberScenarioBeforeQuit(ctx, browser, result) {
3206
3643
  const scenario = ctx.getCurrentScenarioSuite();
3207
3644
  try {
3645
+ ctx.browserProxy?.closeUnrunWaits();
3208
3646
  const assertCalls = ctx.browserProxy?.drainNativeAssertCalls() ?? [];
3209
3647
  if (assertCalls.length > 0) {
3210
3648
  await captureNativeAssertions(
@@ -3215,14 +3653,14 @@ async function captureCucumberScenarioBeforeQuit(ctx, browser, result) {
3215
3653
  assertCalls
3216
3654
  );
3217
3655
  }
3218
- await ctx.sessionCapturer.captureTrace(browser, true);
3656
+ await ctx.sessionCapturer.captureTraceAtTestEnd(browser);
3219
3657
  flushTestSlice(ctx);
3220
3658
  await ctx.emitTestArtifacts(
3221
3659
  scenario?.uid,
3222
3660
  cucumberResultToTestState(result) === TEST_STATE.FAILED
3223
3661
  );
3224
3662
  } catch (err) {
3225
- log12.error(
3663
+ log13.error(
3226
3664
  `Failed to capture Cucumber scenario before quit: ${errorMessage(err)}`
3227
3665
  );
3228
3666
  }
@@ -3268,7 +3706,7 @@ async function cucumberAfter(ctx, browser, result, pickle) {
3268
3706
  }
3269
3707
 
3270
3708
  // src/test-lifecycle.ts
3271
- import logger14 from "@wdio/logger";
3709
+ import logger15 from "@wdio/logger";
3272
3710
 
3273
3711
  // src/helpers/specFileResolver.ts
3274
3712
  import fs4 from "fs";
@@ -3316,8 +3754,8 @@ function resolveSpecFilePath(testFile, modulePath, srcFolders, cachedPath) {
3316
3754
  }
3317
3755
 
3318
3756
  // src/helpers/closePreviousTest.ts
3319
- import logger13 from "@wdio/logger";
3320
- var log13 = logger13("@wdio/nightwatch-devtools:closePreviousTest");
3757
+ import logger14 from "@wdio/logger";
3758
+ var log14 = logger14("@wdio/nightwatch-devtools:closePreviousTest");
3321
3759
  async function closePreviousTest(input) {
3322
3760
  const {
3323
3761
  runningTest,
@@ -3336,7 +3774,7 @@ async function closePreviousTest(input) {
3336
3774
  testManager.updateTestState(runningTest, testState);
3337
3775
  testManager.markTestAsProcessed(testFile, runningTest.title);
3338
3776
  incrementCount(testState);
3339
- log13.info(
3777
+ log14.info(
3340
3778
  ` ${testIcon(testState)} ${runningTest.title} (${(runningTest._duration / 1e3).toFixed(2)}s)`
3341
3779
  );
3342
3780
  } else {
@@ -3350,13 +3788,13 @@ async function closePreviousTest(input) {
3350
3788
  );
3351
3789
  testManager.markTestAsProcessed(testFile, runningTest.title);
3352
3790
  incrementCount(TEST_STATE.PASSED);
3353
- log13.info(` \u2705 ${runningTest.title} (${(duration / 1e3).toFixed(2)}s)`);
3791
+ log14.info(` \u2705 ${runningTest.title} (${(duration / 1e3).toFixed(2)}s)`);
3354
3792
  }
3355
3793
  await new Promise((resolve2) => setTimeout(resolve2, TIMING.UI_RENDER_DELAY));
3356
3794
  }
3357
3795
 
3358
3796
  // src/test-lifecycle.ts
3359
- var log14 = logger14("@wdio/nightwatch-devtools:test-lifecycle");
3797
+ var log15 = logger15("@wdio/nightwatch-devtools:test-lifecycle");
3360
3798
  function resolveSuiteMetadata(ctx, currentTest) {
3361
3799
  const moduleName = currentTest.module ?? "";
3362
3800
  const testFile = moduleName.split("/").pop() || moduleName || DEFAULTS.FILE_NAME;
@@ -3367,7 +3805,7 @@ function resolveSuiteMetadata(ctx, currentTest) {
3367
3805
  ctx.browserProxy.getCurrentTestFullPath() || void 0
3368
3806
  );
3369
3807
  if (!fullPath) {
3370
- log14.warn(
3808
+ log15.warn(
3371
3809
  `[beforeEach] Could not resolve file path for "${testFile}" \u2014 source view will be unavailable`
3372
3810
  );
3373
3811
  }
@@ -3412,10 +3850,10 @@ async function startNextTest(ctx, currentSuite, currentTestName, processedTests,
3412
3850
  test.end = null;
3413
3851
  ctx.testReporter.onTestStart(test);
3414
3852
  ctx.setCurrentTest(test);
3415
- log14.info(` \u25B6 ${currentTestName}`);
3853
+ log15.info(` \u25B6 ${currentTestName}`);
3416
3854
  await new Promise((resolve2) => setTimeout(resolve2, TIMING.TEST_START_DELAY));
3417
3855
  } else {
3418
- log14.warn(
3856
+ log15.warn(
3419
3857
  `Test "${currentTestName}" not found in suite "${currentSuite.title}"`
3420
3858
  );
3421
3859
  ctx.setCurrentTest(null);
@@ -3460,7 +3898,7 @@ function closeUnreportedRunningTest(ctx, currentSuite, testFile, results, proces
3460
3898
  ctx.testManager.markTestAsProcessed(testFile, runningTest.title);
3461
3899
  ctx.incrementCount(testState);
3462
3900
  const icon = ctx.testIcon(testState);
3463
- log14.info(` ${icon} ${runningTest.title} (${(duration / 1e3).toFixed(2)}s)`);
3901
+ log15.info(` ${icon} ${runningTest.title} (${(duration / 1e3).toFixed(2)}s)`);
3464
3902
  }
3465
3903
  async function closeReportedTestcases(ctx, currentSuite, testFile, testcases, processedTests) {
3466
3904
  const testcaseNames = Object.keys(testcases);
@@ -3476,7 +3914,7 @@ async function closeReportedTestcases(ctx, currentSuite, testFile, testcases, pr
3476
3914
  ctx.testManager.updateTestState(test, testState, /* @__PURE__ */ new Date(), dur);
3477
3915
  ctx.incrementCount(testState);
3478
3916
  const icon = ctx.testIcon(testState);
3479
- log14.info(` ${icon} ${currentTestName} (${(dur / 1e3).toFixed(2)}s)`);
3917
+ log15.info(` ${icon} ${currentTestName} (${(dur / 1e3).toFixed(2)}s)`);
3480
3918
  }
3481
3919
  ctx.testManager.markTestAsProcessed(testFile, currentTestName);
3482
3920
  }
@@ -3518,7 +3956,7 @@ async function closeOutTestcases(ctx, browser) {
3518
3956
  }
3519
3957
 
3520
3958
  // src/index.ts
3521
- var log15 = logger15("@wdio/nightwatch-devtools");
3959
+ var log16 = logger16("@wdio/nightwatch-devtools");
3522
3960
  var NightwatchDevToolsPlugin = class {
3523
3961
  options;
3524
3962
  sessionCapturer;
@@ -3572,7 +4010,7 @@ var NightwatchDevToolsPlugin = class {
3572
4010
  screencast: options.screencast
3573
4011
  });
3574
4012
  if (screencast.warning) {
3575
- log15.warn(screencast.warning);
4013
+ log16.warn(screencast.warning);
3576
4014
  }
3577
4015
  this.options = {
3578
4016
  port: options.port ?? 3e3,
@@ -3593,7 +4031,7 @@ var NightwatchDevToolsPlugin = class {
3593
4031
  };
3594
4032
  const policyWarning = tracePolicyModeWarning(options.tracePolicy, mode);
3595
4033
  if (policyWarning) {
3596
- log15.warn(policyWarning);
4034
+ log16.warn(policyWarning);
3597
4035
  }
3598
4036
  this.#screencast = new PluginScreencast(
3599
4037
  screencast.options,
@@ -3814,7 +4252,8 @@ var NightwatchDevToolsPlugin = class {
3814
4252
  // Rows are tagged per-`it` (BrowserProxy.rowTestUid); `#currentTestUid`
3815
4253
  // — the lifecycle test, which the BDD interface pins to the module's
3816
4254
  // first `it` — is only the fallback, and stays the artifact/slice key.
3817
- () => this.browserProxy?.rowTestUid() ?? this.#currentTestUid()
4255
+ () => this.browserProxy?.rowTestUid() ?? this.#currentTestUid(),
4256
+ () => this.browserProxy?.scopeReadValuesToRowTest()
3818
4257
  );
3819
4258
  }
3820
4259
  }
@@ -3926,6 +4365,7 @@ var NightwatchDevToolsPlugin = class {
3926
4365
  }
3927
4366
  if (browser && this.sessionCapturer) {
3928
4367
  try {
4368
+ this.browserProxy.closeUnrunWaits();
3929
4369
  await this.#closeOutTestcases(browser);
3930
4370
  if (this.options.captureAssertions) {
3931
4371
  await captureNativeAssertions(
@@ -3936,13 +4376,13 @@ var NightwatchDevToolsPlugin = class {
3936
4376
  this.browserProxy.drainNativeAssertCalls()
3937
4377
  );
3938
4378
  }
3939
- await this.sessionCapturer.captureTrace(browser, true);
4379
+ await this.sessionCapturer.captureTraceAtTestEnd(browser);
3940
4380
  flushTestSlice(this.#getInternals());
3941
4381
  const results = browser.currentTest?.results;
3942
4382
  const failed = !!results && ((results.errors ?? 0) > 0 || (results.failed ?? 0) > 0);
3943
4383
  await this.#emitTestArtifacts(this.#currentTestUid(), failed);
3944
4384
  } catch (err) {
3945
- log15.error(`Failed to capture trace: ${errorMessage(err)}`);
4385
+ log16.error(`Failed to capture trace: ${errorMessage(err)}`);
3946
4386
  }
3947
4387
  }
3948
4388
  }
@@ -3978,10 +4418,12 @@ var NightwatchDevToolsPlugin = class {
3978
4418
  outputDir: this.#outputDir,
3979
4419
  captureFormat: this.#screencast.options.captureFormat,
3980
4420
  onArtifact: (a) => this.#artifacts.push(a),
3981
- onLog: (level, msg) => log15[level](msg)
4421
+ onLog: (level, msg) => log16[level](msg)
3982
4422
  });
3983
4423
  }
3984
4424
  async after(browser) {
4425
+ this.sessionCapturer?.nativeNetwork.stop();
4426
+ this.browserProxy?.closeUnrunWaits();
3985
4427
  await this.#screencast.rotation;
3986
4428
  await this.#finalizeCurrentScreencast();
3987
4429
  try {
@@ -3995,14 +4437,14 @@ var NightwatchDevToolsPlugin = class {
3995
4437
  }
3996
4438
  if (!this.#devtoolsBrowser) {
3997
4439
  this.testReporter?.updateSuites();
3998
- log15.info("\u267B Rerun complete \u2014 flushing WebSocket...");
4440
+ log16.info("\u267B Rerun complete \u2014 flushing WebSocket...");
3999
4441
  await this.sessionCapturer?.closeWebSocket();
4000
4442
  return;
4001
4443
  }
4002
- log15.info("\u{1F4A1} Please close the DevTools browser window to finish...");
4444
+ log16.info("\u{1F4A1} Please close the DevTools browser window to finish...");
4003
4445
  await this.#waitForDevtoolsBrowserClose();
4004
4446
  } catch (err) {
4005
- log15.error(`Failed to stop backend: ${errorMessage(err)}`);
4447
+ log16.error(`Failed to stop backend: ${errorMessage(err)}`);
4006
4448
  }
4007
4449
  }
4008
4450
  async #finalizeAllSuites(browser) {
@@ -4043,7 +4485,7 @@ var NightwatchDevToolsPlugin = class {
4043
4485
  screencastFrames: this.options.filmstrip ? this.#screencast.frames : void 0,
4044
4486
  configPath: this.#configPath,
4045
4487
  testFilePath: this.browserProxy?.getCurrentTestFullPath?.() ?? void 0,
4046
- log: (level, msg) => log15[level](msg)
4488
+ log: (level, msg) => log16[level](msg)
4047
4489
  },
4048
4490
  sessionId2
4049
4491
  );