@swmansion/argent 0.16.1-next.2 → 0.16.1-next.3
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/cli-cmds.mjs +12 -2
- package/dist/tool-server.cjs +272 -49
- package/package.json +1 -1
- package/skills/argent-create-flow/SKILL.md +2 -0
package/dist/cli-cmds.mjs
CHANGED
|
@@ -8489,6 +8489,14 @@ function parseRunArgs(argv) {
|
|
|
8489
8489
|
}
|
|
8490
8490
|
return out;
|
|
8491
8491
|
}
|
|
8492
|
+
function renderEchoLine(s) {
|
|
8493
|
+
if (!s.message) return void 0;
|
|
8494
|
+
if (s.status === "skip") {
|
|
8495
|
+
const reason = s.reason ? ` \u2014 ${s.reason}` : "";
|
|
8496
|
+
return ` ${STATUS_GLYPH.skip} \u203A ${s.message}${reason}`;
|
|
8497
|
+
}
|
|
8498
|
+
return ` \u203A ${s.message}`;
|
|
8499
|
+
}
|
|
8492
8500
|
function renderStepLine(s, n2, topFlow) {
|
|
8493
8501
|
const where = s.flow && s.flow !== topFlow ? ` [${s.flow}]` : "";
|
|
8494
8502
|
const what = s.tool ?? s.target;
|
|
@@ -8576,7 +8584,8 @@ function renderReport(report) {
|
|
|
8576
8584
|
let n2 = 0;
|
|
8577
8585
|
for (const s of report.steps) {
|
|
8578
8586
|
if (s.kind === "echo") {
|
|
8579
|
-
|
|
8587
|
+
const line = renderEchoLine(s);
|
|
8588
|
+
if (line) lines.push(line);
|
|
8580
8589
|
continue;
|
|
8581
8590
|
}
|
|
8582
8591
|
n2++;
|
|
@@ -8653,7 +8662,8 @@ async function flow(argv, options) {
|
|
|
8653
8662
|
if (liveSteps === 0) console.log(`Flow "${flowName}"`);
|
|
8654
8663
|
liveSteps++;
|
|
8655
8664
|
if (s.kind === "echo") {
|
|
8656
|
-
|
|
8665
|
+
const line = renderEchoLine(s);
|
|
8666
|
+
if (line) console.log(line);
|
|
8657
8667
|
return;
|
|
8658
8668
|
}
|
|
8659
8669
|
liveIndex++;
|
package/dist/tool-server.cjs
CHANGED
|
@@ -144776,6 +144776,16 @@ function toYamlStep(step) {
|
|
|
144776
144776
|
return { launch: step.app };
|
|
144777
144777
|
case "run":
|
|
144778
144778
|
return { run: step.flow };
|
|
144779
|
+
case "when": {
|
|
144780
|
+
const when = step.condition.kind === "platform" ? { platform: step.condition.platform } : waitToYaml(
|
|
144781
|
+
step.condition.condition,
|
|
144782
|
+
step.condition.selector,
|
|
144783
|
+
step.condition.expectedText,
|
|
144784
|
+
step.condition.textMatch,
|
|
144785
|
+
void 0
|
|
144786
|
+
);
|
|
144787
|
+
return { when, steps: step.steps.map(toYamlStep) };
|
|
144788
|
+
}
|
|
144779
144789
|
case "tap": {
|
|
144780
144790
|
const target = targetToYaml(step);
|
|
144781
144791
|
return { tap: step.times !== void 0 ? { on: target, times: step.times } : target };
|
|
@@ -144843,7 +144853,13 @@ function toYamlStep(step) {
|
|
|
144843
144853
|
}
|
|
144844
144854
|
}
|
|
144845
144855
|
function badEntry(raw, detail) {
|
|
144846
|
-
|
|
144856
|
+
let rendered;
|
|
144857
|
+
try {
|
|
144858
|
+
rendered = JSON.stringify(raw);
|
|
144859
|
+
} catch {
|
|
144860
|
+
rendered = "[cyclic entry]";
|
|
144861
|
+
}
|
|
144862
|
+
throw new FailureError(`Unrecognized flow entry (${detail}): ${rendered}`, {
|
|
144847
144863
|
error_code: FAILURE_CODES.FLOW_ENTRY_UNRECOGNIZED,
|
|
144848
144864
|
failure_stage: "flow_file_parse_step",
|
|
144849
144865
|
failure_area: "tool_server",
|
|
@@ -145086,6 +145102,7 @@ var STEP_DIRECTIVE_KEYS = [
|
|
|
145086
145102
|
"echo",
|
|
145087
145103
|
"launch",
|
|
145088
145104
|
"run",
|
|
145105
|
+
"when",
|
|
145089
145106
|
"tool",
|
|
145090
145107
|
"tap",
|
|
145091
145108
|
"long-press",
|
|
@@ -145190,8 +145207,85 @@ function parseLongPress(body, entry) {
|
|
|
145190
145207
|
}
|
|
145191
145208
|
return { kind: "long-press", ...parseTarget(body, "long-press") };
|
|
145192
145209
|
}
|
|
145193
|
-
function
|
|
145210
|
+
function parseWhenCondition(raw) {
|
|
145211
|
+
const conditionKeys = `${WAIT_CONDITIONS.join(", ")}, platform`;
|
|
145212
|
+
if (raw === null || typeof raw !== "object") {
|
|
145213
|
+
badEntry({ when: raw }, `when needs exactly one condition key (${conditionKeys})`);
|
|
145214
|
+
}
|
|
145215
|
+
const b = raw;
|
|
145216
|
+
const present = [...WAIT_CONDITIONS, "platform"].filter((c) => c in b);
|
|
145217
|
+
if (present.length !== 1) {
|
|
145218
|
+
badEntry({ when: raw }, `when needs exactly one condition key (${conditionKeys})`);
|
|
145219
|
+
}
|
|
145220
|
+
if ("timeout" in b) {
|
|
145221
|
+
badEntry(
|
|
145222
|
+
{ when: raw },
|
|
145223
|
+
"when takes no timeout \u2014 the guard is evaluated with the short assert grace so a skipped block never adds a full await wait"
|
|
145224
|
+
);
|
|
145225
|
+
}
|
|
145226
|
+
if (present[0] === "platform") {
|
|
145227
|
+
if (Object.keys(b).length !== 1) {
|
|
145228
|
+
badEntry({ when: raw }, "when.platform takes no other keys");
|
|
145229
|
+
}
|
|
145230
|
+
const p = b.platform;
|
|
145231
|
+
if (typeof p !== "string" || !LAUNCH_PLATFORMS.includes(p)) {
|
|
145232
|
+
badEntry({ when: raw }, `when.platform must be one of ${LAUNCH_PLATFORMS.join(", ")}`);
|
|
145233
|
+
}
|
|
145234
|
+
return { kind: "platform", platform: p };
|
|
145235
|
+
}
|
|
145236
|
+
const { timeout: _timeout, ...cond } = parseWaitFields(raw, "when");
|
|
145237
|
+
const { selector, expectedText } = cond;
|
|
145238
|
+
for (const s of [
|
|
145239
|
+
expectedText,
|
|
145240
|
+
selector.text,
|
|
145241
|
+
selector.textMatches,
|
|
145242
|
+
selector.identifier,
|
|
145243
|
+
selector.role
|
|
145244
|
+
]) {
|
|
145245
|
+
if (s !== void 0 && s.includes(SECRET_PLACEHOLDER_MARKER)) {
|
|
145246
|
+
badEntry(
|
|
145247
|
+
{ when: raw },
|
|
145248
|
+
"when takes no {{secret:\u2026}} placeholder \u2014 secrets resolve only in text-entry steps (`type:`), never in condition evaluation, so the guard tests literal placeholder text that is never on screen: permanently false (for `hidden`, vacuously true); use the literal on-screen text instead"
|
|
145249
|
+
);
|
|
145250
|
+
}
|
|
145251
|
+
}
|
|
145252
|
+
return { kind: "ui", ...cond };
|
|
145253
|
+
}
|
|
145254
|
+
var MAX_WHEN_DEPTH = 20;
|
|
145255
|
+
function parseWhenStep(raw, depth) {
|
|
145256
|
+
if (depth >= MAX_WHEN_DEPTH) {
|
|
145257
|
+
badEntry(
|
|
145258
|
+
raw,
|
|
145259
|
+
`when blocks nest deeper than ${MAX_WHEN_DEPTH} levels \u2014 check for a cyclic YAML alias (\`steps: &s \u2026 steps: *s\`)`
|
|
145260
|
+
);
|
|
145261
|
+
}
|
|
145262
|
+
if ("else" in raw) {
|
|
145263
|
+
badEntry(
|
|
145264
|
+
raw,
|
|
145265
|
+
"when has no else \u2014 paths may only reconverge, never diverge; two genuinely different paths are two flows"
|
|
145266
|
+
);
|
|
145267
|
+
}
|
|
145268
|
+
if (!Object.keys(raw).every((k) => k === "when" || k === "steps")) {
|
|
145269
|
+
badEntry(raw, "a when step takes exactly { when: <condition>, steps: [...] }");
|
|
145270
|
+
}
|
|
145271
|
+
const condition = parseWhenCondition(raw.when);
|
|
145272
|
+
if (!Array.isArray(raw.steps) || raw.steps.length === 0) {
|
|
145273
|
+
badEntry(raw, "when needs a non-empty steps list to guard");
|
|
145274
|
+
}
|
|
145275
|
+
const steps = raw.steps.map((s) => {
|
|
145276
|
+
if (s !== null && typeof s === "object") return fromYamlStep(s, depth + 1);
|
|
145277
|
+
return badEntry(s, "step must be an object");
|
|
145278
|
+
});
|
|
145279
|
+
return { kind: "when", condition, steps };
|
|
145280
|
+
}
|
|
145281
|
+
function fromYamlStep(raw, whenDepth = 0) {
|
|
145194
145282
|
const entry = raw;
|
|
145283
|
+
if ("optional" in raw) {
|
|
145284
|
+
badEntry(
|
|
145285
|
+
raw,
|
|
145286
|
+
"optional is not supported \u2014 guard the step with a when: block instead (`when: { visible: <target> }` + `steps:`)"
|
|
145287
|
+
);
|
|
145288
|
+
}
|
|
145195
145289
|
const kinds = STEP_DIRECTIVE_KEYS.filter((k) => k in entry);
|
|
145196
145290
|
if (kinds.length === 0) {
|
|
145197
145291
|
const hint = Object.keys(entry).map((k) => closestKey(k, STEP_DIRECTIVE_KEYS)).find((h) => h !== null);
|
|
@@ -145204,17 +145298,20 @@ function fromYamlStep(raw) {
|
|
|
145204
145298
|
);
|
|
145205
145299
|
}
|
|
145206
145300
|
const kind = kinds[0];
|
|
145207
|
-
|
|
145208
|
-
|
|
145209
|
-
|
|
145210
|
-
|
|
145211
|
-
|
|
145212
|
-
|
|
145213
|
-
|
|
145301
|
+
if (kind !== "when") {
|
|
145302
|
+
const siblings = kind === "tool" ? ["tool", "args", "delayMs"] : [kind];
|
|
145303
|
+
const extras = Object.keys(entry).filter((k) => !siblings.includes(k));
|
|
145304
|
+
if (extras.length > 0) {
|
|
145305
|
+
badEntry(
|
|
145306
|
+
raw,
|
|
145307
|
+
`a \`${kind}\` step has ${describeUnknownKeys(extras, siblings)}` + (kind === "tool" ? " \u2014 a tool step takes only `tool`, `args`, `delayMs`" : ` \u2014 step options go inside the \`${kind}:\` value, not beside it`)
|
|
145308
|
+
);
|
|
145309
|
+
}
|
|
145214
145310
|
}
|
|
145215
145311
|
if ("echo" in raw) return { kind: "echo", message: String(raw.echo) };
|
|
145216
145312
|
if ("launch" in raw) return { kind: "launch", app: parseLaunch(raw.launch) };
|
|
145217
145313
|
if ("run" in raw) return { kind: "run", flow: String(raw.run) };
|
|
145314
|
+
if ("when" in raw) return parseWhenStep(entry, whenDepth);
|
|
145218
145315
|
if ("tap" in raw) return parseTap(raw.tap, raw);
|
|
145219
145316
|
if ("long-press" in raw) {
|
|
145220
145317
|
return parseLongPress(raw["long-press"], raw);
|
|
@@ -146115,6 +146212,11 @@ var fs38 = __toESM(require("node:fs/promises"));
|
|
|
146115
146212
|
function selectorLabel(sel) {
|
|
146116
146213
|
return JSON.stringify(selectorToYaml(sel));
|
|
146117
146214
|
}
|
|
146215
|
+
function textConditionLabel(sel, expectedText, textMatch) {
|
|
146216
|
+
const selector = selectorLabel(sel);
|
|
146217
|
+
const expected = expectedText ?? "";
|
|
146218
|
+
return textMatch === "matches" ? `text ${selector} matches /${expected}/` : textMatch === "equals" ? `text ${selector} == ${JSON.stringify(expected)}` : `text ${selector} contains ${JSON.stringify(expected)}`;
|
|
146219
|
+
}
|
|
146118
146220
|
var zodSchema61 = external_exports.object({});
|
|
146119
146221
|
var flowFinishRecordingTool = {
|
|
146120
146222
|
id: "flow-finish-recording",
|
|
@@ -146152,18 +146254,20 @@ You can still edit the .yaml file directly afterwards to remove or reorder steps
|
|
|
146152
146254
|
return `${n}. type: ${selectorLabel(step.into)} \u2190 "${step.text}"`;
|
|
146153
146255
|
case "await":
|
|
146154
146256
|
case "assert": {
|
|
146155
|
-
|
|
146156
|
-
if (step.condition !== "text") {
|
|
146157
|
-
tail = `${step.condition} ${selectorLabel(step.selector)}`;
|
|
146158
|
-
} else {
|
|
146159
|
-
const selector = selectorLabel(step.selector);
|
|
146160
|
-
const expected = step.expectedText ?? "";
|
|
146161
|
-
tail = step.textMatch === "matches" ? `text ${selector} matches /${expected}/` : step.textMatch === "equals" ? `text ${selector} == ${JSON.stringify(expected)}` : `text ${selector} contains ${JSON.stringify(expected)}`;
|
|
146162
|
-
}
|
|
146257
|
+
const tail = step.condition === "text" ? textConditionLabel(step.selector, step.expectedText, step.textMatch) : `${step.condition} ${selectorLabel(step.selector)}`;
|
|
146163
146258
|
return `${n}. ${step.kind}: ${tail}`;
|
|
146164
146259
|
}
|
|
146165
146260
|
case "wait":
|
|
146166
146261
|
return `${n}. wait: ${step.ms}ms`;
|
|
146262
|
+
case "when": {
|
|
146263
|
+
const cond = step.condition.kind === "platform" ? `platform ${step.condition.platform}` : step.condition.condition === "text" ? textConditionLabel(
|
|
146264
|
+
step.condition.selector,
|
|
146265
|
+
step.condition.expectedText,
|
|
146266
|
+
step.condition.textMatch
|
|
146267
|
+
) : `${step.condition.condition} ${selectorLabel(step.condition.selector)}`;
|
|
146268
|
+
const count = step.steps.length;
|
|
146269
|
+
return `${n}. when: ${cond} (${count} step${count === 1 ? "" : "s"})`;
|
|
146270
|
+
}
|
|
146167
146271
|
case "scroll-to":
|
|
146168
146272
|
return `${n}. scroll-to: ${selectorLabel(step.target)} (${step.direction})`;
|
|
146169
146273
|
case "snapshot":
|
|
@@ -146235,6 +146339,10 @@ function axisFullyInside(frame, direction, clip2) {
|
|
|
146235
146339
|
return direction === "down" || direction === "right" ? fEnd <= clipEnd - EDGE_EPS && fStart >= clipStart - EDGE_EPS : fStart >= clipStart + EDGE_EPS && fEnd <= clipEnd + EDGE_EPS;
|
|
146236
146340
|
}
|
|
146237
146341
|
var DEFAULT_ASSERT_TIMEOUT_MS = 1e3;
|
|
146342
|
+
var CONDITION_DARK_TAIL_TOLERANCE_MS = POLL_INTERVAL_MS * 2;
|
|
146343
|
+
function probeWhenCondition(env, cond) {
|
|
146344
|
+
return waitForCondition(env, cond, DEFAULT_ASSERT_TIMEOUT_MS);
|
|
146345
|
+
}
|
|
146238
146346
|
function selectorAlternatives(sel) {
|
|
146239
146347
|
return sel.loose && sel.text !== void 0 ? [{ identifier: sel.text }, { text: sel.text }] : [sel];
|
|
146240
146348
|
}
|
|
@@ -146495,7 +146603,8 @@ async function waitForCondition(env, step, timeoutMs) {
|
|
|
146495
146603
|
let lastMatches = [];
|
|
146496
146604
|
let fetchError;
|
|
146497
146605
|
let everMatched = false;
|
|
146498
|
-
let
|
|
146606
|
+
let lastTrustedReadAt;
|
|
146607
|
+
let lastReadTrusted;
|
|
146499
146608
|
let finalPoll = false;
|
|
146500
146609
|
for (; ; ) {
|
|
146501
146610
|
if (env.signal?.aborted) return ABORTED_OUTCOME;
|
|
@@ -146505,12 +146614,14 @@ async function waitForCondition(env, step, timeoutMs) {
|
|
|
146505
146614
|
fetchError = void 0;
|
|
146506
146615
|
everMatched ||= lastMatches.length > 0;
|
|
146507
146616
|
const blind = data.tree.children.length === 0 && Boolean(data.hint || data.should_restart || everMatched);
|
|
146508
|
-
|
|
146617
|
+
if (!blind) lastTrustedReadAt = Date.now();
|
|
146618
|
+
lastReadTrusted = !blind;
|
|
146509
146619
|
if (!blind && evaluateCondition(step.condition, step.expectedText, lastMatches, step.textMatch)) {
|
|
146510
146620
|
return { ok: true };
|
|
146511
146621
|
}
|
|
146512
146622
|
} catch (err) {
|
|
146513
146623
|
fetchError = err instanceof Error ? err.message : String(err);
|
|
146624
|
+
lastReadTrusted = false;
|
|
146514
146625
|
}
|
|
146515
146626
|
if (Date.now() >= deadline) {
|
|
146516
146627
|
if (finalPoll) break;
|
|
@@ -146522,22 +146633,34 @@ async function waitForCondition(env, step, timeoutMs) {
|
|
|
146522
146633
|
return ABORTED_OUTCOME;
|
|
146523
146634
|
}
|
|
146524
146635
|
}
|
|
146525
|
-
if (
|
|
146526
|
-
if (step.condition === "hidden" && !everTrustedRead) {
|
|
146636
|
+
if (lastTrustedReadAt === void 0) {
|
|
146527
146637
|
return {
|
|
146528
146638
|
ok: false,
|
|
146529
|
-
|
|
146639
|
+
indeterminate: true,
|
|
146640
|
+
reason: fetchError ? `could not read the UI tree: ${fetchError}` : "could not evaluate the condition \u2014 every read of the UI tree was empty or degraded"
|
|
146530
146641
|
};
|
|
146531
146642
|
}
|
|
146643
|
+
if (!lastReadTrusted) {
|
|
146644
|
+
if (step.condition === "hidden") {
|
|
146645
|
+
return {
|
|
146646
|
+
ok: false,
|
|
146647
|
+
indeterminate: true,
|
|
146648
|
+
reason: fetchError ? `could not confirm the element is hidden \u2014 it was visible earlier, but the last UI read failed: ${fetchError}` : "could not confirm the element is hidden \u2014 it was visible earlier, but the last UI reads were empty"
|
|
146649
|
+
};
|
|
146650
|
+
}
|
|
146651
|
+
const darkTailMs = Date.now() - lastTrustedReadAt;
|
|
146652
|
+
if (darkTailMs > CONDITION_DARK_TAIL_TOLERANCE_MS) {
|
|
146653
|
+
return {
|
|
146654
|
+
ok: false,
|
|
146655
|
+
indeterminate: true,
|
|
146656
|
+
reason: fetchError ? `could not evaluate the condition \u2014 the UI tree was unreadable for the final ${darkTailMs}ms of the window: ${fetchError}` : `could not evaluate the condition \u2014 the UI tree reads were empty or degraded for the final ${darkTailMs}ms of the window`
|
|
146657
|
+
};
|
|
146658
|
+
}
|
|
146659
|
+
}
|
|
146660
|
+
const blipNote = !lastReadTrusted && fetchError ? ` (the final poll could not read the UI tree: ${fetchError})` : "";
|
|
146532
146661
|
return {
|
|
146533
146662
|
ok: false,
|
|
146534
|
-
reason: assertReason(
|
|
146535
|
-
step.condition,
|
|
146536
|
-
step.selector,
|
|
146537
|
-
step.expectedText,
|
|
146538
|
-
step.textMatch,
|
|
146539
|
-
lastMatches
|
|
146540
|
-
)
|
|
146663
|
+
reason: assertReason(step.condition, step.selector, step.expectedText, step.textMatch, lastMatches) + blipNote
|
|
146541
146664
|
};
|
|
146542
146665
|
}
|
|
146543
146666
|
function assertReason(condition, selector, expectedText, textMatch, matches2) {
|
|
@@ -146548,7 +146671,7 @@ function assertReason(condition, selector, expectedText, textMatch, matches2) {
|
|
|
146548
146671
|
case "visible":
|
|
146549
146672
|
return matches2.length > 0 ? `element(s) matched ${sel} but none was visible (zero-area frame)` : `no element matched selector ${sel}`;
|
|
146550
146673
|
case "hidden":
|
|
146551
|
-
return
|
|
146674
|
+
return `an element matching ${sel} was still visible`;
|
|
146552
146675
|
case "text": {
|
|
146553
146676
|
const first = firstInReadingOrder(matches2.filter(isVisible)) ?? firstInReadingOrder(matches2);
|
|
146554
146677
|
if (!first) return `no element matched selector ${sel}`;
|
|
@@ -148784,7 +148907,7 @@ var zodSchema62 = external_exports.object({
|
|
|
148784
148907
|
device: external_exports.string().optional().describe(
|
|
148785
148908
|
"Device id to run against (iOS UDID, Android/Vega serial, Chromium id). Auto-detected when omitted."
|
|
148786
148909
|
),
|
|
148787
|
-
platform: external_exports.enum(
|
|
148910
|
+
platform: external_exports.enum(LAUNCH_PLATFORMS).optional().describe("Restrict auto-detection to this platform when several devices are booted."),
|
|
148788
148911
|
updateBaselines: external_exports.boolean().optional().describe(
|
|
148789
148912
|
"Write/refresh screenshot baselines for `snapshot` steps instead of diffing against them."
|
|
148790
148913
|
),
|
|
@@ -148919,6 +149042,9 @@ holds; \`tap\`/\`long-press\` alternatively take a raw normalized point \u2014 b
|
|
|
148919
149042
|
condition; \`wait\` pauses for a fixed number of milliseconds; \`assert\` checks one now; \`snapshot\`
|
|
148920
149043
|
diffs a screenshot against a stored baseline (a missing baseline fails the step \u2014 set updateBaselines
|
|
148921
149044
|
to adopt the current screen); \`echo\` annotates; \`run\` executes a referenced fragment inline.
|
|
149045
|
+
A \`when:\` block (condition + \`steps:\`, no else) runs its steps only if the condition holds \u2014
|
|
149046
|
+
checked once with the short assert grace \u2014 for one-sided divergences like interstitials and coach
|
|
149047
|
+
marks; a skipped block reports distinctly and failures inside an entered block are real failures.
|
|
148922
149048
|
A flow that begins with a \`launch\` step is a self-contained e2e flow; one that doesn't runs against the
|
|
148923
149049
|
device's current state. Device id is injected by the runner (flows store none) \u2014 pass \`device\` or
|
|
148924
149050
|
\`platform\` to pick one, else the single booted device is used. For a Chromium e2e flow the \`launch\`
|
|
@@ -148962,13 +149088,15 @@ returns a notice with the prerequisite instead of running.`,
|
|
|
148962
149088
|
chromiumLaunched: false,
|
|
148963
149089
|
...ctx?.emitProgress ? { onStepReport: ctx.emitProgress } : {}
|
|
148964
149090
|
};
|
|
149091
|
+
let aborted2;
|
|
148965
149092
|
try {
|
|
148966
149093
|
await execSteps(state3, flow.steps, params.name, [params.name]);
|
|
148967
149094
|
} finally {
|
|
149095
|
+
aborted2 = state3.signal?.aborted === true;
|
|
148968
149096
|
if (state3.pinned) await restoreStatusBar(device);
|
|
148969
149097
|
if (resolved.booted) await teardownBootedChromium(registry2, resolved.booted);
|
|
148970
149098
|
}
|
|
148971
|
-
return summarize(params.name, device.id, flow.executionPrerequisite, state3.reports);
|
|
149099
|
+
return summarize(params.name, device.id, flow.executionPrerequisite, state3.reports, aborted2);
|
|
148972
149100
|
}
|
|
148973
149101
|
};
|
|
148974
149102
|
}
|
|
@@ -149024,14 +149152,12 @@ async function frontChromiumPage(registry2, device) {
|
|
|
149024
149152
|
} catch {
|
|
149025
149153
|
}
|
|
149026
149154
|
}
|
|
149027
|
-
function summarize(flowName, deviceId, executionPrerequisite, steps) {
|
|
149155
|
+
function summarize(flowName, deviceId, executionPrerequisite, steps, aborted2) {
|
|
149028
149156
|
let passed = 0;
|
|
149029
149157
|
let failed = 0;
|
|
149030
149158
|
let skipped = 0;
|
|
149031
149159
|
let errored = 0;
|
|
149032
|
-
let hasSkippedReport = false;
|
|
149033
149160
|
for (const s of steps) {
|
|
149034
|
-
hasSkippedReport ||= s.status === "skip";
|
|
149035
149161
|
if (s.kind === "echo") continue;
|
|
149036
149162
|
if (s.status === "pass") passed++;
|
|
149037
149163
|
else if (s.status === "fail") failed++;
|
|
@@ -149042,11 +149168,13 @@ function summarize(flowName, deviceId, executionPrerequisite, steps) {
|
|
|
149042
149168
|
flow: flowName,
|
|
149043
149169
|
device: deviceId,
|
|
149044
149170
|
executionPrerequisite,
|
|
149045
|
-
// A
|
|
149046
|
-
//
|
|
149047
|
-
//
|
|
149048
|
-
//
|
|
149049
|
-
|
|
149171
|
+
// A cancelled run must never read as PASS — it may contain skips alone
|
|
149172
|
+
// (no fail/error report), so the verdict folds the abort in directly. A
|
|
149173
|
+
// skip by itself is NOT a failure: an unmet `when:` guard skips its block
|
|
149174
|
+
// as a successful omission, and a hard stop already carries its own
|
|
149175
|
+
// fail/error report.
|
|
149176
|
+
ok: failed === 0 && errored === 0 && !aborted2,
|
|
149177
|
+
...aborted2 ? { aborted: true } : {},
|
|
149050
149178
|
passed,
|
|
149051
149179
|
failed,
|
|
149052
149180
|
skipped,
|
|
@@ -149066,6 +149194,13 @@ function selectorLabel2(sel) {
|
|
|
149066
149194
|
if (sel.role) parts.push(`role=${sel.role}`);
|
|
149067
149195
|
return parts.join(" ");
|
|
149068
149196
|
}
|
|
149197
|
+
function conditionLabel(cond, renderSelector) {
|
|
149198
|
+
const sel = renderSelector(cond.selector);
|
|
149199
|
+
if (cond.condition === "text") {
|
|
149200
|
+
return `${sel} ${describeTextExpectation(cond.expectedText, cond.textMatch)}`;
|
|
149201
|
+
}
|
|
149202
|
+
return `${cond.condition} ${sel}`;
|
|
149203
|
+
}
|
|
149069
149204
|
function stepTarget(step) {
|
|
149070
149205
|
switch (step.kind) {
|
|
149071
149206
|
case "tap":
|
|
@@ -149076,13 +149211,10 @@ function stepTarget(step) {
|
|
|
149076
149211
|
case "type":
|
|
149077
149212
|
return `into ${selectorLabel2(step.into)}`;
|
|
149078
149213
|
case "await":
|
|
149079
|
-
case "assert":
|
|
149080
|
-
|
|
149081
|
-
|
|
149082
|
-
|
|
149083
|
-
}
|
|
149084
|
-
return `${step.condition} ${sel}`;
|
|
149085
|
-
}
|
|
149214
|
+
case "assert":
|
|
149215
|
+
return conditionLabel(step, selectorLabel2);
|
|
149216
|
+
case "when":
|
|
149217
|
+
return step.condition.kind === "platform" ? `platform ${step.condition.platform}` : conditionLabel(step.condition, selectorLabel2);
|
|
149086
149218
|
case "scroll-to": {
|
|
149087
149219
|
const dir = step.direction !== "down" ? ` (${step.direction})` : "";
|
|
149088
149220
|
return `${selectorLabel2(step.target)}${dir}`;
|
|
@@ -149102,8 +149234,12 @@ async function execSteps(state3, steps, sourceFlow, runStack) {
|
|
|
149102
149234
|
kind: step.kind,
|
|
149103
149235
|
status: "skip",
|
|
149104
149236
|
flow: sourceFlow,
|
|
149105
|
-
target: stepTarget(step)
|
|
149237
|
+
target: stepTarget(step),
|
|
149238
|
+
// Carry the echo's message so a skipped narration renders as a skip
|
|
149239
|
+
// line rather than vanishing — matching reportBlockSkipped.
|
|
149240
|
+
...step.kind === "echo" ? { message: step.message } : {}
|
|
149106
149241
|
});
|
|
149242
|
+
if (step.kind === "when") reportBlockSkipped(state3, step.steps, sourceFlow);
|
|
149107
149243
|
continue;
|
|
149108
149244
|
}
|
|
149109
149245
|
if (state3.signal?.aborted) {
|
|
@@ -149114,19 +149250,106 @@ async function execSteps(state3, steps, sourceFlow, runStack) {
|
|
|
149114
149250
|
status: "skip",
|
|
149115
149251
|
reason: "run aborted",
|
|
149116
149252
|
flow: sourceFlow,
|
|
149117
|
-
target: stepTarget(step)
|
|
149253
|
+
target: stepTarget(step),
|
|
149254
|
+
...step.kind === "echo" ? { message: step.message } : {}
|
|
149118
149255
|
});
|
|
149256
|
+
if (step.kind === "when") reportBlockSkipped(state3, step.steps, sourceFlow, "run aborted");
|
|
149119
149257
|
continue;
|
|
149120
149258
|
}
|
|
149121
149259
|
if (step.kind === "run") {
|
|
149122
149260
|
await execRunStep(state3, step, runStack);
|
|
149123
149261
|
continue;
|
|
149124
149262
|
}
|
|
149263
|
+
if (step.kind === "when") {
|
|
149264
|
+
await execWhenStep(state3, step, sourceFlow, runStack);
|
|
149265
|
+
continue;
|
|
149266
|
+
}
|
|
149125
149267
|
const report = await execLeafStep(state3, step, index, sourceFlow);
|
|
149126
149268
|
pushReport(state3, report);
|
|
149127
149269
|
if (report.status === "fail" || report.status === "error") state3.stopped = true;
|
|
149128
149270
|
}
|
|
149129
149271
|
}
|
|
149272
|
+
function describeWhenCondition(cond) {
|
|
149273
|
+
if (cond.kind === "platform") return `platform ${cond.platform}`;
|
|
149274
|
+
return conditionLabel(cond, describeSelector);
|
|
149275
|
+
}
|
|
149276
|
+
function reportBlockSkipped(state3, steps, sourceFlow, reason) {
|
|
149277
|
+
for (const step of steps) {
|
|
149278
|
+
pushReport(state3, {
|
|
149279
|
+
index: state3.reports.length,
|
|
149280
|
+
kind: step.kind,
|
|
149281
|
+
status: "skip",
|
|
149282
|
+
reason,
|
|
149283
|
+
// A `run:` line is attributed to the fragment it names, matching the
|
|
149284
|
+
// executed marker in execRunStep; everything else belongs to the
|
|
149285
|
+
// enclosing flow.
|
|
149286
|
+
flow: step.kind === "run" ? step.flow : sourceFlow,
|
|
149287
|
+
target: stepTarget(step),
|
|
149288
|
+
...step.kind === "echo" ? { message: step.message } : {}
|
|
149289
|
+
});
|
|
149290
|
+
if (step.kind === "when") reportBlockSkipped(state3, step.steps, sourceFlow, reason);
|
|
149291
|
+
}
|
|
149292
|
+
}
|
|
149293
|
+
async function execWhenStep(state3, step, sourceFlow, runStack) {
|
|
149294
|
+
const index = state3.reports.length;
|
|
149295
|
+
const label = describeWhenCondition(step.condition);
|
|
149296
|
+
const target = stepTarget(step);
|
|
149297
|
+
let met;
|
|
149298
|
+
if (step.condition.kind === "platform") {
|
|
149299
|
+
const platform = state3.device.platform === "ios-remote" ? "ios" : state3.device.platform;
|
|
149300
|
+
met = platform === step.condition.platform;
|
|
149301
|
+
} else {
|
|
149302
|
+
const probe3 = await probeWhenCondition(state3, step.condition);
|
|
149303
|
+
if (probe3.aborted) {
|
|
149304
|
+
pushReport(state3, {
|
|
149305
|
+
index,
|
|
149306
|
+
kind: "when",
|
|
149307
|
+
status: "skip",
|
|
149308
|
+
reason: "run aborted",
|
|
149309
|
+
flow: sourceFlow,
|
|
149310
|
+
target
|
|
149311
|
+
});
|
|
149312
|
+
reportBlockSkipped(state3, step.steps, sourceFlow, "run aborted");
|
|
149313
|
+
return;
|
|
149314
|
+
}
|
|
149315
|
+
if (!probe3.ok && probe3.indeterminate) {
|
|
149316
|
+
pushReport(state3, {
|
|
149317
|
+
index,
|
|
149318
|
+
kind: "when",
|
|
149319
|
+
status: "error",
|
|
149320
|
+
reason: `could not evaluate when guard (${label}): ${probe3.reason}`,
|
|
149321
|
+
flow: sourceFlow,
|
|
149322
|
+
target
|
|
149323
|
+
});
|
|
149324
|
+
state3.stopped = true;
|
|
149325
|
+
reportBlockSkipped(state3, step.steps, sourceFlow, "when guard errored");
|
|
149326
|
+
return;
|
|
149327
|
+
}
|
|
149328
|
+
met = probe3.ok;
|
|
149329
|
+
}
|
|
149330
|
+
if (!met) {
|
|
149331
|
+
const n = step.steps.length;
|
|
149332
|
+
pushReport(state3, {
|
|
149333
|
+
index,
|
|
149334
|
+
kind: "when",
|
|
149335
|
+
status: "skip",
|
|
149336
|
+
reason: `condition not met (${label}) \u2014 block skipped (${n} step${n === 1 ? "" : "s"})`,
|
|
149337
|
+
flow: sourceFlow,
|
|
149338
|
+
target
|
|
149339
|
+
});
|
|
149340
|
+
reportBlockSkipped(state3, step.steps, sourceFlow, "when block skipped");
|
|
149341
|
+
return;
|
|
149342
|
+
}
|
|
149343
|
+
pushReport(state3, {
|
|
149344
|
+
index,
|
|
149345
|
+
kind: "when",
|
|
149346
|
+
status: "pass",
|
|
149347
|
+
reason: `condition met (${label})`,
|
|
149348
|
+
flow: sourceFlow,
|
|
149349
|
+
target
|
|
149350
|
+
});
|
|
149351
|
+
await execSteps(state3, step.steps, sourceFlow, runStack);
|
|
149352
|
+
}
|
|
149130
149353
|
async function execRunStep(state3, step, runStack) {
|
|
149131
149354
|
const index = state3.reports.length;
|
|
149132
149355
|
const target = step.flow;
|
package/package.json
CHANGED
|
@@ -32,6 +32,7 @@ Beyond raw `tool:` steps and `echo:`, flows support declarative directives inter
|
|
|
32
32
|
| `assert` | `- assert: { visible: Welcome }` | check a condition, hard-fail if it never holds |
|
|
33
33
|
| `snapshot` | `- snapshot: home` or `- snapshot: { name: home, maxMismatch: 0.5 }` | diff a screenshot against a stored baseline |
|
|
34
34
|
| `run` | `- run: login` | execute another flow's steps inline (fragment or e2e) |
|
|
35
|
+
| `when` | `- when: { visible: "What's new" }` + `steps: [...]` | run a guarded step block only when the condition holds (no else) |
|
|
35
36
|
|
|
36
37
|
### Selectors
|
|
37
38
|
|
|
@@ -159,6 +160,7 @@ The top-level is an object with `steps` (array) and — fragments only — `exec
|
|
|
159
160
|
|
|
160
161
|
- `- echo: <message>` — a label printed during replay
|
|
161
162
|
- `- tool: <name>` with optional `args:` — a raw tool call. A tool step may also carry `delayMs: <ms>` to sleep that long before it runs. (`await-ui-element` is an ordinary tool step; see _flow-add-step arguments_ and _Making flows resilient_ for when to gate a transition with one.)
|
|
163
|
+
- **`when:` blocks** handle one-sided divergences (interstitials, coach marks): `- when: { visible: "What's new" }` with a sibling `steps: [...]` list runs the block only if the condition holds — checked once with the short assert grace (~1s), so a skipped block barely costs a clean run. Guards are one condition key (`exists`/`visible`/`hidden`/`text`, the await/assert shapes) or `platform: ios|android|chromium|vega`. **No else** (parse-rejected): a block exists to dismiss the divergence and reconverge, never to test two paths — two paths are two flows. Failures inside an entered block are real failures; a skipped block reports `skip` lines. Tap-if-present is a one-step block (`when: { visible: "Got it" }` + `steps: [tap: "Got it"]`); there is NO per-step `optional:` key — it is rejected at parse with a pointer to `when:`.
|
|
162
164
|
|
|
163
165
|
The polished result of the example session above:
|
|
164
166
|
|