@swmansion/argent 0.16.1-next.2 → 0.16.1-next.4
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 +277 -50
- 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
|
@@ -121584,6 +121584,10 @@ var zodSchema2 = external_exports.object({
|
|
|
121584
121584
|
var nativeDevtoolsStatusTool = {
|
|
121585
121585
|
id: "native-devtools-status",
|
|
121586
121586
|
capability: { apple: { simulator: true, device: true }, appleRemote: { simulator: true } },
|
|
121587
|
+
// The "injectable is false" recovery sentence inlines NON_INJECTABLE_RECOVERY
|
|
121588
|
+
// verbatim: the description must stay a plain literal so scripts/extract-tools.mjs
|
|
121589
|
+
// can read it statically for the spidershield scan. The verbatim match is pinned
|
|
121590
|
+
// by native-devtools-status.test.ts.
|
|
121587
121591
|
description: `Check whether native devtools are connected to a specific app and whether the next launch is prepared for injection.
|
|
121588
121592
|
Use when you need to verify native devtools readiness before calling native-full-hierarchy, native-describe-screen, or native-network-logs.
|
|
121589
121593
|
|
|
@@ -121596,7 +121600,7 @@ Returns { envSetup, appRunning, connected, requiresRestart, nextLaunchWillBeInje
|
|
|
121596
121600
|
- injectable: whether native devtools can ever be injected into this app. Apple system apps (bundle ids under com.apple.) are platform binaries with library validation, so the dylib can never load into them.
|
|
121597
121601
|
|
|
121598
121602
|
Call this before using app-scoped native hierarchy tools or native-network-logs.
|
|
121599
|
-
If injectable is false: this is a TERMINAL state \u2014 the app can never be injected. Do NOT restart/retry.
|
|
121603
|
+
If injectable is false: this is a TERMINAL state \u2014 the app can never be injected. Do NOT restart/retry. Use the standard \`describe\` tool (its accessibility path reads the screen without injection) or \`screenshot\` (then interact by coordinate). Do not fall back to the native-devtools feature tools (native-describe-screen, native-find-views, native-full-hierarchy, native-network-logs, native-view-at-point, native-user-interactable-view-at-point) \u2014 they run the same injection precheck and fail with the same non-injectable error.
|
|
121600
121604
|
If appRunning is false and nextLaunchWillBeInjected is true: use launch-app normally.
|
|
121601
121605
|
If requiresRestart is true: call restart-app, then proceed with the native feature.
|
|
121602
121606
|
Returns { status: "init_failed", message, attempts } instead when the simulator's native-devtools environment failed to initialize.
|
|
@@ -144776,6 +144780,16 @@ function toYamlStep(step) {
|
|
|
144776
144780
|
return { launch: step.app };
|
|
144777
144781
|
case "run":
|
|
144778
144782
|
return { run: step.flow };
|
|
144783
|
+
case "when": {
|
|
144784
|
+
const when = step.condition.kind === "platform" ? { platform: step.condition.platform } : waitToYaml(
|
|
144785
|
+
step.condition.condition,
|
|
144786
|
+
step.condition.selector,
|
|
144787
|
+
step.condition.expectedText,
|
|
144788
|
+
step.condition.textMatch,
|
|
144789
|
+
void 0
|
|
144790
|
+
);
|
|
144791
|
+
return { when, steps: step.steps.map(toYamlStep) };
|
|
144792
|
+
}
|
|
144779
144793
|
case "tap": {
|
|
144780
144794
|
const target = targetToYaml(step);
|
|
144781
144795
|
return { tap: step.times !== void 0 ? { on: target, times: step.times } : target };
|
|
@@ -144843,7 +144857,13 @@ function toYamlStep(step) {
|
|
|
144843
144857
|
}
|
|
144844
144858
|
}
|
|
144845
144859
|
function badEntry(raw, detail) {
|
|
144846
|
-
|
|
144860
|
+
let rendered;
|
|
144861
|
+
try {
|
|
144862
|
+
rendered = JSON.stringify(raw);
|
|
144863
|
+
} catch {
|
|
144864
|
+
rendered = "[cyclic entry]";
|
|
144865
|
+
}
|
|
144866
|
+
throw new FailureError(`Unrecognized flow entry (${detail}): ${rendered}`, {
|
|
144847
144867
|
error_code: FAILURE_CODES.FLOW_ENTRY_UNRECOGNIZED,
|
|
144848
144868
|
failure_stage: "flow_file_parse_step",
|
|
144849
144869
|
failure_area: "tool_server",
|
|
@@ -145086,6 +145106,7 @@ var STEP_DIRECTIVE_KEYS = [
|
|
|
145086
145106
|
"echo",
|
|
145087
145107
|
"launch",
|
|
145088
145108
|
"run",
|
|
145109
|
+
"when",
|
|
145089
145110
|
"tool",
|
|
145090
145111
|
"tap",
|
|
145091
145112
|
"long-press",
|
|
@@ -145190,8 +145211,85 @@ function parseLongPress(body, entry) {
|
|
|
145190
145211
|
}
|
|
145191
145212
|
return { kind: "long-press", ...parseTarget(body, "long-press") };
|
|
145192
145213
|
}
|
|
145193
|
-
function
|
|
145214
|
+
function parseWhenCondition(raw) {
|
|
145215
|
+
const conditionKeys = `${WAIT_CONDITIONS.join(", ")}, platform`;
|
|
145216
|
+
if (raw === null || typeof raw !== "object") {
|
|
145217
|
+
badEntry({ when: raw }, `when needs exactly one condition key (${conditionKeys})`);
|
|
145218
|
+
}
|
|
145219
|
+
const b = raw;
|
|
145220
|
+
const present = [...WAIT_CONDITIONS, "platform"].filter((c) => c in b);
|
|
145221
|
+
if (present.length !== 1) {
|
|
145222
|
+
badEntry({ when: raw }, `when needs exactly one condition key (${conditionKeys})`);
|
|
145223
|
+
}
|
|
145224
|
+
if ("timeout" in b) {
|
|
145225
|
+
badEntry(
|
|
145226
|
+
{ when: raw },
|
|
145227
|
+
"when takes no timeout \u2014 the guard is evaluated with the short assert grace so a skipped block never adds a full await wait"
|
|
145228
|
+
);
|
|
145229
|
+
}
|
|
145230
|
+
if (present[0] === "platform") {
|
|
145231
|
+
if (Object.keys(b).length !== 1) {
|
|
145232
|
+
badEntry({ when: raw }, "when.platform takes no other keys");
|
|
145233
|
+
}
|
|
145234
|
+
const p = b.platform;
|
|
145235
|
+
if (typeof p !== "string" || !LAUNCH_PLATFORMS.includes(p)) {
|
|
145236
|
+
badEntry({ when: raw }, `when.platform must be one of ${LAUNCH_PLATFORMS.join(", ")}`);
|
|
145237
|
+
}
|
|
145238
|
+
return { kind: "platform", platform: p };
|
|
145239
|
+
}
|
|
145240
|
+
const { timeout: _timeout, ...cond } = parseWaitFields(raw, "when");
|
|
145241
|
+
const { selector, expectedText } = cond;
|
|
145242
|
+
for (const s of [
|
|
145243
|
+
expectedText,
|
|
145244
|
+
selector.text,
|
|
145245
|
+
selector.textMatches,
|
|
145246
|
+
selector.identifier,
|
|
145247
|
+
selector.role
|
|
145248
|
+
]) {
|
|
145249
|
+
if (s !== void 0 && s.includes(SECRET_PLACEHOLDER_MARKER)) {
|
|
145250
|
+
badEntry(
|
|
145251
|
+
{ when: raw },
|
|
145252
|
+
"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"
|
|
145253
|
+
);
|
|
145254
|
+
}
|
|
145255
|
+
}
|
|
145256
|
+
return { kind: "ui", ...cond };
|
|
145257
|
+
}
|
|
145258
|
+
var MAX_WHEN_DEPTH = 20;
|
|
145259
|
+
function parseWhenStep(raw, depth) {
|
|
145260
|
+
if (depth >= MAX_WHEN_DEPTH) {
|
|
145261
|
+
badEntry(
|
|
145262
|
+
raw,
|
|
145263
|
+
`when blocks nest deeper than ${MAX_WHEN_DEPTH} levels \u2014 check for a cyclic YAML alias (\`steps: &s \u2026 steps: *s\`)`
|
|
145264
|
+
);
|
|
145265
|
+
}
|
|
145266
|
+
if ("else" in raw) {
|
|
145267
|
+
badEntry(
|
|
145268
|
+
raw,
|
|
145269
|
+
"when has no else \u2014 paths may only reconverge, never diverge; two genuinely different paths are two flows"
|
|
145270
|
+
);
|
|
145271
|
+
}
|
|
145272
|
+
if (!Object.keys(raw).every((k) => k === "when" || k === "steps")) {
|
|
145273
|
+
badEntry(raw, "a when step takes exactly { when: <condition>, steps: [...] }");
|
|
145274
|
+
}
|
|
145275
|
+
const condition = parseWhenCondition(raw.when);
|
|
145276
|
+
if (!Array.isArray(raw.steps) || raw.steps.length === 0) {
|
|
145277
|
+
badEntry(raw, "when needs a non-empty steps list to guard");
|
|
145278
|
+
}
|
|
145279
|
+
const steps = raw.steps.map((s) => {
|
|
145280
|
+
if (s !== null && typeof s === "object") return fromYamlStep(s, depth + 1);
|
|
145281
|
+
return badEntry(s, "step must be an object");
|
|
145282
|
+
});
|
|
145283
|
+
return { kind: "when", condition, steps };
|
|
145284
|
+
}
|
|
145285
|
+
function fromYamlStep(raw, whenDepth = 0) {
|
|
145194
145286
|
const entry = raw;
|
|
145287
|
+
if ("optional" in raw) {
|
|
145288
|
+
badEntry(
|
|
145289
|
+
raw,
|
|
145290
|
+
"optional is not supported \u2014 guard the step with a when: block instead (`when: { visible: <target> }` + `steps:`)"
|
|
145291
|
+
);
|
|
145292
|
+
}
|
|
145195
145293
|
const kinds = STEP_DIRECTIVE_KEYS.filter((k) => k in entry);
|
|
145196
145294
|
if (kinds.length === 0) {
|
|
145197
145295
|
const hint = Object.keys(entry).map((k) => closestKey(k, STEP_DIRECTIVE_KEYS)).find((h) => h !== null);
|
|
@@ -145204,17 +145302,20 @@ function fromYamlStep(raw) {
|
|
|
145204
145302
|
);
|
|
145205
145303
|
}
|
|
145206
145304
|
const kind = kinds[0];
|
|
145207
|
-
|
|
145208
|
-
|
|
145209
|
-
|
|
145210
|
-
|
|
145211
|
-
|
|
145212
|
-
|
|
145213
|
-
|
|
145305
|
+
if (kind !== "when") {
|
|
145306
|
+
const siblings = kind === "tool" ? ["tool", "args", "delayMs"] : [kind];
|
|
145307
|
+
const extras = Object.keys(entry).filter((k) => !siblings.includes(k));
|
|
145308
|
+
if (extras.length > 0) {
|
|
145309
|
+
badEntry(
|
|
145310
|
+
raw,
|
|
145311
|
+
`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`)
|
|
145312
|
+
);
|
|
145313
|
+
}
|
|
145214
145314
|
}
|
|
145215
145315
|
if ("echo" in raw) return { kind: "echo", message: String(raw.echo) };
|
|
145216
145316
|
if ("launch" in raw) return { kind: "launch", app: parseLaunch(raw.launch) };
|
|
145217
145317
|
if ("run" in raw) return { kind: "run", flow: String(raw.run) };
|
|
145318
|
+
if ("when" in raw) return parseWhenStep(entry, whenDepth);
|
|
145218
145319
|
if ("tap" in raw) return parseTap(raw.tap, raw);
|
|
145219
145320
|
if ("long-press" in raw) {
|
|
145220
145321
|
return parseLongPress(raw["long-press"], raw);
|
|
@@ -146115,6 +146216,11 @@ var fs38 = __toESM(require("node:fs/promises"));
|
|
|
146115
146216
|
function selectorLabel(sel) {
|
|
146116
146217
|
return JSON.stringify(selectorToYaml(sel));
|
|
146117
146218
|
}
|
|
146219
|
+
function textConditionLabel(sel, expectedText, textMatch) {
|
|
146220
|
+
const selector = selectorLabel(sel);
|
|
146221
|
+
const expected = expectedText ?? "";
|
|
146222
|
+
return textMatch === "matches" ? `text ${selector} matches /${expected}/` : textMatch === "equals" ? `text ${selector} == ${JSON.stringify(expected)}` : `text ${selector} contains ${JSON.stringify(expected)}`;
|
|
146223
|
+
}
|
|
146118
146224
|
var zodSchema61 = external_exports.object({});
|
|
146119
146225
|
var flowFinishRecordingTool = {
|
|
146120
146226
|
id: "flow-finish-recording",
|
|
@@ -146152,18 +146258,20 @@ You can still edit the .yaml file directly afterwards to remove or reorder steps
|
|
|
146152
146258
|
return `${n}. type: ${selectorLabel(step.into)} \u2190 "${step.text}"`;
|
|
146153
146259
|
case "await":
|
|
146154
146260
|
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
|
-
}
|
|
146261
|
+
const tail = step.condition === "text" ? textConditionLabel(step.selector, step.expectedText, step.textMatch) : `${step.condition} ${selectorLabel(step.selector)}`;
|
|
146163
146262
|
return `${n}. ${step.kind}: ${tail}`;
|
|
146164
146263
|
}
|
|
146165
146264
|
case "wait":
|
|
146166
146265
|
return `${n}. wait: ${step.ms}ms`;
|
|
146266
|
+
case "when": {
|
|
146267
|
+
const cond = step.condition.kind === "platform" ? `platform ${step.condition.platform}` : step.condition.condition === "text" ? textConditionLabel(
|
|
146268
|
+
step.condition.selector,
|
|
146269
|
+
step.condition.expectedText,
|
|
146270
|
+
step.condition.textMatch
|
|
146271
|
+
) : `${step.condition.condition} ${selectorLabel(step.condition.selector)}`;
|
|
146272
|
+
const count = step.steps.length;
|
|
146273
|
+
return `${n}. when: ${cond} (${count} step${count === 1 ? "" : "s"})`;
|
|
146274
|
+
}
|
|
146167
146275
|
case "scroll-to":
|
|
146168
146276
|
return `${n}. scroll-to: ${selectorLabel(step.target)} (${step.direction})`;
|
|
146169
146277
|
case "snapshot":
|
|
@@ -146235,6 +146343,10 @@ function axisFullyInside(frame, direction, clip2) {
|
|
|
146235
146343
|
return direction === "down" || direction === "right" ? fEnd <= clipEnd - EDGE_EPS && fStart >= clipStart - EDGE_EPS : fStart >= clipStart + EDGE_EPS && fEnd <= clipEnd + EDGE_EPS;
|
|
146236
146344
|
}
|
|
146237
146345
|
var DEFAULT_ASSERT_TIMEOUT_MS = 1e3;
|
|
146346
|
+
var CONDITION_DARK_TAIL_TOLERANCE_MS = POLL_INTERVAL_MS * 2;
|
|
146347
|
+
function probeWhenCondition(env, cond) {
|
|
146348
|
+
return waitForCondition(env, cond, DEFAULT_ASSERT_TIMEOUT_MS);
|
|
146349
|
+
}
|
|
146238
146350
|
function selectorAlternatives(sel) {
|
|
146239
146351
|
return sel.loose && sel.text !== void 0 ? [{ identifier: sel.text }, { text: sel.text }] : [sel];
|
|
146240
146352
|
}
|
|
@@ -146495,7 +146607,8 @@ async function waitForCondition(env, step, timeoutMs) {
|
|
|
146495
146607
|
let lastMatches = [];
|
|
146496
146608
|
let fetchError;
|
|
146497
146609
|
let everMatched = false;
|
|
146498
|
-
let
|
|
146610
|
+
let lastTrustedReadAt;
|
|
146611
|
+
let lastReadTrusted;
|
|
146499
146612
|
let finalPoll = false;
|
|
146500
146613
|
for (; ; ) {
|
|
146501
146614
|
if (env.signal?.aborted) return ABORTED_OUTCOME;
|
|
@@ -146505,12 +146618,14 @@ async function waitForCondition(env, step, timeoutMs) {
|
|
|
146505
146618
|
fetchError = void 0;
|
|
146506
146619
|
everMatched ||= lastMatches.length > 0;
|
|
146507
146620
|
const blind = data.tree.children.length === 0 && Boolean(data.hint || data.should_restart || everMatched);
|
|
146508
|
-
|
|
146621
|
+
if (!blind) lastTrustedReadAt = Date.now();
|
|
146622
|
+
lastReadTrusted = !blind;
|
|
146509
146623
|
if (!blind && evaluateCondition(step.condition, step.expectedText, lastMatches, step.textMatch)) {
|
|
146510
146624
|
return { ok: true };
|
|
146511
146625
|
}
|
|
146512
146626
|
} catch (err) {
|
|
146513
146627
|
fetchError = err instanceof Error ? err.message : String(err);
|
|
146628
|
+
lastReadTrusted = false;
|
|
146514
146629
|
}
|
|
146515
146630
|
if (Date.now() >= deadline) {
|
|
146516
146631
|
if (finalPoll) break;
|
|
@@ -146522,22 +146637,34 @@ async function waitForCondition(env, step, timeoutMs) {
|
|
|
146522
146637
|
return ABORTED_OUTCOME;
|
|
146523
146638
|
}
|
|
146524
146639
|
}
|
|
146525
|
-
if (
|
|
146526
|
-
if (step.condition === "hidden" && !everTrustedRead) {
|
|
146640
|
+
if (lastTrustedReadAt === void 0) {
|
|
146527
146641
|
return {
|
|
146528
146642
|
ok: false,
|
|
146529
|
-
|
|
146643
|
+
indeterminate: true,
|
|
146644
|
+
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
146645
|
};
|
|
146531
146646
|
}
|
|
146647
|
+
if (!lastReadTrusted) {
|
|
146648
|
+
if (step.condition === "hidden") {
|
|
146649
|
+
return {
|
|
146650
|
+
ok: false,
|
|
146651
|
+
indeterminate: true,
|
|
146652
|
+
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"
|
|
146653
|
+
};
|
|
146654
|
+
}
|
|
146655
|
+
const darkTailMs = Date.now() - lastTrustedReadAt;
|
|
146656
|
+
if (darkTailMs > CONDITION_DARK_TAIL_TOLERANCE_MS) {
|
|
146657
|
+
return {
|
|
146658
|
+
ok: false,
|
|
146659
|
+
indeterminate: true,
|
|
146660
|
+
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`
|
|
146661
|
+
};
|
|
146662
|
+
}
|
|
146663
|
+
}
|
|
146664
|
+
const blipNote = !lastReadTrusted && fetchError ? ` (the final poll could not read the UI tree: ${fetchError})` : "";
|
|
146532
146665
|
return {
|
|
146533
146666
|
ok: false,
|
|
146534
|
-
reason: assertReason(
|
|
146535
|
-
step.condition,
|
|
146536
|
-
step.selector,
|
|
146537
|
-
step.expectedText,
|
|
146538
|
-
step.textMatch,
|
|
146539
|
-
lastMatches
|
|
146540
|
-
)
|
|
146667
|
+
reason: assertReason(step.condition, step.selector, step.expectedText, step.textMatch, lastMatches) + blipNote
|
|
146541
146668
|
};
|
|
146542
146669
|
}
|
|
146543
146670
|
function assertReason(condition, selector, expectedText, textMatch, matches2) {
|
|
@@ -146548,7 +146675,7 @@ function assertReason(condition, selector, expectedText, textMatch, matches2) {
|
|
|
146548
146675
|
case "visible":
|
|
146549
146676
|
return matches2.length > 0 ? `element(s) matched ${sel} but none was visible (zero-area frame)` : `no element matched selector ${sel}`;
|
|
146550
146677
|
case "hidden":
|
|
146551
|
-
return
|
|
146678
|
+
return `an element matching ${sel} was still visible`;
|
|
146552
146679
|
case "text": {
|
|
146553
146680
|
const first = firstInReadingOrder(matches2.filter(isVisible)) ?? firstInReadingOrder(matches2);
|
|
146554
146681
|
if (!first) return `no element matched selector ${sel}`;
|
|
@@ -148784,7 +148911,7 @@ var zodSchema62 = external_exports.object({
|
|
|
148784
148911
|
device: external_exports.string().optional().describe(
|
|
148785
148912
|
"Device id to run against (iOS UDID, Android/Vega serial, Chromium id). Auto-detected when omitted."
|
|
148786
148913
|
),
|
|
148787
|
-
platform: external_exports.enum(
|
|
148914
|
+
platform: external_exports.enum(LAUNCH_PLATFORMS).optional().describe("Restrict auto-detection to this platform when several devices are booted."),
|
|
148788
148915
|
updateBaselines: external_exports.boolean().optional().describe(
|
|
148789
148916
|
"Write/refresh screenshot baselines for `snapshot` steps instead of diffing against them."
|
|
148790
148917
|
),
|
|
@@ -148919,6 +149046,9 @@ holds; \`tap\`/\`long-press\` alternatively take a raw normalized point \u2014 b
|
|
|
148919
149046
|
condition; \`wait\` pauses for a fixed number of milliseconds; \`assert\` checks one now; \`snapshot\`
|
|
148920
149047
|
diffs a screenshot against a stored baseline (a missing baseline fails the step \u2014 set updateBaselines
|
|
148921
149048
|
to adopt the current screen); \`echo\` annotates; \`run\` executes a referenced fragment inline.
|
|
149049
|
+
A \`when:\` block (condition + \`steps:\`, no else) runs its steps only if the condition holds \u2014
|
|
149050
|
+
checked once with the short assert grace \u2014 for one-sided divergences like interstitials and coach
|
|
149051
|
+
marks; a skipped block reports distinctly and failures inside an entered block are real failures.
|
|
148922
149052
|
A flow that begins with a \`launch\` step is a self-contained e2e flow; one that doesn't runs against the
|
|
148923
149053
|
device's current state. Device id is injected by the runner (flows store none) \u2014 pass \`device\` or
|
|
148924
149054
|
\`platform\` to pick one, else the single booted device is used. For a Chromium e2e flow the \`launch\`
|
|
@@ -148962,13 +149092,15 @@ returns a notice with the prerequisite instead of running.`,
|
|
|
148962
149092
|
chromiumLaunched: false,
|
|
148963
149093
|
...ctx?.emitProgress ? { onStepReport: ctx.emitProgress } : {}
|
|
148964
149094
|
};
|
|
149095
|
+
let aborted2;
|
|
148965
149096
|
try {
|
|
148966
149097
|
await execSteps(state3, flow.steps, params.name, [params.name]);
|
|
148967
149098
|
} finally {
|
|
149099
|
+
aborted2 = state3.signal?.aborted === true;
|
|
148968
149100
|
if (state3.pinned) await restoreStatusBar(device);
|
|
148969
149101
|
if (resolved.booted) await teardownBootedChromium(registry2, resolved.booted);
|
|
148970
149102
|
}
|
|
148971
|
-
return summarize(params.name, device.id, flow.executionPrerequisite, state3.reports);
|
|
149103
|
+
return summarize(params.name, device.id, flow.executionPrerequisite, state3.reports, aborted2);
|
|
148972
149104
|
}
|
|
148973
149105
|
};
|
|
148974
149106
|
}
|
|
@@ -149024,14 +149156,12 @@ async function frontChromiumPage(registry2, device) {
|
|
|
149024
149156
|
} catch {
|
|
149025
149157
|
}
|
|
149026
149158
|
}
|
|
149027
|
-
function summarize(flowName, deviceId, executionPrerequisite, steps) {
|
|
149159
|
+
function summarize(flowName, deviceId, executionPrerequisite, steps, aborted2) {
|
|
149028
149160
|
let passed = 0;
|
|
149029
149161
|
let failed = 0;
|
|
149030
149162
|
let skipped = 0;
|
|
149031
149163
|
let errored = 0;
|
|
149032
|
-
let hasSkippedReport = false;
|
|
149033
149164
|
for (const s of steps) {
|
|
149034
|
-
hasSkippedReport ||= s.status === "skip";
|
|
149035
149165
|
if (s.kind === "echo") continue;
|
|
149036
149166
|
if (s.status === "pass") passed++;
|
|
149037
149167
|
else if (s.status === "fail") failed++;
|
|
@@ -149042,11 +149172,13 @@ function summarize(flowName, deviceId, executionPrerequisite, steps) {
|
|
|
149042
149172
|
flow: flowName,
|
|
149043
149173
|
device: deviceId,
|
|
149044
149174
|
executionPrerequisite,
|
|
149045
|
-
// A
|
|
149046
|
-
//
|
|
149047
|
-
//
|
|
149048
|
-
//
|
|
149049
|
-
|
|
149175
|
+
// A cancelled run must never read as PASS — it may contain skips alone
|
|
149176
|
+
// (no fail/error report), so the verdict folds the abort in directly. A
|
|
149177
|
+
// skip by itself is NOT a failure: an unmet `when:` guard skips its block
|
|
149178
|
+
// as a successful omission, and a hard stop already carries its own
|
|
149179
|
+
// fail/error report.
|
|
149180
|
+
ok: failed === 0 && errored === 0 && !aborted2,
|
|
149181
|
+
...aborted2 ? { aborted: true } : {},
|
|
149050
149182
|
passed,
|
|
149051
149183
|
failed,
|
|
149052
149184
|
skipped,
|
|
@@ -149066,6 +149198,13 @@ function selectorLabel2(sel) {
|
|
|
149066
149198
|
if (sel.role) parts.push(`role=${sel.role}`);
|
|
149067
149199
|
return parts.join(" ");
|
|
149068
149200
|
}
|
|
149201
|
+
function conditionLabel(cond, renderSelector) {
|
|
149202
|
+
const sel = renderSelector(cond.selector);
|
|
149203
|
+
if (cond.condition === "text") {
|
|
149204
|
+
return `${sel} ${describeTextExpectation(cond.expectedText, cond.textMatch)}`;
|
|
149205
|
+
}
|
|
149206
|
+
return `${cond.condition} ${sel}`;
|
|
149207
|
+
}
|
|
149069
149208
|
function stepTarget(step) {
|
|
149070
149209
|
switch (step.kind) {
|
|
149071
149210
|
case "tap":
|
|
@@ -149076,13 +149215,10 @@ function stepTarget(step) {
|
|
|
149076
149215
|
case "type":
|
|
149077
149216
|
return `into ${selectorLabel2(step.into)}`;
|
|
149078
149217
|
case "await":
|
|
149079
|
-
case "assert":
|
|
149080
|
-
|
|
149081
|
-
|
|
149082
|
-
|
|
149083
|
-
}
|
|
149084
|
-
return `${step.condition} ${sel}`;
|
|
149085
|
-
}
|
|
149218
|
+
case "assert":
|
|
149219
|
+
return conditionLabel(step, selectorLabel2);
|
|
149220
|
+
case "when":
|
|
149221
|
+
return step.condition.kind === "platform" ? `platform ${step.condition.platform}` : conditionLabel(step.condition, selectorLabel2);
|
|
149086
149222
|
case "scroll-to": {
|
|
149087
149223
|
const dir = step.direction !== "down" ? ` (${step.direction})` : "";
|
|
149088
149224
|
return `${selectorLabel2(step.target)}${dir}`;
|
|
@@ -149102,8 +149238,12 @@ async function execSteps(state3, steps, sourceFlow, runStack) {
|
|
|
149102
149238
|
kind: step.kind,
|
|
149103
149239
|
status: "skip",
|
|
149104
149240
|
flow: sourceFlow,
|
|
149105
|
-
target: stepTarget(step)
|
|
149241
|
+
target: stepTarget(step),
|
|
149242
|
+
// Carry the echo's message so a skipped narration renders as a skip
|
|
149243
|
+
// line rather than vanishing — matching reportBlockSkipped.
|
|
149244
|
+
...step.kind === "echo" ? { message: step.message } : {}
|
|
149106
149245
|
});
|
|
149246
|
+
if (step.kind === "when") reportBlockSkipped(state3, step.steps, sourceFlow);
|
|
149107
149247
|
continue;
|
|
149108
149248
|
}
|
|
149109
149249
|
if (state3.signal?.aborted) {
|
|
@@ -149114,19 +149254,106 @@ async function execSteps(state3, steps, sourceFlow, runStack) {
|
|
|
149114
149254
|
status: "skip",
|
|
149115
149255
|
reason: "run aborted",
|
|
149116
149256
|
flow: sourceFlow,
|
|
149117
|
-
target: stepTarget(step)
|
|
149257
|
+
target: stepTarget(step),
|
|
149258
|
+
...step.kind === "echo" ? { message: step.message } : {}
|
|
149118
149259
|
});
|
|
149260
|
+
if (step.kind === "when") reportBlockSkipped(state3, step.steps, sourceFlow, "run aborted");
|
|
149119
149261
|
continue;
|
|
149120
149262
|
}
|
|
149121
149263
|
if (step.kind === "run") {
|
|
149122
149264
|
await execRunStep(state3, step, runStack);
|
|
149123
149265
|
continue;
|
|
149124
149266
|
}
|
|
149267
|
+
if (step.kind === "when") {
|
|
149268
|
+
await execWhenStep(state3, step, sourceFlow, runStack);
|
|
149269
|
+
continue;
|
|
149270
|
+
}
|
|
149125
149271
|
const report = await execLeafStep(state3, step, index, sourceFlow);
|
|
149126
149272
|
pushReport(state3, report);
|
|
149127
149273
|
if (report.status === "fail" || report.status === "error") state3.stopped = true;
|
|
149128
149274
|
}
|
|
149129
149275
|
}
|
|
149276
|
+
function describeWhenCondition(cond) {
|
|
149277
|
+
if (cond.kind === "platform") return `platform ${cond.platform}`;
|
|
149278
|
+
return conditionLabel(cond, describeSelector);
|
|
149279
|
+
}
|
|
149280
|
+
function reportBlockSkipped(state3, steps, sourceFlow, reason) {
|
|
149281
|
+
for (const step of steps) {
|
|
149282
|
+
pushReport(state3, {
|
|
149283
|
+
index: state3.reports.length,
|
|
149284
|
+
kind: step.kind,
|
|
149285
|
+
status: "skip",
|
|
149286
|
+
reason,
|
|
149287
|
+
// A `run:` line is attributed to the fragment it names, matching the
|
|
149288
|
+
// executed marker in execRunStep; everything else belongs to the
|
|
149289
|
+
// enclosing flow.
|
|
149290
|
+
flow: step.kind === "run" ? step.flow : sourceFlow,
|
|
149291
|
+
target: stepTarget(step),
|
|
149292
|
+
...step.kind === "echo" ? { message: step.message } : {}
|
|
149293
|
+
});
|
|
149294
|
+
if (step.kind === "when") reportBlockSkipped(state3, step.steps, sourceFlow, reason);
|
|
149295
|
+
}
|
|
149296
|
+
}
|
|
149297
|
+
async function execWhenStep(state3, step, sourceFlow, runStack) {
|
|
149298
|
+
const index = state3.reports.length;
|
|
149299
|
+
const label = describeWhenCondition(step.condition);
|
|
149300
|
+
const target = stepTarget(step);
|
|
149301
|
+
let met;
|
|
149302
|
+
if (step.condition.kind === "platform") {
|
|
149303
|
+
const platform = state3.device.platform === "ios-remote" ? "ios" : state3.device.platform;
|
|
149304
|
+
met = platform === step.condition.platform;
|
|
149305
|
+
} else {
|
|
149306
|
+
const probe3 = await probeWhenCondition(state3, step.condition);
|
|
149307
|
+
if (probe3.aborted) {
|
|
149308
|
+
pushReport(state3, {
|
|
149309
|
+
index,
|
|
149310
|
+
kind: "when",
|
|
149311
|
+
status: "skip",
|
|
149312
|
+
reason: "run aborted",
|
|
149313
|
+
flow: sourceFlow,
|
|
149314
|
+
target
|
|
149315
|
+
});
|
|
149316
|
+
reportBlockSkipped(state3, step.steps, sourceFlow, "run aborted");
|
|
149317
|
+
return;
|
|
149318
|
+
}
|
|
149319
|
+
if (!probe3.ok && probe3.indeterminate) {
|
|
149320
|
+
pushReport(state3, {
|
|
149321
|
+
index,
|
|
149322
|
+
kind: "when",
|
|
149323
|
+
status: "error",
|
|
149324
|
+
reason: `could not evaluate when guard (${label}): ${probe3.reason}`,
|
|
149325
|
+
flow: sourceFlow,
|
|
149326
|
+
target
|
|
149327
|
+
});
|
|
149328
|
+
state3.stopped = true;
|
|
149329
|
+
reportBlockSkipped(state3, step.steps, sourceFlow, "when guard errored");
|
|
149330
|
+
return;
|
|
149331
|
+
}
|
|
149332
|
+
met = probe3.ok;
|
|
149333
|
+
}
|
|
149334
|
+
if (!met) {
|
|
149335
|
+
const n = step.steps.length;
|
|
149336
|
+
pushReport(state3, {
|
|
149337
|
+
index,
|
|
149338
|
+
kind: "when",
|
|
149339
|
+
status: "skip",
|
|
149340
|
+
reason: `condition not met (${label}) \u2014 block skipped (${n} step${n === 1 ? "" : "s"})`,
|
|
149341
|
+
flow: sourceFlow,
|
|
149342
|
+
target
|
|
149343
|
+
});
|
|
149344
|
+
reportBlockSkipped(state3, step.steps, sourceFlow, "when block skipped");
|
|
149345
|
+
return;
|
|
149346
|
+
}
|
|
149347
|
+
pushReport(state3, {
|
|
149348
|
+
index,
|
|
149349
|
+
kind: "when",
|
|
149350
|
+
status: "pass",
|
|
149351
|
+
reason: `condition met (${label})`,
|
|
149352
|
+
flow: sourceFlow,
|
|
149353
|
+
target
|
|
149354
|
+
});
|
|
149355
|
+
await execSteps(state3, step.steps, sourceFlow, runStack);
|
|
149356
|
+
}
|
|
149130
149357
|
async function execRunStep(state3, step, runStack) {
|
|
149131
149358
|
const index = state3.reports.length;
|
|
149132
149359
|
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
|
|