@swmansion/argent 0.15.1-next.5 → 0.15.1-next.6
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 +176 -29
- package/package.json +1 -1
- package/skills/argent-create-flow/SKILL.md +4 -1
package/dist/tool-server.cjs
CHANGED
|
@@ -133168,7 +133168,7 @@ async function describeVega(_serial) {
|
|
|
133168
133168
|
|
|
133169
133169
|
// ../tool-server/src/utils/ui-tree-match.ts
|
|
133170
133170
|
init_zod();
|
|
133171
|
-
var
|
|
133171
|
+
var selectorFieldsSchema = external_exports.object({
|
|
133172
133172
|
text: external_exports.string().min(1).optional().describe("Case-insensitive substring of the element's visible label or value."),
|
|
133173
133173
|
identifier: external_exports.string().min(1).optional().describe(
|
|
133174
133174
|
"The element's identifier (accessibilityIdentifier / resource-id / testid), matched case-insensitively as the exact identifier or the unqualified resource-id name ('submit' matches 'com.example.app:id/submit')."
|
|
@@ -133176,9 +133176,13 @@ var selectorSchema = external_exports.object({
|
|
|
133176
133176
|
role: external_exports.string().min(1).optional().describe(
|
|
133177
133177
|
"Case-insensitive substring of the element's role (e.g. AXButton, button, TextView)."
|
|
133178
133178
|
)
|
|
133179
|
-
}).
|
|
133180
|
-
|
|
133181
|
-
|
|
133179
|
+
}).strict();
|
|
133180
|
+
var selectorSchema = selectorFieldsSchema.refine(
|
|
133181
|
+
(s) => Boolean(s.text || s.identifier || s.role),
|
|
133182
|
+
{
|
|
133183
|
+
message: "selector needs at least one of text, identifier, or role"
|
|
133184
|
+
}
|
|
133185
|
+
);
|
|
133182
133186
|
function nodeText(node) {
|
|
133183
133187
|
return [node.label, node.value].filter(Boolean).join(" ");
|
|
133184
133188
|
}
|
|
@@ -133195,15 +133199,32 @@ function identifierMatches(actual, needle) {
|
|
|
133195
133199
|
if (!actual) return false;
|
|
133196
133200
|
return equalsCI(actual, needle) || actual.toLowerCase().endsWith(`:id/${needle.toLowerCase()}`);
|
|
133197
133201
|
}
|
|
133202
|
+
var uiTreeMatchInternals = {
|
|
133203
|
+
createRegExp(pattern) {
|
|
133204
|
+
return new RegExp(pattern);
|
|
133205
|
+
}
|
|
133206
|
+
};
|
|
133207
|
+
function regexMatchesNonEmpty(regex, actual) {
|
|
133208
|
+
if (!actual) return false;
|
|
133209
|
+
return regex.test(actual);
|
|
133210
|
+
}
|
|
133198
133211
|
function textMatches(actual, expected, mode) {
|
|
133212
|
+
if (mode === "matches") {
|
|
133213
|
+
return regexMatchesNonEmpty(uiTreeMatchInternals.createRegExp(expected), actual);
|
|
133214
|
+
}
|
|
133199
133215
|
return mode === "equals" ? equalsCI(actual, expected) : includesCI(actual, expected);
|
|
133200
133216
|
}
|
|
133201
|
-
function
|
|
133217
|
+
function matchNodeWithRegex(node, selector, textRegex) {
|
|
133202
133218
|
if (selector.text !== void 0) {
|
|
133203
133219
|
if (!includesCI(node.label, selector.text) && !includesCI(node.value, selector.text)) {
|
|
133204
133220
|
return false;
|
|
133205
133221
|
}
|
|
133206
133222
|
}
|
|
133223
|
+
if (textRegex !== void 0) {
|
|
133224
|
+
if (!regexMatchesNonEmpty(textRegex, node.label) && !regexMatchesNonEmpty(textRegex, node.value)) {
|
|
133225
|
+
return false;
|
|
133226
|
+
}
|
|
133227
|
+
}
|
|
133207
133228
|
if (selector.identifier !== void 0 && !identifierMatches(node.identifier, selector.identifier)) {
|
|
133208
133229
|
return false;
|
|
133209
133230
|
}
|
|
@@ -133212,13 +133233,17 @@ function matchNode(node, selector) {
|
|
|
133212
133233
|
}
|
|
133213
133234
|
return true;
|
|
133214
133235
|
}
|
|
133215
|
-
function
|
|
133216
|
-
|
|
133217
|
-
|
|
133236
|
+
function selectorTextRegex(selector) {
|
|
133237
|
+
return selector.textMatches === void 0 ? void 0 : uiTreeMatchInternals.createRegExp(selector.textMatches);
|
|
133238
|
+
}
|
|
133239
|
+
function collectMatches(node, selector, textRegex, acc) {
|
|
133240
|
+
if (matchNodeWithRegex(node, selector, textRegex)) acc.push(node);
|
|
133241
|
+
for (const child of node.children) collectMatches(child, selector, textRegex, acc);
|
|
133218
133242
|
}
|
|
133219
133243
|
function findAll(root2, selector) {
|
|
133220
133244
|
const acc = [];
|
|
133221
|
-
|
|
133245
|
+
const textRegex = selectorTextRegex(selector);
|
|
133246
|
+
for (const child of root2.children) collectMatches(child, selector, textRegex, acc);
|
|
133222
133247
|
return acc;
|
|
133223
133248
|
}
|
|
133224
133249
|
function isVisible(node) {
|
|
@@ -133282,21 +133307,29 @@ function nodeAtPoint(root2, point) {
|
|
|
133282
133307
|
for (const child of root2.children) walk(child);
|
|
133283
133308
|
return best;
|
|
133284
133309
|
}
|
|
133285
|
-
function
|
|
133310
|
+
function fullConsumptionRegex(selector) {
|
|
133311
|
+
return selector.textMatches === void 0 ? void 0 : uiTreeMatchInternals.createRegExp(`^(?:${selector.textMatches})$`);
|
|
133312
|
+
}
|
|
133313
|
+
function exactFieldCount(node, selector, fullTextRegex) {
|
|
133286
133314
|
let count = 0;
|
|
133287
133315
|
if (selector.text !== void 0 && (equalsCI(node.label, selector.text) || equalsCI(node.value, selector.text))) {
|
|
133288
133316
|
count++;
|
|
133289
133317
|
}
|
|
133318
|
+
if (fullTextRegex !== void 0 && (regexMatchesNonEmpty(fullTextRegex, node.label) || regexMatchesNonEmpty(fullTextRegex, node.value))) {
|
|
133319
|
+
count++;
|
|
133320
|
+
}
|
|
133290
133321
|
if (selector.identifier !== void 0 && equalsCI(node.identifier, selector.identifier)) count++;
|
|
133291
133322
|
if (selector.role !== void 0 && equalsCI(node.role, selector.role)) count++;
|
|
133292
133323
|
return count;
|
|
133293
133324
|
}
|
|
133294
133325
|
function selectorToFrame(root2, selector) {
|
|
133295
133326
|
const visible = findAll(root2, selector).filter(isVisible);
|
|
133327
|
+
if (visible.length === 0) return void 0;
|
|
133328
|
+
const fullTextRegex = fullConsumptionRegex(selector);
|
|
133296
133329
|
let best;
|
|
133297
133330
|
let bestExact = -1;
|
|
133298
133331
|
for (const n of visible) {
|
|
133299
|
-
const exact = exactFieldCount(n, selector);
|
|
133332
|
+
const exact = exactFieldCount(n, selector, fullTextRegex);
|
|
133300
133333
|
if (best === void 0 || exact !== bestExact) {
|
|
133301
133334
|
if (exact > bestExact) {
|
|
133302
133335
|
best = n;
|
|
@@ -144519,14 +144552,67 @@ function chromiumLaunchSpec(launch) {
|
|
|
144519
144552
|
return typeof c === "string" ? { path: c } : { path: c.path, args: c.args };
|
|
144520
144553
|
}
|
|
144521
144554
|
function selectorToYaml(sel) {
|
|
144555
|
+
if (sel.text !== void 0 && sel.textMatches !== void 0) {
|
|
144556
|
+
throw new Error(
|
|
144557
|
+
'Cannot serialize flow selector without losing constraints: both `text` and `textMatches` are set, but flow YAML can represent only one `text` constraint (a literal string or `{ matches: "<regex>" }`). Use either literal or regex text matching.'
|
|
144558
|
+
);
|
|
144559
|
+
}
|
|
144560
|
+
if (sel.loose && sel.text !== void 0 && (typeof sel.text !== "string" || sel.text.length === 0)) {
|
|
144561
|
+
throw new Error(
|
|
144562
|
+
"Cannot serialize loose flow selector: `text` must be a non-empty string so bare-string YAML can round-trip through selector validation."
|
|
144563
|
+
);
|
|
144564
|
+
}
|
|
144565
|
+
if (sel.loose && (sel.text === void 0 || sel.textMatches !== void 0 || sel.identifier !== void 0 || sel.role !== void 0)) {
|
|
144566
|
+
const incompatible = [
|
|
144567
|
+
sel.textMatches !== void 0 ? "textMatches" : void 0,
|
|
144568
|
+
sel.identifier !== void 0 ? "identifier" : void 0,
|
|
144569
|
+
sel.role !== void 0 ? "role" : void 0
|
|
144570
|
+
].filter((field) => field !== void 0);
|
|
144571
|
+
throw new Error(
|
|
144572
|
+
"Cannot serialize loose flow selector without changing its meaning: bare-string YAML can represent only a loose text-only selector" + (incompatible.length > 0 ? `; incompatible fields: ${incompatible.join(", ")}` : "") + "."
|
|
144573
|
+
);
|
|
144574
|
+
}
|
|
144522
144575
|
if (sel.loose && sel.text !== void 0 && sel.identifier === void 0 && sel.role === void 0) {
|
|
144523
144576
|
return sel.text;
|
|
144524
144577
|
}
|
|
144525
|
-
const { loose: _loose, identifier, ...rest } = sel;
|
|
144526
|
-
|
|
144578
|
+
const { loose: _loose, identifier, textMatches: textMatches2, ...rest } = sel;
|
|
144579
|
+
const out = { ...rest };
|
|
144580
|
+
if (textMatches2 !== void 0) out.text = { matches: textMatches2 };
|
|
144581
|
+
if (identifier !== void 0) out.id = identifier;
|
|
144582
|
+
return out;
|
|
144527
144583
|
}
|
|
144528
144584
|
function describeSelector(s) {
|
|
144529
|
-
return Object.entries(s).filter(([k]) => k !== "loose").map(
|
|
144585
|
+
return Object.entries(s).filter(([k]) => k !== "loose").map(
|
|
144586
|
+
([k, v]) => k === "textMatches" ? `text=/${v}/` : `${k === "identifier" ? "id" : k}="${v}"`
|
|
144587
|
+
).join(" ");
|
|
144588
|
+
}
|
|
144589
|
+
function describeTextExpectation(expectedText, textMatch, verbForm = "mode") {
|
|
144590
|
+
const expected = expectedText ?? "";
|
|
144591
|
+
const mode = textMatch ?? "contains";
|
|
144592
|
+
switch (mode) {
|
|
144593
|
+
case "contains":
|
|
144594
|
+
return `${verbForm === "infinitive" ? "contain" : mode} ${JSON.stringify(expected)}`;
|
|
144595
|
+
case "equals":
|
|
144596
|
+
return `${verbForm === "infinitive" ? "equal" : mode} ${JSON.stringify(expected)}`;
|
|
144597
|
+
case "matches":
|
|
144598
|
+
return `${verbForm === "infinitive" ? "match" : mode} /${expected}/`;
|
|
144599
|
+
}
|
|
144600
|
+
}
|
|
144601
|
+
function textWaitToYaml(selector, expectedText, textMatch) {
|
|
144602
|
+
const expected = expectedText ?? "";
|
|
144603
|
+
const mode = textMatch ?? "contains";
|
|
144604
|
+
switch (mode) {
|
|
144605
|
+
case "contains":
|
|
144606
|
+
return { text: { in: selector, contains: expected } };
|
|
144607
|
+
case "equals":
|
|
144608
|
+
return { text: { in: selector, equals: expected } };
|
|
144609
|
+
case "matches":
|
|
144610
|
+
return { text: { in: selector, matches: expected } };
|
|
144611
|
+
default: {
|
|
144612
|
+
const exhaustive = mode;
|
|
144613
|
+
throw new Error(`Unsupported text match mode: ${exhaustive}`);
|
|
144614
|
+
}
|
|
144615
|
+
}
|
|
144530
144616
|
}
|
|
144531
144617
|
function waitToYaml(condition, selector, expectedText, textMatch, timeoutMs) {
|
|
144532
144618
|
const sel = selectorToYaml(selector);
|
|
@@ -144542,7 +144628,7 @@ function waitToYaml(condition, selector, expectedText, textMatch, timeoutMs) {
|
|
|
144542
144628
|
body = { hidden: sel };
|
|
144543
144629
|
break;
|
|
144544
144630
|
case "text":
|
|
144545
|
-
body =
|
|
144631
|
+
body = textWaitToYaml(sel, expectedText, textMatch);
|
|
144546
144632
|
break;
|
|
144547
144633
|
}
|
|
144548
144634
|
if (timeoutMs !== void 0) body.timeout = timeoutMs;
|
|
@@ -144635,6 +144721,16 @@ function badEntry(raw, detail) {
|
|
|
144635
144721
|
error_kind: "validation"
|
|
144636
144722
|
});
|
|
144637
144723
|
}
|
|
144724
|
+
function validatePattern(raw, pattern, where) {
|
|
144725
|
+
try {
|
|
144726
|
+
new RegExp(pattern);
|
|
144727
|
+
} catch (err) {
|
|
144728
|
+
badEntry(
|
|
144729
|
+
raw,
|
|
144730
|
+
`${where} \`matches\` is not a valid regular expression: ${err instanceof Error ? err.message : String(err)}`
|
|
144731
|
+
);
|
|
144732
|
+
}
|
|
144733
|
+
}
|
|
144638
144734
|
function editDistance(a, b) {
|
|
144639
144735
|
let prevPrev = new Array(b.length + 1);
|
|
144640
144736
|
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
@@ -144698,11 +144794,46 @@ function parseSelector(raw, where) {
|
|
|
144698
144794
|
}
|
|
144699
144795
|
normalized = { ...rest, identifier: id };
|
|
144700
144796
|
}
|
|
144797
|
+
if (normalized !== null && typeof normalized === "object") {
|
|
144798
|
+
const { text, ...rest } = normalized;
|
|
144799
|
+
if (text !== null && typeof text === "object") {
|
|
144800
|
+
const keys = Object.keys(text);
|
|
144801
|
+
if (!Array.isArray(text)) {
|
|
144802
|
+
rejectUnknownKeys(
|
|
144803
|
+
raw,
|
|
144804
|
+
text,
|
|
144805
|
+
["matches"],
|
|
144806
|
+
`${where}: text matcher`
|
|
144807
|
+
);
|
|
144808
|
+
}
|
|
144809
|
+
const pattern = text.matches;
|
|
144810
|
+
if (keys.length !== 1 || keys[0] !== "matches") {
|
|
144811
|
+
badEntry(
|
|
144812
|
+
raw,
|
|
144813
|
+
`${where}: a text matcher takes exactly { matches: '<regex>' } \u2014 for a substring, use the plain-string form (text: "\u2026")`
|
|
144814
|
+
);
|
|
144815
|
+
}
|
|
144816
|
+
if (typeof pattern !== "string" || pattern.length === 0) {
|
|
144817
|
+
badEntry(raw, `${where}: text matcher needs a non-empty \`matches\` pattern`);
|
|
144818
|
+
}
|
|
144819
|
+
validatePattern(raw, pattern, `${where}: text`);
|
|
144820
|
+
const fields = selectorFieldsSchema.safeParse(rest);
|
|
144821
|
+
if (!fields.success) {
|
|
144822
|
+
badEntry(raw, `${where}: ${fields.error.issues[0]?.message ?? "invalid selector"}`);
|
|
144823
|
+
}
|
|
144824
|
+
return { ...fields.data, textMatches: pattern };
|
|
144825
|
+
}
|
|
144826
|
+
}
|
|
144701
144827
|
const r = selectorSchema.safeParse(normalized);
|
|
144702
144828
|
if (!r.success) badEntry(raw, `${where}: ${r.error.issues[0]?.message ?? "invalid selector"}`);
|
|
144703
144829
|
return r.data;
|
|
144704
144830
|
}
|
|
144705
144831
|
var WAIT_CONDITIONS = ["exists", "visible", "hidden", "text"];
|
|
144832
|
+
var TEXT_MATCH_MODES = Object.keys({
|
|
144833
|
+
contains: true,
|
|
144834
|
+
equals: true,
|
|
144835
|
+
matches: true
|
|
144836
|
+
});
|
|
144706
144837
|
var SCROLL_DIRECTIONS = ["up", "down", "left", "right"];
|
|
144707
144838
|
function parseWaitFields(raw, kind) {
|
|
144708
144839
|
if (raw === null || typeof raw !== "object") {
|
|
@@ -144742,22 +144873,30 @@ function parseWaitFields(raw, kind) {
|
|
|
144742
144873
|
if (condition === "text") {
|
|
144743
144874
|
const t = b.text;
|
|
144744
144875
|
if (t === null || typeof t !== "object") {
|
|
144745
|
-
badEntry(
|
|
144876
|
+
badEntry(
|
|
144877
|
+
{ [kind]: b },
|
|
144878
|
+
`${kind} text needs { in: <selector>, contains|equals|matches: <string> }`
|
|
144879
|
+
);
|
|
144746
144880
|
}
|
|
144747
144881
|
const tb = t;
|
|
144748
144882
|
if (!Array.isArray(tb)) {
|
|
144749
|
-
rejectUnknownKeys({ [kind]: b }, tb, ["in",
|
|
144883
|
+
rejectUnknownKeys({ [kind]: b }, tb, ["in", ...TEXT_MATCH_MODES], `${kind}.text`);
|
|
144750
144884
|
}
|
|
144751
|
-
const
|
|
144752
|
-
|
|
144753
|
-
|
|
144754
|
-
|
|
144885
|
+
const comparators = TEXT_MATCH_MODES.filter((mode) => mode in tb);
|
|
144886
|
+
if (comparators.length !== 1) {
|
|
144887
|
+
badEntry(
|
|
144888
|
+
{ [kind]: b },
|
|
144889
|
+
`${kind} text needs exactly one of \`contains\`, \`equals\`, or \`matches\``
|
|
144890
|
+
);
|
|
144755
144891
|
}
|
|
144756
|
-
const textMatch =
|
|
144757
|
-
const expected =
|
|
144892
|
+
const textMatch = comparators[0];
|
|
144893
|
+
const expected = tb[textMatch];
|
|
144758
144894
|
if (typeof expected !== "string" || expected.length === 0) {
|
|
144759
144895
|
badEntry({ [kind]: b }, `${kind} text needs a non-empty \`${textMatch}\``);
|
|
144760
144896
|
}
|
|
144897
|
+
if (textMatch === "matches") {
|
|
144898
|
+
validatePattern({ [kind]: b }, expected, `${kind} text`);
|
|
144899
|
+
}
|
|
144761
144900
|
return {
|
|
144762
144901
|
condition: "text",
|
|
144763
144902
|
selector: parseSelector(tb.in, `${kind}.text.in`),
|
|
@@ -145869,7 +146008,14 @@ You can still edit the .yaml file directly afterwards to remove or reorder steps
|
|
|
145869
146008
|
return `${n}. type: ${selectorLabel(step.into)} \u2190 "${step.text}"`;
|
|
145870
146009
|
case "await":
|
|
145871
146010
|
case "assert": {
|
|
145872
|
-
|
|
146011
|
+
let tail;
|
|
146012
|
+
if (step.condition !== "text") {
|
|
146013
|
+
tail = `${step.condition} ${selectorLabel(step.selector)}`;
|
|
146014
|
+
} else {
|
|
146015
|
+
const selector = selectorLabel(step.selector);
|
|
146016
|
+
const expected = step.expectedText ?? "";
|
|
146017
|
+
tail = step.textMatch === "matches" ? `text ${selector} matches /${expected}/` : step.textMatch === "equals" ? `text ${selector} == ${JSON.stringify(expected)}` : `text ${selector} contains ${JSON.stringify(expected)}`;
|
|
146018
|
+
}
|
|
145873
146019
|
return `${n}. ${step.kind}: ${tail}`;
|
|
145874
146020
|
}
|
|
145875
146021
|
case "wait":
|
|
@@ -146263,11 +146409,11 @@ function assertReason(condition, selector, expectedText, textMatch, matches2) {
|
|
|
146263
146409
|
case "text": {
|
|
146264
146410
|
const first = firstInReadingOrder(matches2.filter(isVisible)) ?? firstInReadingOrder(matches2);
|
|
146265
146411
|
if (!first) return `no element matched selector ${sel}`;
|
|
146266
|
-
const wanted = textMatch
|
|
146412
|
+
const wanted = describeTextExpectation(expectedText, textMatch, "infinitive");
|
|
146267
146413
|
const shown = assertText(first);
|
|
146268
146414
|
const own = nodeText(first);
|
|
146269
146415
|
const ownNote = own && own !== shown ? ` (own text "${own}")` : "";
|
|
146270
|
-
return `element matched ${sel} but its text was "${shown}"${ownNote} (wanted to ${wanted}
|
|
146416
|
+
return `element matched ${sel} but its text was "${shown}"${ownNote} (wanted to ${wanted})`;
|
|
146271
146417
|
}
|
|
146272
146418
|
default:
|
|
146273
146419
|
return `assertion failed for selector ${sel}`;
|
|
@@ -148763,6 +148909,7 @@ function pushReport(state3, report) {
|
|
|
148763
148909
|
function selectorLabel2(sel) {
|
|
148764
148910
|
const parts = [];
|
|
148765
148911
|
if (sel.text !== void 0) parts.push(`"${sel.text}"`);
|
|
148912
|
+
if (sel.textMatches !== void 0) parts.push(`/${sel.textMatches}/`);
|
|
148766
148913
|
if (sel.identifier) parts.push(`id=${sel.identifier}`);
|
|
148767
148914
|
if (sel.role) parts.push(`role=${sel.role}`);
|
|
148768
148915
|
return parts.join(" ");
|
|
@@ -148781,7 +148928,7 @@ function stepTarget(step) {
|
|
|
148781
148928
|
case "assert": {
|
|
148782
148929
|
const sel = selectorLabel2(step.selector);
|
|
148783
148930
|
if (step.condition === "text") {
|
|
148784
|
-
return `${sel} ${step.
|
|
148931
|
+
return `${sel} ${describeTextExpectation(step.expectedText, step.textMatch)}`;
|
|
148785
148932
|
}
|
|
148786
148933
|
return `${step.condition} ${sel}`;
|
|
148787
148934
|
}
|
|
@@ -149682,7 +149829,7 @@ function normLabel(s) {
|
|
|
149682
149829
|
return (s || "").toLowerCase().replace(/-/g, "").replace(/[\s,]+/g, " ").trim();
|
|
149683
149830
|
}
|
|
149684
149831
|
var MAX_FRAME_AREA = 0.85;
|
|
149685
|
-
function
|
|
149832
|
+
function matchNode(n, match, needle) {
|
|
149686
149833
|
const label = normLabel(n.label);
|
|
149687
149834
|
const ident = normLabel(n.identifier);
|
|
149688
149835
|
const value = normLabel(n.value);
|
|
@@ -149709,7 +149856,7 @@ function findElementMatch(tree, match) {
|
|
|
149709
149856
|
const candidates = [];
|
|
149710
149857
|
const walk = (n) => {
|
|
149711
149858
|
if (!n || typeof n !== "object") return;
|
|
149712
|
-
const m =
|
|
149859
|
+
const m = matchNode(n, match, needle);
|
|
149713
149860
|
if (m && n.frame) {
|
|
149714
149861
|
const f = n.frame;
|
|
149715
149862
|
const cx = f.x + f.width / 2;
|
package/package.json
CHANGED
|
@@ -37,7 +37,9 @@ Beyond raw `tool:` steps and `echo:`, flows support declarative directives inter
|
|
|
37
37
|
|
|
38
38
|
A **selector** is `{ text?, id?, role? }` (all-must-match; `text`/`role` are case-insensitive substrings, `id` matches the element's testID / accessibilityIdentifier / resource-id exactly, case-insensitive, also accepting the unqualified Android resource-id name — `submit` matches `com.example.app:id/submit`) — the same semantics `await-ui-element` uses, though that tool spells the `id` field `identifier` (flow YAML also accepts `identifier` as an alias for `id`, but `id` is the canonical spelling and what the recorder writes). A bare string is a _loose_ selector: it resolves **identifier-first, then falls back to text** (label/value), so `tap: Login` matches a `testID="Login"` or, failing that, visible text "Login" — no need to know which. Loose fallback applies uniformly to every selector slot (`tap`, `type.into`, `await`, `assert`, `scroll-to`). Use the map form to be strict: `{ id: submit-btn }` (identifier only) or `{ text: Login }` (text only, no fallback).
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
`text` also takes a **regex matcher map** — `{ text: { matches: '^Order #\d+$' } }`, in any selector slot — for dynamic text no literal can pin. It tests each node's native **own** label/value (not the adapter-hoisted `subtreeText`), though on iOS a container's own label may itself aggregate descendant text, so a wrapper and its leaf can both match. Same regex rules as `text.in`'s `matches` (see _`await` and `assert`_): unanchored, **case-sensitive**, single-quoted, invalid pattern fails at parse. So `assert: { visible: { text: { matches: '^Taps: \d+$' } } }` asserts a counter is on screen with no locator at all, and `tap: { text: { matches: '^Order #\d+$' } }` taps a dynamic row — though a stable `id` stays the more robust action target.
|
|
41
|
+
|
|
42
|
+
Selectors resolve against the **full native hierarchy** (iOS: the UIView tree; Android: the complete accessibility hierarchy including not-important views) — strictly more than `describe` or the raw `await-ui-element` tool see (both use the trimmed tree), with complete `testID`/`resource-id` coverage. So an `id` selector works even when `describe` collapses or omits the element — don't fall back to coordinate taps just because a testID isn't visible in `describe` output. And when several elements match — including wrappers whose native text aggregates descendant content — the action directives (`tap`, `type`, `scroll-to`) pick the **most specific** match: an exact text/identifier match beats a substring hit (for a regex matcher, a pattern consuming the element's whole text counts as exact), then the smallest frame wins.
|
|
41
43
|
|
|
42
44
|
**Quote strings YAML would mangle.** An unquoted `#` starts a YAML comment — `tap: Order #1234` silently parses as `tap: Order` — and bare `yes`/`no`/`on`/`off`/numbers coerce to non-strings. When a selector or typed text contains `#`, `:`, quotes, or could read as a boolean/number, wrap it: `tap: "Order #1234"`.
|
|
43
45
|
|
|
@@ -47,6 +49,7 @@ The **condition is the key**, and its value is the selector:
|
|
|
47
49
|
|
|
48
50
|
- `{ visible: Home }`, `{ exists: { id: row } }`, `{ hidden: spinner }`
|
|
49
51
|
- `{ text: { in: <selector>, contains: "Taps:" } }` or `{ text: { in: <selector>, equals: "Taps: 0" } }` — `text` locates an element (`in`) and checks its rendered content against exactly one of `contains` (case-insensitive substring) or `equals` (case-insensitive exact match — use it when boundaries matter: `contains: "Taps: 3"` is also satisfied by "Taps: 30"). Reach for `text` only when the locator is an identifier/role; to assert a string is simply on screen, prefer `{ visible: "Taps: 0" }`.
|
|
52
|
+
- `{ text: { in: total, matches: 'Total: \$\d+\.\d{2}' } }` — the third comparator: a JS regex for dynamic content (counters, prices, dates) that neither literal mode can pin. Unanchored like `contains` (anchor with `^…$` for the `equals` analog) and — unlike the literal modes — **case-sensitive**: the pattern carries its own semantics. An invalid pattern fails at parse time. **Quote the pattern in single quotes**: single-quoted and plain YAML scalars keep backslashes; double quotes would need `\\d`. To assert a dynamic string is simply on screen with no locator, prefer a regex **selector** — `{ visible: { text: { matches: '^Taps: \d+$' } } }` (see Selectors); `text.in` + `matches` is for checking a specific element's aggregated text.
|
|
50
53
|
- A container's text aggregates its descendants' text (space-joined), so `text` can assert what a testID wrapper visibly shows even when the string lives in a child node. That also means `equals` against a wrapper must match _everything_ it shows or exactly the wrapper's own label/value — targeting the leaf holding exactly the value (or using `contains`) stays the clearer spelling.
|
|
51
54
|
|
|
52
55
|
This condition-as-key form is the only spelling. `await` also accepts an optional `timeout` sibling key in milliseconds — `- await: { visible: Home, timeout: 15000 }` — for a transition that legitimately needs longer than the default budget. **Omit `timeout` by default**: the default budget covers normal transitions, and a habitual generous override just delays failure reporting on every broken step. Add one only after a step demonstrably needs it — it timed out at the default and the wait is legitimately slow (a cold start, a network round-trip, a long animation). `assert` has no timeout override: a check that needs seconds to become true is a wait — spell it `await`.
|