Skip to content

Commit 9b592f6

Browse files
committed
feat: group gesture commands
1 parent 52a747d commit 9b592f6

16 files changed

Lines changed: 334 additions & 197 deletions

File tree

examples/test-app/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,9 @@ pnpm test-app:replay:android
8888

8989
These run the `.ad` replay suite in `examples/test-app/replays`.
9090

91-
`gesture-lab.ad` is iOS-only and verifies `pan`, `fling`, `pinch`, and
92-
`rotate-gesture` against the gesture metrics rendered by the Home screen.
91+
`gesture-lab.ad` is iOS-only and verifies `gesture pan`, `gesture fling`,
92+
`gesture pinch`, and `gesture rotate` against the gesture metrics rendered by
93+
the Home screen.
9394

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

examples/test-app/replays/gesture-lab.ad

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,34 @@ env APP_URL=""
66
open "${APP_TARGET}" --relaunch --launch-url "${APP_URL}"
77
wait "Gesture lab" 30000
88

9-
fling left 195 443 180
9+
gesture fling left 195 443 180
1010
wait "fling 1" 5000
1111

12-
fling right 195 443 180
12+
gesture fling right 195 443 180
1313
wait "fling 2" 5000
1414

15-
fling up 195 443 80 80
15+
gesture fling up 195 443 80 80
1616
wait "fling 3" 5000
1717

18-
fling down 195 443 80 80
18+
gesture fling down 195 443 80 80
1919
wait "fling 4" 5000
2020

21-
pan 195 443 -80 0
21+
gesture pan 195 443 -80 0
2222
wait "x -" 5000
2323

24-
pan 195 443 160 0
24+
gesture pan 195 443 160 0
2525
wait "x 72" 5000
2626

27-
pan 195 443 0 -80
27+
gesture pan 195 443 0 -80
2828
wait "y -" 5000
2929

30-
pan 195 443 0 160
30+
gesture pan 195 443 0 160
3131
wait "y 56" 5000
3232

33-
pinch 1.25 195 443
33+
gesture pinch 1.25 195 443
3434
wait "pinch changed yes" 5000
3535

36-
rotate-gesture 35 195 443
36+
gesture rotate 35 195 443
3737
wait "rotate changed yes" 5000
3838

3939
close

