@@ -5,6 +5,8 @@ import { AppError } from '../../../kernel/errors.ts';
55import { makeSnapshotState } from '../../../__tests__/test-utils/index.ts' ;
66import type { AgentDeviceBackend } from '../../../backend.ts' ;
77import type { GesturePlan } from '../../../contracts/gesture-plan.ts' ;
8+ import type { Rect } from '../../../kernel/snapshot.ts' ;
9+ import { parseAndroidDisplayGeometry } from '../../../platforms/android/display-geometry.ts' ;
810import {
911 createInteractionDevice ,
1012 runtimeScrollSnapshot ,
@@ -345,6 +347,114 @@ test('runtime directional swipe uses the visible viewport instead of off-screen
345347 ] ) ;
346348} ) ;
347349
350+ test ( 'runtime coordinate swipe uses backend viewport geometry without accessibility capture' , async ( ) => {
351+ let capturedViewport : unknown ;
352+ let executionCount = 0 ;
353+ const device = createInteractionDevice ( selectorSnapshot ( ) , {
354+ platform : 'android' ,
355+ captureSnapshot : async ( ) => {
356+ throw new Error ( 'coordinate swipe must not capture Android accessibility state' ) ;
357+ } ,
358+ resolveGestureViewport : async ( ) => ( { x : 0 , y : 0 , width : 400 , height : 800 } ) ,
359+ performGesture : async ( _context , plan ) => {
360+ capturedViewport = plan . viewport ;
361+ executionCount += 1 ;
362+ } ,
363+ } ) ;
364+
365+ const result = await device . interactions . swipe ( {
366+ from : { x : 300 , y : 400 } ,
367+ to : { x : 100 , y : 400 } ,
368+ session : 'default' ,
369+ } ) ;
370+
371+ assert . equal ( result . kind , 'fling' ) ;
372+ assert . deepEqual ( capturedViewport , { x : 0 , y : 0 , width : 400 , height : 800 } ) ;
373+ assert . equal ( executionCount , 1 ) ;
374+ } ) ;
375+
376+ test ( 'runtime coordinate swipe keeps deterministic viewport bounds validation on the fast path' , async ( ) => {
377+ let executionCount = 0 ;
378+ const device = createInteractionDevice ( selectorSnapshot ( ) , {
379+ platform : 'android' ,
380+ captureSnapshot : async ( ) => {
381+ throw new Error ( 'coordinate swipe must not capture Android accessibility state' ) ;
382+ } ,
383+ resolveGestureViewport : async ( ) => ( { x : 0 , y : 0 , width : 400 , height : 800 } ) ,
384+ performGesture : async ( ) => {
385+ executionCount += 1 ;
386+ } ,
387+ } ) ;
388+
389+ await assert . rejects (
390+ ( ) =>
391+ device . interactions . swipe ( {
392+ from : { x : 400 , y : 400 } ,
393+ to : { x : 100 , y : 400 } ,
394+ session : 'default' ,
395+ } ) ,
396+ ( error : unknown ) =>
397+ error instanceof AppError &&
398+ error . code === 'INVALID_ARGS' &&
399+ error . details ?. reason === 'GESTURE_TRAJECTORY_OUT_OF_BOUNDS' ,
400+ ) ;
401+ assert . equal ( executionCount , 0 ) ;
402+ } ) ;
403+
404+ test ( 'runtime coordinate swipe falls back to the snapshot viewport when display probing fails' , async ( ) => {
405+ let snapshotCaptureCount = 0 ;
406+ let capturedViewport : unknown ;
407+ const snapshot = selectorSnapshot ( ) ;
408+ const device = createInteractionDevice ( snapshot , {
409+ platform : 'android' ,
410+ resolveGestureViewport : async ( ) => {
411+ throw parseAndroidDisplayGeometry ( 'Displays:\n' ) ;
412+ } ,
413+ captureSnapshot : async ( ) => {
414+ snapshotCaptureCount += 1 ;
415+ return { snapshot } ;
416+ } ,
417+ performGesture : async ( _context , plan ) => {
418+ capturedViewport = plan . viewport ;
419+ } ,
420+ } ) ;
421+
422+ const result = await device . interactions . swipe ( {
423+ from : { x : 20 , y : 30 } ,
424+ to : { x : 90 , y : 30 } ,
425+ session : 'default' ,
426+ } ) ;
427+
428+ assert . equal ( result . kind , 'fling' ) ;
429+ assert . equal ( snapshotCaptureCount , 1 ) ;
430+ assert . deepEqual ( capturedViewport , { x : 10 , y : 20 , width : 100 , height : 40 } ) ;
431+ } ) ;
432+
433+ test ( 'runtime gesture falls back when the backend returns an unusable viewport shape' , async ( ) => {
434+ let snapshotCaptureCount = 0 ;
435+ let capturedViewport : unknown ;
436+ const snapshot = selectorSnapshot ( ) ;
437+ const device = createInteractionDevice ( snapshot , {
438+ platform : 'android' ,
439+ resolveGestureViewport : async ( ) => ( { x : 0 , y : 0 , width : 0 , height : 800 } ) as unknown as Rect ,
440+ captureSnapshot : async ( ) => {
441+ snapshotCaptureCount += 1 ;
442+ return { snapshot } ;
443+ } ,
444+ performGesture : async ( _context , plan ) => {
445+ capturedViewport = plan . viewport ;
446+ } ,
447+ } ) ;
448+
449+ await device . interactions . pan ( {
450+ origin : { x : 20 , y : 30 } ,
451+ delta : { x : 10 , y : 0 } ,
452+ } ) ;
453+
454+ assert . equal ( snapshotCaptureCount , 1 ) ;
455+ assert . deepEqual ( capturedViewport , { x : 10 , y : 20 , width : 100 , height : 40 } ) ;
456+ } ) ;
457+
348458test ( 'runtime gesture swipe presets use stable viewport lanes' , async ( ) => {
349459 const calls : unknown [ ] = [ ] ;
350460 const device = createInteractionDevice ( snapshotWithOffscreenContent ( ) , {
0 commit comments