@swmansion/argent 0.16.1-next.0 → 0.16.1-next.2
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/tool-server.cjs +121 -87
- package/package.json +1 -1
- package/skills/argent-create-flow/SKILL.md +13 -13
package/dist/tool-server.cjs
CHANGED
|
@@ -144726,6 +144726,28 @@ function textWaitToYaml(selector, expectedText, textMatch) {
|
|
|
144726
144726
|
}
|
|
144727
144727
|
}
|
|
144728
144728
|
}
|
|
144729
|
+
function targetToYaml(step) {
|
|
144730
|
+
const hasPointField = step.x !== void 0 || step.y !== void 0;
|
|
144731
|
+
if (step.selector !== void 0) {
|
|
144732
|
+
if (hasPointField) {
|
|
144733
|
+
throw new Error(
|
|
144734
|
+
"Cannot serialize flow gesture target: use a selector or x/y coordinates, not both"
|
|
144735
|
+
);
|
|
144736
|
+
}
|
|
144737
|
+
return selectorToYaml(step.selector);
|
|
144738
|
+
}
|
|
144739
|
+
if (typeof step.x !== "number" || typeof step.y !== "number") {
|
|
144740
|
+
throw new Error(
|
|
144741
|
+
"Cannot serialize flow gesture target: a coordinate target needs numeric x and y"
|
|
144742
|
+
);
|
|
144743
|
+
}
|
|
144744
|
+
if (!(step.x >= 0 && step.x <= 1) || !(step.y >= 0 && step.y <= 1)) {
|
|
144745
|
+
throw new Error(
|
|
144746
|
+
"Cannot serialize flow gesture target: coordinates are normalized 0\u20131 fractions of the screen, not pixels"
|
|
144747
|
+
);
|
|
144748
|
+
}
|
|
144749
|
+
return { x: step.x, y: step.y };
|
|
144750
|
+
}
|
|
144729
144751
|
function waitToYaml(condition, selector, expectedText, textMatch, timeoutMs) {
|
|
144730
144752
|
const sel = selectorToYaml(selector);
|
|
144731
144753
|
let body;
|
|
@@ -144755,18 +144777,13 @@ function toYamlStep(step) {
|
|
|
144755
144777
|
case "run":
|
|
144756
144778
|
return { run: step.flow };
|
|
144757
144779
|
case "tap": {
|
|
144758
|
-
|
|
144759
|
-
|
|
144760
|
-
return { tap: step.times !== void 0 ? { on: sel, times: step.times } : sel };
|
|
144761
|
-
}
|
|
144762
|
-
const body = { x: step.x, y: step.y };
|
|
144763
|
-
if (step.times !== void 0) body.times = step.times;
|
|
144764
|
-
return { tap: body };
|
|
144780
|
+
const target = targetToYaml(step);
|
|
144781
|
+
return { tap: step.times !== void 0 ? { on: target, times: step.times } : target };
|
|
144765
144782
|
}
|
|
144766
144783
|
case "long-press": {
|
|
144767
|
-
const
|
|
144784
|
+
const target = targetToYaml(step);
|
|
144768
144785
|
return {
|
|
144769
|
-
"long-press": step.duration !== void 0 ? { on:
|
|
144786
|
+
"long-press": step.duration !== void 0 ? { on: target, duration: step.duration } : target
|
|
144770
144787
|
};
|
|
144771
144788
|
}
|
|
144772
144789
|
case "type": {
|
|
@@ -145079,35 +145096,6 @@ var STEP_DIRECTIVE_KEYS = [
|
|
|
145079
145096
|
"scroll-to",
|
|
145080
145097
|
"snapshot"
|
|
145081
145098
|
];
|
|
145082
|
-
function parseLongPress(body, entry) {
|
|
145083
|
-
const obj = body !== null && typeof body === "object" ? body : {};
|
|
145084
|
-
if (obj.on !== void 0 || obj.duration !== void 0) {
|
|
145085
|
-
if (obj.text !== void 0 || obj.id !== void 0 || obj.identifier !== void 0 || obj.role !== void 0) {
|
|
145086
|
-
badEntry(
|
|
145087
|
-
entry,
|
|
145088
|
-
'the long-press options form takes a nested selector \u2014 e.g. long-press: { on: { text: "Row" }, duration: 1200 }'
|
|
145089
|
-
);
|
|
145090
|
-
}
|
|
145091
|
-
if (!Object.keys(obj).every((k) => k === "on" || k === "duration")) {
|
|
145092
|
-
badEntry(entry, "the long-press options form accepts only { on, duration }");
|
|
145093
|
-
}
|
|
145094
|
-
if (obj.on === void 0) {
|
|
145095
|
-
badEntry(entry, 'long-press needs a target \u2014 e.g. long-press: { on: "Row", duration: 1200 }');
|
|
145096
|
-
}
|
|
145097
|
-
const step = { kind: "long-press", selector: parseSelector(obj.on, "long-press.on") };
|
|
145098
|
-
if (obj.duration !== void 0) {
|
|
145099
|
-
if (typeof obj.duration !== "number" || !Number.isFinite(obj.duration) || obj.duration <= 0) {
|
|
145100
|
-
badEntry(
|
|
145101
|
-
entry,
|
|
145102
|
-
"long-press.duration needs a positive number of milliseconds (e.g. `duration: 1200`)"
|
|
145103
|
-
);
|
|
145104
|
-
}
|
|
145105
|
-
step.duration = obj.duration;
|
|
145106
|
-
}
|
|
145107
|
-
return step;
|
|
145108
|
-
}
|
|
145109
|
-
return { kind: "long-press", selector: parseSelector(body, "long-press") };
|
|
145110
|
-
}
|
|
145111
145099
|
function parseTapTimes(raw, entry) {
|
|
145112
145100
|
if (raw === void 0) return void 0;
|
|
145113
145101
|
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1 || raw > 10) {
|
|
@@ -145115,23 +145103,32 @@ function parseTapTimes(raw, entry) {
|
|
|
145115
145103
|
}
|
|
145116
145104
|
return raw === 1 ? void 0 : raw;
|
|
145117
145105
|
}
|
|
145118
|
-
function
|
|
145119
|
-
|
|
145120
|
-
|
|
145121
|
-
if (obj.
|
|
145122
|
-
|
|
145123
|
-
|
|
145124
|
-
|
|
145125
|
-
|
|
145126
|
-
|
|
145127
|
-
|
|
145128
|
-
|
|
145106
|
+
function parseTarget(raw, where) {
|
|
145107
|
+
if (raw !== null && typeof raw === "object") {
|
|
145108
|
+
const obj = raw;
|
|
145109
|
+
if (obj.x !== void 0 || obj.y !== void 0) {
|
|
145110
|
+
if (obj.text !== void 0 || obj.id !== void 0 || obj.identifier !== void 0 || obj.role !== void 0) {
|
|
145111
|
+
badEntry(raw, `${where} takes a selector or x/y coordinates, not both`);
|
|
145112
|
+
}
|
|
145113
|
+
if (typeof obj.x !== "number" || typeof obj.y !== "number") {
|
|
145114
|
+
badEntry(raw, `${where}: a coordinate target needs numeric x and y`);
|
|
145115
|
+
}
|
|
145116
|
+
if (!(obj.x >= 0 && obj.x <= 1) || !(obj.y >= 0 && obj.y <= 1)) {
|
|
145117
|
+
badEntry(
|
|
145118
|
+
raw,
|
|
145119
|
+
`${where}: coordinates are normalized 0\u20131 fractions of the screen, not pixels`
|
|
145120
|
+
);
|
|
145121
|
+
}
|
|
145122
|
+
if (!Object.keys(obj).every((k) => k === "x" || k === "y")) {
|
|
145123
|
+
badEntry(raw, `${where}: a coordinate target takes only { x, y }`);
|
|
145124
|
+
}
|
|
145125
|
+
return { x: obj.x, y: obj.y };
|
|
145129
145126
|
}
|
|
145130
|
-
const step = { kind: "tap", x: obj.x, y: obj.y };
|
|
145131
|
-
const times = parseTapTimes(obj.times, entry);
|
|
145132
|
-
if (times !== void 0) step.times = times;
|
|
145133
|
-
return step;
|
|
145134
145127
|
}
|
|
145128
|
+
return { selector: parseSelector(raw, where) };
|
|
145129
|
+
}
|
|
145130
|
+
function parseTap(body, entry) {
|
|
145131
|
+
const obj = body !== null && typeof body === "object" ? body : {};
|
|
145135
145132
|
if (obj.on !== void 0 || obj.times !== void 0) {
|
|
145136
145133
|
if (obj.text !== void 0 || obj.id !== void 0 || obj.identifier !== void 0 || obj.role !== void 0) {
|
|
145137
145134
|
badEntry(
|
|
@@ -145139,18 +145136,59 @@ function parseTap(body, entry) {
|
|
|
145139
145136
|
'the tap options form takes a nested selector \u2014 e.g. tap: { on: { text: "Photo" }, times: 2 }'
|
|
145140
145137
|
);
|
|
145141
145138
|
}
|
|
145139
|
+
if (obj.x !== void 0 || obj.y !== void 0) {
|
|
145140
|
+
badEntry(
|
|
145141
|
+
entry,
|
|
145142
|
+
"the tap options form takes a nested point \u2014 e.g. tap: { on: { x: 0.5, y: 0.5 }, times: 2 }"
|
|
145143
|
+
);
|
|
145144
|
+
}
|
|
145142
145145
|
if (!Object.keys(obj).every((k) => k === "on" || k === "times")) {
|
|
145143
145146
|
badEntry(entry, "the tap options form accepts only { on, times }");
|
|
145144
145147
|
}
|
|
145145
145148
|
if (obj.on === void 0) {
|
|
145146
145149
|
badEntry(entry, 'tap with times needs a target \u2014 e.g. tap: { on: "Photo", times: 2 }');
|
|
145147
145150
|
}
|
|
145148
|
-
const step = { kind: "tap",
|
|
145151
|
+
const step = { kind: "tap", ...parseTarget(obj.on, "tap.on") };
|
|
145149
145152
|
const times = parseTapTimes(obj.times, entry);
|
|
145150
145153
|
if (times !== void 0) step.times = times;
|
|
145151
145154
|
return step;
|
|
145152
145155
|
}
|
|
145153
|
-
return { kind: "tap",
|
|
145156
|
+
return { kind: "tap", ...parseTarget(body, "tap") };
|
|
145157
|
+
}
|
|
145158
|
+
function parseLongPress(body, entry) {
|
|
145159
|
+
const obj = body !== null && typeof body === "object" ? body : {};
|
|
145160
|
+
if (obj.on !== void 0 || obj.duration !== void 0) {
|
|
145161
|
+
if (obj.text !== void 0 || obj.id !== void 0 || obj.identifier !== void 0 || obj.role !== void 0) {
|
|
145162
|
+
badEntry(
|
|
145163
|
+
entry,
|
|
145164
|
+
'the long-press options form takes a nested selector \u2014 e.g. long-press: { on: { text: "Row" }, duration: 1200 }'
|
|
145165
|
+
);
|
|
145166
|
+
}
|
|
145167
|
+
if (obj.x !== void 0 || obj.y !== void 0) {
|
|
145168
|
+
badEntry(
|
|
145169
|
+
entry,
|
|
145170
|
+
"the long-press options form takes a nested point \u2014 e.g. long-press: { on: { x: 0.5, y: 0.5 }, duration: 1200 }"
|
|
145171
|
+
);
|
|
145172
|
+
}
|
|
145173
|
+
if (!Object.keys(obj).every((k) => k === "on" || k === "duration")) {
|
|
145174
|
+
badEntry(entry, "the long-press options form accepts only { on, duration }");
|
|
145175
|
+
}
|
|
145176
|
+
if (obj.on === void 0) {
|
|
145177
|
+
badEntry(entry, 'long-press needs a target \u2014 e.g. long-press: { on: "Row", duration: 1200 }');
|
|
145178
|
+
}
|
|
145179
|
+
const step = { kind: "long-press", ...parseTarget(obj.on, "long-press.on") };
|
|
145180
|
+
if (obj.duration !== void 0) {
|
|
145181
|
+
if (typeof obj.duration !== "number" || !Number.isFinite(obj.duration) || obj.duration <= 0) {
|
|
145182
|
+
badEntry(
|
|
145183
|
+
entry,
|
|
145184
|
+
"long-press.duration needs a positive number of milliseconds (e.g. `duration: 1200`)"
|
|
145185
|
+
);
|
|
145186
|
+
}
|
|
145187
|
+
step.duration = obj.duration;
|
|
145188
|
+
}
|
|
145189
|
+
return step;
|
|
145190
|
+
}
|
|
145191
|
+
return { kind: "long-press", ...parseTarget(body, "long-press") };
|
|
145154
145192
|
}
|
|
145155
145193
|
function fromYamlStep(raw) {
|
|
145156
145194
|
const entry = raw;
|
|
@@ -145980,12 +146018,7 @@ async function captureRunTarget(session, args) {
|
|
|
145980
146018
|
try {
|
|
145981
146019
|
assertSafeFlowName(name);
|
|
145982
146020
|
const fragPath = path28.join(path28.dirname(session.filePath), `${name}.yaml`);
|
|
145983
|
-
|
|
145984
|
-
if (isE2eFlow(fragment)) {
|
|
145985
|
-
return {
|
|
145986
|
-
warning: `"${name}" is an e2e flow (starts with a launch step); only fragments compose via run: \u2014 kept the raw flow-execute step`
|
|
145987
|
-
};
|
|
145988
|
-
}
|
|
146021
|
+
parseFlow(await fs37.readFile(fragPath, "utf8"));
|
|
145989
146022
|
return { flow: name };
|
|
145990
146023
|
} catch (err) {
|
|
145991
146024
|
return {
|
|
@@ -146113,9 +146146,8 @@ You can still edit the .yaml file directly afterwards to remove or reorder steps
|
|
|
146113
146146
|
case "run":
|
|
146114
146147
|
return `${n}. run: ${step.flow}`;
|
|
146115
146148
|
case "tap":
|
|
146116
|
-
return `${n}. tap: ${step.selector ? selectorLabel(step.selector) : `(${step.x}, ${step.y})`}`;
|
|
146117
146149
|
case "long-press":
|
|
146118
|
-
return `${n}.
|
|
146150
|
+
return `${n}. ${step.kind}: ${step.selector ? selectorLabel(step.selector) : `(${step.x}, ${step.y})`}`;
|
|
146119
146151
|
case "type":
|
|
146120
146152
|
return `${n}. type: ${selectorLabel(step.into)} \u2190 "${step.text}"`;
|
|
146121
146153
|
case "await":
|
|
@@ -146393,20 +146425,23 @@ async function runDirective(env, step) {
|
|
|
146393
146425
|
}
|
|
146394
146426
|
}
|
|
146395
146427
|
}
|
|
146396
|
-
async function
|
|
146397
|
-
let point;
|
|
146428
|
+
async function resolveTargetPoint(env, target) {
|
|
146398
146429
|
if (target.selector) {
|
|
146399
146430
|
const frame = await waitForFrame(env, target.selector);
|
|
146400
|
-
if (frame === "aborted") return ABORTED_OUTCOME;
|
|
146431
|
+
if (frame === "aborted") return { fail: ABORTED_OUTCOME };
|
|
146401
146432
|
if (!frame) {
|
|
146402
|
-
return { ok: false, reason: offscreenHint(target.selector) };
|
|
146433
|
+
return { fail: { ok: false, reason: offscreenHint(target.selector) } };
|
|
146403
146434
|
}
|
|
146404
|
-
|
|
146405
|
-
}
|
|
146406
|
-
|
|
146407
|
-
|
|
146408
|
-
return { ok: false, reason: "tap needs a selector or x/y coordinates" };
|
|
146435
|
+
return getDescribeTapPoint(frame);
|
|
146436
|
+
}
|
|
146437
|
+
if (typeof target.x === "number" && typeof target.y === "number") {
|
|
146438
|
+
return { x: target.x, y: target.y };
|
|
146409
146439
|
}
|
|
146440
|
+
return { fail: { ok: false, reason: "gesture needs a selector or x/y coordinates" } };
|
|
146441
|
+
}
|
|
146442
|
+
async function runTap(env, target) {
|
|
146443
|
+
const point = await resolveTargetPoint(env, target);
|
|
146444
|
+
if ("fail" in point) return point.fail;
|
|
146410
146445
|
await invokeOnDevice(env, "gesture-tap", {
|
|
146411
146446
|
...point,
|
|
146412
146447
|
...target.times !== void 0 ? { clickCount: target.times } : {}
|
|
@@ -146415,12 +146450,8 @@ async function runTap(env, target) {
|
|
|
146415
146450
|
}
|
|
146416
146451
|
var DEFAULT_LONG_PRESS_MS = 800;
|
|
146417
146452
|
async function runLongPress(env, step) {
|
|
146418
|
-
const
|
|
146419
|
-
if (
|
|
146420
|
-
if (!frame) {
|
|
146421
|
-
return { ok: false, reason: offscreenHint(step.selector) };
|
|
146422
|
-
}
|
|
146423
|
-
const point = getDescribeTapPoint(frame);
|
|
146453
|
+
const point = await resolveTargetPoint(env, step);
|
|
146454
|
+
if ("fail" in point) return point.fail;
|
|
146424
146455
|
const duration3 = step.duration ?? DEFAULT_LONG_PRESS_MS;
|
|
146425
146456
|
if (env.device.platform === "chromium") {
|
|
146426
146457
|
await invokeOnDevice(env, "gesture-drag", {
|
|
@@ -148830,6 +148861,13 @@ async function treeSourceGate(registry2, device, bundleId, signal) {
|
|
|
148830
148861
|
async function runLaunch(state3, app) {
|
|
148831
148862
|
const { registry: registry2, device, signal } = state3;
|
|
148832
148863
|
if (device.platform === "chromium") {
|
|
148864
|
+
if (state3.chromiumLaunched) {
|
|
148865
|
+
return {
|
|
148866
|
+
ok: false,
|
|
148867
|
+
reason: `chromium launches only the top-level flow's app, once per run \u2014 a nested launch can't boot its own instance and would run against the already-launched app. Nested chromium e2e flows aren't supported: run this flow at the top level, or drop its launch step to make it a fragment.`
|
|
148868
|
+
};
|
|
148869
|
+
}
|
|
148870
|
+
state3.chromiumLaunched = true;
|
|
148833
148871
|
if (state3.chromiumBooted) {
|
|
148834
148872
|
if (!await sleepOrAbort(POST_LAUNCH_SETTLE_MS, signal)) return ABORTED_OUTCOME;
|
|
148835
148873
|
return { ok: true };
|
|
@@ -148875,8 +148913,9 @@ function createRunFlowTool(registry2) {
|
|
|
148875
148913
|
description: `Run a saved flow from the .argent/flows/ directory.
|
|
148876
148914
|
Steps run in order: \`launch\` starts an app from scratch (terminate + relaunch) and waits until it is
|
|
148877
148915
|
ready; \`tool\` calls dispatch through the registry; \`tap\`/\`long-press\`/\`type\` resolve a selector to an
|
|
148878
|
-
element and act on it (\`long-press: { on, duration }\` presses and
|
|
148879
|
-
|
|
148916
|
+
element and act on it (\`tap: { on, times: 2 }\` double-taps; \`long-press: { on, duration }\` presses and
|
|
148917
|
+
holds; \`tap\`/\`long-press\` alternatively take a raw normalized point \u2014 bare \`{ x, y }\` or \`on: { x, y }\`);
|
|
148918
|
+
\`scroll-to\` scrolls (momentum-free) until a target is visible; \`await\` waits for a UI
|
|
148880
148919
|
condition; \`wait\` pauses for a fixed number of milliseconds; \`assert\` checks one now; \`snapshot\`
|
|
148881
148920
|
diffs a screenshot against a stored baseline (a missing baseline fails the step \u2014 set updateBaselines
|
|
148882
148921
|
to adopt the current screen); \`echo\` annotates; \`run\` executes a referenced fragment inline.
|
|
@@ -148920,6 +148959,7 @@ returns a notice with the prerequisite instead of running.`,
|
|
|
148920
148959
|
stopped: false,
|
|
148921
148960
|
pinned: statusBarPinned,
|
|
148922
148961
|
chromiumBooted: resolved.booted !== null,
|
|
148962
|
+
chromiumLaunched: false,
|
|
148923
148963
|
...ctx?.emitProgress ? { onStepReport: ctx.emitProgress } : {}
|
|
148924
148964
|
};
|
|
148925
148965
|
try {
|
|
@@ -149029,11 +149069,10 @@ function selectorLabel2(sel) {
|
|
|
149029
149069
|
function stepTarget(step) {
|
|
149030
149070
|
switch (step.kind) {
|
|
149031
149071
|
case "tap":
|
|
149072
|
+
case "long-press":
|
|
149032
149073
|
if (step.selector) return selectorLabel2(step.selector);
|
|
149033
149074
|
if (step.x !== void 0 && step.y !== void 0) return `(${step.x}, ${step.y})`;
|
|
149034
149075
|
return void 0;
|
|
149035
|
-
case "long-press":
|
|
149036
|
-
return selectorLabel2(step.selector);
|
|
149037
149076
|
case "type":
|
|
149038
149077
|
return `into ${selectorLabel2(step.into)}`;
|
|
149039
149078
|
case "await":
|
|
@@ -149109,11 +149148,6 @@ async function execRunStep(state3, step, runStack) {
|
|
|
149109
149148
|
} catch (err) {
|
|
149110
149149
|
return fail(`could not load fragment "${target}": ${errMsg2(err)}`);
|
|
149111
149150
|
}
|
|
149112
|
-
if (isE2eFlow(fragment)) {
|
|
149113
|
-
return fail(
|
|
149114
|
-
`"${target}" is an e2e flow (starts with a launch step); only fragments can be run from another flow`
|
|
149115
|
-
);
|
|
149116
|
-
}
|
|
149117
149151
|
pushReport(state3, { index, kind: "run", status: "pass", flow: target });
|
|
149118
149152
|
await execSteps(state3, fragment.steps, target, [...runStack, target]);
|
|
149119
149153
|
}
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ Flows store **no device id**: the runner binds a device (the single booted one,
|
|
|
11
11
|
|
|
12
12
|
**Two flow types**
|
|
13
13
|
|
|
14
|
-
- **e2e** — begins with a `launch:` step, which starts that app from scratch (terminate + relaunch), so the flow controls its own start state. No `executionPrerequisite`. May `run:`
|
|
14
|
+
- **e2e** — begins with a `launch:` step, which starts that app from scratch (terminate + relaunch), so the flow controls its own start state. No `executionPrerequisite`. May `run:` other flows, and (on iOS/Android) may itself be a `run:` target — when nested, its `launch` runs inline, restarting the app for that sub-scenario. **Chromium is the exception:** the runner boots one Electron app per run (the top-level flow's), so a nested chromium e2e flow's `launch` can't boot its own instance and fails the run — keep chromium e2e flows top-level. Record one by adding a `restart-app` of the app under test as the **first** step — it is captured as the `launch` step.
|
|
15
15
|
- **fragment** — doesn't begin with a launch; runs against the device's current state. May declare an `executionPrerequisite` (a documented entry-state contract). Invoked from other flows via a `run:` step, or directly by you at any time.
|
|
16
16
|
|
|
17
17
|
Both run via `argent flow run <name>` — a fragment simply runs against whatever is on screen (its prerequisite is printed as a reminder). Only e2e flows are meaningful CI/suite entries, since only they give a deterministic verdict from a clean start.
|
|
@@ -20,18 +20,18 @@ Both run via `argent flow run <name>` — a fragment simply runs against whateve
|
|
|
20
20
|
|
|
21
21
|
Beyond raw `tool:` steps and `echo:`, flows support declarative directives interpreted by the runner (they are **not** agent-callable tools). **Every directive hard-stops the flow on failure**; later steps are reported `skip`.
|
|
22
22
|
|
|
23
|
-
| Directive | YAML
|
|
24
|
-
| ------------ |
|
|
25
|
-
| `launch` | `- launch: com.acme.app` or `- launch: { ios: …, android: … }`
|
|
26
|
-
| `tap` | `- tap: Login`, `- tap: { x: 0.5, y: 0.57 }`, `- tap: { on: Login, times: 2 }`
|
|
27
|
-
| `long-press` | `- long-press: Row 3
|
|
28
|
-
| `type` | `- type: { into: email, text: "a@b.com" }`
|
|
29
|
-
| `scroll-to` | `- scroll-to: "Order #1234"` (scrolls down) or `- scroll-to: { target: …, direction: right, within: … }`
|
|
30
|
-
| `await` | `- await: { visible: Home }`
|
|
31
|
-
| `wait` | `- wait: 500`
|
|
32
|
-
| `assert` | `- assert: { visible: Welcome }`
|
|
33
|
-
| `snapshot` | `- snapshot: home` or `- snapshot: { name: home, maxMismatch: 0.5 }`
|
|
34
|
-
| `run` | `- run: login`
|
|
23
|
+
| Directive | YAML | Meaning |
|
|
24
|
+
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
25
|
+
| `launch` | `- launch: com.acme.app` or `- launch: { ios: …, android: … }` | start the app from scratch (terminate + relaunch) and wait until ready |
|
|
26
|
+
| `tap` | `- tap: Login`, `- tap: { x: 0.5, y: 0.57 }`, `- tap: { on: Login, times: 2 }`, `- tap: { on: { x: 0.5, y: 0.57 }, times: 2 }` | tap by selector (auto-waits) or raw point; `times` (2 = double-tap) needs the target nested under `on:` — a selector or a point (`{ x, y, times }` is rejected) |
|
|
27
|
+
| `long-press` | `- long-press: Row 3`, `- long-press: { x: 0.5, y: 0.6 }`, `- long-press: { on: <sel>, duration: 1200 }`, `- long-press: { on: { x: 0.5, y: 0.6 }, duration: 1200 }` | press and hold an element or raw point (default 800ms; Chromium: mouse press-hold); `duration` needs the target nested under `on:` — a selector or a point |
|
|
28
|
+
| `type` | `- type: { into: email, text: "a@b.com" }` | focus a field, type, then press Enter to submit + dismiss the keyboard |
|
|
29
|
+
| `scroll-to` | `- scroll-to: "Order #1234"` (scrolls down) or `- scroll-to: { target: …, direction: right, within: … }` | momentum-free scroll until the target is visible |
|
|
30
|
+
| `await` | `- await: { visible: Home }` | wait for a UI condition |
|
|
31
|
+
| `wait` | `- wait: 500` | pause for a fixed number of milliseconds (last resort — prefer `await`) |
|
|
32
|
+
| `assert` | `- assert: { visible: Welcome }` | check a condition, hard-fail if it never holds |
|
|
33
|
+
| `snapshot` | `- snapshot: home` or `- snapshot: { name: home, maxMismatch: 0.5 }` | diff a screenshot against a stored baseline |
|
|
34
|
+
| `run` | `- run: login` | execute another flow's steps inline (fragment or e2e) |
|
|
35
35
|
|
|
36
36
|
### Selectors
|
|
37
37
|
|