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 +61 -8
- package/dist/{chunk-2RNOK64Y.js → chunk-SRWAF2RI.js} +156 -19
- package/dist/chunk-SRWAF2RI.js.map +1 -0
- package/dist/{chunk-JFJQNJSJ.js → chunk-TTZ2IL3P.js} +27 -1
- package/dist/chunk-TTZ2IL3P.js.map +1 -0
- package/dist/index.cjs +207 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +29 -7
- package/dist/index.js.map +1 -1
- package/dist/lib.cjs +180 -17
- package/dist/lib.cjs.map +1 -1
- package/dist/lib.d.cts +48 -1
- package/dist/lib.d.ts +48 -1
- package/dist/lib.js +2 -2
- package/dist/{loader-JTHA4BYG.js → loader-CAE4FPBK.js} +2 -2
- package/package.json +4 -2
- package/dist/chunk-2RNOK64Y.js.map +0 -1
- package/dist/chunk-JFJQNJSJ.js.map +0 -1
- /package/dist/{loader-JTHA4BYG.js.map → loader-CAE4FPBK.js.map} +0 -0
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 (
|
|
7101
|
-
lastResult
|
|
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
|
-
|
|
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 (
|
|
7332
|
-
lastResult
|
|
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
|
-
|
|
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)
|