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/dist/lib.cjs CHANGED
@@ -394,6 +394,9 @@ function createMacDriver(client, options = {}) {
394
394
  waitForNetworkIdle() {
395
395
  return rejectUnsupported("waitForNetworkIdle");
396
396
  },
397
+ waitForResponse() {
398
+ return rejectUnsupported("waitForResponse");
399
+ },
397
400
  // scripting & artifacts ------------------------------------------------
398
401
  evaluate() {
399
402
  return rejectUnsupported("evalScript");
@@ -1611,6 +1614,9 @@ function createAndroidDriver(client, options = {}) {
1611
1614
  waitForNetworkIdle() {
1612
1615
  return rejectUnsupported("waitForNetworkIdle");
1613
1616
  },
1617
+ waitForResponse() {
1618
+ return rejectUnsupported("waitForResponse");
1619
+ },
1614
1620
  // scripting & artifacts ------------------------------------------------
1615
1621
  evaluate() {
1616
1622
  return rejectUnsupported("evalScript");
@@ -2074,6 +2080,7 @@ var WEB_ONLY_STEP_TYPES = /* @__PURE__ */ new Set([
2074
2080
  "navigate",
2075
2081
  "waitForUrl",
2076
2082
  "waitForNetworkIdle",
2083
+ "waitForResponse",
2077
2084
  "mockRoute",
2078
2085
  "unmockRoute",
2079
2086
  "evalScript",
@@ -2692,6 +2699,9 @@ function createIosDriver(client, options) {
2692
2699
  waitForNetworkIdle() {
2693
2700
  return rejectUnsupported("waitForNetworkIdle");
2694
2701
  },
2702
+ waitForResponse() {
2703
+ return rejectUnsupported("waitForResponse");
2704
+ },
2695
2705
  // scripting & artifacts ------------------------------------------------
2696
2706
  evaluate() {
2697
2707
  return rejectUnsupported("evalScript");
@@ -3697,6 +3707,21 @@ var waitForUrlStepSchema = import_zod.z.object({
3697
3707
  var waitForNetworkIdleStepSchema = import_zod.z.object({
3698
3708
  waitForNetworkIdle: import_zod.z.object({ timeout: import_zod.z.number().optional() }).strict()
3699
3709
  }).strict();
3710
+ var waitForResponseStatusSchema = import_zod.z.number().int().min(100).max(599);
3711
+ var waitForResponseTimeoutSchema = import_zod.z.number().int().min(0).finite();
3712
+ var waitForResponseStepSchema = import_zod.z.object({
3713
+ waitForResponse: import_zod.z.object({
3714
+ // Matched against each response URL as a glob (`*`/`**` match any run of
3715
+ // characters including `/`, `?` matches one character); matching is
3716
+ // unanchored, so a pattern with no wildcards behaves as a substring match.
3717
+ url: import_zod.z.string().min(1).describe(
3718
+ "URL glob to wait for; unanchored, so a wildcard-free pattern is a substring match"
3719
+ ),
3720
+ // Optional status filter: only resolve on a response with this exact HTTP status.
3721
+ status: waitForResponseStatusSchema.optional(),
3722
+ timeout: waitForResponseTimeoutSchema.optional()
3723
+ }).strict()
3724
+ }).strict();
3700
3725
  var selectOptionStepSchema = import_zod.z.object({
3701
3726
  selectOption: import_zod.z.object({ selector: import_zod.z.string().min(1), value: import_zod.z.string() }).strict()
3702
3727
  }).strict();
@@ -3858,6 +3883,7 @@ var stepSchema = import_zod.z.union([
3858
3883
  waitForSelectorStepSchema,
3859
3884
  waitForUrlStepSchema,
3860
3885
  waitForNetworkIdleStepSchema,
3886
+ waitForResponseStepSchema,
3861
3887
  hoverStepSchema,
3862
3888
  scrollStepSchema,
3863
3889
  scrollToStepSchema,
@@ -4092,6 +4118,16 @@ function interpolateStep(step, vars, stepPath2, redacted) {
4092
4118
  if ("waitForNetworkIdle" in step) {
4093
4119
  return { waitForNetworkIdle: { timeout: step.waitForNetworkIdle.timeout } };
4094
4120
  }
4121
+ if ("waitForResponse" in step) {
4122
+ const urlResult = interpolateString(step.waitForResponse.url, vars);
4123
+ return {
4124
+ waitForResponse: {
4125
+ url: urlResult.value,
4126
+ ...step.waitForResponse.status !== void 0 ? { status: step.waitForResponse.status } : {},
4127
+ timeout: step.waitForResponse.timeout
4128
+ }
4129
+ };
4130
+ }
4095
4131
  if ("hover" in step) {
4096
4132
  const selectorResult = interpolateString(step.hover.selector, vars);
4097
4133
  return { hover: { selector: selectorResult.value } };
@@ -4711,6 +4747,9 @@ function createPlaywrightDriver(page) {
4711
4747
  async waitForNetworkIdle(options) {
4712
4748
  await page.waitForLoadState("networkidle", { timeout: options?.timeout });
4713
4749
  },
4750
+ async waitForResponse(predicate, options) {
4751
+ await page.waitForResponse((response) => predicate(response), { timeout: options?.timeout });
4752
+ },
4714
4753
  evaluate(pageFunction, arg) {
4715
4754
  const raw = page.evaluate;
4716
4755
  const result = arg === void 0 ? raw.call(page, pageFunction) : raw.call(page, pageFunction, arg);
@@ -5172,6 +5211,7 @@ function getStepType(step) {
5172
5211
  if ("waitForSelector" in step) return "waitForSelector";
5173
5212
  if ("waitForUrl" in step) return "waitForUrl";
5174
5213
  if ("waitForNetworkIdle" in step) return "waitForNetworkIdle";
5214
+ if ("waitForResponse" in step) return "waitForResponse";
5175
5215
  if ("hover" in step) return "hover";
5176
5216
  if ("scroll" in step) return "scroll";
5177
5217
  if ("scrollTo" in step) return "scrollTo";
@@ -5188,6 +5228,11 @@ function getStepType(step) {
5188
5228
  if ("waitForDownload" in step) return "waitForDownload";
5189
5229
  return "step";
5190
5230
  }
5231
+ function compileResponseUrlMatcher(pattern) {
5232
+ const regexBody = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*").replace(/\\\?/g, ".");
5233
+ const regex = new RegExp(regexBody);
5234
+ return (url) => regex.test(url);
5235
+ }
5191
5236
  var RUNTIME_VAR_PATTERN = /\{\{([A-Z0-9_]+)\}\}/g;
5192
5237
  function substituteRuntimeVars(input, vars) {
5193
5238
  return input.replace(RUNTIME_VAR_PATTERN, (match, name) => {
@@ -5226,6 +5271,15 @@ function applyRuntimeVars(step, vars) {
5226
5271
  if ("waitForSelector" in step) {
5227
5272
  return { waitForSelector: { selector: sub(step.waitForSelector.selector), timeout: step.waitForSelector.timeout } };
5228
5273
  }
5274
+ if ("waitForResponse" in step) {
5275
+ return {
5276
+ waitForResponse: {
5277
+ url: sub(step.waitForResponse.url),
5278
+ ...step.waitForResponse.status !== void 0 ? { status: step.waitForResponse.status } : {},
5279
+ ...step.waitForResponse.timeout !== void 0 ? { timeout: step.waitForResponse.timeout } : {}
5280
+ }
5281
+ };
5282
+ }
5229
5283
  if ("evalScript" in step) {
5230
5284
  if (typeof step.evalScript === "string") return { evalScript: sub(step.evalScript) };
5231
5285
  return {
@@ -5835,6 +5889,32 @@ var STEP_HANDLERS = {
5835
5889
  };
5836
5890
  }
5837
5891
  },
5892
+ waitForResponse: {
5893
+ capabilities: ["response", "wait"],
5894
+ run: async (h) => {
5895
+ if (!("waitForResponse" in h.step)) unknownStep();
5896
+ const { url: pattern, status, timeout } = h.step.waitForResponse;
5897
+ const matchesResponseUrl = compileResponseUrlMatcher(pattern);
5898
+ try {
5899
+ await h.driver.waitForResponse(
5900
+ (response) => matchesResponseUrl(response.url()) && (status === void 0 || response.status() === status),
5901
+ { timeout }
5902
+ );
5903
+ } catch (err) {
5904
+ const isTimeout = err instanceof Error && (err.name === "TimeoutError" || /timeout/i.test(err.message));
5905
+ if (!isTimeout) throw err;
5906
+ const statusPart = status !== void 0 ? ` with status ${status}` : "";
5907
+ const timeoutPart = timeout !== void 0 ? ` within ${timeout}ms` : "";
5908
+ throw new Error(
5909
+ `waitForResponse: no response matching "${pattern}"${statusPart} was received${timeoutPart}.`
5910
+ );
5911
+ }
5912
+ return {
5913
+ kind: "result",
5914
+ result: { type: "waitForResponse", status: "pass", durationMs: Date.now() - h.stepStart, value: pattern }
5915
+ };
5916
+ }
5917
+ },
5838
5918
  waitForNetworkIdle: {
5839
5919
  capabilities: ["wait"],
5840
5920
  run: async (h) => {
@@ -6625,6 +6705,15 @@ function formatStep(step) {
6625
6705
  const error = step.error ? ` error=${escapeMd(step.error)}` : "";
6626
6706
  return `${base}${selector}${healed}${value}${error}`;
6627
6707
  }
6708
+ function formatRetryAttempt(attempt) {
6709
+ const base = `- attempt ${attempt.attempt}: ${attempt.status.toUpperCase()} (${attempt.durationMs}ms)`;
6710
+ if (attempt.status === "pass") {
6711
+ return base;
6712
+ }
6713
+ const step = attempt.failedStep ? ` failed-step=[${attempt.failedStep.index}] ${escapeMd(attempt.failedStep.type)}` : "";
6714
+ const error = attempt.error ? ` error=${escapeMd(attempt.error)}` : "";
6715
+ return `${base}${step}${error}`;
6716
+ }
6628
6717
  function formatAssertion(assertion) {
6629
6718
  const value = assertion.value !== void 0 ? ` value=${typeof assertion.value === "string" ? escapeMd(assertion.value) : assertion.value}` : "";
6630
6719
  const error = assertion.error ? ` error=${escapeMd(assertion.error)}` : "";
@@ -6639,6 +6728,16 @@ function writeSummary(runDir, result) {
6639
6728
  lines.push(`Target: ${result.targetUrl}`);
6640
6729
  lines.push(`Started: ${result.startedAt}`);
6641
6730
  lines.push(`Duration: ${result.durationMs}ms`);
6731
+ if (result.retryHistory && result.retryHistory.length > 1) {
6732
+ lines.push("");
6733
+ lines.push("## Retries");
6734
+ if (result.retrySummary) {
6735
+ lines.push(escapeMd(result.retrySummary));
6736
+ }
6737
+ for (const attempt of result.retryHistory) {
6738
+ lines.push(formatRetryAttempt(attempt));
6739
+ }
6740
+ }
6642
6741
  lines.push("");
6643
6742
  lines.push("## Steps");
6644
6743
  for (const step of result.steps) {
@@ -6799,7 +6898,8 @@ function isHistoryEntry(value) {
6799
6898
  return false;
6800
6899
  }
6801
6900
  const entry = value;
6802
- 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");
6901
+ 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`.
6902
+ (entry.retries === void 0 || typeof entry.retries === "number" && Number.isInteger(entry.retries) && entry.retries >= 0);
6803
6903
  }
6804
6904
  function readHistory(configDir) {
6805
6905
  const filePath = historyPath(configDir);
@@ -7051,6 +7151,60 @@ async function executeHuntAttempt(options, config, configDir, interpolatedHunt,
7051
7151
  function delay3(ms) {
7052
7152
  return new Promise((resolve) => setTimeout(resolve, ms));
7053
7153
  }
7154
+ function toRetryAttempt(attempt, result) {
7155
+ const record = {
7156
+ attempt,
7157
+ status: result.status,
7158
+ durationMs: result.durationMs
7159
+ };
7160
+ if (result.status === "fail") {
7161
+ const failedIndex = result.steps.findIndex((step) => step.status === "fail");
7162
+ if (failedIndex >= 0) {
7163
+ const failed = result.steps[failedIndex];
7164
+ record.failedStep = { index: failedIndex, type: failed.type };
7165
+ if (failed.error) {
7166
+ record.error = failed.error;
7167
+ }
7168
+ }
7169
+ if (!record.error) {
7170
+ const failedAssertion = result.assertions.find((assertion) => assertion.status === "fail");
7171
+ if (failedAssertion?.error) {
7172
+ record.error = failedAssertion.error;
7173
+ }
7174
+ }
7175
+ }
7176
+ return record;
7177
+ }
7178
+ function describeAttemptFailure(attempt) {
7179
+ if (attempt.status !== "fail") {
7180
+ return void 0;
7181
+ }
7182
+ const step = attempt.failedStep?.type;
7183
+ if (step && attempt.error) {
7184
+ return `${step} (${attempt.error})`;
7185
+ }
7186
+ return step ?? attempt.error;
7187
+ }
7188
+ function buildRetrySummary(attempts, maxRetries) {
7189
+ const final = attempts[attempts.length - 1];
7190
+ const firstFailure = describeAttemptFailure(attempts[0]);
7191
+ const suffix = firstFailure ? ` \u2014 first failure: ${firstFailure}` : "";
7192
+ if (final.status === "pass") {
7193
+ return `Passed on attempt ${final.attempt} of ${maxRetries + 1}${suffix}`;
7194
+ }
7195
+ return `Failed after ${attempts.length} attempts${suffix}`;
7196
+ }
7197
+ function finalizeRetryOutcome(outcome, attempts, maxRetries, junit) {
7198
+ outcome.result = writeReports(
7199
+ outcome.runDir,
7200
+ {
7201
+ ...outcome.result,
7202
+ retryHistory: attempts,
7203
+ retrySummary: buildRetrySummary(attempts, maxRetries)
7204
+ },
7205
+ { junit }
7206
+ );
7207
+ }
7054
7208
  async function runHunt(options) {
7055
7209
  const { config, configDir } = loadConfig(options.configPath);
7056
7210
  if (config.target.type === "macos") {
@@ -7080,7 +7234,9 @@ async function runHunt(options) {
7080
7234
  }
7081
7235
  const maxRetries = hunt.retry?.maxRetries ?? 0;
7082
7236
  const retryDelay = hunt.retry?.delay ?? 0;
7237
+ const junit = options.junit ?? config.artifacts.junit;
7083
7238
  let lastResult;
7239
+ const attempts = [];
7084
7240
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
7085
7241
  if (attempt > 0 && retryDelay > 0) {
7086
7242
  await delay3(retryDelay);
@@ -7096,19 +7252,20 @@ async function runHunt(options) {
7096
7252
  targetUrl,
7097
7253
  allowedDomains
7098
7254
  );
7255
+ attempts.push(toRetryAttempt(attempt + 1, lastResult.result));
7099
7256
  if (lastResult.result.status === "pass") {
7100
- if (attempt > 0) {
7101
- lastResult.result.artifacts.summary = `Passed on attempt ${attempt + 1} of ${maxRetries + 1}`;
7257
+ if (attempts.length > 1) {
7258
+ finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
7102
7259
  }
7103
- recordHistory(configDir, lastResult, config.history.maxRuns);
7260
+ recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
7104
7261
  return lastResult;
7105
7262
  }
7106
7263
  }
7107
- if (maxRetries > 0 && lastResult) {
7108
- lastResult.result.artifacts.summary = `Failed after ${maxRetries + 1} attempts`;
7109
- }
7110
7264
  if (lastResult) {
7111
- recordHistory(configDir, lastResult, config.history.maxRuns);
7265
+ if (attempts.length > 1) {
7266
+ finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
7267
+ }
7268
+ recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
7112
7269
  }
7113
7270
  return lastResult;
7114
7271
  }
@@ -7312,7 +7469,9 @@ async function runNativeHunt(options, config, configDir, target, native) {
7312
7469
  }
7313
7470
  const maxRetries = hunt.retry?.maxRetries ?? 0;
7314
7471
  const retryDelay = hunt.retry?.delay ?? 0;
7472
+ const junit = options.junit ?? config.artifacts.junit;
7315
7473
  let lastResult;
7474
+ const attempts = [];
7316
7475
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
7317
7476
  if (attempt > 0 && retryDelay > 0) {
7318
7477
  await delay3(retryDelay);
@@ -7327,23 +7486,24 @@ async function runNativeHunt(options, config, configDir, target, native) {
7327
7486
  randomVars,
7328
7487
  config.guardrails.allowedApps
7329
7488
  );
7489
+ attempts.push(toRetryAttempt(attempt + 1, lastResult.result));
7330
7490
  if (lastResult.result.status === "pass") {
7331
- if (attempt > 0) {
7332
- lastResult.result.artifacts.summary = `Passed on attempt ${attempt + 1} of ${maxRetries + 1}`;
7491
+ if (attempts.length > 1) {
7492
+ finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
7333
7493
  }
7334
- recordHistory(configDir, lastResult, config.history.maxRuns);
7494
+ recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
7335
7495
  return lastResult;
7336
7496
  }
7337
7497
  }
7338
- if (maxRetries > 0 && lastResult) {
7339
- lastResult.result.artifacts.summary = `Failed after ${maxRetries + 1} attempts`;
7340
- }
7341
7498
  if (lastResult) {
7342
- recordHistory(configDir, lastResult, config.history.maxRuns);
7499
+ if (attempts.length > 1) {
7500
+ finalizeRetryOutcome(lastResult, attempts, maxRetries, junit);
7501
+ }
7502
+ recordHistory(configDir, lastResult, config.history.maxRuns, attempts.length - 1);
7343
7503
  }
7344
7504
  return lastResult;
7345
7505
  }
7346
- function recordHistory(configDir, outcome, maxRuns) {
7506
+ function recordHistory(configDir, outcome, maxRuns, retries = 0) {
7347
7507
  try {
7348
7508
  const relativeRunDir = import_node_path16.default.relative(configDir, outcome.runDir);
7349
7509
  appendEntry(
@@ -7353,7 +7513,9 @@ function recordHistory(configDir, outcome, maxRuns) {
7353
7513
  status: outcome.result.status,
7354
7514
  durationMs: outcome.result.durationMs,
7355
7515
  startedAt: outcome.result.startedAt,
7356
- runDir: relativeRunDir || void 0
7516
+ runDir: relativeRunDir || void 0,
7517
+ // Omit when no retry ran so entries stay additive/backward-compatible.
7518
+ retries: retries > 0 ? retries : void 0
7357
7519
  },
7358
7520
  maxRuns
7359
7521
  );
@@ -8421,6 +8583,7 @@ var STEP_REFERENCE = `
8421
8583
  - waitForSelector: { selector: "#el", timeout: 5000 }
8422
8584
  - waitForUrl: { value: "/path", timeout: 5000 }
8423
8585
  - waitForNetworkIdle: { timeout: 5000 }
8586
+ - waitForResponse: { url: "**/api/orders", status: 200, timeout: 10000 } \u2014 wait for a matching network response (url is a glob/substring; status is optional)
8424
8587
 
8425
8588
  ### Interaction
8426
8589
  - click: "Button Text" \u2014 click by text (tries role=button first)