@wdio/nightwatch-devtools 2.0.0 → 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,9 +1,11 @@
1
1
  import {
2
2
  CAPTURE_PERFORMANCE_SCRIPT,
3
- COLLECTOR_READY_EXPRESSION,
3
+ COLLECTOR_INJECT_SCRIPT,
4
+ COLLECTOR_READY_SCRIPT,
4
5
  NETWORK_CAPTURE_SOURCE,
5
6
  NativeNetworkCapture,
6
7
  REUSE_ENV,
8
+ RUNNER_ENV,
7
9
  RetryTracker,
8
10
  SCREENCAST_DEFAULTS,
9
11
  ScreencastRecorderBase,
@@ -18,14 +20,17 @@ import {
18
20
  captureActionSnapshot,
19
21
  captureAndAttachScreenshot,
20
22
  captureAndAttachVideo,
23
+ clearReadValues,
21
24
  collectSuiteTestMetadata,
22
25
  collectorDrainExpression,
26
+ commandFailure,
23
27
  computeSuiteFinalStateStrict,
24
28
  computeSuiteRunningState,
25
29
  createSuiteStats,
26
30
  createTestStats,
27
31
  deterministicUid,
28
32
  drainCollectorWithRecovery,
33
+ encodeInitialArgs,
29
34
  errorMessage,
30
35
  finalizeScreencast,
31
36
  finalizeTraceExport,
@@ -37,9 +42,9 @@ import {
37
42
  getCallSourceFromStack,
38
43
  getRequestType,
39
44
  isUserCodeFrame,
45
+ isWaitCommand,
40
46
  lastRenderedScreenshot,
41
47
  loadInjectableScript,
42
- mapChromeBrowserLogs,
43
48
  mapCommandToAction,
44
49
  matcherAssertionToCommandLog,
45
50
  normalizeFilePath,
@@ -65,7 +70,7 @@ import {
65
70
  webdriverExecute,
66
71
  webdriverGet,
67
72
  webdriverPost
68
- } from "./chunk-TUK7ZLQI.js";
73
+ } from "./chunk-DRZC45YG.js";
69
74
 
70
75
  // src/index.ts
71
76
  import { fileURLToPath } from "url";
@@ -169,14 +174,18 @@ async function emitTestArtifacts(input) {
169
174
  // src/helpers/assertCapture.ts
170
175
  import logger from "@wdio/logger";
171
176
  var log = logger("@wdio/nightwatch-devtools:assertCapture");
172
- function wireAssertCapture(getCapturer, getTestUid) {
177
+ function wireAssertCapture(getCapturer, getTestUid, scopeReadValues = () => {
178
+ }) {
173
179
  patchNodeAssert(
174
180
  (cmd) => captureAssert(getCapturer(), getTestUid(), cmd),
175
181
  (level, message) => log[level](message),
176
182
  // No handle resolver: Nightwatch commands take selector strings, so an
177
183
  // element object never reaches an assert — only the value's provenance,
178
184
  // recorded by the command path (BrowserProxy), can name the target.
179
- (args) => resolveAssertTargetFromArgs(args)
185
+ (args) => {
186
+ scopeReadValues();
187
+ return resolveAssertTargetFromArgs(args);
188
+ }
180
189
  );
181
190
  }
182
191
  function captureAssert(capturer, testUid, cmd) {
@@ -201,7 +210,7 @@ function captureAssert(capturer, testUid, cmd) {
201
210
 
202
211
  // src/index.ts
203
212
  import { stop as stopBackend } from "@wdio/devtools-backend";
204
- import logger15 from "@wdio/logger";
213
+ import logger16 from "@wdio/logger";
205
214
 
206
215
  // src/run-lifecycle.ts
207
216
  import * as fs2 from "fs";
@@ -258,6 +267,7 @@ var TIMING = {
258
267
  };
259
268
  var BOOLEAN_COMMAND_PATTERN = /^waitFor|^is[A-Z]|^has[A-Z]|(Visible|Present|Enabled|Selected|NotVisible|NotPresent)$/;
260
269
  var NAVIGATION_COMMANDS = ["url", "navigate", "navigateTo"];
270
+ var NIGHTWATCH_CONFIG_FLAGS = ["--config", "-c"];
261
271
  var CONFIG_FILENAMES = [
262
272
  "nightwatch.conf.cjs",
263
273
  "nightwatch.conf.js",
@@ -271,10 +281,13 @@ import * as fs from "fs";
271
281
  import * as path from "path";
272
282
  import { parse as parseStackTrace } from "stacktrace-parser";
273
283
  function determineTestState(testcase) {
274
- 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) {
275
288
  return "skipped";
276
289
  }
277
- return testcase.passed > 0 && testcase.failed === 0 ? "passed" : "failed";
290
+ return "passed";
278
291
  }
279
292
  function getTestIcon(state) {
280
293
  return state === "passed" ? "\u2705" : state === "skipped" ? "\u23ED" : "\u274C";
@@ -295,13 +308,53 @@ function buildPluginMetadataOptions(input) {
295
308
  return {
296
309
  framework: nightwatchRunnerId(input.isCucumberRunner),
297
310
  configFile: input.configPath,
298
- baseDir: process.cwd(),
299
- runCapabilities: {
300
- canRunSuites: true,
301
- canRunTests: !input.isCucumberRunner,
302
- canRunAll: false
303
- }
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;
304
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;
305
358
  }
306
359
  function generateStableUid2(itemOrFile, name) {
307
360
  let file, testName;
@@ -522,6 +575,13 @@ async function runPluginBefore(ctx) {
522
575
  ctx.options.port,
523
576
  ctx.options.hostname
524
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();
525
585
  log2.info("\u{1F680} Starting DevTools backend...");
526
586
  const { port } = await start(ctx.options);
527
587
  ctx.options.port = port;
@@ -639,7 +699,7 @@ async function waitForDevtoolsBrowserClose(ctx) {
639
699
  }
640
700
 
641
701
  // src/session-init.ts
642
- import logger10 from "@wdio/logger";
702
+ import logger11 from "@wdio/logger";
643
703
 
644
704
  // src/helpers/webdriverHttp.ts
645
705
  import logger3 from "@wdio/logger";
@@ -674,11 +734,29 @@ function configuredWebDriver(browser) {
674
734
  getProp(opts, "webdriver"),
675
735
  getProp(nightwatchSettings, "webdriver")
676
736
  ];
677
- const pick = (...keys) => sources.map((source) => firstDefined(source, ...keys)).find(Boolean);
737
+ const values = (...keys) => sources.map((source) => firstDefined(source, ...keys));
738
+ const pick = (...keys) => values(...keys).find(Boolean);
678
739
  return {
679
740
  host: pick("host", "server_address"),
680
741
  port: pick("port"),
681
- pathPrefix: pick("default_path_prefix")
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
+ };
748
+ }
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;
753
+ return {
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) } : {},
759
+ onWarn: (message) => log3.warn(message)
682
760
  };
683
761
  }
684
762
  function resolveWebDriverAddress(browser) {
@@ -690,11 +768,7 @@ function resolveWebDriverAddress(browser) {
690
768
  }
691
769
  function address(browser) {
692
770
  const { driverHost, driverPort } = resolveWebDriverAddress(browser);
693
- return {
694
- hostname: driverHost,
695
- port: driverPort,
696
- onWarn: (message) => log3.warn(message)
697
- };
771
+ return connection(configuredWebDriver(browser), driverHost, driverPort);
698
772
  }
699
773
  function sessionId(browser) {
700
774
  return browser.sessionId;
@@ -709,20 +783,19 @@ function webdriverPost2(browser, path5, body) {
709
783
  }
710
784
  function sessionExecutor(browser) {
711
785
  const id = sessionId(browser);
712
- const { host, port, pathPrefix } = configuredWebDriver(browser);
713
- if (!id || !port) {
786
+ const configured = configuredWebDriver(browser);
787
+ if (!id || !configured.port) {
714
788
  return void 0;
715
789
  }
716
- const at = {
717
- hostname: String(host || "localhost"),
718
- port: Number(port),
719
- path: typeof pathPrefix === "string" ? pathPrefix : void 0,
720
- onWarn: (message) => log3.warn(message)
721
- };
790
+ const at = connection(
791
+ configured,
792
+ String(configured.host || "localhost"),
793
+ Number(configured.port)
794
+ );
722
795
  return (script, args) => webdriverExecute(at, id, script, args);
723
796
  }
724
- function webdriverExecute2(browser, body) {
725
- return webdriverPost2(browser, "execute/sync", { script: body, args: [] });
797
+ function webdriverExecute2(browser, body, args = []) {
798
+ return webdriverPost2(browser, "execute/sync", { script: body, args });
726
799
  }
727
800
 
728
801
  // src/session.ts
@@ -960,12 +1033,6 @@ function dedupeNetworkRequests(incoming, existing) {
960
1033
 
961
1034
  // src/session.ts
962
1035
  var log4 = logger4("@wdio/nightwatch-devtools:SessionCapturer");
963
- function unwrapDriverValue(result) {
964
- if (result && typeof result === "object" && "value" in result) {
965
- return result.value;
966
- }
967
- return result;
968
- }
969
1036
  var SessionCapturer = class extends SessionCapturerBase {
970
1037
  #browser;
971
1038
  // True once BiDi inspectors are attached — the per-command perf-log network
@@ -1029,6 +1096,7 @@ var SessionCapturer = class extends SessionCapturerBase {
1029
1096
  const commandId = this.commandCounter++;
1030
1097
  const commandLogEntry = {
1031
1098
  _id: commandId,
1099
+ id: commandId,
1032
1100
  command,
1033
1101
  args,
1034
1102
  result,
@@ -1038,7 +1106,8 @@ var SessionCapturer = class extends SessionCapturerBase {
1038
1106
  sequence: meta.sequence,
1039
1107
  callSource: meta.callSource,
1040
1108
  testUid: meta.testUid,
1041
- selector: meta.selector
1109
+ selector: meta.selector,
1110
+ parentId: meta.parentId
1042
1111
  };
1043
1112
  this.commandsLog.push(commandLogEntry);
1044
1113
  const isNavigationCommand = NAVIGATION_COMMANDS.some(
@@ -1081,15 +1150,18 @@ var SessionCapturer = class extends SessionCapturerBase {
1081
1150
  return;
1082
1151
  }
1083
1152
  await new Promise((resolve2) => setTimeout(resolve2, 500));
1084
- const raw = await this.#browser.execute(CAPTURE_PERFORMANCE_SCRIPT);
1085
- const payload = unwrapDriverValue(
1086
- raw
1153
+ const payload = await webdriverExecute2(
1154
+ this.#browser,
1155
+ CAPTURE_PERFORMANCE_SCRIPT
1087
1156
  );
1088
- applyPerformanceData(
1157
+ if (applyPerformanceData(
1089
1158
  commandLogEntry,
1090
- payload,
1159
+ payload ?? void 0,
1091
1160
  typeof args[0] === "string" ? args[0] : void 0
1092
- );
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
+ }
1093
1165
  }
1094
1166
  /**
1095
1167
  * Ingest a pre-built assertion entry (native `browser.assert`/`verify`
@@ -1119,7 +1191,8 @@ var SessionCapturer = class extends SessionCapturerBase {
1119
1191
  const idx = this.commandsLog.findIndex(
1120
1192
  (c) => c._id === oldId
1121
1193
  );
1122
- 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;
1123
1196
  if (idx !== -1) {
1124
1197
  this.commandsLog.splice(idx, 1);
1125
1198
  }
@@ -1128,6 +1201,7 @@ var SessionCapturer = class extends SessionCapturerBase {
1128
1201
  const commandId = this.commandCounter++;
1129
1202
  const entry = {
1130
1203
  _id: commandId,
1204
+ id: previous?.id ?? commandId,
1131
1205
  command,
1132
1206
  args,
1133
1207
  result,
@@ -1137,11 +1211,61 @@ var SessionCapturer = class extends SessionCapturerBase {
1137
1211
  sequence: meta.sequence,
1138
1212
  callSource: meta.callSource,
1139
1213
  testUid: meta.testUid,
1140
- selector: meta.selector
1214
+ selector: meta.selector,
1215
+ parentId: meta.parentId
1141
1216
  };
1142
1217
  this.commandsLog.push(entry);
1143
1218
  return { entry, oldTimestamp };
1144
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
+ }
1145
1269
  /** Birth time of the document this capturer last observed, so a replacement
1146
1270
  * is still visible to a poll that starts after the navigation committed. */
1147
1271
  lastDocumentOrigin;
@@ -1197,11 +1321,7 @@ var SessionCapturer = class extends SessionCapturerBase {
1197
1321
  if (!replaced) {
1198
1322
  return;
1199
1323
  }
1200
- await this.captureTrace(
1201
- browser,
1202
- true,
1203
- this.commandsLog[this.commandsLog.length - 1]?.timestamp
1204
- );
1324
+ await this.captureTrace(browser, true, this.#newestEndedTimestamp());
1205
1325
  }
1206
1326
  /**
1207
1327
  * Take a screenshot by calling the WebDriver HTTP endpoint directly, bypassing
@@ -1233,23 +1353,21 @@ var SessionCapturer = class extends SessionCapturerBase {
1233
1353
  try {
1234
1354
  const alreadyPresent = await webdriverExecute2(
1235
1355
  browser,
1236
- `return ${COLLECTOR_READY_EXPRESSION};`
1356
+ COLLECTOR_READY_SCRIPT
1237
1357
  );
1238
1358
  if (alreadyPresent === true) {
1239
1359
  return;
1240
1360
  }
1241
1361
  const scriptContent = await loadInjectableScript();
1242
- const injectionScript = `
1243
- const script = document.createElement('script');
1244
- script.textContent = ${JSON.stringify(scriptContent)};
1245
- document.head.appendChild(script);
1246
- return ${COLLECTOR_READY_EXPRESSION};
1247
- `;
1248
- const injected = await webdriverExecute2(browser, injectionScript);
1362
+ const injected = await webdriverExecute2(
1363
+ browser,
1364
+ COLLECTOR_INJECT_SCRIPT,
1365
+ [scriptContent]
1366
+ );
1249
1367
  const hasCollector = injected === true || await pollUntilReady(
1250
1368
  async () => await webdriverExecute2(
1251
1369
  browser,
1252
- `return ${COLLECTOR_READY_EXPRESSION};`
1370
+ COLLECTOR_READY_SCRIPT
1253
1371
  ) === true,
1254
1372
  { checkFirst: true }
1255
1373
  );
@@ -1263,23 +1381,6 @@ var SessionCapturer = class extends SessionCapturerBase {
1263
1381
  throw err;
1264
1382
  }
1265
1383
  }
1266
- /**
1267
- * Capture Chrome DevTools browser console logs via WebDriver log API.
1268
- * Requires loggingPrefs: { browser: 'ALL' } in Chrome capabilities.
1269
- */
1270
- async captureBrowserLogs(browser) {
1271
- try {
1272
- const rawLogs = await browser.getLog("browser");
1273
- const logs = unwrapDriverValue(rawLogs);
1274
- if (!Array.isArray(logs) || logs.length === 0) {
1275
- return;
1276
- }
1277
- const entries = mapChromeBrowserLogs(logs);
1278
- this.consoleLogs.push(...entries);
1279
- this.sendUpstream("consoleLogs", entries);
1280
- } catch {
1281
- }
1282
- }
1283
1384
  /**
1284
1385
  * Parse Chrome performance logs to extract network request entries.
1285
1386
  * Requires loggingPrefs: { performance: 'ALL' } in Chrome capabilities.
@@ -1288,21 +1389,15 @@ var SessionCapturer = class extends SessionCapturerBase {
1288
1389
  if (this.bidiActive || !this.perfLogsRequested) {
1289
1390
  return;
1290
1391
  }
1291
- try {
1292
- const rawLogs = await browser.getLog("performance");
1293
- const logs = unwrapDriverValue(rawLogs);
1294
- if (!Array.isArray(logs) || logs.length === 0) {
1295
- return;
1296
- }
1297
- const networkEntries = parseNetworkFromPerfLogs(logs);
1298
- if (networkEntries.length > 0) {
1299
- this.#ingestPerfLogEntries(networkEntries);
1300
- }
1301
- } catch (err) {
1302
- const msg = errorMessage(err) ?? "";
1303
- if (!msg.includes("log type") && !msg.includes("performance")) {
1304
- log4.warn(`Performance log capture failed: ${msg}`);
1305
- }
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);
1306
1401
  }
1307
1402
  }
1308
1403
  /** A log entry for a load the page already reported replaces that row in
@@ -1777,7 +1872,7 @@ var SuiteManager = class {
1777
1872
  };
1778
1873
 
1779
1874
  // src/helpers/browserProxy.ts
1780
- import logger7 from "@wdio/logger";
1875
+ import logger8 from "@wdio/logger";
1781
1876
 
1782
1877
  // src/helpers/callbackError.ts
1783
1878
  function callbackError(result, depth = 0) {
@@ -2279,8 +2374,126 @@ var NativeAssertRecorder = class {
2279
2374
  }
2280
2375
  };
2281
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
+
2282
2495
  // src/helpers/browserProxy.ts
2283
- var log7 = logger7("@wdio/nightwatch-devtools:browserProxy");
2496
+ var log8 = logger8("@wdio/nightwatch-devtools:browserProxy");
2284
2497
  var BrowserProxy = class {
2285
2498
  constructor(sessionCapturer, testManager, getCurrentTest, captureAssertions = true, observeSession) {
2286
2499
  this.sessionCapturer = sessionCapturer;
@@ -2288,6 +2501,7 @@ var BrowserProxy = class {
2288
2501
  this.getCurrentTest = getCurrentTest;
2289
2502
  this.captureAssertions = captureAssertions;
2290
2503
  this.observeSession = observeSession;
2504
+ this.waits = new WaitRows(sessionCapturer);
2291
2505
  this.assertRecorder = new NativeAssertRecorder(
2292
2506
  sessionCapturer,
2293
2507
  () => this.rowTest(),
@@ -2310,6 +2524,7 @@ var BrowserProxy = class {
2310
2524
  * rather than appending, showing only the final execution result.
2311
2525
  */
2312
2526
  retryTracker = new RetryTracker();
2527
+ waits;
2313
2528
  /**
2314
2529
  * Monotonic issue-order counter. Stamped when the test ISSUES a row — a
2315
2530
  * driver command at invocation, an assert at enqueue — which is source order.
@@ -2327,6 +2542,8 @@ var BrowserProxy = class {
2327
2542
  * the assert path, which runs outside any command callback.
2328
2543
  */
2329
2544
  browser = null;
2545
+ /** The test core's value registry currently holds reads for; see {@link scopeReadValuesToRowTest}. */
2546
+ readValueTestUid;
2330
2547
  /**
2331
2548
  * The test a captured row belongs to: the `it` Nightwatch reports as running
2332
2549
  * right now, falling back to the lifecycle test. On the BDD `describe/it`
@@ -2345,6 +2562,19 @@ var BrowserProxy = class {
2345
2562
  rowTestUid() {
2346
2563
  return this.rowTest()?.uid;
2347
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
+ }
2348
2578
  /** Per-command-scope dedup state only. Cucumber calls this per STEP, so it
2349
2579
  * must not touch the native-assert buffer — that buffer spans the whole
2350
2580
  * scenario and is drained once, at the scenario's pre-quit hook. */
@@ -2358,6 +2588,8 @@ var BrowserProxy = class {
2358
2588
  resetTestTracking() {
2359
2589
  this.resetCommandTracking();
2360
2590
  this.assertRecorder.clear();
2591
+ clearReadValues();
2592
+ this.readValueTestUid = void 0;
2361
2593
  }
2362
2594
  /** Hand off this test's recorded native-assertion calls and clear the
2363
2595
  * buffer so the next test starts fresh. */
@@ -2370,47 +2602,9 @@ var BrowserProxy = class {
2370
2602
  setCurrentTestFullPath(path5) {
2371
2603
  this.currentTestFullPath = path5;
2372
2604
  }
2373
- /**
2374
- * Wrap browser navigation methods (url / navigate / navigateTo) to inject
2375
- * the DevTools script after every navigation.
2376
- *
2377
- * Uses `browser` from the closure (not `this` inside perform) so it works
2378
- * for both standard Nightwatch (chainable API) and Cucumber async/await mode
2379
- * where `this` inside a perform callback is not the browser.
2380
- */
2605
+ /** Inject the DevTools script after every navigation; see {@link wrapNavigation}. */
2381
2606
  wrapUrlMethod(browser) {
2382
- const sessionCapturer = this.sessionCapturer;
2383
- const b = browser;
2384
- const wrapNav = (methodName) => {
2385
- if (typeof b[methodName] !== "function") {
2386
- return;
2387
- }
2388
- const original = b[methodName].bind(browser);
2389
- b[methodName] = function(...args) {
2390
- if (typeof args[0] !== "string") {
2391
- return original(...args);
2392
- }
2393
- const result = original(...args);
2394
- const injectAndCapture = () => {
2395
- log7.info(`[nav] ${methodName}(${args[0] ?? ""}) \u2014 injecting script`);
2396
- return sessionCapturer.injectScript(browser).then(() => sessionCapturer.captureTrace(browser, true)).catch(
2397
- (err) => log7.error(`Failed to inject script: ${err.message}`)
2398
- );
2399
- };
2400
- const chainable = result;
2401
- if (chainable && typeof chainable.perform === "function") {
2402
- void Promise.resolve(result).then(injectAndCapture).catch(() => {
2403
- });
2404
- return result;
2405
- }
2406
- return Promise.resolve(result).then(injectAndCapture).catch(() => {
2407
- });
2408
- };
2409
- };
2410
- wrapNav("url");
2411
- wrapNav("navigate");
2412
- wrapNav("navigateTo");
2413
- log7.info("\u2713 Script injection wrapped");
2607
+ wrapNavigation(browser, this.sessionCapturer);
2414
2608
  }
2415
2609
  wrapBrowserCommands(browser) {
2416
2610
  this.browser = browser;
@@ -2448,39 +2642,81 @@ var BrowserProxy = class {
2448
2642
  this.assertRecorder.wrapNamespaces(browser);
2449
2643
  }
2450
2644
  this.proxiedBrowsers.add(browser);
2451
- log7.info(`\u2713 Wrapped ${wrappedMethods.length} browser methods`);
2645
+ log8.info(`\u2713 Wrapped ${wrappedMethods.length} browser methods`);
2452
2646
  }
2453
- handleRetryReplacement(browser, methodName, logArgs, serializedResult, meta, commandError) {
2647
+ handleRetryReplacement(browser, oldId, methodName, logArgs, serializedResult, meta, commandError) {
2454
2648
  const { entry, oldTimestamp } = this.sessionCapturer.replaceCommand(
2455
- this.retryTracker.lastId,
2649
+ oldId,
2456
2650
  methodName,
2457
2651
  logArgs,
2458
2652
  serializedResult,
2459
2653
  commandError,
2460
2654
  meta
2461
2655
  );
2462
- this.retryTracker.setLastId(entry._id ?? null);
2463
2656
  this.sessionCapturer.sendReplaceCommand(oldTimestamp, entry);
2464
2657
  this.attachScreenshot(browser, entry, methodName, " (retry)");
2658
+ return entry._id ?? null;
2465
2659
  }
2466
- captureFreshCommand(browser, methodName, logArgs, serializedResult, meta, cmdSig, commandError) {
2467
- this.retryTracker.setLastSig(cmdSig);
2468
- this.retryTracker.setLastId(null);
2660
+ captureFreshCommand(browser, methodName, logArgs, serializedResult, meta, commandError) {
2469
2661
  this.drainOutgoingPage(browser, methodName);
2470
2662
  this.sessionCapturer.captureCommand(methodName, logArgs, serializedResult, commandError, meta).catch(
2471
- (err) => log7.error(`Failed to capture ${methodName}: ${err.message}`)
2663
+ (err) => log8.error(`Failed to capture ${methodName}: ${err.message}`)
2472
2664
  );
2473
2665
  const lastCommand = this.sessionCapturer.commandsLog[this.sessionCapturer.commandsLog.length - 1];
2474
2666
  if (lastCommand) {
2475
- this.retryTracker.setLastId(lastCommand._id ?? null);
2476
2667
  this.sessionCapturer.sendCommand(lastCommand);
2477
- log7.info(`[command] ${methodName}`);
2668
+ log8.info(`[command] ${methodName}`);
2478
2669
  this.attachScreenshot(browser, lastCommand, methodName);
2479
2670
  }
2480
2671
  this.anchorDestination(browser, methodName);
2481
2672
  if (!this.isDomMutating(methodName)) {
2482
2673
  void this.sessionCapturer.nativeNetwork.forward({ throttle: true });
2483
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 });
2484
2720
  }
2485
2721
  isDomMutating(methodName) {
2486
2722
  return NAVIGATION_COMMANDS.includes(methodName) || [
@@ -2550,47 +2786,44 @@ var BrowserProxy = class {
2550
2786
  * into one call because `makeCaptureCallback` is already over the line cap. */
2551
2787
  serializeAndAttributeResult(methodName, logArgs, callbackResult) {
2552
2788
  const result = serializeCommandResult(callbackResult, methodName);
2789
+ this.scopeReadValuesToRowTest();
2553
2790
  rememberReadValue(result, commandTargetSelector(methodName, logArgs));
2554
2791
  return result;
2555
2792
  }
2556
2793
  // Result-capturing callback factory — called by Nightwatch's async queue
2557
2794
  // when the command completes. This is where we get the *actual* result.
2558
- makeCaptureCallback(browser, methodName, logArgs, cmdSig, callSource, hasUserSource, invokedAt, sequence, testUid, userCallback) {
2795
+ makeCaptureCallback(browser, cmd) {
2796
+ const { methodName, logArgs, cmdSig, userCallback } = cmd;
2559
2797
  return (callbackResult) => {
2560
2798
  const commandTimestamp = Date.now();
2561
2799
  this.popCommandStackIfMatches(methodName, cmdSig);
2562
2800
  const commandError = callbackError(callbackResult);
2563
2801
  const serializedResult = commandError ? void 0 : this.serializeAndAttributeResult(methodName, logArgs, callbackResult);
2564
- const effectiveUid = this.rowTestUid() ?? testUid;
2565
- if (effectiveUid && hasUserSource) {
2566
- const meta = {
2567
- testUid: effectiveUid,
2568
- callSource,
2569
- timestamp: commandTimestamp,
2570
- startTime: invokedAt,
2571
- sequence,
2572
- selector: commandRowSelector(methodName, logArgs)
2573
- };
2574
- if (this.retryTracker.isRetry(cmdSig)) {
2575
- this.handleRetryReplacement(
2576
- browser,
2577
- methodName,
2578
- logArgs,
2579
- serializedResult,
2580
- meta,
2581
- commandError
2582
- );
2583
- } else {
2584
- this.captureFreshCommand(
2585
- browser,
2586
- methodName,
2587
- logArgs,
2588
- serializedResult,
2589
- meta,
2590
- cmdSig,
2591
- commandError
2592
- );
2593
- }
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
+ );
2594
2827
  }
2595
2828
  if (userCallback) {
2596
2829
  return userCallback(callbackResult);
@@ -2614,7 +2847,7 @@ var BrowserProxy = class {
2614
2847
  this.currentTestFullPath = callInfo.filePath;
2615
2848
  }
2616
2849
  const lastArg = args[args.length - 1];
2617
- const hasUserCallback = typeof lastArg === "function";
2850
+ const hasUserCallback = typeof lastArg === "function" && !(methodName === "waitUntil" && args.length === 1);
2618
2851
  const logArgs = hasUserCallback ? args.slice(0, -1) : args;
2619
2852
  const cmdSig = JSON.stringify({
2620
2853
  command: methodName,
@@ -2637,49 +2870,59 @@ var BrowserProxy = class {
2637
2870
  browserAny.currentTest
2638
2871
  )
2639
2872
  );
2640
- const { userCallback, logArgs, cmdSig, callSource, hasUserSource } = this.describeInvocation(methodName, args);
2641
- const invokedAt = Date.now();
2642
- const sequence = this.issueCounter++;
2643
- const captureCallback = this.makeCaptureCallback(
2644
- browser,
2645
- methodName,
2646
- logArgs,
2647
- cmdSig,
2648
- callSource,
2649
- hasUserSource,
2650
- invokedAt,
2651
- sequence,
2652
- this.rowTestUid(),
2653
- userCallback
2654
- );
2873
+ const cmd = this.pendingCommand(methodName, args);
2874
+ const captureCallback = this.makeCaptureCallback(browser, cmd);
2655
2875
  const closeInputWindow = beginInputDispatch(methodName);
2656
2876
  const gatedCallback = (callbackResult) => {
2657
2877
  closeInputWindow();
2658
2878
  return captureCallback(callbackResult);
2659
2879
  };
2660
- const modifiedArgs = [...logArgs, gatedCallback];
2880
+ const modifiedArgs = [...this.waits.args(cmd), gatedCallback];
2661
2881
  try {
2662
2882
  return originalMethod(...modifiedArgs);
2663
2883
  } catch (error) {
2664
2884
  closeInputWindow();
2665
- this.popCommandStackIfMatches(methodName, cmdSig);
2666
- 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
+ }
2667
2891
  throw error;
2668
2892
  }
2669
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
+ }
2670
2913
  captureCommandError(methodName, args, error, callSource) {
2671
2914
  const currentTest = this.rowTest();
2672
2915
  if (!currentTest) {
2673
2916
  return;
2674
2917
  }
2675
2918
  const normalizedError = toError(error);
2676
- log7.error(`[command error] ${methodName}: ${normalizedError.message}`);
2919
+ log8.error(`[command error] ${methodName}: ${normalizedError.message}`);
2677
2920
  this.sessionCapturer.captureCommand(methodName, args, void 0, normalizedError, {
2678
2921
  testUid: currentTest.uid,
2679
2922
  callSource,
2680
2923
  selector: commandRowSelector(methodName, args)
2681
2924
  }).catch(
2682
- (err) => log7.error(`Failed to capture ${methodName}: ${err.message}`)
2925
+ (err) => log8.error(`Failed to capture ${methodName}: ${err.message}`)
2683
2926
  );
2684
2927
  const lastCommand = this.sessionCapturer.commandsLog[this.sessionCapturer.commandsLog.length - 1];
2685
2928
  if (lastCommand) {
@@ -2699,10 +2942,10 @@ var BrowserProxy = class {
2699
2942
  attachScreenshot(browser, entry, methodName, suffix = "") {
2700
2943
  const ts = entry.timestamp ?? 0;
2701
2944
  this.sessionCapturer.takeScreenshotViaHttp(browser).then((screenshot) => {
2702
- if (screenshot) {
2945
+ if (screenshot && this.sessionCapturer.commandsLog.includes(entry)) {
2703
2946
  entry.screenshot = screenshot;
2704
2947
  this.sessionCapturer.sendReplaceCommand(ts, entry);
2705
- log7.info(`[screenshot] Attached to ${methodName}${suffix}`);
2948
+ log8.info(`[screenshot] Attached to ${methodName}${suffix}`);
2706
2949
  }
2707
2950
  }).catch(() => {
2708
2951
  });
@@ -2710,11 +2953,11 @@ var BrowserProxy = class {
2710
2953
  };
2711
2954
 
2712
2955
  // src/screencast-session.ts
2713
- import logger9 from "@wdio/logger";
2956
+ import logger10 from "@wdio/logger";
2714
2957
 
2715
2958
  // src/screencast.ts
2716
- import logger8 from "@wdio/logger";
2717
- var log8 = logger8("@wdio/nightwatch-devtools:ScreencastRecorder");
2959
+ import logger9 from "@wdio/logger";
2960
+ var log9 = logger9("@wdio/nightwatch-devtools:ScreencastRecorder");
2718
2961
  var ScreencastRecorder = class extends ScreencastRecorderBase {
2719
2962
  #sessionCapturer;
2720
2963
  constructor(sessionCapturer, options) {
@@ -2722,15 +2965,15 @@ var ScreencastRecorder = class extends ScreencastRecorderBase {
2722
2965
  this.#sessionCapturer = sessionCapturer;
2723
2966
  }
2724
2967
  onPollingStarted(intervalMs) {
2725
- log8.info(
2968
+ log9.info(
2726
2969
  `\u2713 Screencast recording started (polling mode, ${intervalMs} ms interval)`
2727
2970
  );
2728
2971
  }
2729
2972
  onPollingStopped(frameCount) {
2730
- log8.info(`\u2713 Screencast stopped \u2014 ${frameCount} frame(s) collected`);
2973
+ log9.info(`\u2713 Screencast stopped \u2014 ${frameCount} frame(s) collected`);
2731
2974
  }
2732
2975
  onUnavailable(err) {
2733
- log8.warn(
2976
+ log9.warn(
2734
2977
  `Screencast unavailable (${errorMessage(err)}). Recording skipped.`
2735
2978
  );
2736
2979
  }
@@ -2744,7 +2987,7 @@ var ScreencastRecorder = class extends ScreencastRecorderBase {
2744
2987
  };
2745
2988
 
2746
2989
  // src/screencast-session.ts
2747
- var log9 = logger9("@wdio/nightwatch-devtools:screencast-session");
2990
+ var log10 = logger10("@wdio/nightwatch-devtools:screencast-session");
2748
2991
  async function startScreencast(ctx, browser, sessionId2) {
2749
2992
  if (!ctx.screencastOptions.enabled || ctx.screencastRecorder || !sessionId2) {
2750
2993
  return;
@@ -2754,7 +2997,7 @@ async function startScreencast(ctx, browser, sessionId2) {
2754
2997
  ctx.screencastOptions
2755
2998
  );
2756
2999
  ctx.screencastSessionId = sessionId2;
2757
- log9.info(`\u{1F3AC} Starting screencast for session ${sessionId2}`);
3000
+ log10.info(`\u{1F3AC} Starting screencast for session ${sessionId2}`);
2758
3001
  await ctx.screencastRecorder.start(browser);
2759
3002
  }
2760
3003
  function needsScreencastRotation(ctx, sessionId2) {
@@ -2768,16 +3011,16 @@ function rotateScreencastForSession(ctx, browser) {
2768
3011
  if (!needsScreencastRotation(ctx, sessionId2)) {
2769
3012
  return;
2770
3013
  }
2771
- log9.info(`\u{1F3AC} Session replaced \u2014 rotating screencast to ${sessionId2}`);
3014
+ log10.info(`\u{1F3AC} Session replaced \u2014 rotating screencast to ${sessionId2}`);
2772
3015
  ctx.screencastRotation = ctx.finalizeCurrentScreencast().then(() => startScreencast(ctx, browser, sessionId2)).catch(
2773
- (err) => log9.warn(`Screencast rotation failed: ${errorMessage(err)}`)
3016
+ (err) => log10.warn(`Screencast rotation failed: ${errorMessage(err)}`)
2774
3017
  ).finally(() => {
2775
3018
  ctx.screencastRotation = void 0;
2776
3019
  });
2777
3020
  }
2778
3021
 
2779
3022
  // src/session-init.ts
2780
- var log10 = logger10("@wdio/nightwatch-devtools:session-init");
3023
+ var log11 = logger11("@wdio/nightwatch-devtools:session-init");
2781
3024
  function initReporterChain(ctx) {
2782
3025
  ctx.testReporter = new TestReporter(
2783
3026
  (suitesData) => {
@@ -2811,7 +3054,7 @@ function readViewport(browser) {
2811
3054
  return resolveViewport(browser.capabilities || {}, {
2812
3055
  runScript: (body) => webdriverExecute2(browser, body),
2813
3056
  getWindowSize: () => webdriverGet2(browser, "window/rect"),
2814
- onWarn: (message) => log10.warn(message)
3057
+ onWarn: (message) => log11.warn(message)
2815
3058
  });
2816
3059
  }
2817
3060
  function wantsPerformanceLog(capabilities, desiredCapabilities) {
@@ -2844,7 +3087,7 @@ async function broadcastSessionMetadata(ctx, browser) {
2844
3087
  ctx.sessionCapturer.sendUpstream("metadata", metadata);
2845
3088
  const browserName = capabilities.browserName || desiredCapabilities.browserName || "unknown";
2846
3089
  const browserVersion = capabilities.browserVersion || capabilities.version || "";
2847
- log10.info(
3090
+ log11.info(
2848
3091
  `\u2713 Browser: ${browserName}${browserVersion ? " " + browserVersion : ""} (session: ${sessionId2})`
2849
3092
  );
2850
3093
  if (ctx.mode === "live" && restoreBrowserFocus({
@@ -2853,9 +3096,9 @@ async function broadcastSessionMetadata(ctx, browser) {
2853
3096
  capabilities.platformName || desiredCapabilities.platformName || ""
2854
3097
  ),
2855
3098
  host: resolveWebDriverAddress(browser).driverHost,
2856
- onError: (message) => log10.warn(message)
3099
+ onError: (message) => log11.warn(message)
2857
3100
  })) {
2858
- log10.info(
3101
+ log11.info(
2859
3102
  "\u2713 Safari brought back to the front (it delivers clicks only there)"
2860
3103
  );
2861
3104
  }
@@ -2865,7 +3108,7 @@ async function broadcastSessionMetadata(ctx, browser) {
2865
3108
  );
2866
3109
  ctx.sessionCapturer.perfLogsRequested = wantsPerfLog;
2867
3110
  if (!wantsPerfLog && !ctx.bidiEnabled) {
2868
- log10.warn(
3111
+ log11.warn(
2869
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"
2870
3113
  );
2871
3114
  }
@@ -2876,17 +3119,17 @@ async function tryAttachBidi(ctx, browser) {
2876
3119
  }
2877
3120
  const driver = browser.driver;
2878
3121
  if (!driver) {
2879
- log10.warn("bidi:true set but browser.driver unavailable \u2014 skipping");
3122
+ log11.warn("bidi:true set but browser.driver unavailable \u2014 skipping");
2880
3123
  return;
2881
3124
  }
2882
- const { attachBidiHandlers, buildBidiSinks } = await import("./bidi-AZX3WRGJ.js");
3125
+ const { attachBidiHandlers, buildBidiSinks } = await import("./bidi-C67DYUCA.js");
2883
3126
  const ok = await attachBidiHandlers(
2884
3127
  driver,
2885
3128
  buildBidiSinks(ctx.sessionCapturer)
2886
3129
  );
2887
3130
  if (ok) {
2888
3131
  ctx.sessionCapturer.bidiActive = true;
2889
- log10.info("\u2713 BiDi attached \u2014 perf-log network capture disabled");
3132
+ log11.info("\u2713 BiDi attached \u2014 perf-log network capture disabled");
2890
3133
  }
2891
3134
  }
2892
3135
  async function tryRegisterPreload(ctx, browser) {
@@ -2896,7 +3139,7 @@ async function tryRegisterPreload(ctx, browser) {
2896
3139
  }
2897
3140
  ctx.sessionCapturer.preloadRegistered = await registerCollectorPreload(
2898
3141
  driver,
2899
- (level, message) => log10[level](message)
3142
+ (level, message) => log11[level](message)
2900
3143
  );
2901
3144
  }
2902
3145
  async function armCaptureForSession(ctx, browser) {
@@ -2923,16 +3166,16 @@ async function armReplacedSession(ctx, browser) {
2923
3166
  ctx.sessionCapturer.bidiNetworkObserved = false;
2924
3167
  ctx.sessionCapturer.loadsListedByLog = [];
2925
3168
  ctx.sessionCapturer.nativeNetwork.stop();
2926
- log10.info(`Session replaced \u2014 re-arming capture for ${sessionId2}`);
3169
+ log11.info(`Session replaced \u2014 re-arming capture for ${sessionId2}`);
2927
3170
  await armCaptureForSession(ctx, browser);
2928
3171
  }
2929
3172
  function rearmCaptureForSession(ctx, browser) {
2930
3173
  void armReplacedSession(ctx, browser).catch(
2931
- (err) => log10.warn(`Capture re-arm failed: ${errorMessage(err)}`)
3174
+ (err) => log11.warn(`Capture re-arm failed: ${errorMessage(err)}`)
2932
3175
  );
2933
3176
  }
2934
3177
  async function rebindSessionToBrowser(ctx, browser) {
2935
- log10.info("Browser session changed \u2014 re-targeting the run capturer");
3178
+ log11.info("Browser session changed \u2014 re-targeting the run capturer");
2936
3179
  ctx.isScriptInjected = false;
2937
3180
  await broadcastSessionMetadata(ctx, browser);
2938
3181
  await armReplacedSession(ctx, browser);
@@ -2962,7 +3205,7 @@ async function ensureSessionInitialized(ctx, browser) {
2962
3205
  if (ctx.mode !== "trace") {
2963
3206
  const connected = await ctx.sessionCapturer.waitForConnection(3e3);
2964
3207
  if (!connected) {
2965
- log10.error("\u274C Worker WebSocket failed to connect!");
3208
+ log11.error("\u274C Worker WebSocket failed to connect!");
2966
3209
  }
2967
3210
  }
2968
3211
  initReporterChain(ctx);
@@ -2987,7 +3230,7 @@ async function finalizeCurrentScreencast(ctx) {
2987
3230
  }),
2988
3231
  captureFormat: ctx.screencastOptions.captureFormat,
2989
3232
  sendUpstream: (scope, data) => ctx.sessionCapturer?.sendUpstream(scope, data),
2990
- onLog: (level, message) => log10[level](message)
3233
+ onLog: (level, message) => log11[level](message)
2991
3234
  });
2992
3235
  }
2993
3236
  ctx.screencastRecorder = void 0;
@@ -3045,8 +3288,8 @@ var PluginScreencast = class {
3045
3288
  };
3046
3289
 
3047
3290
  // src/event-hub.ts
3048
- import logger11 from "@wdio/logger";
3049
- var log11 = logger11("@wdio/nightwatch-devtools:event-hub");
3291
+ import logger12 from "@wdio/logger";
3292
+ var log12 = logger12("@wdio/nightwatch-devtools:event-hub");
3050
3293
  function makeSessionMetadataHandler(bindings) {
3051
3294
  return (data) => {
3052
3295
  try {
@@ -3067,23 +3310,23 @@ function makeSessionMetadataHandler(bindings) {
3067
3310
  options: bindings.buildMetadataOptions()
3068
3311
  });
3069
3312
  } catch (err) {
3070
- log11.error(`Error in event handler: ${errorMessage(err)}`);
3313
+ log12.error(`Error in event handler: ${errorMessage(err)}`);
3071
3314
  }
3072
3315
  };
3073
3316
  }
3074
3317
  function registerEventHandlers(eventHub, bindings) {
3075
3318
  bindings.setCucumberRunner(eventHub.runner === "cucumber");
3076
3319
  if (eventHub.runner === "cucumber") {
3077
- log11.info("\u2713 Cucumber runner detected via NightwatchEventHub");
3320
+ log12.info("\u2713 Cucumber runner detected via NightwatchEventHub");
3078
3321
  }
3079
- log11.info("\u2713 NightwatchEventHub registered \u2014 enriched metadata enabled");
3322
+ log12.info("\u2713 NightwatchEventHub registered \u2014 enriched metadata enabled");
3080
3323
  const handler = makeSessionMetadataHandler(bindings);
3081
3324
  eventHub.on("TestSuiteStarted", handler);
3082
3325
  eventHub.on("TestRunStarted", handler);
3083
3326
  }
3084
3327
 
3085
3328
  // src/cucumber-lifecycle.ts
3086
- import logger12 from "@wdio/logger";
3329
+ import logger13 from "@wdio/logger";
3087
3330
 
3088
3331
  // src/helpers/cucumberResult.ts
3089
3332
  function cucumberResultToTestState(result) {
@@ -3278,7 +3521,7 @@ function flushTestSlice(ctx) {
3278
3521
  }
3279
3522
 
3280
3523
  // src/cucumber-lifecycle.ts
3281
- var log12 = logger12("@wdio/nightwatch-devtools:cucumber");
3524
+ var log13 = logger13("@wdio/nightwatch-devtools:cucumber");
3282
3525
  function attachScenarioToFeature(ctx, featureSuite, scenarioSuite) {
3283
3526
  const existingIdx = featureSuite.suites.findIndex(
3284
3527
  (s) => s.uid === scenarioSuite.uid
@@ -3362,7 +3605,7 @@ async function initCucumberScenario(ctx, browser, pickle) {
3362
3605
  ctx.setCurrentTest(null);
3363
3606
  ctx.testReporter.updateSuites();
3364
3607
  ctx.wrapBrowserOnce(browser);
3365
- log12.info(`\u{1F952} Scenario: ${scenarioName}`);
3608
+ log13.info(`\u{1F952} Scenario: ${scenarioName}`);
3366
3609
  }
3367
3610
  async function finalizeCucumberScenario(ctx, _browser, result, pickle) {
3368
3611
  try {
@@ -3384,7 +3627,7 @@ async function finalizeCucumberScenario(ctx, _browser, result, pickle) {
3384
3627
  }
3385
3628
  ctx.incrementCount(scenarioState);
3386
3629
  const icon = ctx.testIcon(scenarioState);
3387
- log12.info(
3630
+ log13.info(
3388
3631
  ` ${icon} ${pickle?.name ?? "Unknown"} (${(duration / 1e3).toFixed(2)}s)`
3389
3632
  );
3390
3633
  ctx.testReporter.updateSuites();
@@ -3393,12 +3636,13 @@ async function finalizeCucumberScenario(ctx, _browser, result, pickle) {
3393
3636
  ctx.setCurrentTest(null);
3394
3637
  }
3395
3638
  } catch (err) {
3396
- log12.error(`Failed to finalize Cucumber scenario: ${errorMessage(err)}`);
3639
+ log13.error(`Failed to finalize Cucumber scenario: ${errorMessage(err)}`);
3397
3640
  }
3398
3641
  }
3399
3642
  async function captureCucumberScenarioBeforeQuit(ctx, browser, result) {
3400
3643
  const scenario = ctx.getCurrentScenarioSuite();
3401
3644
  try {
3645
+ ctx.browserProxy?.closeUnrunWaits();
3402
3646
  const assertCalls = ctx.browserProxy?.drainNativeAssertCalls() ?? [];
3403
3647
  if (assertCalls.length > 0) {
3404
3648
  await captureNativeAssertions(
@@ -3416,7 +3660,7 @@ async function captureCucumberScenarioBeforeQuit(ctx, browser, result) {
3416
3660
  cucumberResultToTestState(result) === TEST_STATE.FAILED
3417
3661
  );
3418
3662
  } catch (err) {
3419
- log12.error(
3663
+ log13.error(
3420
3664
  `Failed to capture Cucumber scenario before quit: ${errorMessage(err)}`
3421
3665
  );
3422
3666
  }
@@ -3462,7 +3706,7 @@ async function cucumberAfter(ctx, browser, result, pickle) {
3462
3706
  }
3463
3707
 
3464
3708
  // src/test-lifecycle.ts
3465
- import logger14 from "@wdio/logger";
3709
+ import logger15 from "@wdio/logger";
3466
3710
 
3467
3711
  // src/helpers/specFileResolver.ts
3468
3712
  import fs4 from "fs";
@@ -3510,8 +3754,8 @@ function resolveSpecFilePath(testFile, modulePath, srcFolders, cachedPath) {
3510
3754
  }
3511
3755
 
3512
3756
  // src/helpers/closePreviousTest.ts
3513
- import logger13 from "@wdio/logger";
3514
- var log13 = logger13("@wdio/nightwatch-devtools:closePreviousTest");
3757
+ import logger14 from "@wdio/logger";
3758
+ var log14 = logger14("@wdio/nightwatch-devtools:closePreviousTest");
3515
3759
  async function closePreviousTest(input) {
3516
3760
  const {
3517
3761
  runningTest,
@@ -3530,7 +3774,7 @@ async function closePreviousTest(input) {
3530
3774
  testManager.updateTestState(runningTest, testState);
3531
3775
  testManager.markTestAsProcessed(testFile, runningTest.title);
3532
3776
  incrementCount(testState);
3533
- log13.info(
3777
+ log14.info(
3534
3778
  ` ${testIcon(testState)} ${runningTest.title} (${(runningTest._duration / 1e3).toFixed(2)}s)`
3535
3779
  );
3536
3780
  } else {
@@ -3544,13 +3788,13 @@ async function closePreviousTest(input) {
3544
3788
  );
3545
3789
  testManager.markTestAsProcessed(testFile, runningTest.title);
3546
3790
  incrementCount(TEST_STATE.PASSED);
3547
- log13.info(` \u2705 ${runningTest.title} (${(duration / 1e3).toFixed(2)}s)`);
3791
+ log14.info(` \u2705 ${runningTest.title} (${(duration / 1e3).toFixed(2)}s)`);
3548
3792
  }
3549
3793
  await new Promise((resolve2) => setTimeout(resolve2, TIMING.UI_RENDER_DELAY));
3550
3794
  }
3551
3795
 
3552
3796
  // src/test-lifecycle.ts
3553
- var log14 = logger14("@wdio/nightwatch-devtools:test-lifecycle");
3797
+ var log15 = logger15("@wdio/nightwatch-devtools:test-lifecycle");
3554
3798
  function resolveSuiteMetadata(ctx, currentTest) {
3555
3799
  const moduleName = currentTest.module ?? "";
3556
3800
  const testFile = moduleName.split("/").pop() || moduleName || DEFAULTS.FILE_NAME;
@@ -3561,7 +3805,7 @@ function resolveSuiteMetadata(ctx, currentTest) {
3561
3805
  ctx.browserProxy.getCurrentTestFullPath() || void 0
3562
3806
  );
3563
3807
  if (!fullPath) {
3564
- log14.warn(
3808
+ log15.warn(
3565
3809
  `[beforeEach] Could not resolve file path for "${testFile}" \u2014 source view will be unavailable`
3566
3810
  );
3567
3811
  }
@@ -3606,10 +3850,10 @@ async function startNextTest(ctx, currentSuite, currentTestName, processedTests,
3606
3850
  test.end = null;
3607
3851
  ctx.testReporter.onTestStart(test);
3608
3852
  ctx.setCurrentTest(test);
3609
- log14.info(` \u25B6 ${currentTestName}`);
3853
+ log15.info(` \u25B6 ${currentTestName}`);
3610
3854
  await new Promise((resolve2) => setTimeout(resolve2, TIMING.TEST_START_DELAY));
3611
3855
  } else {
3612
- log14.warn(
3856
+ log15.warn(
3613
3857
  `Test "${currentTestName}" not found in suite "${currentSuite.title}"`
3614
3858
  );
3615
3859
  ctx.setCurrentTest(null);
@@ -3654,7 +3898,7 @@ function closeUnreportedRunningTest(ctx, currentSuite, testFile, results, proces
3654
3898
  ctx.testManager.markTestAsProcessed(testFile, runningTest.title);
3655
3899
  ctx.incrementCount(testState);
3656
3900
  const icon = ctx.testIcon(testState);
3657
- log14.info(` ${icon} ${runningTest.title} (${(duration / 1e3).toFixed(2)}s)`);
3901
+ log15.info(` ${icon} ${runningTest.title} (${(duration / 1e3).toFixed(2)}s)`);
3658
3902
  }
3659
3903
  async function closeReportedTestcases(ctx, currentSuite, testFile, testcases, processedTests) {
3660
3904
  const testcaseNames = Object.keys(testcases);
@@ -3670,7 +3914,7 @@ async function closeReportedTestcases(ctx, currentSuite, testFile, testcases, pr
3670
3914
  ctx.testManager.updateTestState(test, testState, /* @__PURE__ */ new Date(), dur);
3671
3915
  ctx.incrementCount(testState);
3672
3916
  const icon = ctx.testIcon(testState);
3673
- log14.info(` ${icon} ${currentTestName} (${(dur / 1e3).toFixed(2)}s)`);
3917
+ log15.info(` ${icon} ${currentTestName} (${(dur / 1e3).toFixed(2)}s)`);
3674
3918
  }
3675
3919
  ctx.testManager.markTestAsProcessed(testFile, currentTestName);
3676
3920
  }
@@ -3712,7 +3956,7 @@ async function closeOutTestcases(ctx, browser) {
3712
3956
  }
3713
3957
 
3714
3958
  // src/index.ts
3715
- var log15 = logger15("@wdio/nightwatch-devtools");
3959
+ var log16 = logger16("@wdio/nightwatch-devtools");
3716
3960
  var NightwatchDevToolsPlugin = class {
3717
3961
  options;
3718
3962
  sessionCapturer;
@@ -3766,7 +4010,7 @@ var NightwatchDevToolsPlugin = class {
3766
4010
  screencast: options.screencast
3767
4011
  });
3768
4012
  if (screencast.warning) {
3769
- log15.warn(screencast.warning);
4013
+ log16.warn(screencast.warning);
3770
4014
  }
3771
4015
  this.options = {
3772
4016
  port: options.port ?? 3e3,
@@ -3787,7 +4031,7 @@ var NightwatchDevToolsPlugin = class {
3787
4031
  };
3788
4032
  const policyWarning = tracePolicyModeWarning(options.tracePolicy, mode);
3789
4033
  if (policyWarning) {
3790
- log15.warn(policyWarning);
4034
+ log16.warn(policyWarning);
3791
4035
  }
3792
4036
  this.#screencast = new PluginScreencast(
3793
4037
  screencast.options,
@@ -4008,7 +4252,8 @@ var NightwatchDevToolsPlugin = class {
4008
4252
  // Rows are tagged per-`it` (BrowserProxy.rowTestUid); `#currentTestUid`
4009
4253
  // — the lifecycle test, which the BDD interface pins to the module's
4010
4254
  // first `it` — is only the fallback, and stays the artifact/slice key.
4011
- () => this.browserProxy?.rowTestUid() ?? this.#currentTestUid()
4255
+ () => this.browserProxy?.rowTestUid() ?? this.#currentTestUid(),
4256
+ () => this.browserProxy?.scopeReadValuesToRowTest()
4012
4257
  );
4013
4258
  }
4014
4259
  }
@@ -4120,6 +4365,7 @@ var NightwatchDevToolsPlugin = class {
4120
4365
  }
4121
4366
  if (browser && this.sessionCapturer) {
4122
4367
  try {
4368
+ this.browserProxy.closeUnrunWaits();
4123
4369
  await this.#closeOutTestcases(browser);
4124
4370
  if (this.options.captureAssertions) {
4125
4371
  await captureNativeAssertions(
@@ -4136,7 +4382,7 @@ var NightwatchDevToolsPlugin = class {
4136
4382
  const failed = !!results && ((results.errors ?? 0) > 0 || (results.failed ?? 0) > 0);
4137
4383
  await this.#emitTestArtifacts(this.#currentTestUid(), failed);
4138
4384
  } catch (err) {
4139
- log15.error(`Failed to capture trace: ${errorMessage(err)}`);
4385
+ log16.error(`Failed to capture trace: ${errorMessage(err)}`);
4140
4386
  }
4141
4387
  }
4142
4388
  }
@@ -4172,11 +4418,12 @@ var NightwatchDevToolsPlugin = class {
4172
4418
  outputDir: this.#outputDir,
4173
4419
  captureFormat: this.#screencast.options.captureFormat,
4174
4420
  onArtifact: (a) => this.#artifacts.push(a),
4175
- onLog: (level, msg) => log15[level](msg)
4421
+ onLog: (level, msg) => log16[level](msg)
4176
4422
  });
4177
4423
  }
4178
4424
  async after(browser) {
4179
4425
  this.sessionCapturer?.nativeNetwork.stop();
4426
+ this.browserProxy?.closeUnrunWaits();
4180
4427
  await this.#screencast.rotation;
4181
4428
  await this.#finalizeCurrentScreencast();
4182
4429
  try {
@@ -4190,14 +4437,14 @@ var NightwatchDevToolsPlugin = class {
4190
4437
  }
4191
4438
  if (!this.#devtoolsBrowser) {
4192
4439
  this.testReporter?.updateSuites();
4193
- log15.info("\u267B Rerun complete \u2014 flushing WebSocket...");
4440
+ log16.info("\u267B Rerun complete \u2014 flushing WebSocket...");
4194
4441
  await this.sessionCapturer?.closeWebSocket();
4195
4442
  return;
4196
4443
  }
4197
- 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...");
4198
4445
  await this.#waitForDevtoolsBrowserClose();
4199
4446
  } catch (err) {
4200
- log15.error(`Failed to stop backend: ${errorMessage(err)}`);
4447
+ log16.error(`Failed to stop backend: ${errorMessage(err)}`);
4201
4448
  }
4202
4449
  }
4203
4450
  async #finalizeAllSuites(browser) {
@@ -4238,7 +4485,7 @@ var NightwatchDevToolsPlugin = class {
4238
4485
  screencastFrames: this.options.filmstrip ? this.#screencast.frames : void 0,
4239
4486
  configPath: this.#configPath,
4240
4487
  testFilePath: this.browserProxy?.getCurrentTestFullPath?.() ?? void 0,
4241
- log: (level, msg) => log15[level](msg)
4488
+ log: (level, msg) => log16[level](msg)
4242
4489
  },
4243
4490
  sessionId2
4244
4491
  );