@swmansion/argent 0.15.1-next.4 → 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 +272 -74
- package/package.json +1 -1
- package/skills/argent-create-flow/SKILL.md +5 -2
package/dist/tool-server.cjs
CHANGED
|
@@ -126102,7 +126102,10 @@ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
126102
126102
|
var zodSchema16 = external_exports.object({
|
|
126103
126103
|
udid: external_exports.string().describe("Target device id from `list-devices` (iOS UDID, Android serial, or Chromium id)."),
|
|
126104
126104
|
x: external_exports.number().describe("Normalized horizontal position 0.0\u20131.0 (left=0, right=1), not pixels"),
|
|
126105
|
-
y: external_exports.number().describe("Normalized vertical position 0.0\u20131.0 (top=0, bottom=1), not pixels")
|
|
126105
|
+
y: external_exports.number().describe("Normalized vertical position 0.0\u20131.0 (top=0, bottom=1), not pixels"),
|
|
126106
|
+
clickCount: external_exports.number().int().min(1).max(10).optional().describe(
|
|
126107
|
+
"Number of taps/clicks dispatched as ONE multi-tap gesture (2 = double-tap / double-click). The taps land inside the OS double-tap window; on Chromium each click carries an escalating CDP clickCount so dblclick actually fires. Default 1."
|
|
126108
|
+
)
|
|
126106
126109
|
});
|
|
126107
126110
|
var capability8 = {
|
|
126108
126111
|
apple: { simulator: true, device: true },
|
|
@@ -126110,19 +126113,25 @@ var capability8 = {
|
|
|
126110
126113
|
android: { emulator: true, device: true, unknown: true },
|
|
126111
126114
|
chromium: { app: true }
|
|
126112
126115
|
};
|
|
126113
|
-
|
|
126116
|
+
var TAP_HOLD_MS = 50;
|
|
126117
|
+
var MULTI_TAP_GAP_MS = 100;
|
|
126118
|
+
async function tapChromium(api, x, y, clickCount) {
|
|
126114
126119
|
const vp = api.getViewport();
|
|
126115
126120
|
const pxX = Math.max(0, Math.min(vp.width, x * vp.width));
|
|
126116
126121
|
const pxY = Math.max(0, Math.min(vp.height, y * vp.height));
|
|
126117
126122
|
await api.dispatchMouseEvent({ type: "mouseMoved", x: pxX, y: pxY });
|
|
126118
|
-
|
|
126119
|
-
|
|
126120
|
-
|
|
126123
|
+
for (let i = 1; i <= clickCount; i++) {
|
|
126124
|
+
if (i > 1) await sleep2(MULTI_TAP_GAP_MS);
|
|
126125
|
+
await api.dispatchMouseEvent({ type: "mousePressed", x: pxX, y: pxY, clickCount: i });
|
|
126126
|
+
await sleep2(TAP_HOLD_MS);
|
|
126127
|
+
await api.dispatchMouseEvent({ type: "mouseReleased", x: pxX, y: pxY, clickCount: i });
|
|
126128
|
+
}
|
|
126121
126129
|
}
|
|
126122
126130
|
var gestureTapTool = {
|
|
126123
126131
|
id: "gesture-tap",
|
|
126124
126132
|
description: `Press the device screen (iOS simulator, Android emulator, or Chromium app) at normalized coordinates: x and y are fractions of screen width and height in 0.0\u20131.0 (not pixels).
|
|
126125
126133
|
Sends a Down event followed by an Up event at the same point. For Chromium, this dispatches a CDP mouse-press/release on the renderer.
|
|
126134
|
+
Set clickCount: 2 for a double-tap / double-click \u2014 the taps are dispatched as one gesture with proper click counting, which two separate tap calls cannot guarantee.
|
|
126126
126135
|
Use when you need to tap a button, link, or any tappable element on the screen.
|
|
126127
126136
|
Returns { tapped: true, timestampMs }. Fails if the simulator-server / emulator backend / Chromium CDP is not reachable for the given device.
|
|
126128
126137
|
Before tapping, determine the correct coordinates by using discovery tools \u2014 pick by platform: iOS / Android use \`describe\`, \`native-describe-screen\`, or \`debugger-component-tree\`; Chromium uses \`describe\` (the DOM walker), since the native and RN-specific discovery tools don't apply. More information in \`argent-device-interact\` skill`,
|
|
@@ -126140,29 +126149,33 @@ Before tapping, determine the correct coordinates by using discovery tools \u201
|
|
|
126140
126149
|
async execute(services, params) {
|
|
126141
126150
|
const device = resolveDevice(params.udid);
|
|
126142
126151
|
const timestampMs = Date.now();
|
|
126152
|
+
const clickCount = params.clickCount ?? 1;
|
|
126143
126153
|
if (device.platform === "chromium") {
|
|
126144
126154
|
const chromium = services.chromium;
|
|
126145
|
-
await tapChromium(chromium, params.x, params.y);
|
|
126155
|
+
await tapChromium(chromium, params.x, params.y, clickCount);
|
|
126146
126156
|
return { tapped: true, timestampMs };
|
|
126147
126157
|
}
|
|
126148
126158
|
const api = services.simulatorServer;
|
|
126149
|
-
|
|
126150
|
-
|
|
126151
|
-
|
|
126152
|
-
|
|
126153
|
-
|
|
126154
|
-
|
|
126155
|
-
|
|
126156
|
-
|
|
126157
|
-
|
|
126158
|
-
|
|
126159
|
-
|
|
126160
|
-
|
|
126161
|
-
|
|
126162
|
-
|
|
126163
|
-
|
|
126164
|
-
|
|
126165
|
-
|
|
126159
|
+
for (let i = 1; i <= clickCount; i++) {
|
|
126160
|
+
if (i > 1) await sleep2(MULTI_TAP_GAP_MS);
|
|
126161
|
+
sendCommand(api, {
|
|
126162
|
+
cmd: "touch",
|
|
126163
|
+
type: "Down",
|
|
126164
|
+
x: params.x,
|
|
126165
|
+
y: params.y,
|
|
126166
|
+
second_x: null,
|
|
126167
|
+
second_y: null
|
|
126168
|
+
});
|
|
126169
|
+
await sleep2(TAP_HOLD_MS);
|
|
126170
|
+
sendCommand(api, {
|
|
126171
|
+
cmd: "touch",
|
|
126172
|
+
type: "Up",
|
|
126173
|
+
x: params.x,
|
|
126174
|
+
y: params.y,
|
|
126175
|
+
second_x: null,
|
|
126176
|
+
second_y: null
|
|
126177
|
+
});
|
|
126178
|
+
}
|
|
126166
126179
|
return { tapped: true, timestampMs };
|
|
126167
126180
|
}
|
|
126168
126181
|
};
|
|
@@ -133155,7 +133168,7 @@ async function describeVega(_serial) {
|
|
|
133155
133168
|
|
|
133156
133169
|
// ../tool-server/src/utils/ui-tree-match.ts
|
|
133157
133170
|
init_zod();
|
|
133158
|
-
var
|
|
133171
|
+
var selectorFieldsSchema = external_exports.object({
|
|
133159
133172
|
text: external_exports.string().min(1).optional().describe("Case-insensitive substring of the element's visible label or value."),
|
|
133160
133173
|
identifier: external_exports.string().min(1).optional().describe(
|
|
133161
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')."
|
|
@@ -133163,9 +133176,13 @@ var selectorSchema = external_exports.object({
|
|
|
133163
133176
|
role: external_exports.string().min(1).optional().describe(
|
|
133164
133177
|
"Case-insensitive substring of the element's role (e.g. AXButton, button, TextView)."
|
|
133165
133178
|
)
|
|
133166
|
-
}).
|
|
133167
|
-
|
|
133168
|
-
|
|
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
|
+
);
|
|
133169
133186
|
function nodeText(node) {
|
|
133170
133187
|
return [node.label, node.value].filter(Boolean).join(" ");
|
|
133171
133188
|
}
|
|
@@ -133182,15 +133199,32 @@ function identifierMatches(actual, needle) {
|
|
|
133182
133199
|
if (!actual) return false;
|
|
133183
133200
|
return equalsCI(actual, needle) || actual.toLowerCase().endsWith(`:id/${needle.toLowerCase()}`);
|
|
133184
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
|
+
}
|
|
133185
133211
|
function textMatches(actual, expected, mode) {
|
|
133212
|
+
if (mode === "matches") {
|
|
133213
|
+
return regexMatchesNonEmpty(uiTreeMatchInternals.createRegExp(expected), actual);
|
|
133214
|
+
}
|
|
133186
133215
|
return mode === "equals" ? equalsCI(actual, expected) : includesCI(actual, expected);
|
|
133187
133216
|
}
|
|
133188
|
-
function
|
|
133217
|
+
function matchNodeWithRegex(node, selector, textRegex) {
|
|
133189
133218
|
if (selector.text !== void 0) {
|
|
133190
133219
|
if (!includesCI(node.label, selector.text) && !includesCI(node.value, selector.text)) {
|
|
133191
133220
|
return false;
|
|
133192
133221
|
}
|
|
133193
133222
|
}
|
|
133223
|
+
if (textRegex !== void 0) {
|
|
133224
|
+
if (!regexMatchesNonEmpty(textRegex, node.label) && !regexMatchesNonEmpty(textRegex, node.value)) {
|
|
133225
|
+
return false;
|
|
133226
|
+
}
|
|
133227
|
+
}
|
|
133194
133228
|
if (selector.identifier !== void 0 && !identifierMatches(node.identifier, selector.identifier)) {
|
|
133195
133229
|
return false;
|
|
133196
133230
|
}
|
|
@@ -133199,13 +133233,17 @@ function matchNode(node, selector) {
|
|
|
133199
133233
|
}
|
|
133200
133234
|
return true;
|
|
133201
133235
|
}
|
|
133202
|
-
function
|
|
133203
|
-
|
|
133204
|
-
|
|
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);
|
|
133205
133242
|
}
|
|
133206
133243
|
function findAll(root2, selector) {
|
|
133207
133244
|
const acc = [];
|
|
133208
|
-
|
|
133245
|
+
const textRegex = selectorTextRegex(selector);
|
|
133246
|
+
for (const child of root2.children) collectMatches(child, selector, textRegex, acc);
|
|
133209
133247
|
return acc;
|
|
133210
133248
|
}
|
|
133211
133249
|
function isVisible(node) {
|
|
@@ -133269,21 +133307,29 @@ function nodeAtPoint(root2, point) {
|
|
|
133269
133307
|
for (const child of root2.children) walk(child);
|
|
133270
133308
|
return best;
|
|
133271
133309
|
}
|
|
133272
|
-
function
|
|
133310
|
+
function fullConsumptionRegex(selector) {
|
|
133311
|
+
return selector.textMatches === void 0 ? void 0 : uiTreeMatchInternals.createRegExp(`^(?:${selector.textMatches})$`);
|
|
133312
|
+
}
|
|
133313
|
+
function exactFieldCount(node, selector, fullTextRegex) {
|
|
133273
133314
|
let count = 0;
|
|
133274
133315
|
if (selector.text !== void 0 && (equalsCI(node.label, selector.text) || equalsCI(node.value, selector.text))) {
|
|
133275
133316
|
count++;
|
|
133276
133317
|
}
|
|
133318
|
+
if (fullTextRegex !== void 0 && (regexMatchesNonEmpty(fullTextRegex, node.label) || regexMatchesNonEmpty(fullTextRegex, node.value))) {
|
|
133319
|
+
count++;
|
|
133320
|
+
}
|
|
133277
133321
|
if (selector.identifier !== void 0 && equalsCI(node.identifier, selector.identifier)) count++;
|
|
133278
133322
|
if (selector.role !== void 0 && equalsCI(node.role, selector.role)) count++;
|
|
133279
133323
|
return count;
|
|
133280
133324
|
}
|
|
133281
133325
|
function selectorToFrame(root2, selector) {
|
|
133282
133326
|
const visible = findAll(root2, selector).filter(isVisible);
|
|
133327
|
+
if (visible.length === 0) return void 0;
|
|
133328
|
+
const fullTextRegex = fullConsumptionRegex(selector);
|
|
133283
133329
|
let best;
|
|
133284
133330
|
let bestExact = -1;
|
|
133285
133331
|
for (const n of visible) {
|
|
133286
|
-
const exact = exactFieldCount(n, selector);
|
|
133332
|
+
const exact = exactFieldCount(n, selector, fullTextRegex);
|
|
133287
133333
|
if (best === void 0 || exact !== bestExact) {
|
|
133288
133334
|
if (exact > bestExact) {
|
|
133289
133335
|
best = n;
|
|
@@ -133567,7 +133613,7 @@ a prior tap), use individual tool calls instead.
|
|
|
133567
133613
|
|
|
133568
133614
|
Allowed tools and their args (udid is auto-injected, do NOT include it in args):
|
|
133569
133615
|
|
|
133570
|
-
gesture-tap: { x: number, y: number }
|
|
133616
|
+
gesture-tap: { x: number, y: number, clickCount?: number } [ios/android/chromium]
|
|
133571
133617
|
gesture-swipe: { fromX: number, fromY: number, toX: number, toY: number, durationMs?: number } [ios/android]
|
|
133572
133618
|
gesture-scroll: { x: number, y: number, deltaX?: number, deltaY?: number, durationMs?: number } [chromium only]
|
|
133573
133619
|
gesture-drag: { fromX: number, fromY: number, toX: number, toY: number, durationMs?: number } [chromium only]
|
|
@@ -144506,14 +144552,67 @@ function chromiumLaunchSpec(launch) {
|
|
|
144506
144552
|
return typeof c === "string" ? { path: c } : { path: c.path, args: c.args };
|
|
144507
144553
|
}
|
|
144508
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
|
+
}
|
|
144509
144575
|
if (sel.loose && sel.text !== void 0 && sel.identifier === void 0 && sel.role === void 0) {
|
|
144510
144576
|
return sel.text;
|
|
144511
144577
|
}
|
|
144512
|
-
const { loose: _loose, identifier, ...rest } = sel;
|
|
144513
|
-
|
|
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;
|
|
144514
144583
|
}
|
|
144515
144584
|
function describeSelector(s) {
|
|
144516
|
-
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
|
+
}
|
|
144517
144616
|
}
|
|
144518
144617
|
function waitToYaml(condition, selector, expectedText, textMatch, timeoutMs) {
|
|
144519
144618
|
const sel = selectorToYaml(selector);
|
|
@@ -144529,7 +144628,7 @@ function waitToYaml(condition, selector, expectedText, textMatch, timeoutMs) {
|
|
|
144529
144628
|
body = { hidden: sel };
|
|
144530
144629
|
break;
|
|
144531
144630
|
case "text":
|
|
144532
|
-
body =
|
|
144631
|
+
body = textWaitToYaml(sel, expectedText, textMatch);
|
|
144533
144632
|
break;
|
|
144534
144633
|
}
|
|
144535
144634
|
if (timeoutMs !== void 0) body.timeout = timeoutMs;
|
|
@@ -144544,7 +144643,12 @@ function toYamlStep(step) {
|
|
|
144544
144643
|
case "run":
|
|
144545
144644
|
return { run: step.flow };
|
|
144546
144645
|
case "tap": {
|
|
144547
|
-
|
|
144646
|
+
if (step.selector) {
|
|
144647
|
+
const sel = selectorToYaml(step.selector);
|
|
144648
|
+
return { tap: step.times !== void 0 ? { on: sel, times: step.times } : sel };
|
|
144649
|
+
}
|
|
144650
|
+
const body = { x: step.x, y: step.y };
|
|
144651
|
+
if (step.times !== void 0) body.times = step.times;
|
|
144548
144652
|
return { tap: body };
|
|
144549
144653
|
}
|
|
144550
144654
|
case "long-press": {
|
|
@@ -144617,6 +144721,16 @@ function badEntry(raw, detail) {
|
|
|
144617
144721
|
error_kind: "validation"
|
|
144618
144722
|
});
|
|
144619
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
|
+
}
|
|
144620
144734
|
function editDistance(a, b) {
|
|
144621
144735
|
let prevPrev = new Array(b.length + 1);
|
|
144622
144736
|
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
@@ -144680,11 +144794,46 @@ function parseSelector(raw, where) {
|
|
|
144680
144794
|
}
|
|
144681
144795
|
normalized = { ...rest, identifier: id };
|
|
144682
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
|
+
}
|
|
144683
144827
|
const r = selectorSchema.safeParse(normalized);
|
|
144684
144828
|
if (!r.success) badEntry(raw, `${where}: ${r.error.issues[0]?.message ?? "invalid selector"}`);
|
|
144685
144829
|
return r.data;
|
|
144686
144830
|
}
|
|
144687
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
|
+
});
|
|
144688
144837
|
var SCROLL_DIRECTIONS = ["up", "down", "left", "right"];
|
|
144689
144838
|
function parseWaitFields(raw, kind) {
|
|
144690
144839
|
if (raw === null || typeof raw !== "object") {
|
|
@@ -144724,22 +144873,30 @@ function parseWaitFields(raw, kind) {
|
|
|
144724
144873
|
if (condition === "text") {
|
|
144725
144874
|
const t = b.text;
|
|
144726
144875
|
if (t === null || typeof t !== "object") {
|
|
144727
|
-
badEntry(
|
|
144876
|
+
badEntry(
|
|
144877
|
+
{ [kind]: b },
|
|
144878
|
+
`${kind} text needs { in: <selector>, contains|equals|matches: <string> }`
|
|
144879
|
+
);
|
|
144728
144880
|
}
|
|
144729
144881
|
const tb = t;
|
|
144730
144882
|
if (!Array.isArray(tb)) {
|
|
144731
|
-
rejectUnknownKeys({ [kind]: b }, tb, ["in",
|
|
144883
|
+
rejectUnknownKeys({ [kind]: b }, tb, ["in", ...TEXT_MATCH_MODES], `${kind}.text`);
|
|
144732
144884
|
}
|
|
144733
|
-
const
|
|
144734
|
-
|
|
144735
|
-
|
|
144736
|
-
|
|
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
|
+
);
|
|
144737
144891
|
}
|
|
144738
|
-
const textMatch =
|
|
144739
|
-
const expected =
|
|
144892
|
+
const textMatch = comparators[0];
|
|
144893
|
+
const expected = tb[textMatch];
|
|
144740
144894
|
if (typeof expected !== "string" || expected.length === 0) {
|
|
144741
144895
|
badEntry({ [kind]: b }, `${kind} text needs a non-empty \`${textMatch}\``);
|
|
144742
144896
|
}
|
|
144897
|
+
if (textMatch === "matches") {
|
|
144898
|
+
validatePattern({ [kind]: b }, expected, `${kind} text`);
|
|
144899
|
+
}
|
|
144743
144900
|
return {
|
|
144744
144901
|
condition: "text",
|
|
144745
144902
|
selector: parseSelector(tb.in, `${kind}.text.in`),
|
|
@@ -144839,6 +144996,50 @@ function parseLongPress(body, entry) {
|
|
|
144839
144996
|
}
|
|
144840
144997
|
return { kind: "long-press", selector: parseSelector(body, "long-press") };
|
|
144841
144998
|
}
|
|
144999
|
+
function parseTapTimes(raw, entry) {
|
|
145000
|
+
if (raw === void 0) return void 0;
|
|
145001
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 1 || raw > 10) {
|
|
145002
|
+
badEntry(entry, "tap.times must be an integer between 1 and 10 (2 = double-tap)");
|
|
145003
|
+
}
|
|
145004
|
+
return raw === 1 ? void 0 : raw;
|
|
145005
|
+
}
|
|
145006
|
+
function parseTap(body, entry) {
|
|
145007
|
+
const obj = body !== null && typeof body === "object" ? body : {};
|
|
145008
|
+
if (obj.x !== void 0 || obj.y !== void 0) {
|
|
145009
|
+
if (obj.on !== void 0 || obj.text !== void 0 || obj.id !== void 0 || obj.identifier !== void 0 || obj.role !== void 0) {
|
|
145010
|
+
badEntry(entry, "tap takes a selector or x/y coordinates, not both");
|
|
145011
|
+
}
|
|
145012
|
+
if (typeof obj.x !== "number" || typeof obj.y !== "number") {
|
|
145013
|
+
badEntry(entry, "a coordinate tap needs numeric x and y");
|
|
145014
|
+
}
|
|
145015
|
+
if (!Object.keys(obj).every((k) => k === "x" || k === "y" || k === "times")) {
|
|
145016
|
+
badEntry(entry, "a coordinate tap takes only { x, y, times }");
|
|
145017
|
+
}
|
|
145018
|
+
const step = { kind: "tap", x: obj.x, y: obj.y };
|
|
145019
|
+
const times = parseTapTimes(obj.times, entry);
|
|
145020
|
+
if (times !== void 0) step.times = times;
|
|
145021
|
+
return step;
|
|
145022
|
+
}
|
|
145023
|
+
if (obj.on !== void 0 || obj.times !== void 0) {
|
|
145024
|
+
if (obj.text !== void 0 || obj.id !== void 0 || obj.identifier !== void 0 || obj.role !== void 0) {
|
|
145025
|
+
badEntry(
|
|
145026
|
+
entry,
|
|
145027
|
+
'the tap options form takes a nested selector \u2014 e.g. tap: { on: { text: "Photo" }, times: 2 }'
|
|
145028
|
+
);
|
|
145029
|
+
}
|
|
145030
|
+
if (!Object.keys(obj).every((k) => k === "on" || k === "times")) {
|
|
145031
|
+
badEntry(entry, "the tap options form accepts only { on, times }");
|
|
145032
|
+
}
|
|
145033
|
+
if (obj.on === void 0) {
|
|
145034
|
+
badEntry(entry, 'tap with times needs a target \u2014 e.g. tap: { on: "Photo", times: 2 }');
|
|
145035
|
+
}
|
|
145036
|
+
const step = { kind: "tap", selector: parseSelector(obj.on, "tap.on") };
|
|
145037
|
+
const times = parseTapTimes(obj.times, entry);
|
|
145038
|
+
if (times !== void 0) step.times = times;
|
|
145039
|
+
return step;
|
|
145040
|
+
}
|
|
145041
|
+
return { kind: "tap", selector: parseSelector(body, "tap") };
|
|
145042
|
+
}
|
|
144842
145043
|
function fromYamlStep(raw) {
|
|
144843
145044
|
const entry = raw;
|
|
144844
145045
|
const kinds = STEP_DIRECTIVE_KEYS.filter((k) => k in entry);
|
|
@@ -144864,23 +145065,7 @@ function fromYamlStep(raw) {
|
|
|
144864
145065
|
if ("echo" in raw) return { kind: "echo", message: String(raw.echo) };
|
|
144865
145066
|
if ("launch" in raw) return { kind: "launch", app: parseLaunch(raw.launch) };
|
|
144866
145067
|
if ("run" in raw) return { kind: "run", flow: String(raw.run) };
|
|
144867
|
-
if ("tap" in raw)
|
|
144868
|
-
const body = raw.tap;
|
|
144869
|
-
const obj = body !== null && typeof body === "object" ? body : {};
|
|
144870
|
-
if (body !== null && typeof body === "object" && !Array.isArray(body)) {
|
|
144871
|
-
rejectUnknownKeys(raw, obj, [...SELECTOR_KEYS, "x", "y"], "tap");
|
|
144872
|
-
}
|
|
144873
|
-
if (obj.x !== void 0 || obj.y !== void 0) {
|
|
144874
|
-
if (obj.text !== void 0 || obj.id !== void 0 || obj.identifier !== void 0 || obj.role !== void 0) {
|
|
144875
|
-
badEntry(raw, "tap takes a selector or x/y coordinates, not both");
|
|
144876
|
-
}
|
|
144877
|
-
if (typeof obj.x !== "number" || typeof obj.y !== "number") {
|
|
144878
|
-
badEntry(raw, "a coordinate tap needs numeric x and y");
|
|
144879
|
-
}
|
|
144880
|
-
return { kind: "tap", x: obj.x, y: obj.y };
|
|
144881
|
-
}
|
|
144882
|
-
return { kind: "tap", selector: parseSelector(body, "tap") };
|
|
144883
|
-
}
|
|
145068
|
+
if ("tap" in raw) return parseTap(raw.tap, raw);
|
|
144884
145069
|
if ("long-press" in raw) {
|
|
144885
145070
|
return parseLongPress(raw["long-press"], raw);
|
|
144886
145071
|
}
|
|
@@ -145719,13 +145904,15 @@ If a step was recorded by mistake, edit the .yaml file directly to remove it.`,
|
|
|
145719
145904
|
const runTarget = params.command === RUN_TARGET_COMMAND && params.delayMs === void 0 ? await captureRunTarget(session, args) : void 0;
|
|
145720
145905
|
const strippedArgs = stripDeviceKeys(args);
|
|
145721
145906
|
const isLaunch = params.command === "restart-app" && params.delayMs === void 0 && typeof strippedArgs.bundleId === "string" && Object.keys(strippedArgs).length === 1;
|
|
145907
|
+
const cc = args.clickCount;
|
|
145908
|
+
const tapTimes = isTap && typeof cc === "number" && Number.isInteger(cc) && cc >= 2 && cc <= 10 ? { times: cc } : {};
|
|
145722
145909
|
let step;
|
|
145723
145910
|
let warning;
|
|
145724
145911
|
if (captured?.selector) {
|
|
145725
|
-
step = { kind: "tap", selector: captured.selector };
|
|
145912
|
+
step = { kind: "tap", selector: captured.selector, ...tapTimes };
|
|
145726
145913
|
warning = captured.warning;
|
|
145727
145914
|
} else if (isTap) {
|
|
145728
|
-
step = { kind: "tap", x: args.x, y: args.y };
|
|
145915
|
+
step = { kind: "tap", x: args.x, y: args.y, ...tapTimes };
|
|
145729
145916
|
warning = captured?.warning;
|
|
145730
145917
|
} else if (isLaunch) {
|
|
145731
145918
|
step = { kind: "launch", app: strippedArgs.bundleId };
|
|
@@ -145821,7 +146008,14 @@ You can still edit the .yaml file directly afterwards to remove or reorder steps
|
|
|
145821
146008
|
return `${n}. type: ${selectorLabel(step.into)} \u2190 "${step.text}"`;
|
|
145822
146009
|
case "await":
|
|
145823
146010
|
case "assert": {
|
|
145824
|
-
|
|
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
|
+
}
|
|
145825
146019
|
return `${n}. ${step.kind}: ${tail}`;
|
|
145826
146020
|
}
|
|
145827
146021
|
case "wait":
|
|
@@ -146101,7 +146295,10 @@ async function runTap(env, target) {
|
|
|
146101
146295
|
} else {
|
|
146102
146296
|
return { ok: false, reason: "tap needs a selector or x/y coordinates" };
|
|
146103
146297
|
}
|
|
146104
|
-
await invokeOnDevice(env, "gesture-tap",
|
|
146298
|
+
await invokeOnDevice(env, "gesture-tap", {
|
|
146299
|
+
...point,
|
|
146300
|
+
...target.times !== void 0 ? { clickCount: target.times } : {}
|
|
146301
|
+
});
|
|
146105
146302
|
return { ok: true };
|
|
146106
146303
|
}
|
|
146107
146304
|
var DEFAULT_LONG_PRESS_MS = 800;
|
|
@@ -146212,11 +146409,11 @@ function assertReason(condition, selector, expectedText, textMatch, matches2) {
|
|
|
146212
146409
|
case "text": {
|
|
146213
146410
|
const first = firstInReadingOrder(matches2.filter(isVisible)) ?? firstInReadingOrder(matches2);
|
|
146214
146411
|
if (!first) return `no element matched selector ${sel}`;
|
|
146215
|
-
const wanted = textMatch
|
|
146412
|
+
const wanted = describeTextExpectation(expectedText, textMatch, "infinitive");
|
|
146216
146413
|
const shown = assertText(first);
|
|
146217
146414
|
const own = nodeText(first);
|
|
146218
146415
|
const ownNote = own && own !== shown ? ` (own text "${own}")` : "";
|
|
146219
|
-
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})`;
|
|
146220
146417
|
}
|
|
146221
146418
|
default:
|
|
146222
146419
|
return `assertion failed for selector ${sel}`;
|
|
@@ -148712,6 +148909,7 @@ function pushReport(state3, report) {
|
|
|
148712
148909
|
function selectorLabel2(sel) {
|
|
148713
148910
|
const parts = [];
|
|
148714
148911
|
if (sel.text !== void 0) parts.push(`"${sel.text}"`);
|
|
148912
|
+
if (sel.textMatches !== void 0) parts.push(`/${sel.textMatches}/`);
|
|
148715
148913
|
if (sel.identifier) parts.push(`id=${sel.identifier}`);
|
|
148716
148914
|
if (sel.role) parts.push(`role=${sel.role}`);
|
|
148717
148915
|
return parts.join(" ");
|
|
@@ -148730,7 +148928,7 @@ function stepTarget(step) {
|
|
|
148730
148928
|
case "assert": {
|
|
148731
148929
|
const sel = selectorLabel2(step.selector);
|
|
148732
148930
|
if (step.condition === "text") {
|
|
148733
|
-
return `${sel} ${step.
|
|
148931
|
+
return `${sel} ${describeTextExpectation(step.expectedText, step.textMatch)}`;
|
|
148734
148932
|
}
|
|
148735
148933
|
return `${step.condition} ${sel}`;
|
|
148736
148934
|
}
|
|
@@ -149631,7 +149829,7 @@ function normLabel(s) {
|
|
|
149631
149829
|
return (s || "").toLowerCase().replace(/-/g, "").replace(/[\s,]+/g, " ").trim();
|
|
149632
149830
|
}
|
|
149633
149831
|
var MAX_FRAME_AREA = 0.85;
|
|
149634
|
-
function
|
|
149832
|
+
function matchNode(n, match, needle) {
|
|
149635
149833
|
const label = normLabel(n.label);
|
|
149636
149834
|
const ident = normLabel(n.identifier);
|
|
149637
149835
|
const value = normLabel(n.value);
|
|
@@ -149658,7 +149856,7 @@ function findElementMatch(tree, match) {
|
|
|
149658
149856
|
const candidates = [];
|
|
149659
149857
|
const walk = (n) => {
|
|
149660
149858
|
if (!n || typeof n !== "object") return;
|
|
149661
|
-
const m =
|
|
149859
|
+
const m = matchNode(n, match, needle);
|
|
149662
149860
|
if (m && n.frame) {
|
|
149663
149861
|
const f = n.frame;
|
|
149664
149862
|
const cx = f.x + f.width / 2;
|
package/package.json
CHANGED
|
@@ -23,7 +23,7 @@ Beyond raw `tool:` steps and `echo:`, flows support declarative directives inter
|
|
|
23
23
|
| Directive | YAML | Meaning |
|
|
24
24
|
| ------------ | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
25
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
|
|
26
|
+
| `tap` | `- tap: Login`, `- tap: { x: 0.5, y: 0.57 }`, `- tap: { on: Login, times: 2 }` | tap by selector (auto-waits) or raw point; `times: 2` = double-tap |
|
|
27
27
|
| `long-press` | `- long-press: Row 3` or `- long-press: { on: <sel>, duration: 1200 }` | press and hold an element (default 800ms; Chromium: mouse press-hold) |
|
|
28
28
|
| `type` | `- type: { into: email, text: "a@b.com" }` | focus a field, type, then press Enter to submit + dismiss the keyboard |
|
|
29
29
|
| `scroll-to` | `- scroll-to: "Order #1234"` (scrolls down) or `- scroll-to: { target: …, direction: right, within: … }` | momentum-free scroll until the target is visible |
|
|
@@ -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`.
|