@@ -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,60 @@ 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 requestedDurationMs = positionals [ 4 ] ? Number ( positionals [ 4 ] ) : 250 ;
162+ const durationMs = requireIntInRange ( requestedDurationMs , 'durationMs' , 16 , 10_000 ) ;
163+ const effectiveDurationMs = device . platform === 'ios' ? 60 : durationMs ;
164+ const count = requireIntInRange ( context ?. count ?? 1 , 'count' , 1 , 200 ) ;
165+ const pauseMs = requireIntInRange ( context ?. pauseMs ?? 0 , 'pause-ms' , 0 , 10_000 ) ;
166+ const pattern = context ?. pattern ?? 'one-way' ;
167+ if ( pattern !== 'one-way' && pattern !== 'ping-pong' ) {
168+ throw new AppError ( 'INVALID_ARGS' , `Invalid pattern: ${ pattern } ` ) ;
169+ }
170+
171+ for ( let index = 0 ; index < count ; index += 1 ) {
172+ const reverse = pattern === 'ping-pong' && index % 2 === 1 ;
173+ if ( reverse ) await interactor . swipe ( x2 , y2 , x1 , y1 , effectiveDurationMs ) ;
174+ else await interactor . swipe ( x1 , y1 , x2 , y2 , effectiveDurationMs ) ;
175+ if ( index < count - 1 && pauseMs > 0 ) await sleep ( pauseMs ) ;
176+ }
177+
178+ return {
179+ x1,
180+ y1,
181+ x2,
182+ y2,
183+ durationMs,
184+ effectiveDurationMs,
185+ timingMode : device . platform === 'ios' ? 'safe-normalized' : 'direct' ,
186+ count,
187+ pauseMs,
188+ pattern,
189+ } ;
126190 }
127191 case 'long-press' : {
128192 const x = Number ( positionals [ 0 ] ) ;
@@ -171,6 +235,12 @@ export async function dispatchCommand(
171235 return { text } ;
172236 }
173237 case 'pinch' : {
238+ if ( device . platform === 'android' ) {
239+ throw new AppError (
240+ 'UNSUPPORTED_OPERATION' ,
241+ 'Android pinch is not supported in current adb backend; requires instrumentation-based backend.' ,
242+ ) ;
243+ }
174244 const scale = Number ( positionals [ 0 ] ) ;
175245 const x = positionals [ 1 ] ? Number ( positionals [ 1 ] ) : undefined ;
176246 const y = positionals [ 2 ] ? Number ( positionals [ 2 ] ) : undefined ;
@@ -280,3 +350,32 @@ export async function dispatchCommand(
280350 throw new AppError ( 'INVALID_ARGS' , `Unknown command: ${ command } ` ) ;
281351 }
282352}
353+
354+ const DETERMINISTIC_JITTER_PATTERN : ReadonlyArray < readonly [ number , number ] > = [
355+ [ 0 , 0 ] ,
356+ [ 1 , 0 ] ,
357+ [ 0 , 1 ] ,
358+ [ - 1 , 0 ] ,
359+ [ 0 , - 1 ] ,
360+ [ 1 , 1 ] ,
361+ [ - 1 , 1 ] ,
362+ [ 1 , - 1 ] ,
363+ [ - 1 , - 1 ] ,
364+ ] ;
365+
366+ function requireIntInRange ( value : number , name : string , min : number , max : number ) : number {
367+ if ( ! Number . isFinite ( value ) || ! Number . isInteger ( value ) || value < min || value > max ) {
368+ throw new AppError ( 'INVALID_ARGS' , `${ name } must be an integer between ${ min } and ${ max } ` ) ;
369+ }
370+ return value ;
371+ }
372+
373+ function computeDeterministicJitter ( index : number , jitterPx : number ) : [ number , number ] {
374+ if ( jitterPx <= 0 ) return [ 0 , 0 ] ;
375+ const [ dx , dy ] = DETERMINISTIC_JITTER_PATTERN [ index % DETERMINISTIC_JITTER_PATTERN . length ] ;
376+ return [ dx * jitterPx , dy * jitterPx ] ;
377+ }
378+
379+ async function sleep ( ms : number ) : Promise < void > {
380+ await new Promise ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
381+ }
0 commit comments