prowl-tools 0.1.9 → 0.1.10

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/README.md CHANGED
@@ -389,7 +389,7 @@ macOS target (with a clear error), and `prowl login` / URL guardrails do not app
389
389
 
390
390
  | Portable (web + macOS) | Web-only (rejected on macOS) |
391
391
  |---|---|
392
- | `click`, `fill`, `type`, `press` | `navigate`, `waitForUrl`, `waitForNetworkIdle` |
392
+ | `click`, `fill`, `type`, `press` | `navigate`, `waitForUrl`, `waitForNetworkIdle`, `waitForResponse` |
393
393
  | `wait`, `waitForSelector` | `mockRoute` / `unmockRoute` |
394
394
  | `assert: visible` / `notVisible` | `evalScript`, `runScript` |
395
395
  | `screenshot`, `assertScreenshot` | `onDialog`, `select` / `selectOption` |
@@ -665,6 +665,21 @@ Wait for all network requests to complete.
665
665
  timeout: 5000
666
666
  ```
667
667
 
668
+ ### waitForResponse
669
+
670
+ Wait for a specific network response before continuing — more precise than `waitForNetworkIdle`, which waits for *all* traffic to settle.
671
+
672
+ ```yaml
673
+ - waitForResponse:
674
+ url: "**/api/orders"
675
+ status: 200
676
+ timeout: 10000
677
+ ```
678
+
679
+ - `url` (required) is matched against each response URL as a glob: `*`/`**` match any run of characters (including `/`) and `?` matches a single character. Matching is unanchored, so a pattern with no wildcards behaves as a substring match (e.g. `/api/orders` matches `https://shop.test/api/orders?page=2`).
680
+ - `status` (optional) resolves the step only on a response with that exact HTTP status.
681
+ - `timeout` (optional, milliseconds) fails the step with a clear message if no matching response arrives in time.
682
+
668
683
  ### onDialog
669
684
 
670
685
  Handle browser-native dialogs (alert, confirm, prompt). Register the handler **before** the action that triggers the dialog.
@@ -1014,6 +1029,39 @@ tracing:
1014
1029
  This is a correlation bridge only — Prowl does not generate or propagate its own
1015
1030
  spans. When the app emits no trace headers, nothing is recorded (no noise).
1016
1031
 
1032
+ ### Retry Diagnostics (why a retried hunt passed)
1033
+
1034
+ When a hunt sets a `retry` block and eventually passes — or exhausts its
1035
+ attempts — Prowl records what each attempt did instead of retrying silently:
1036
+
1037
+ ```yaml
1038
+ # In a hunt file
1039
+ retry:
1040
+ maxRetries: 2 # up to 2 extra attempts (3 total)
1041
+ delay: 500 # optional ms to wait between attempts
1042
+ ```
1043
+
1044
+ The final attempt's `result.json` (and the returned run result) then carries:
1045
+
1046
+ - `retryHistory` — one record per attempt: its `attempt` number, `status`,
1047
+ `durationMs`, the first `failedStep` (`index` + `type`), and the `error`.
1048
+ - `retrySummary` — a one-line headline, e.g.
1049
+ `Passed on attempt 2 of 3 — first failure: navigate (timeout)`, or
1050
+ `Failed after 3 attempts — first failure: …` when the retries were exhausted.
1051
+
1052
+ Both fields appear **only** when more than one attempt ran, so a hunt that passes
1053
+ on the first try (and any run artifact written before this feature) stays
1054
+ unchanged and keeps parsing. `summary.md` gains a matching **Retries** section
1055
+ with the headline and a per-attempt breakdown, and the `prowl run` summary prints
1056
+ the headline. JUnit continues to report the final attempt's outcome — the
1057
+ per-attempt history lives in `result.json`, not the XML — so a retried-and-passed
1058
+ hunt is a green testcase for CI while the diagnostics remain available alongside
1059
+ it.
1060
+
1061
+ This is what lets you distinguish a **flaky test** (passes on retry, intermittent
1062
+ first-attempt failures) from a **slow environment** (retries needed but the same
1063
+ step lags) from a **real regression** (all attempts fail the same way).
1064
+
1017
1065
  ---
1018
1066
 
1019
1067
  ## CLI Reference
@@ -1097,10 +1145,11 @@ remedy.
1097
1145
  ### Run History
