@swmansion/argent 0.16.2-next.3 → 0.16.2-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/tool-server.cjs
CHANGED
|
@@ -126717,6 +126717,12 @@ var zodSchema21 = external_exports.object({
|
|
|
126717
126717
|
endDistance: external_exports.number().describe(
|
|
126718
126718
|
"Final distance between the two fingers: normalized 0.0\u20131.0 (fraction of screen, not pixels). E.g. 0.6 = fingers 60% of screen apart. Use a larger endDistance than startDistance to pinch out (zoom in)."
|
|
126719
126719
|
),
|
|
126720
|
+
endCenterX: external_exports.number().optional().describe(
|
|
126721
|
+
"Final horizontal center of the pinch: normalized 0.0\u20131.0. When set, the centroid drifts linearly from centerX to endCenterX over the gesture (e.g. to keep expanding fingers on-screen near an edge). Omit for a fixed center."
|
|
126722
|
+
),
|
|
126723
|
+
endCenterY: external_exports.number().optional().describe(
|
|
126724
|
+
"Final vertical center of the pinch: normalized 0.0\u20131.0. When set, the centroid drifts linearly from centerY to endCenterY over the gesture. Omit for a fixed center."
|
|
126725
|
+
),
|
|
126720
126726
|
angle: external_exports.number().optional().describe("Axis angle in degrees along which the fingers are placed (default 0 = horizontal)."),
|
|
126721
126727
|
durationMs: external_exports.number().optional().describe("Total gesture duration in milliseconds (default 300)")
|
|
126722
126728
|
});
|
|
@@ -126730,7 +126736,7 @@ var gesturePinchTool = {
|
|
|
126730
126736
|
description: `Execute a pinch-to-zoom gesture by moving two fingers toward or away from a center point to change the scale of on-screen content. All positions and distances are normalized 0.0\u20131.0 (fractions of screen width/height, not pixels)\u2014same coordinate space as gesture-tap and gesture-swipe.
|
|
126731
126737
|
startDistance > endDistance = pinch in (zoom out). startDistance < endDistance = pinch out (zoom in).
|
|
126732
126738
|
Typical values: startDistance 0.2, endDistance 0.6 for a zoom-in pinch at screen center.
|
|
126733
|
-
Auto-generates interpolated frames at ~60fps. The angle parameter controls the axis (0 = horizontal, 90 = vertical).
|
|
126739
|
+
Auto-generates interpolated frames at ~60fps. The angle parameter controls the axis (0 = horizontal, 90 = vertical). Optional endCenterX/endCenterY drift the centroid linearly over the gesture (omitted = fixed center).
|
|
126734
126740
|
Use when you need to zoom in or out on a map, image, or zoomable view. Returns { pinched: true, timestampMs }. Fails if the simulator-server / emulator backend is not reachable for the given device.`,
|
|
126735
126741
|
zodSchema: zodSchema21,
|
|
126736
126742
|
capability: capability13,
|
|
@@ -126745,15 +126751,19 @@ Use when you need to zoom in or out on a map, image, or zoomable view. Returns {
|
|
|
126745
126751
|
const angleRad = angleDeg * Math.PI / 180;
|
|
126746
126752
|
const cosA = Math.cos(angleRad);
|
|
126747
126753
|
const sinA = Math.sin(angleRad);
|
|
126754
|
+
const endCenterX = params.endCenterX ?? params.centerX;
|
|
126755
|
+
const endCenterY = params.endCenterY ?? params.centerY;
|
|
126748
126756
|
let timestampMs = 0;
|
|
126749
126757
|
for (let i = 0; i <= steps; i++) {
|
|
126750
126758
|
const t = i / steps;
|
|
126751
126759
|
const dist = params.startDistance + (params.endDistance - params.startDistance) * t;
|
|
126752
126760
|
const halfDist = dist / 2;
|
|
126753
|
-
const
|
|
126754
|
-
const
|
|
126755
|
-
const
|
|
126756
|
-
const
|
|
126761
|
+
const cx = params.centerX + (endCenterX - params.centerX) * t;
|
|
126762
|
+
const cy = params.centerY + (endCenterY - params.centerY) * t;
|
|
126763
|
+
const x1 = cx - halfDist * cosA;
|
|
126764
|
+
const y1 = cy - halfDist * sinA;
|
|
126765
|
+
const x2 = cx + halfDist * cosA;
|
|
126766
|
+
const y2 = cy + halfDist * sinA;
|
|
126757
126767
|
const type = i === 0 ? "Down" : i === steps ? "Up" : "Move";
|
|
126758
126768
|
if (i === 0) timestampMs = Date.now();
|
|
126759
126769
|
sendTouchEvent(api, type, x1, y1, x2, y2);
|
|
@@ -133889,7 +133899,7 @@ Allowed tools and their args (udid is auto-injected, do NOT include it in args):
|
|
|
133889
133899
|
gesture-scroll: { x: number, y: number, deltaX?: number, deltaY?: number, durationMs?: number } [chromium only]
|
|
133890
133900
|
gesture-drag: { fromX: number, fromY: number, toX: number, toY: number, durationMs?: number } [chromium only]
|
|
133891
133901
|
gesture-custom: { events: [{ type: "Down"|"Move"|"Up", x: number, y: number, x2?: number, y2?: number, delayMs?: number }], interpolate?: number } [ios/android]
|
|
133892
|
-
gesture-pinch: { centerX: number, centerY: number, startDistance: number, endDistance: number, angle?: number, durationMs?: number }
|
|
133902
|
+
gesture-pinch: { centerX: number, centerY: number, startDistance: number, endDistance: number, endCenterX?: number, endCenterY?: number, angle?: number, durationMs?: number } [ios only]
|
|
133893
133903
|
gesture-rotate: { centerX: number, centerY: number, radius: number, startAngle: number, endAngle: number, durationMs?: number } [ios only]
|
|
133894
133904
|
button: { button: "home"|"back"|"power"|"volumeUp"|"volumeDown"|"appSwitch"|"actionButton" } [ios/android]
|
|
133895
133905
|
keyboard: { text?: string, key?: string, delayMs?: number } (key pressed after text; TV: text only) [ios/android/chromium/vega/tv]
|
|
@@ -145031,6 +145041,10 @@ function toYamlStep(step) {
|
|
|
145031
145041
|
}
|
|
145032
145042
|
};
|
|
145033
145043
|
}
|
|
145044
|
+
case "pinch":
|
|
145045
|
+
return {
|
|
145046
|
+
pinch: step.selector ? { on: selectorToYaml(step.selector), scale: step.scale } : { scale: step.scale }
|
|
145047
|
+
};
|
|
145034
145048
|
case "snapshot":
|
|
145035
145049
|
return step.maxMismatch === void 0 ? { snapshot: step.name } : { snapshot: { name: step.name, maxMismatch: step.maxMismatch } };
|
|
145036
145050
|
case "tool":
|
|
@@ -145303,6 +145317,7 @@ var STEP_DIRECTIVE_KEYS = [
|
|
|
145303
145317
|
"assert",
|
|
145304
145318
|
"wait",
|
|
145305
145319
|
"scroll-to",
|
|
145320
|
+
"pinch",
|
|
145306
145321
|
"snapshot"
|
|
145307
145322
|
];
|
|
145308
145323
|
function parseTapTimes(raw, entry) {
|
|
@@ -145399,6 +145414,28 @@ function parseLongPress(body, entry) {
|
|
|
145399
145414
|
}
|
|
145400
145415
|
return { kind: "long-press", ...parseTarget(body, "long-press") };
|
|
145401
145416
|
}
|
|
145417
|
+
function parsePinch(body, entry) {
|
|
145418
|
+
if (body === null || typeof body !== "object") {
|
|
145419
|
+
badEntry(
|
|
145420
|
+
entry,
|
|
145421
|
+
'pinch takes an options map \u2014 e.g. pinch: { on: "Map", scale: 3 } (a bare "pinch: Map" is ambiguous: in or out?)'
|
|
145422
|
+
);
|
|
145423
|
+
}
|
|
145424
|
+
const obj = body;
|
|
145425
|
+
if (obj.text !== void 0 || obj.id !== void 0 || obj.identifier !== void 0 || obj.role !== void 0) {
|
|
145426
|
+
badEntry(entry, 'pinch takes a nested selector \u2014 e.g. pinch: { on: "Map", scale: 3 }');
|
|
145427
|
+
}
|
|
145428
|
+
rejectUnknownKeys(entry, obj, ["on", "scale"], "pinch");
|
|
145429
|
+
if (typeof obj.scale !== "number" || !Number.isFinite(obj.scale) || obj.scale <= 0 || obj.scale === 1) {
|
|
145430
|
+
badEntry(
|
|
145431
|
+
entry,
|
|
145432
|
+
"pinch.scale must be a finite number > 0 and \u2260 1 (2 = zoom in 2\xD7, 0.5 = zoom out to half)"
|
|
145433
|
+
);
|
|
145434
|
+
}
|
|
145435
|
+
const step = { kind: "pinch", scale: obj.scale };
|
|
145436
|
+
if (obj.on !== void 0) step.selector = parseSelector(obj.on, "pinch.on");
|
|
145437
|
+
return step;
|
|
145438
|
+
}
|
|
145402
145439
|
function parseWhenCondition(raw) {
|
|
145403
145440
|
const conditionKeys = `${WAIT_CONDITIONS.join(", ")}, platform`;
|
|
145404
145441
|
if (raw === null || typeof raw !== "object") {
|
|
@@ -145566,6 +145603,7 @@ function fromYamlStep(raw, whenDepth = 0) {
|
|
|
145566
145603
|
if (b.within !== void 0) step.within = parseSelector(b.within, "scroll-to.within");
|
|
145567
145604
|
return step;
|
|
145568
145605
|
}
|
|
145606
|
+
if ("pinch" in raw) return parsePinch(raw.pinch, raw);
|
|
145569
145607
|
if ("snapshot" in raw) {
|
|
145570
145608
|
const body = raw.snapshot;
|
|
145571
145609
|
if (body !== null && typeof body === "object" && !Array.isArray(body)) {
|
|
@@ -146462,6 +146500,8 @@ You can still edit the .yaml file directly afterwards to remove or reorder steps
|
|
|
146462
146500
|
}
|
|
146463
146501
|
case "scroll-to":
|
|
146464
146502
|
return `${n}. scroll-to: ${selectorLabel(step.target)} (${step.direction})`;
|
|
146503
|
+
case "pinch":
|
|
146504
|
+
return `${n}. pinch: scale ${step.scale}${step.selector ? ` on ${selectorLabel(step.selector)}` : ""}`;
|
|
146465
146505
|
case "snapshot":
|
|
146466
146506
|
return `${n}. snapshot: ${step.name}`;
|
|
146467
146507
|
case "tool":
|
|
@@ -146488,6 +146528,73 @@ var fs41 = __toESM(require("node:fs/promises"));
|
|
|
146488
146528
|
var path31 = __toESM(require("node:path"));
|
|
146489
146529
|
init_src();
|
|
146490
146530
|
|
|
146531
|
+
// ../tool-server/src/tools/flows/flow-pinch-geometry.ts
|
|
146532
|
+
var PINCH_RATIO_PER_GESTURE = 4;
|
|
146533
|
+
var PINCH_SETTLE_MS = 250;
|
|
146534
|
+
var TARGET_START_FRACTION = 0.82;
|
|
146535
|
+
var SCREEN_EDGE_INSET = 0.02;
|
|
146536
|
+
var MIN_VIABLE_TRAVEL = 0.03;
|
|
146537
|
+
function systemEdgeGuards(device) {
|
|
146538
|
+
return device.platform === "android" ? { left: 0.13, right: 0.13, top: 0.08, bottom: 0.08 } : { left: 0.08, right: 0.08, top: 0.08, bottom: 0.08 };
|
|
146539
|
+
}
|
|
146540
|
+
function decomposePinch(scale) {
|
|
146541
|
+
const n = Math.max(
|
|
146542
|
+
1,
|
|
146543
|
+
Math.ceil(Math.abs(Math.log(scale)) / Math.log(PINCH_RATIO_PER_GESTURE) - 1e-9)
|
|
146544
|
+
);
|
|
146545
|
+
return { n, per: scale ** (1 / n) };
|
|
146546
|
+
}
|
|
146547
|
+
function clamp2(v, lo, hi) {
|
|
146548
|
+
return v < lo ? lo : v > hi ? hi : v;
|
|
146549
|
+
}
|
|
146550
|
+
function buildAxisCandidate(input) {
|
|
146551
|
+
const { angle, center, targetSpan, per, guards } = input;
|
|
146552
|
+
const c = angle === 0 ? center.x : center.y;
|
|
146553
|
+
const gLow = angle === 0 ? guards.left : guards.top;
|
|
146554
|
+
const gHigh = angle === 0 ? guards.right : guards.bottom;
|
|
146555
|
+
const p = angle === 0 ? center.y : center.x;
|
|
146556
|
+
const pLow = angle === 0 ? guards.top : guards.left;
|
|
146557
|
+
const pHigh = angle === 0 ? guards.bottom : guards.right;
|
|
146558
|
+
const screenEndSpan = 1 - 2 * SCREEN_EDGE_INSET;
|
|
146559
|
+
const edgeSafeStartSpan = 2 * Math.max(0, Math.min(c - gLow, 1 - gHigh - c));
|
|
146560
|
+
const insetStartSpan = 2 * Math.max(0, Math.min(c - SCREEN_EDGE_INSET, 1 - SCREEN_EDGE_INSET - c));
|
|
146561
|
+
const screenStartLimit = edgeSafeStartSpan > 0 ? Math.min(screenEndSpan, edgeSafeStartSpan) : Math.min(screenEndSpan, insetStartSpan);
|
|
146562
|
+
const startLimit = targetSpan !== void 0 ? Math.min(screenStartLimit, targetSpan * TARGET_START_FRACTION) : screenStartLimit;
|
|
146563
|
+
const start2 = per > 1 ? Math.min(startLimit, screenEndSpan / per) : startLimit;
|
|
146564
|
+
const end = Math.min(screenEndSpan, start2 * per);
|
|
146565
|
+
const travel = Math.abs(end - start2);
|
|
146566
|
+
if (!(travel > 0)) return void 0;
|
|
146567
|
+
const endCenter = clamp2(c, SCREEN_EDGE_INSET + end / 2, 1 - SCREEN_EDGE_INSET - end / 2);
|
|
146568
|
+
const downLow = c - start2 / 2;
|
|
146569
|
+
const downHigh = c + start2 / 2;
|
|
146570
|
+
const axisSafe = downLow >= gLow && downHigh <= 1 - gHigh;
|
|
146571
|
+
const perpSafe = p >= pLow && p <= 1 - pHigh;
|
|
146572
|
+
const clearance = Math.min(downLow - gLow, 1 - gHigh - downHigh, p - pLow, 1 - pHigh - p);
|
|
146573
|
+
return {
|
|
146574
|
+
angle,
|
|
146575
|
+
start: start2,
|
|
146576
|
+
end,
|
|
146577
|
+
endCenter,
|
|
146578
|
+
travel,
|
|
146579
|
+
viable: travel >= MIN_VIABLE_TRAVEL,
|
|
146580
|
+
axisSafe,
|
|
146581
|
+
fullyEdgeSafe: axisSafe && perpSafe,
|
|
146582
|
+
clearance
|
|
146583
|
+
};
|
|
146584
|
+
}
|
|
146585
|
+
function selectPinchCandidate(candidates) {
|
|
146586
|
+
if (candidates.length === 0) return void 0;
|
|
146587
|
+
const rank2 = (c) => c.fullyEdgeSafe ? 0 : c.axisSafe ? 1 : 2;
|
|
146588
|
+
return [...candidates].sort((a, b) => {
|
|
146589
|
+
const byViable = Number(b.viable) - Number(a.viable);
|
|
146590
|
+
if (byViable !== 0) return byViable;
|
|
146591
|
+
const byRank = rank2(a) - rank2(b);
|
|
146592
|
+
if (byRank !== 0) return byRank;
|
|
146593
|
+
if (a.fullyEdgeSafe && b.fullyEdgeSafe) return b.travel - a.travel;
|
|
146594
|
+
return b.clearance - a.clearance || b.travel - a.travel;
|
|
146595
|
+
})[0];
|
|
146596
|
+
}
|
|
146597
|
+
|
|
146491
146598
|
// ../tool-server/src/tools/flows/flow-actions.ts
|
|
146492
146599
|
var ABORTED_OUTCOME = {
|
|
146493
146600
|
ok: false,
|
|
@@ -146701,12 +146808,18 @@ function offscreenHint(sel) {
|
|
|
146701
146808
|
return `no visible element matched selector ${describeSelector(sel)} \u2014 if it is off-screen, add a scroll-to step before this one`;
|
|
146702
146809
|
}
|
|
146703
146810
|
async function runDirective(env, step) {
|
|
146704
|
-
if (env.device.platform === "vega" && (step.kind === "tap" || step.kind === "long-press" || step.kind === "type" || step.kind === "scroll-to")) {
|
|
146811
|
+
if (env.device.platform === "vega" && (step.kind === "tap" || step.kind === "long-press" || step.kind === "type" || step.kind === "scroll-to" || step.kind === "pinch")) {
|
|
146705
146812
|
return {
|
|
146706
146813
|
ok: false,
|
|
146707
146814
|
reason: `${step.kind} is a touch directive and Vega is remote-driven \u2014 move focus with \`tool: tv-remote\` steps (and type via \`tool: keyboard\`) instead`
|
|
146708
146815
|
};
|
|
146709
146816
|
}
|
|
146817
|
+
if (step.kind === "pinch" && env.device.platform === "chromium") {
|
|
146818
|
+
return {
|
|
146819
|
+
ok: false,
|
|
146820
|
+
reason: "pinch is unsupported on chromium \u2014 desktop apps have no uniform pinch-zoom mapping (they zoom via ctrl+wheel or their own controls); drive the app's zoom UI with tap/keyboard instead"
|
|
146821
|
+
};
|
|
146822
|
+
}
|
|
146710
146823
|
switch (step.kind) {
|
|
146711
146824
|
case "tap":
|
|
146712
146825
|
return runTap(env, step);
|
|
@@ -146723,6 +146836,8 @@ async function runDirective(env, step) {
|
|
|
146723
146836
|
if (r.aborted) return ABORTED_OUTCOME;
|
|
146724
146837
|
return { ok: Boolean(r.frame), reason: r.reason };
|
|
146725
146838
|
}
|
|
146839
|
+
case "pinch":
|
|
146840
|
+
return runPinch(env, step);
|
|
146726
146841
|
}
|
|
146727
146842
|
}
|
|
146728
146843
|
async function resolveTargetPoint(env, target) {
|
|
@@ -146771,6 +146886,47 @@ async function runLongPress(env, step) {
|
|
|
146771
146886
|
}
|
|
146772
146887
|
return { ok: true };
|
|
146773
146888
|
}
|
|
146889
|
+
async function runPinch(env, step) {
|
|
146890
|
+
let center = { x: 0.5, y: 0.5 };
|
|
146891
|
+
let frame;
|
|
146892
|
+
if (step.selector) {
|
|
146893
|
+
const resolved = await waitForFrame(env, step.selector);
|
|
146894
|
+
if (resolved === "aborted") return ABORTED_OUTCOME;
|
|
146895
|
+
if (!resolved) return { ok: false, reason: offscreenHint(step.selector) };
|
|
146896
|
+
frame = resolved;
|
|
146897
|
+
center = getDescribeTapPoint(resolved);
|
|
146898
|
+
}
|
|
146899
|
+
const { n, per } = decomposePinch(step.scale);
|
|
146900
|
+
const guards = systemEdgeGuards(env.device);
|
|
146901
|
+
const candidates = [
|
|
146902
|
+
buildAxisCandidate({ angle: 0, center, targetSpan: frame?.width, per, guards }),
|
|
146903
|
+
buildAxisCandidate({ angle: 90, center, targetSpan: frame?.height, per, guards })
|
|
146904
|
+
].filter((c) => c !== void 0);
|
|
146905
|
+
const selected = selectPinchCandidate(candidates);
|
|
146906
|
+
if (!selected) {
|
|
146907
|
+
return {
|
|
146908
|
+
ok: false,
|
|
146909
|
+
reason: `pinch found no on-screen finger travel around (${center.x}, ${center.y})`
|
|
146910
|
+
};
|
|
146911
|
+
}
|
|
146912
|
+
const args = {
|
|
146913
|
+
centerX: center.x,
|
|
146914
|
+
centerY: center.y,
|
|
146915
|
+
startDistance: selected.start,
|
|
146916
|
+
endDistance: selected.end,
|
|
146917
|
+
angle: selected.angle
|
|
146918
|
+
};
|
|
146919
|
+
const startCenter = selected.angle === 0 ? center.x : center.y;
|
|
146920
|
+
if (selected.endCenter !== startCenter) {
|
|
146921
|
+
args[selected.angle === 0 ? "endCenterX" : "endCenterY"] = selected.endCenter;
|
|
146922
|
+
}
|
|
146923
|
+
for (let i = 0; i < n; i++) {
|
|
146924
|
+
if (env.signal?.aborted) return ABORTED_OUTCOME;
|
|
146925
|
+
await invokeOnDevice(env, "gesture-pinch", args);
|
|
146926
|
+
if (i < n - 1 && !await sleepOrAbort(PINCH_SETTLE_MS, env.signal)) return ABORTED_OUTCOME;
|
|
146927
|
+
}
|
|
146928
|
+
return { ok: true };
|
|
146929
|
+
}
|
|
146774
146930
|
async function runType(env, step) {
|
|
146775
146931
|
const frame = await waitForFrame(env, step.into);
|
|
146776
146932
|
if (frame === "aborted") return ABORTED_OUTCOME;
|
|
@@ -148394,7 +148550,7 @@ function formatSignedNumber(value) {
|
|
|
148394
148550
|
return rounded > 0 ? `+${rounded}` : `${rounded}`;
|
|
148395
148551
|
}
|
|
148396
148552
|
function formatNormalizedPosition(value, divisor) {
|
|
148397
|
-
return formatNormalizedNumber(
|
|
148553
|
+
return formatNormalizedNumber(clamp3(value / divisor, 0, 1));
|
|
148398
148554
|
}
|
|
148399
148555
|
function formatNormalizedNumber(value) {
|
|
148400
148556
|
if (!Number.isFinite(value)) return "0";
|
|
@@ -148410,7 +148566,7 @@ function formatSignedNormalizedNumber(value) {
|
|
|
148410
148566
|
const formatted = formatNormalizedNumber(value);
|
|
148411
148567
|
return value > 0 && formatted !== "0" ? `+${formatted}` : formatted;
|
|
148412
148568
|
}
|
|
148413
|
-
function
|
|
148569
|
+
function clamp3(value, min, max) {
|
|
148414
148570
|
return Math.min(max, Math.max(min, value));
|
|
148415
148571
|
}
|
|
148416
148572
|
function formatPercentage(value) {
|
|
@@ -149230,8 +149386,9 @@ Steps run in order: \`launch\` starts an app from scratch (terminate + relaunch)
|
|
|
149230
149386
|
ready; \`tool\` calls dispatch through the registry; \`tap\`/\`long-press\`/\`type\` resolve a selector to an
|
|
149231
149387
|
element and act on it (\`tap: { on, times: 2 }\` double-taps; \`long-press: { on, duration }\` presses and
|
|
149232
149388
|
holds; \`tap\`/\`long-press\` alternatively take a raw normalized point \u2014 bare \`{ x, y }\` or \`on: { x, y }\`);
|
|
149233
|
-
\`scroll-to\` scrolls (momentum-free) until a target is visible; \`
|
|
149234
|
-
|
|
149389
|
+
\`scroll-to\` scrolls (momentum-free) until a target is visible; \`pinch\` zooms
|
|
149390
|
+
(\`pinch: { on?, scale }\` \u2014 scale > 1 in, < 1 out; screen center when \`on\` is omitted); \`await\` waits
|
|
149391
|
+
for a UI condition; \`wait\` pauses for a fixed number of milliseconds; \`assert\` checks one now; \`snapshot\`
|
|
149235
149392
|
diffs a screenshot against a stored baseline (a missing baseline fails the step \u2014 set updateBaselines
|
|
149236
149393
|
to adopt the current screen); \`echo\` annotates; \`run\` executes a referenced fragment inline.
|
|
149237
149394
|
A \`when:\` block (condition + \`steps:\`, no else) runs its steps only if the condition holds \u2014
|
|
@@ -149415,6 +149572,10 @@ function stepTarget(step) {
|
|
|
149415
149572
|
const dir = step.direction !== "down" ? ` (${step.direction})` : "";
|
|
149416
149573
|
return `${selectorLabel2(step.target)}${dir}`;
|
|
149417
149574
|
}
|
|
149575
|
+
case "pinch": {
|
|
149576
|
+
const scale = `scale ${step.scale}`;
|
|
149577
|
+
return step.selector ? `${selectorLabel2(step.selector)} (${scale})` : scale;
|
|
149578
|
+
}
|
|
149418
149579
|
case "snapshot":
|
|
149419
149580
|
return `"${step.name}"`;
|
|
149420
149581
|
default:
|
|
@@ -149596,7 +149757,8 @@ async function execLeafStep(state3, step, index, scope) {
|
|
|
149596
149757
|
case "type":
|
|
149597
149758
|
case "await":
|
|
149598
149759
|
case "assert":
|
|
149599
|
-
case "scroll-to":
|
|
149760
|
+
case "scroll-to":
|
|
149761
|
+
case "pinch": {
|
|
149600
149762
|
try {
|
|
149601
149763
|
const r = await runDirective(state3, step);
|
|
149602
149764
|
if (r.aborted) return { ...base, status: "skip", reason: r.reason };
|
package/package.json
CHANGED
|
@@ -27,6 +27,7 @@ Beyond raw `tool:` steps and `echo:`, flows support declarative directives inter
|
|
|
27
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
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 |
|
|
30
|
+
| `pinch` | `- pinch: { on: "Map", scale: 3 }` or `- pinch: { scale: 0.5 }` | two-finger zoom in (`scale` > 1) or out (`< 1`); big scales chain gestures; `on` optional — defaults to screen center; open-loop — assert the visible result |
|
|
30
31
|
| `await` | `- await: { visible: Home }` | wait for a UI condition |
|
|
31
32
|
| `wait` | `- wait: 500` | pause for a fixed number of milliseconds (last resort — prefer `await`) |
|
|
32
33
|
| `assert` | `- assert: { visible: Welcome }` | check a condition, hard-fail if it never holds |
|
|
@@ -67,7 +68,7 @@ Never record a real credential into a flow — the YAML is committed to the repo
|
|
|
67
68
|
|
|
68
69
|
### TV targets (Vega)
|
|
69
70
|
|
|
70
|
-
A Vega (Fire TV) device is remote-driven — there is no touch input, so the touch directives (`tap`, `long-press`, `type`, `scroll-to`) fail on it with guidance. Drive focus with `tool: tv-remote` steps and type with `tool: keyboard` instead; everything else (`launch`, `await`, `assert`, `wait`, `snapshot`, `echo`, `run`, selectors) works unchanged — the tree comes from the on-device automation toolkit, which attaches at app launch (the `launch` step waits for it, so a leading `launch` also guarantees selectors resolve).
|
|
71
|
+
A Vega (Fire TV) device is remote-driven — there is no touch input, so the touch directives (`tap`, `long-press`, `type`, `scroll-to`, `pinch`) fail on it with guidance. Drive focus with `tool: tv-remote` steps and type with `tool: keyboard` instead; everything else (`launch`, `await`, `assert`, `wait`, `snapshot`, `echo`, `run`, selectors) works unchanged — the tree comes from the on-device automation toolkit, which attaches at app launch (the `launch` step waits for it, so a leading `launch` also guarantees selectors resolve).
|
|
71
72
|
|
|
72
73
|
```yaml
|
|
73
74
|
steps:
|
|
@@ -127,9 +128,10 @@ Record an `await-ui-element` step to **gate** the next step on a screen transiti
|
|
|
127
128
|
5. **Polish**: **read the saved `.yaml` file** and convert the raw `tool:` steps that have a cleaner directive form (the recorder leaves these as tools):
|
|
128
129
|
- `tool: keyboard` typing into a field → `type: { into: "<field>", text: "…" }`, folding in the `tap` that focused the field.
|
|
129
130
|
- `tool: await-ui-element` gating a transition → `await: { visible: "…" }` / `{ hidden: … }` / `{ text: { in: …, equals: … } }`, carrying a custom `timeoutMs` over as a `timeout` sibling key. Converting also upgrades the wait from the trimmed `describe` tree to the flow's full-hierarchy tree (see Selectors). Keep the raw `tool: await-ui-element` step only when it sets a custom `pollIntervalMs`/`bundleId` the directive can't express.
|
|
130
|
-
- A scroll-to-reach-an-element — a `tool: gesture-swipe` used to bring a specific element on screen before interacting with it (a `tap`, `type`, `assert`, …) → `scroll-to: { target: "<that element>", direction: … }`, dropping the swipe. This is far more robust than a fixed-distance swipe: it scrolls momentum-free and stops exactly when the target appears, so it survives layout and content changes. (`tap`/`type` do not scroll, so a raw swipe whose fling lands differently on another device leaves the following tap unresolved — always prefer the `scroll-to` rewrite.) Keep a `gesture-swipe` as a raw `tool:` step when it isn't scrolling toward a specific element — especially a velocity-dependent gesture like swipe-to-dismiss, edge-swipe-back, or swipe-to-reveal a row action, which a momentum-free `scroll-to` would not reproduce.
|
|
131
|
+
- A scroll-to-reach-an-element — a `tool: gesture-swipe` (or its chromium analog, `gesture-scroll`) used to bring a specific element on screen before interacting with it (a `tap`, `type`, `assert`, …) → `scroll-to: { target: "<that element>", direction: … }`, dropping the swipe. This is far more robust than a fixed-distance swipe: it scrolls momentum-free and stops exactly when the target appears, so it survives layout and content changes. (`tap`/`type` do not scroll, so a raw swipe whose fling lands differently on another device leaves the following tap unresolved — always prefer the `scroll-to` rewrite.) Keep a `gesture-swipe` as a raw `tool:` step when it isn't scrolling toward a specific element — especially a velocity-dependent gesture like swipe-to-dismiss, edge-swipe-back, or swipe-to-reveal a row action, which a momentum-free `scroll-to` would not reproduce.
|
|
132
|
+
- `tool: gesture-pinch` → `pinch: { on: "<target>", scale: … }`, deriving `scale` as `endDistance / startDistance`. Set `on:` to the element under the pinch center when the pinch was aimed at one (the map or image being zoomed); omit it for a screen-center pinch. Don't carry the recorded distances/angle over — the directive re-derives the geometry (finger placement, system-edge avoidance, chaining of large scales) at run time, so the conversion swaps device-specific coordinates for a portable selector with auto-wait. Keep the raw `tool: gesture-pinch` step when the pinch is anchored at a specific point _inside_ a large element (zooming toward a particular map location, not the map's center) or deliberately pans via `endCenterX`/`endCenterY` — `on:` takes only a selector and re-centers the pinch on the element's frame center, so converting would silently move the zoom anchor.
|
|
131
133
|
|
|
132
|
-
Every other recorded tool (`gesture-swipe`, `gesture-scroll
|
|
134
|
+
Every other recorded tool (a velocity-dependent `gesture-swipe`, a fixed-distance `gesture-scroll` not aimed at an element, `button`, `screenshot`, …) has no directive form — leave it as a `tool:` step. The recorder already handles the rest: coordinate `gesture-tap`s are captured as portable `tap:` selector steps, a `restart-app` is captured as a `launch:` step, a `flow-execute` of a sibling fragment is captured as a `run: <name>` composition directive, and device ids are stripped. Captured selectors are emitted in the strict map form (`tap: { text: General }`), never as a loose bare string — the recorder verified the exact element the tap hit, and a bare string would re-parse as loose and route through the identifier-first fallback it was never checked against. After editing, re-run with `flow-execute` to confirm the cleaned flow still passes.
|
|
133
135
|
|
|
134
136
|
### Example session
|
|
135
137
|
|
|
@@ -134,7 +134,7 @@ Swipe **up** (`fromY > toY`) = scroll content **down**. Default duration: 300ms.
|
|
|
134
134
|
{ "udid": "<UDID>", "centerX": 0.5, "centerY": 0.5, "startDistance": 0.2, "endDistance": 0.6 }
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
-
All values are normalized 0.0–1.0 (fractions of screen, not pixels) — same as all other gesture tools. `startDistance: 0.2` means fingers start 20% of the screen apart; `endDistance: 0.6` means they end 60% apart. `startDistance < endDistance` = pinch out (zoom in). `startDistance > endDistance` = pinch in (zoom out). Defaults: `angle: 0` (horizontal), `durationMs: 300`. Optional: `"angle": 90` for vertical axis, `"durationMs": 500` for slower pinch.
|
|
137
|
+
All values are normalized 0.0–1.0 (fractions of screen, not pixels) — same as all other gesture tools. `startDistance: 0.2` means fingers start 20% of the screen apart; `endDistance: 0.6` means they end 60% apart. `startDistance < endDistance` = pinch out (zoom in). `startDistance > endDistance` = pinch in (zoom out). Defaults: `angle: 0` (horizontal), `durationMs: 300`. Optional: `"angle": 90` for vertical axis, `"durationMs": 500` for slower pinch, `"endCenterX"`/`"endCenterY"` to let the centroid drift to a new center over the gesture (omitted = fixed center).
|
|
138
138
|
|
|
139
139
|
### gesture-rotate — Two-finger rotation
|
|
140
140
|
|