scripts/integration-progress.mjs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -488,17 +488,17 @@ function summarizeCommandFamilyOwnership(files) {
488488
commands: ['snapshot', 'diff', 'screenshot'],
489489
},
490490
{
491-
name: 'press/click/fill/type/scroll/swipe/pinch/rotate/app-switcher',
491+
name: 'press/click/fill/type/scroll/swipe/gesture/rotate/app-switcher',
492492
commands: [
493493
'press',
494494
'click',
495495
'focus',
496496
'longpress',
497497
'swipe',
498498
'scroll',
499+
'gesture',
499500
'type',
500501
'fill',
501-
'pinch',
502502
'rotate',
503503
'app-switcher',
504504
'back',
@@ -609,7 +609,10 @@ function extractProviderScenarioCommandReferences(text) {
609609
['interactions.get', 'get'],
610610
['interactions.is', 'is'],
611611
['interactions.longPress', 'longpress'],
612-
['interactions.pinch', 'pinch'],
612+
['interactions.pan', 'gesture'],
613+
['interactions.fling', 'gesture'],
614+
['interactions.pinch', 'gesture'],
615+
['interactions.rotateGesture', 'gesture'],
613616
['interactions.press', 'press'],
614617
['interactions.scroll', 'scroll'],
615618
['interactions.swipe', 'swipe'],

src/cli/commands/generic.ts

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -111,23 +111,11 @@ const genericClientCommandRunners = {
111111
pauseMs: flags.pauseMs,
112112
pattern: flags.pattern,
113113
}),
114-
pan: ({ client, positionals, flags }) =>
115-
client.interactions.pan({
116-
...buildSelectionOptions(flags),
117-
x: Number(positionals[0]),
118-
y: Number(positionals[1]),
119-
dx: Number(positionals[2]),
120-
dy: Number(positionals[3]),
121-
durationMs: optionalNumber(positionals[4]),
122-
}),
123-
fling: ({ client, positionals, flags }) =>
124-
client.interactions.fling({
125-
...buildSelectionOptions(flags),
126-
direction: readGestureDirection(positionals[0], 'fling'),
127-
x: Number(positionals[1]),
128-
y: Number(positionals[2]),
129-
distance: optionalNumber(positionals[3]),
130-
durationMs: optionalNumber(positionals[4]),
114+
gesture: ({ client, positionals, flags }) =>
115+
runGestureCommand({
116+
client,
117+
positionals,
118+
flags,
131119
}),
132120
focus: ({ client, positionals, flags }) =>
133121
client.interactions.focus({
@@ -153,21 +141,6 @@ const genericClientCommandRunners = {
153141
amount: optionalNumber(positionals[1]),
154142
pixels: flags.pixels,
155143
}),
156-
pinch: ({ client, positionals, flags }) =>
157-
client.interactions.pinch({
158-
...buildSelectionOptions(flags),
159-
scale: Number(positionals[0]),
160-
x: optionalNumber(positionals[1]),
161-
y: optionalNumber(positionals[2]),
162-
}),
163-
'rotate-gesture': ({ client, positionals, flags }) =>
164-
client.interactions.rotateGesture({
165-
...buildSelectionOptions(flags),
166-
degrees: Number(positionals[0]),
167-
x: optionalNumber(positionals[1]),
168-
y: optionalNumber(positionals[2]),
169-
velocity: optionalNumber(positionals[3]),
170-
}),
171144
'trigger-app-event': ({ client, positionals, flags }) =>
172145
client.apps.triggerEvent({
173146
...buildSelectionOptions(flags),
@@ -218,6 +191,53 @@ const genericClientCommandRunners = {
218191
client.settings.update(settingsCommandCodec.decode(positionals, flags)),
219192
} satisfies Partial<Record<PublicCommandName, GenericClientCommandRunner>>;
220193

194+
function runGestureCommand(params: {
195+
client: AgentDeviceClient;
196+
positionals: string[];
197+
flags: CliFlags;
198+
}): Promise<CommandRequestResult> {
199+
const { client, positionals, flags } = params;
200+
const subcommand = required(positionals[0], 'gesture requires subcommand');
201+
const args = positionals.slice(1);
202+
switch (subcommand) {
203+
case 'pan':
204+
return client.interactions.pan({
205+
...buildSelectionOptions(flags),
206+
x: Number(args[0]),
207+
y: Number(args[1]),
208+
dx: Number(args[2]),
209+
dy: Number(args[3]),
210+
durationMs: optionalNumber(args[4]),
211+
});
212+
case 'fling':
213+
return client.interactions.fling({
214+
...buildSelectionOptions(flags),
215+
direction: readGestureDirection(args[0], 'gesture fling'),
216+
x: Number(args[1]),
217+
y: Number(args[2]),
218+
distance: optionalNumber(args[3]),
219+
durationMs: optionalNumber(args[4]),
220+
});
221+
case 'pinch':
222+
return client.interactions.pinch({
223+
...buildSelectionOptions(flags),
224+
scale: Number(args[0]),
225+
x: optionalNumber(args[1]),
226+
y: optionalNumber(args[2]),
227+
});
228+
case 'rotate':
229+
return client.interactions.rotateGesture({
230+
...buildSelectionOptions(flags),
231+
degrees: Number(args[0]),
232+
x: optionalNumber(args[1]),
233+
y: optionalNumber(args[2]),
234+
velocity: optionalNumber(args[3]),
235+
});
236+
default:
237+
throw new AppError('INVALID_ARGS', 'gesture requires one of: pan, fling, pinch, rotate');
238+
}
239+
}
240+
221241
export const genericClientCommandHandlers = Object.fromEntries(
222242
Object.entries(genericClientCommandRunners).map(([command, run]) => [
223243
command,

src/client.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -333,8 +333,9 @@ export function createAgentDeviceClient(
333333
),
334334
pan: async (options) =>
335335
await executeCommandRequest(
336-
PUBLIC_COMMANDS.pan,
336+
PUBLIC_COMMANDS.gesture,
337337
[
338+
'pan',
338339
String(options.x),
339340
String(options.y),
340341
String(options.dx),
@@ -347,8 +348,9 @@ export function createAgentDeviceClient(
347348
const distance =
348349
options.durationMs !== undefined ? (options.distance ?? 180) : options.distance;
349350
return await executeCommandRequest(
350-
PUBLIC_COMMANDS.fling,
351+
PUBLIC_COMMANDS.gesture,
351352
[
353+
'fling',
352354
options.direction,
353355
String(options.x),
354356
String(options.y),
@@ -384,15 +386,21 @@ export function createAgentDeviceClient(
384386
),
385387
pinch: async (options) =>
386388
await executeCommandRequest(
387-
PUBLIC_COMMANDS.pinch,
388-
[String(options.scale), ...optionalNumber(options.x), ...optionalNumber(options.y)],
389+
PUBLIC_COMMANDS.gesture,
390+
[
391+
'pinch',
392+
String(options.scale),
393+
...optionalNumber(options.x),
394+
...optionalNumber(options.y),
395+
],
389396
options,
390397
),
391398
rotateGesture: async (options) => {
392399
const center = options.x !== undefined || options.y !== undefined;
393400
return await executeCommandRequest(
394-
PUBLIC_COMMANDS.rotateGesture,
401+
PUBLIC_COMMANDS.gesture,
395402
[
403+
'rotate',
396404
String(options.degrees),
397405
...(center ? [String(options.x), String(options.y)] : []),
398406
...optionalNumber(options.velocity),

src/command-catalog.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ export const PUBLIC_COMMANDS = {
1313
diff: 'diff',
1414
fill: 'fill',
1515
find: 'find',
16-
fling: 'fling',
1716
focus: 'focus',
17+
gesture: 'gesture',
1818
get: 'get',
1919
home: 'home',
2020
install: 'install',
@@ -25,17 +25,14 @@ export const PUBLIC_COMMANDS = {
2525
longPress: 'longpress',
2626
network: 'network',
2727
open: 'open',
28-
pan: 'pan',
2928
perf: 'perf',
30-
pinch: 'pinch',
3129
press: 'press',
3230
push: 'push',
3331
record: 'record',
3432
reactNative: 'react-native',
3533
reinstall: 'reinstall',
3634
replay: 'replay',
3735
rotate: 'rotate',
38-
rotateGesture: 'rotate-gesture',
3936
scroll: 'scroll',
4037
screenshot: 'screenshot',
4138
settings: 'settings',
@@ -91,19 +88,17 @@ export const DAEMON_COMMAND_GROUPS = {
9188
PUBLIC_COMMANDS.diff,
9289
PUBLIC_COMMANDS.fill,
9390
PUBLIC_COMMANDS.find,
94-
PUBLIC_COMMANDS.fling,
91+
PUBLIC_COMMANDS.gesture,
9592
PUBLIC_COMMANDS.get,
9693
PUBLIC_COMMANDS.home,
9794
PUBLIC_COMMANDS.is,
9895
PUBLIC_COMMANDS.keyboard,
9996
PUBLIC_COMMANDS.longPress,
100-
PUBLIC_COMMANDS.pan,
101-
PUBLIC_COMMANDS.pinch,
97+
'pinch',
10298
PUBLIC_COMMANDS.press,
10399
PUBLIC_COMMANDS.record,
104100
PUBLIC_COMMANDS.reactNative,
105101
PUBLIC_COMMANDS.rotate,
106-
PUBLIC_COMMANDS.rotateGesture,
107102
PUBLIC_COMMANDS.screenshot,
108103
PUBLIC_COMMANDS.scroll,
109104
PUBLIC_COMMANDS.settings,

src/core/dispatch-interactions.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ export async function handlePanCommand(
456456
const dx = Number(positionals[2]);
457457
const dy = Number(positionals[3]);
458458
if ([x, y, dx, dy].some((value) => !Number.isFinite(value))) {
459-
throw new AppError('INVALID_ARGS', 'pan requires x y dx dy [durationMs]');
459+
throw new AppError('INVALID_ARGS', 'gesture pan requires x y dx dy [durationMs]');
460460
}
461461
const requestedDurationMs = positionals[4] ? Number(positionals[4]) : 500;
462462
const durationMs = requireIntInRange(requestedDurationMs, 'durationMs', 16, 10_000);
@@ -483,7 +483,10 @@ export async function handleFlingCommand(
483483
const x = Number(positionals[1]);
484484
const y = Number(positionals[2]);
485485
if (![x, y].every(Number.isFinite)) {
486-
throw new AppError('INVALID_ARGS', 'fling requires direction x y [distance] [durationMs]');
486+
throw new AppError(
487+
'INVALID_ARGS',
488+
'gesture fling requires direction x y [distance] [durationMs]',
489+
);
487490
}
488491
const distanceInput = positionals[3] ? Number(positionals[3]) : 180;
489492
const distance = requireFinitePositiveNumber(distanceInput, 'distance');
@@ -601,23 +604,23 @@ export async function handlePinchCommand(
601604
if (device.platform === 'android') {
602605
throw new AppError(
603606
'UNSUPPORTED_OPERATION',
604-
'Android pinch is not supported in current adb backend; requires instrumentation-based backend.',
607+
'Android gesture pinch is not supported in current adb backend; requires instrumentation-based backend.',
605608
);
606609
}
607610
if (device.target === 'tv') {
608-
throw new AppError('UNSUPPORTED_OPERATION', 'pinch is not supported on tvOS');
611+
throw new AppError('UNSUPPORTED_OPERATION', 'gesture pinch is not supported on tvOS');
609612
}
610613
if (device.platform === 'macos' && context?.surface && context.surface !== 'app') {
611614
throw new AppError(
612615
'UNSUPPORTED_OPERATION',
613-
'pinch is only supported in macOS app sessions. Re-open the target app without --surface desktop|menubar|frontmost-app first.',
616+
'gesture pinch is only supported in macOS app sessions. Re-open the target app without --surface desktop|menubar|frontmost-app first.',
614617
);
615618
}
616619
const scale = Number(positionals[0]);
617620
const x = positionals[1] ? Number(positionals[1]) : undefined;
618621
const y = positionals[2] ? Number(positionals[2]) : undefined;
619622
if (Number.isNaN(scale) || scale <= 0) {
620-
throw new AppError('INVALID_ARGS', 'pinch requires scale > 0');
623+
throw new AppError('INVALID_ARGS', 'gesture pinch requires scale > 0');
621624
}
622625
await runIosRunnerCommand(
623626
device,
@@ -640,16 +643,16 @@ export async function handleRotateGestureCommand(
640643
if (device.platform === 'android') {
641644
throw new AppError(
642645
'UNSUPPORTED_OPERATION',
643-
'Android rotate-gesture is not supported in current adb backend; requires instrumentation-based backend.',
646+
'Android gesture rotate is not supported in current adb backend; requires instrumentation-based backend.',
644647
);
645648
}
646649
if (device.target === 'tv') {
647-
throw new AppError('UNSUPPORTED_OPERATION', 'rotate-gesture is not supported on tvOS');
650+
throw new AppError('UNSUPPORTED_OPERATION', 'gesture rotate is not supported on tvOS');
648651
}
649652
if (device.platform === 'macos') {
650653
throw new AppError(
651654
'UNSUPPORTED_OPERATION',
652-
'rotate-gesture is not supported on macOS; XCTest rotation gestures are available only for iOS app sessions.',
655+
'gesture rotate is not supported on macOS; XCTest rotation gestures are available only for iOS app sessions.',
653656
);
654657
}
655658

@@ -676,13 +679,13 @@ type RotateGestureParams = {
676679
function parseRotateGestureParams(positionals: string[]): RotateGestureParams {
677680
const degrees = Number(positionals[0]);
678681
if (!Number.isFinite(degrees)) {
679-
throw new AppError('INVALID_ARGS', 'rotate-gesture requires degrees [x] [y] [velocity]');
682+
throw new AppError('INVALID_ARGS', 'gesture rotate requires degrees [x] [y] [velocity]');
680683
}
681684

682685
const center = parseOptionalGestureCenter(positionals[1], positionals[2]);
683686
const velocity = Number(positionals[3] ?? (degrees >= 0 ? 1 : -1));
684687
if (!Number.isFinite(velocity) || velocity === 0) {
685-
throw new AppError('INVALID_ARGS', 'rotate-gesture velocity must be a non-zero number');
688+
throw new AppError('INVALID_ARGS', 'gesture rotate velocity must be a non-zero number');
686689
}
687690

688691
return { degrees, ...center, velocity };
@@ -694,13 +697,13 @@ function parseOptionalGestureCenter(
694697
): Pick<RotateGestureParams, 'x' | 'y'> {
695698
if (xInput === undefined && yInput === undefined) return {};
696699
if (xInput === undefined || yInput === undefined) {
697-
throw new AppError('INVALID_ARGS', 'rotate-gesture center requires both x and y');
700+
throw new AppError('INVALID_ARGS', 'gesture rotate center requires both x and y');
698701
}
699702

700703
const x = Number(xInput);
701704
const y = Number(yInput);
702705
if (!Number.isFinite(x) || !Number.isFinite(y)) {
703-
throw new AppError('INVALID_ARGS', 'rotate-gesture center requires finite x and y');
706+
throw new AppError('INVALID_ARGS', 'gesture rotate center requires finite x and y');
704707
}
705708
return { x, y };
706709
}

0 commit comments

Comments
 (0)