1098
1146
 
1099
1147
  Every `prowl run` and `prowl ci` appends an entry to `.prowl/history.json`
1100
- with the hunt name, status, start time, duration, and run directory. Retention
1101
- is capped per hunt by `history.maxRuns` (default 100) — once a hunt exceeds the
1102
- cap, its oldest entries are dropped on the next write. Other hunts are not
1103
- affected.
1148
+ with the hunt name, status, start time, duration, run directory, and — when the
1149
+ run used `retry` — a `retries` count (the number of extra attempts beyond the
1150
+ first; omitted when the run passed on its first attempt). Retention is capped
1151
+ per hunt by `history.maxRuns` (default 100) — once a hunt exceeds the cap, its
1152
+ oldest entries are dropped on the next write. Other hunts are not affected.
1104
1153
 
1105
1154
  ```yaml
1106
1155
  # In .prowl/config.yml
@@ -1109,7 +1158,11 @@ history:
1109
1158
  ```
1110
1159
 
1111
1160
  Use `prowl history <hunt-name>` for a quick status/duration table, or
1112
- `--json` to feed the entries into dashboards, flake detectors, or agents.
1161
+ `--json` to feed the entries into dashboards, flake detectors, or agents. The
1162
+ table includes a **Retries** column and a retry-frequency line
1163
+ (`Retried in N of M runs`) so you can tell a flaky hunt apart from a slow
1164
+ environment or a real regression at a glance. History from before retry
1165
+ tracking (entries without a `retries` field) still loads and reads as `0`.
1113
1166
 
1114
1167
  ### Failure Clustering
1115
1168
 
@@ -1395,7 +1448,7 @@ Portable steps run on the Android target; web-only steps are rejected up front
1395
1448
 
1396
1449
  | Portable (Android) | Not supported on Android |
1397
1450
  |---|---|
1398
- | `click`, `fill`, `type`, `press` | `navigate`, `waitForUrl`, `waitForNetworkIdle` |
1451
+ | `click`, `fill`, `type`, `press` | `navigate`, `waitForUrl`, `waitForNetworkIdle`, `waitForResponse` |
1399
1452
  | `wait`, `waitForSelector` | `mockRoute` / `unmockRoute`, `evalScript`, `runScript` |
1400
1453
  | `assert: visible` / `notVisible` | `onDialog`, `select` / `selectOption`, `setInputFiles` |
1401
1454
  | `screenshot`, `assertScreenshot` | `waitForDownload`, `scroll`, `assert: urlIncludes` / `urlEquals` |
@@ -1545,7 +1598,7 @@ clear error), and URL guardrails do not apply.
1545
1598
 
1546
1599
  | Portable (iOS) | Not supported on iOS |
1547
1600
  |---|---|
1548
- | `click`, `fill`, `type`, `press` | `navigate`, `waitForUrl`, `waitForNetworkIdle` |
1601
+ | `click`, `fill`, `type`, `press` | `navigate`, `waitForUrl`, `waitForNetworkIdle`, `waitForResponse` |
1549
1602
  | `wait`, `waitForSelector` | `mockRoute` / `unmockRoute`, `evalScript`, `runScript` |
1550
1603
  | `assert: visible` / `notVisible` | `onDialog`, `select` / `selectOption`, `setInputFiles` |
1551
1604
  | `screenshot`, `assertScreenshot` | `waitForDownload`, `scroll`, `assert: urlIncludes` / `urlEquals` |
@@ -8,7 +8,7 @@ import {
8
8
  loadHunt,
9
9
  loadHuntTags,
10
10
  resolveViewport
11
- } from "./chunk-JFJQNJSJ.js";
11
+ } from "./chunk-TTZ2IL3P.js";
12
12
 
13
13
  // src/config/target.ts
14
14
  import { execFileSync } from "child_process";
