Skip to content

Commit 5f75b64

Browse files
committed
fix: clarify Android gesture transform behavior
1 parent 93b04b2 commit 5f75b64

6 files changed

Lines changed: 44 additions & 6 deletions

File tree

examples/test-app/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ These run the `.ad` replay suite in `examples/test-app/replays`.
9191
`gesture-lab.ad` verifies `gesture pan`, `gesture fling`, `gesture pinch`, and
9292
`gesture rotate` against the gesture metrics rendered by the Home screen on iOS
9393
and Android. Android and iOS simulator sessions also support `gesture transform`
94-
for a combined pan/zoom/rotate gesture.
94+
for a combined pan/zoom/rotate gesture. On Android, treat combined transform
95+
assertions as qualitative because recognizers can report non-exact centroid,
96+
scale, and rotation values for one simultaneous two-finger gesture.
9597

9698
To target a specific iOS simulator or an installed Expo development build, run the
9799
underlying command directly so global flags stay before replay inputs:

examples/test-app/src/screens/GestureLab.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,23 @@ export function GestureLab() {
6060
const pinchRef = useRef(null);
6161
const rotationRef = useRef(null);
6262
const flingRefs = [flingLeftRef, flingRightRef, flingUpRef, flingDownRef];
63+
const activeTransformHandlerTagsRef = useRef(new Set<number>());
6364

6465
function updateTransform(nextTransform: TransformState) {
6566
transformRef.current = nextTransform;
6667
setTransform(nextTransform);
6768
}
6869

69-
function beginTransformGesture() {
70-
gestureStartRef.current = transformRef.current;
70+
function beginTransformGesture(handlerTag: number) {
71+
const activeHandlerTags = activeTransformHandlerTagsRef.current;
72+
if (!activeHandlerTags.has(handlerTag) && activeHandlerTags.size === 0) {
73+
gestureStartRef.current = transformRef.current;
74+
}
75+
activeHandlerTags.add(handlerTag);
76+
}
77+
78+
function endTransformGesture(handlerTag: number) {
79+
activeTransformHandlerTagsRef.current.delete(handlerTag);
7180
}
7281

7382
function handlePan(event: PanGestureHandlerGestureEvent) {
@@ -102,7 +111,16 @@ export function GestureLab() {
102111
| RotationGestureHandlerStateChangeEvent,
103112
) {
104113
if (event.nativeEvent.state === State.BEGAN) {
105-
beginTransformGesture();
114+
beginTransformGesture(event.nativeEvent.handlerTag);
115+
return;
116+
}
117+
118+
if (
119+
event.nativeEvent.state === State.END ||
120+
event.nativeEvent.state === State.FAILED ||
121+
event.nativeEvent.state === State.CANCELLED
122+
) {
123+
endTransformGesture(event.nativeEvent.handlerTag);
106124
}
107125
}
108126

@@ -163,6 +181,7 @@ export function GestureLab() {
163181
simultaneousHandlers={[panRef, rotationRef, ...flingRefs]}
164182
>
165183
<PanGestureHandler
184+
avgTouches
166185
minDist={4}
167186
onGestureEvent={handlePan}
168187
onHandlerStateChange={handleTransformStateChange}

src/utils/__tests__/args.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,9 @@ test('usageForCommand resolves workflow help topic', () => {
948948
assert.match(help, /wait for a concrete result before returning to chat\/form state/);
949949
assert.match(help, /choose a point near the center of the intended app-owned target/);
950950
assert.match(help, /Avoid screen edges, tab bars, navigation bars, and home indicators/);
951+
assert.match(help, /Android transform injects a geometric two-finger path/);
952+
assert.match(help, /verify qualitative state such as "pan changed yes"/);
953+
assert.match(help, /prefer isolated gesture pan, gesture pinch, or gesture rotate/);
951954
assert.match(help, /longpress accepts coordinates, @refs, or selectors/);
952955
assert.match(help, /use help react-native for Metro\/Fast Refresh/);
953956
assert.match(help, /iOS Allow Paste prompt cannot be exercised under XCUITest/);

src/utils/command-schema.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,8 @@ Navigation and gestures:
324324
agent-device gesture rotate 35 200 420
325325
agent-device gesture transform 200 420 80 -40 2 35 700
326326
iOS simulator transform uses XCTest gesture primitives; verify app metrics instead of assuming requested degrees map exactly to recognizer output.
327+
Android transform injects a geometric two-finger path; app recognizers may report non-exact pan/scale/rotation. For Android combined transforms, verify qualitative state such as "pan changed yes" / "pinch changed yes" / "rotate changed yes" unless the app explicitly promises exact centroid metrics.
328+
If Android needs exact app-state values, prefer isolated gesture pan, gesture pinch, or gesture rotate commands over one combined transform.
327329
328330
Validation and evidence:
329331
Nearby mutation diff: agent-device diff snapshot -i.

test/skillgym/suites/agent-device-smoke-suite.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1530,14 +1530,25 @@ const SKILL_GUIDANCE_CASES: Case[] = [
15301530
'Zoom scale is 2',
15311531
'Rotation is 35 degrees',
15321532
'Duration is 700ms',
1533+
'After the command, verify Android changed qualitatively instead of asserting exact x, y, scale, or rotate values',
1534+
],
1535+
task: 'Plan the direct agent-device command for the combined pan, zoom, and rotate gesture, then verify qualitative state.',
1536+
outputs: [
1537+
plannedCommand('gesture transform'),
1538+
/200\s+420\s+80\s+-40\s+2\s+35\s+700/i,
1539+
plannedCommand('wait'),
1540+
/pan changed yes/i,
1541+
/pinch changed yes/i,
1542+
/rotate changed yes/i,
15331543
],
1534-
task: 'Plan the direct agent-device command for the combined pan, zoom, and rotate gesture.',
1535-
outputs: [plannedCommand('gesture transform'), /200\s+420\s+80\s+-40\s+2\s+35\s+700/i],
15361544
forbiddenOutputs: [
15371545
plannedCommand('gesture pan'),
15381546
plannedCommand('gesture pinch'),
15391547
plannedCommand('gesture rotate'),
15401548
plannedCommand('compose-gestures'),
1549+
/wait\s+["']?x\s/i,
1550+
/wait\s+["']?scale\s/i,
1551+
/wait\s+["']?rotate\s+\d/i,
15411552
],
15421553
}),
15431554
makeCase({

website/docs/docs/commands.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ On iOS, swipe duration is clamped to a safe range (`16..60ms`) to avoid longpres
281281
`gesture rotate` accepts `degrees [x] [y] [velocity]`; the degree sign controls direction and velocity controls speed.
282282
`gesture transform` accepts `x y dx dy scale degrees [durationMs]` for one combined pan/zoom/rotate gesture on Android and iOS simulators.
283283
On iOS simulators it is implemented with XCTest gesture primitives, so verify app-level metrics instead of assuming the requested degrees map exactly to recognizer output.
284+
On Android, `gesture transform` injects a geometric two-finger path. App recognizers may report non-exact pan, scale, and rotation values, so verify qualitative state such as `pan changed yes`, `pinch changed yes`, and `rotate changed yes` unless the app explicitly promises exact centroid metrics. If exact app-state values matter, prefer isolated `gesture pan`, `gesture pinch`, or `gesture rotate` commands.
284285
`scroll` accepts either a relative amount (`0.5` means roughly half of the viewport on that axis) or `--pixels <n>` for a fixed-distance gesture. Large distances are clamped to the usable drag band so the gesture stays reliable across Android, iOS, and macOS.
285286
Default snapshot text output is visible-first, so off-screen interactive content is summarized instead of shown as tappable refs.
286287
When a target only appears in an off-screen summary, use `scroll <direction>` and then take a fresh `snapshot -i`. For repeated checks, a small shell loop is enough:

0 commit comments

Comments
 (0)