@@ -38,6 +38,12 @@ export type CommandFlags = {
3838 noRecord ?: boolean ;
3939 appsFilter ?: 'launchable' | 'user-installed' | 'all' ;
4040 appsMetadata ?: boolean ;
41+ count ?: number ;
42+ intervalMs ?: number ;
43+ holdMs ?: number ;
44+ jitterPx ?: number ;
45+ pauseMs ?: number ;
46+ pattern ?: 'one-way' | 'ping-pong' ;
4147 replayUpdate ?: boolean ;
4248} ;
4349
@@ -91,6 +97,12 @@ export async function dispatchCommand(
9197 snapshotScope ?: string ;
9298 snapshotRaw ?: boolean ;
9399 snapshotBackend ?: 'ax' | 'xctest' ;
100+ count ?: number ;
101+ intervalMs ?: number ;
102+ holdMs ?: number ;
103+ jitterPx ?: number ;
104+ pauseMs ?: number ;
105+ pattern ?: 'one-way' | 'ping-pong' ;
94106 } ,
95107) : Promise < Record < string , unknown > | void > {
96108 const runnerCtx : RunnerContext = {
@@ -121,8 +133,48 @@ export async function dispatchCommand(
121133 case 'press' : {
122134 const [ x , y ] = positionals . map ( Number ) ;
123135 if ( Number . isNaN ( x ) || Number . isNaN ( y ) ) throw new AppError ( 'INVALID_ARGS' , 'press requires x y' ) ;
124- await interactor . tap ( x , y ) ;
125- return { x, y } ;
136+ const count = requireIntInRange ( context ?. count ?? 1 , 'count' , 1 , 200 ) ;
137+ const intervalMs = requireIntInRange ( context ?. intervalMs ?? 0 , 'interval-ms' , 0 , 10_000 ) ;
138+ const holdMs = requireIntInRange ( context ?. holdMs ?? 0 , 'hold-ms' , 0 , 10_000 ) ;
139+ const jitterPx = requireIntInRange ( context ?. jitterPx ?? 0 , 'jitter-px' , 0 , 100 ) ;
140+
141+ for ( let index = 0 ; index < count ; index += 1 ) {
142+ const [ dx , dy ] = computeDeterministicJitter ( index , jitterPx ) ;
143+ const targetX = x + dx ;
144+ const targetY = y + dy ;
145+ if ( holdMs > 0 ) await interactor . longPress ( targetX , targetY , holdMs ) ;
146+ else await interactor . tap ( targetX , targetY ) ;
147+ if ( index < count - 1 && intervalMs > 0 ) await sleep ( intervalMs ) ;
148+ }
149+
150+ return { x, y, count, intervalMs, holdMs, jitterPx } ;
151+ }
152+ case 'swipe' : {
153+ const x1 = Number ( positionals [ 0 ] ) ;
154+ const y1 = Number ( positionals [ 1 ] ) ;
155+ const x2 = Number ( positionals [ 2 ] ) ;
156+ const y2 = Number ( positionals [ 3 ] ) ;
157+ if ( [ x1 , y1 , x2 , y2 ] . some ( Number . isNaN ) ) {
158+ throw new AppError ( 'INVALID_ARGS' , 'swipe requires x1 y1 x2 y2 [durationMs]' ) ;
159+ }
160+
161+ const rawDurationMs = positionals [ 4 ] ? Number ( positionals [ 4 ] ) : 250 ;
162+ const durationMs = requireIntInRange ( rawDurationMs , 'durationMs' , 16 , 10_000 ) ;
163+ const count = requireIntInRange ( context ?. count ?? 1 , 'count' , 1 , 200 ) ;
164+ const pauseMs = requireIntInRange ( context ?. pauseMs ?? 0 , 'pause-ms' , 0 , 10_000 ) ;
165+ const pattern = context ?. pattern ?? 'one-way' ;
166+ if ( pattern !== 'one-way' && pattern !== 'ping-pong' ) {
167+ throw new AppError ( 'INVALID_ARGS' , `Invalid pattern: ${ pattern } ` ) ;
168+ }
169+
170+ for ( let index = 0 ; index < count ; index += 1 ) {
171+ const reverse = pattern === 'ping-pong' && index % 2 === 1 ;
172+ if ( reverse ) await interactor . swipe ( x2 , y2 , x1 , y1 , durationMs ) ;
173+ else await interactor . swipe ( x1 , y1 , x2 , y2 , durationMs ) ;
174+ if ( index < count - 1 && pauseMs > 0 ) await sleep ( pauseMs ) ;
175+ }
176+
177+ return { x1, y1, x2, y2, durationMs, count, pauseMs, pattern } ;
126178 }
127179 case 'long-press' : {
128180 const x = Number ( positionals [ 0 ] ) ;
@@ -171,6 +223,12 @@ export async function dispatchCommand(
171223 return { text } ;
172224 }
173225 case 'pinch' : {
226+ if ( device . platform === 'android' ) {
227+ throw new AppError (
228+ 'UNSUPPORTED_OPERATION' ,
229+ 'Android pinch is not supported in current adb backend; requires instrumentation-based backend.' ,
230+ ) ;
231+ }
174232 const scale = Number ( positionals [ 0 ] ) ;
175233 const x = positionals [ 1 ] ? Number ( positionals [ 1 ] ) : undefined ;
176234 const y = positionals [ 2 ] ? Number ( positionals [ 2 ] ) : undefined ;
@@ -280,3 +338,32 @@ export async function dispatchCommand(
280338 throw new AppError ( 'INVALID_ARGS' , `Unknown command: ${ command } ` ) ;
281339 }
282340}
341+
342+ const DETERMINISTIC_JITTER_PATTERN : ReadonlyArray < readonly [ number , number ] > = [
343+ [ 0 , 0 ] ,
344+ [ 1 , 0 ] ,
345+ [ 0 , 1 ] ,
346+ [ - 1 , 0 ] ,
347+ [ 0 , - 1 ] ,
348+ [ 1 , 1 ] ,
349+ [ - 1 , 1 ] ,
350+ [ 1 , - 1 ] ,
351+ [ - 1 , - 1 ] ,
352+ ] ;
353+
354+ function requireIntInRange ( value : number , name : string , min : number , max : number ) : number {
355+ if ( ! Number . isFinite ( value ) || ! Number . isInteger ( value ) || value < min || value > max ) {
356+ throw new AppError ( 'INVALID_ARGS' , `${ name } must be an integer between ${ min } and ${ max } ` ) ;
357+ }
358+ return value ;
359+ }
360+
361+ function computeDeterministicJitter ( index : number , jitterPx : number ) : [ number , number ] {
362+ if ( jitterPx <= 0 ) return [ 0 , 0 ] ;
363+ const [ dx , dy ] = DETERMINISTIC_JITTER_PATTERN [ index % DETERMINISTIC_JITTER_PATTERN . length ] ;
364+ return [ dx * jitterPx , dy * jitterPx ] ;
365+ }
366+
367+ async function sleep ( ms : number ) : Promise < void > {
368+ await new Promise ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
369+ }
0 commit comments