@@ -18,6 +18,7 @@ var WEB_ONLY_STEP_TYPES = /* @__PURE__ */ new Set([
18
18
  "navigate",
19
19
  "waitForUrl",
20
20
  "waitForNetworkIdle",
21
+ "waitForResponse",
21
22
  "mockRoute",
22
23
  "unmockRoute",
23
24
  "evalScript",
@@ -372,6 +373,9 @@ function createMacDriver(client, options = {}) {
372
373
  waitForNetworkIdle() {
373
374
  return rejectUnsupported("waitForNetworkIdle");
374
375
  },
376
+ waitForResponse() {
377
+ return rejectUnsupported("waitForResponse");
378
+ },
375
379
  // scripting & artifacts ------------------------------------------------
376
380
  evaluate() {
377
381
  return rejectUnsupported("evalScript");
@@ -1294,6 +1298,9 @@ function createAndroidDriver(client, options = {}) {
1294
1298
  waitForNetworkIdle() {
1295
1299
  return rejectUnsupported("waitForNetworkIdle");
1296
1300
  },
1301
+ waitForResponse() {
1302
+ return rejectUnsupported("waitForResponse");
1303
+ },
1297
1304
  // scripting & artifacts ------------------------------------------------
1298
1305
  evaluate() {
1299
1306
  return rejectUnsupported("evalScript");
@@ -2156,6 +2163,9 @@ function createIosDriver(client, options) {
2156
2163
  waitForNetworkIdle() {
2157
2164
  return rejectUnsupported("waitForNetworkIdle");
2158
2165
  },
2166
+ waitForResponse() {
2167
+ return rejectUnsupported("waitForResponse");
2168
+ },
2159
2169
  // scripting & artifacts ------------------------------------------------
2160
2170
  evaluate() {
2161
2171
  return rejectUnsupported("evalScript");
@@ -3080,7 +3090,8 @@ function isHistoryEntry(value) {
3080
3090
  return false;
3081
3091
  }
3082
3092
  const entry = value;
3083
- return typeof entry.hunt === "string" && (entry.status === "pass" || entry.status === "fail") && typeof entry.durationMs === "number" && Number.isFinite(entry.durationMs) && typeof entry.startedAt === "string" && (entry.runDir === void 0 || typeof entry.runDir === "string");
3093
+ return typeof entry.hunt === "string" && (entry.status === "pass" || entry.status === "fail") && typeof entry.durationMs === "number" && Number.isFinite(entry.durationMs) && typeof entry.startedAt === "string" && (entry.runDir === void 0 || typeof entry.runDir === "string") && // Optional (PROWL-033): older history.json files predate `retries`.
3094
+ (entry.retries === void 0 || typeof entry.retries === "number" && Number.isInteger(entry.retries) && entry.retries >= 0);
3084
3095
  }
3085
3096
  function readHistory(configDir) {
3086
3097
  const filePath = historyPath(configDir);
@@ -3351,6 +3362,9 @@ function createPlaywrightDriver(page) {
3351
3362
  async waitForNetworkIdle(options) {
3352
3363
  await page.waitForLoadState("networkidle", { timeout: options?.timeout });
3353
3364
  },
3365
+ async waitForResponse(predicate, options) {
3366
+ await page.waitForResponse((response) => predicate(response), { timeout: options?.timeout });
3367
+ },
3354
3368
  evaluate(pageFunction, arg) {
3355
3369
  const raw = page.evaluate;
3356
3370
  const result = arg === void 0 ? raw.call(page, pageFunction) : raw.call(page, pageFunction, arg);
@@ -3758,6 +3772,7 @@ function getStepType(step) {
3758
3772
  if ("waitForSelector" in step) return "waitForSelector";
3759
3773
  if ("waitForUrl" in step) return "waitForUrl";
3760
3774
  if ("waitForNetworkIdle" in step) return "waitForNetworkIdle";
3775
+ if ("waitForResponse" in step) return "waitForResponse";
3761
3776
  if ("hover" in step) return "hover";
3762
3777
  if ("scroll" in step) return "scroll";
3763
3778
  if ("scrollTo" in step) return "scrollTo";
@@ -3774,6 +3789,11 @@ function getStepType(step) {
3774
3789
  if ("waitForDownload" in step) return "waitForDownload";
3775
3790
  return "step";
3776
3791
  }
3792
+ function compileResponseUrlMatcher(pattern) {
3793
+ const regexBody = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*").replace(/\\\?/g, ".");
3794
+ const regex = new RegExp(regexBody);
3795
+ return (url) => regex.test(url);
3796
+ }
3777
3797
  var RUNTIME_VAR_PATTERN = /\{\{([A-Z0-9_]+)\}\}/g;
3778
3798
  function substituteRuntimeVars(input, vars) {
3779
3799
  return input.replace(RUNTIME_VAR_PATTERN, (match, name) => {
@@ -3812,6 +3832,15 @@ function applyRuntimeVars(step, vars) {
3812
3832
  if ("waitForSelector" in step) {
3813
3833
  return { waitForSelector: { selector: sub(step.waitForSelector.selector), timeout: step.waitForSelector.timeout } };
3814
3834
  }
3835
+ if ("waitForResponse" in step) {
3836
+ return {
3837
+ waitForResponse: {
3838
+ url: sub(step.waitForResponse.url),
3839
+ ...step.waitForResponse.status !== void 0 ? { status: step.waitForResponse.status } : {},
3840
+ ...step.waitForResponse.timeout !== void 0 ? { timeout: step.waitForResponse.timeout } : {}
3841
+ }
3842
+ };
3843
+ }
3815
3844
  if ("evalScript" in step) {
3816
3845
  if (typeof step.evalScript === "string") return { evalScript: sub(step.evalScript) };
3817
3846
  return {
@@ -4421,6 +4450,32 @@ var STEP_HANDLERS = {
4421
4450
  };
4422
4451
  }
4423
4452
  },
4453
+ waitForResponse: {
4454
+ capabilities: ["response", "wait"],
4455
+ run: async (h) => {
4456
+ if (!("waitForResponse" in h.step)) unknownStep();
4457
+ const { url: pattern, status, timeout } = h.step.waitForResponse;
4458
+ const matchesResponseUrl = compileResponseUrlMatcher(pattern);
4459
+ try {
4460
+ await h.driver.waitForResponse(
4461
+ (response) => matchesResponseUrl(response.url()) && (status === void 0 || response.status() === status),
4462
+ { timeout }
4463
+ );
4464
+ } catch (err) {
4465
+ const isTimeout = err instanceof Error && (err.name === "TimeoutError" || /timeout/i.test(err.message));
4466
+ if (!isTimeout) throw err;
4467
+ const statusPart = status !== void 0 ? ` with status ${status}` : "";
4468
+ const timeoutPart = timeout !== void 0 ? ` within ${timeout}ms` : "";
4469
+ throw new Error(
4470
+ `waitForResponse: no response matching "${pattern}"${statusPart} was received${timeoutPart}.`
4471
+ );
4472
+ }
4473
+ return {
4474
+ kind: "result",
4475
+ result: { type: "waitForResponse", status: "pass", durationMs: Date.now() - h.stepStart, value: pattern }
4476
+ };
4477
+ }
4478
+ },
4424
4479
  waitForNetworkIdle: {
4425
4480
  capabilities: ["wait"],
4426
4481
  run: async (h) => {
@@ -5211,6 +5266,15 @@ function formatStep(step) {
5211
5266
  const error = step.error ? ` error=${escapeMd(step.error)}` : "";
5212
5267
  return `${base}${selector}${healed}${value}${error}`;
5213
5268
  }
5269
+ function formatRetryAttempt(attempt) {
5270
+ const base = `- attempt ${attempt.attempt}: ${attempt.status.toUpperCase()} (${attempt.durationMs}ms)`;
5271
+ if (attempt.status === "pass") {
5272
+ return base;
5273
+ }
5274
+ const step = attempt.failedStep ? ` failed-step=[${attempt.failedStep.index}] ${escapeMd(attempt.failedStep.type)}` : "";
5275
+ const error = attempt.error ? ` error=${escapeMd(attempt.error)}` : "";
5276
+ return `${base}${step}${error}`;
5277
+ }
5214
5278
  function formatAssertion(assertion) {
5215
5279
  const value = assertion.value !== void 0 ? ` value=${typeof assertion.value === "string" ? escapeMd(assertion.value) : assertion.value}` : "";
5216
5280
  const error = assertion.error ? ` error=${escapeMd(assertion.error)}` : "";
@@ -5225,6 +5289,16 @@ function writeSummary(runDir, result) {
5225
5289
  lines.push(`Target: ${result.targetUrl}`);
5226
5290
  lines.push(`Started: ${result.startedAt}`);
5227
5291
  lines.push(`Duration: ${result.durationMs}ms`);
5292
+ if (result.retryHistory && result.retryHistory.length > 1) {
5293
+ lines.push("");
5294
+ lines.push("## Retries");
5295
+ if (result.retrySummary) {
5296
+ lines.push(escapeMd(result.retrySummary));
5297
+ }
5298
+ for (const attempt of result.retryHistory) {
5299
+ lines.push(formatRetryAttempt(attempt));
5300
+ }
5301
+ }
5228
5302
  lines.push("");
5229
5303
  lines.push("## Steps");
5230
5304
  for (const step of result.steps) {
@@ -5535,6 +5609,60 @@ async function executeHuntAttempt(options, config, configDir, interpolatedHunt,
5535
5609
  function delay3(ms) {
5536
5610
  return new Promise((resolve) => setTimeout(resolve, ms));
5537
5611
  }
5612
+ function toRetryAttempt(attempt, result) {
5613
+ const record = {
5614
+ attempt,
5615
+ status: result.status,
5616
+ durationMs: result.durationMs
5617
+ };
5618
+ if (result.status === "fail") {
5619
+ const failedIndex = result.steps.findIndex((step) => step.status === "fail");
5620
+ if (failedIndex >= 0) {
5621
+ const failed = result.steps[failedIndex];
5622
+ record.failedStep = { index: failedIndex, type: failed.type };
5623
+ if (failed.error) {
5624
+ record.error = failed.error;
5625
+ }
5626
+ }
5627
+ if (!record.error) {
5628
+ const failedAssertion = result.assertions.find((assertion) => assertion.status === "fail");
5629
+ if (failedAssertion?.error) {
5630
+ record.error = failedAssertion.error;
5631
+ }
5632
+ }
5633
+ }
5634
+ return record;
5635
+ }
5636
+ function describeAttemptFailure(attempt) {
5637
+ if (attempt.status !== "fail") {
5638
+ return void 0;
5639
+ }
5640
+ const step = attempt.failedStep?.type;
5641
+ if (step && attempt.error) {
5642
+ return `${step} (${attempt.error})`;
5643
+ }
5644
+ return step ?? attempt.error;
5645
+ }
5646
+ function buildRetrySummary(attempts, maxRetries) {
5647
+ const final = attempts[attempts.length - 1];
5648
+ const firstFailure = describeAttemptFailure(attempts[0]);
5649
+ const suffix = firstFailure ? ` \u2014 first failure: ${firstFailure}` : "";
5650
+ if (final.status === "pass") {
5651
+ return `Passed on attempt ${final.attempt} of ${maxRetries + 1}${suffix}`;
5652
+ }
5653
+ return `Failed after ${attempts.length} attempts${suffix}`;
5654
+ }
5655
+ function finalizeRetryOutcome(outcome, attempts, maxRetries, junit) {
5656
+ outcome.result = writeReports(
5657
+ outcome.runDir,
5658
+ {
5659
+ ...outcome.result,
5660
+ retryHistory: attempts,
5661
+ retrySummary: buildRetrySummary(attempts, maxRetries)
5662
+ },
5663
+ { junit }
5664
+ );
5665
+ }
5538
5666
  async function runHunt(options) {
5539
5667
  const { config, configDir } = loadConfig(options.configPath);
5540
5668
  if (config.target.type === "macos") {
@@ -5564,7 +5692,9 @@ async function runHunt(options) {
5564
5692
  }
5565
5693
  const maxRetries = hunt.retry?.maxRetries ?? 0;
5566
5694
  const retryDelay = hunt.retry?.delay ?? 0;
5695
+ const junit = options.junit ?? config.artifacts.junit;
5567
5696
  let lastResult;
5697
+ const attempts = [];
5568
5698
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
5569
5699
  if (attempt > 0 && retryDelay > 0) {
5570
5700
  await delay3(retryDelay);
@@ -5580,19 +5710,20 @@ async function runHunt(options) {
5580
5710
  targetUrl,
5581
5711
  allowedDomains
5582
5712
  );
5713
+ attempts.push(toRetryAttempt(attempt + 1, lastResult.result));
5583
5714
  if (lastResult.result.status === "pass") {
5584
- if (attempt > 0) {
5585
- lastResult.result.artifacts.summary = `Passed on attempt ${attempt + 1} of ${maxRetries + 1}`;
5715
+ if (attempts.length > 1) {
5716
+ finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
5586
5717
  }
5587
- recordHistory(configDir, lastResult, config.history.maxRuns);
5718
+ recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
5588
5719
  return lastResult;
5589
5720
  }
5590
5721
  }
5591
- if (maxRetries > 0 && lastResult) {
5592
- lastResult.result.artifacts.summary = `Failed after ${maxRetries + 1} attempts`;
5593
- }
5594
5722
  if (lastResult) {
5595
- recordHistory(configDir, lastResult, config.history.maxRuns);
5723
+ if (attempts.length > 1) {
5724
+ finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
5725
+ }
5726
+ recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
5596
5727
  }
5597
5728
  return lastResult;
5598
5729
  }
@@ -5796,7 +5927,9 @@ async function runNativeHunt(options, config, configDir, target, native) {
5796
5927
  }
5797
5928
  const maxRetries = hunt.retry?.maxRetries ?? 0;
5798
5929
  const retryDelay = hunt.retry?.delay ?? 0;
5930
+ const junit = options.junit ?? config.artifacts.junit;
5799
5931
  let lastResult;
5932
+ const attempts = [];
5800
5933
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
5801
5934
  if (attempt > 0 && retryDelay > 0) {
5802
5935
  await delay3(retryDelay);
@@ -5811,23 +5944,24 @@ async function runNativeHunt(options, config, configDir, target, native) {
5811
5944
  randomVars,
5812
5945
  config.guardrails.allowedApps
5813
5946
  );
5947
+ attempts.push(toRetryAttempt(attempt + 1, lastResult.result));
5814
5948
  if (lastResult.result.status === "pass") {
5815
- if (attempt > 0) {
5816
- lastResult.result.artifacts.summary = `Passed on attempt ${attempt + 1} of ${maxRetries + 1}`;
5949
+ if (attempts.length > 1) {
5950
+ finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
5817
5951
  }
5818
- recordHistory(configDir, lastResult, config.history.maxRuns);
5952
+ recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
5819
5953
  return lastResult;
5820
5954
  }
5821
5955
  }
5822
- if (maxRetries > 0 && lastResult) {
5823
- lastResult.result.artifacts.summary = `Failed after ${maxRetries + 1} attempts`;
5824
- }
5825
5956
  if (lastResult) {
5826
- recordHistory(configDir, lastResult, config.history.maxRuns);
5957
+ if (attempts.length > 1) {
5958
+ finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
5959
+ }
5960
+ recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
5827
5961
  }
5828
5962
  return lastResult;
5829
5963
  }
5830
- function recordHistory(configDir, outcome, maxRuns) {
5964
+ function recordHistory(configDir, outcome, maxRuns, retries = 0) {
5831
5965
  try {
5832
5966
  const relativeRunDir = path13.relative(configDir, outcome.runDir);
5833
5967
  appendEntry(
@@ -5837,7 +5971,9 @@ function recordHistory(configDir, outcome, maxRuns) {
5837
5971
  status: outcome.result.status,
5838
5972
  durationMs: outcome.result.durationMs,
5839
5973
  startedAt: outcome.result.startedAt,
5840
- runDir: relativeRunDir || void 0
5974
+ runDir: relativeRunDir || void 0,
5975
+ // Omit when no retry ran so entries stay additive/backward-compatible.
5976
+ retries: retries > 0 ? retries : void 0
5841
5977
  },
5842
5978
  maxRuns
5843
5979
  );
@@ -6939,6 +7075,7 @@ var STEP_REFERENCE = `
6939
7075
  - waitForSelector: { selector: "#el", timeout: 5000 }
6940
7076
  - waitForUrl: { value: "/path", timeout: 5000 }
6941
7077
  - waitForNetworkIdle: { timeout: 5000 }
7078
+ - waitForResponse: { url: "**/api/orders", status: 200, timeout: 10000 } \u2014 wait for a matching network response (url is a glob/substring; status is optional)
6942
7079
 
6943
7080
  ### Interaction
6944
7081
  - click: "Button Text" \u2014 click by text (tries role=button first)
@@ -7506,4 +7643,4 @@ export {
7506
7643
  collectMacdriverStatus,
7507
7644
  tccGuidance
7508
7645
  };
7509
- //# sourceMappingURL=chunk-2RNOK64Y.js.map
7646
+ //# sourceMappingURL=chunk-SRWAF2RI.js.map