@@ -67,6 +67,61 @@ test('runtime press resolves selector targets to the actionable node center', as
6767 assert . deepEqual ( result . backendResult , { ok : true } ) ;
6868} ) ;
6969
70+ test ( 'runtime click keeps distinct tab button centers when iOS reports the tab bar as hittable' , async ( ) => {
71+ const calls : Point [ ] = [ ] ;
72+ const device = createInteractionDevice ( iosTabBarSnapshot ( ) , {
73+ tap : async ( _context , point ) => {
74+ calls . push ( point ) ;
75+ } ,
76+ } ) ;
77+
78+ const refResult = await device . interactions . click ( ref ( '@e4' ) , {
79+ session : 'default' ,
80+ } ) ;
81+ const selectorResult = await device . interactions . click ( selector ( 'label=Settings' ) , {
82+ session : 'default' ,
83+ } ) ;
84+
85+ assert . deepEqual ( calls , [
86+ { x : 166 , y : 822 } ,
87+ { x : 257 , y : 822 } ,
88+ ] ) ;
89+ assert . equal ( refResult . kind , 'ref' ) ;
90+ assert . equal ( refResult . node ?. label , 'Library' ) ;
91+ assert . equal ( selectorResult . kind , 'selector' ) ;
92+ assert . equal ( selectorResult . node ?. label , 'Settings' ) ;
93+ } ) ;
94+
95+ test ( 'runtime click keeps non-button semantic targets at their own center' , async ( ) => {
96+ const calls : Point [ ] = [ ] ;
97+ const device = createInteractionDevice ( nonHittableCellSnapshot ( ) , {
98+ tap : async ( _context , point ) => {
99+ calls . push ( point ) ;
100+ } ,
101+ } ) ;
102+
103+ const result = await clickRefE2 ( device ) ;
104+
105+ assert . deepEqual ( calls , [ { x : 70 , y : 30 } ] ) ;
106+ assert . equal ( result . kind , 'ref' ) ;
107+ assert . equal ( result . node ?. label , 'Account' ) ;
108+ } ) ;
109+
110+ test ( 'runtime click still promotes non-touchable nodes to hittable ancestors' , async ( ) => {
111+ const calls : Point [ ] = [ ] ;
112+ const device = createInteractionDevice ( nonTouchableGroupSnapshot ( ) , {
113+ tap : async ( _context , point ) => {
114+ calls . push ( point ) ;
115+ } ,
116+ } ) ;
117+
118+ const result = await clickRefE2 ( device ) ;
119+
120+ assert . deepEqual ( calls , [ { x : 160 , y : 60 } ] ) ;
121+ assert . equal ( result . kind , 'ref' ) ;
122+ assert . equal ( result . node ?. label , 'Clickable group' ) ;
123+ } ) ;
124+
70125test ( 'runtime fill resolves refs and forwards text to the backend primitive' , async ( ) => {
71126 const calls : Array < { point : Point ; text : string ; delayMs ?: number } > = [ ] ;
72127 const device = createInteractionDevice ( fillableSnapshot ( ) , {
@@ -422,6 +477,107 @@ function fillableSnapshot(): SnapshotState {
422477 ] ) ;
423478}
424479
480+ function iosTabBarSnapshot ( ) : SnapshotState {
481+ return makeSnapshotState ( [
482+ {
483+ index : 0 ,
484+ depth : 0 ,
485+ type : 'XCUIElementTypeApplication' ,
486+ label : 'TabRepro' ,
487+ rect : { x : 0 , y : 0 , width : 402 , height : 874 } ,
488+ hittable : false ,
489+ } ,
490+ {
491+ index : 1 ,
492+ depth : 1 ,
493+ parentIndex : 0 ,
494+ type : 'XCUIElementTypeTabBar' ,
495+ rect : { x : 0 , y : 791 , width : 402 , height : 83 } ,
496+ hittable : true ,
497+ } ,
498+ {
499+ index : 2 ,
500+ depth : 2 ,
501+ parentIndex : 1 ,
502+ type : 'XCUIElementTypeButton' ,
503+ label : 'Home' ,
504+ rect : { x : 30 , y : 800 , width : 91 , height : 44 } ,
505+ hittable : false ,
506+ } ,
507+ {
508+ index : 3 ,
509+ depth : 2 ,
510+ parentIndex : 1 ,
511+ type : 'XCUIElementTypeButton' ,
512+ label : 'Library' ,
513+ rect : { x : 120 , y : 800 , width : 92 , height : 44 } ,
514+ hittable : false ,
515+ } ,
516+ {
517+ index : 4 ,
518+ depth : 2 ,
519+ parentIndex : 1 ,
520+ type : 'XCUIElementTypeButton' ,
521+ label : 'Settings' ,
522+ rect : { x : 211 , y : 800 , width : 91 , height : 44 } ,
523+ hittable : false ,
524+ } ,
525+ {
526+ index : 5 ,
527+ depth : 2 ,
528+ parentIndex : 1 ,
529+ type : 'XCUIElementTypeButton' ,
530+ label : 'Search' ,
531+ rect : { x : 304 , y : 800 , width : 92 , height : 44 } ,
532+ hittable : false ,
533+ } ,
534+ ] ) ;
535+ }
536+
537+ function nonHittableCellSnapshot ( ) : SnapshotState {
538+ return makeSnapshotState ( [
539+ {
540+ index : 0 ,
541+ depth : 0 ,
542+ type : 'XCUIElementTypeOther' ,
543+ label : 'Settings list' ,
544+ rect : { x : 10 , y : 20 , width : 300 , height : 80 } ,
545+ hittable : true ,
546+ } ,
547+ {
548+ index : 1 ,
549+ depth : 1 ,
550+ parentIndex : 0 ,
551+ type : 'XCUIElementTypeCell' ,
552+ label : 'Account' ,
553+ rect : { x : 20 , y : 10 , width : 100 , height : 40 } ,
554+ hittable : false ,
555+ } ,
556+ ] ) ;
557+ }
558+
559+ function nonTouchableGroupSnapshot ( ) : SnapshotState {
560+ return makeSnapshotState ( [
561+ {
562+ index : 0 ,
563+ depth : 0 ,
564+ type : 'XCUIElementTypeOther' ,
565+ label : 'Clickable group' ,
566+ rect : { x : 10 , y : 20 , width : 300 , height : 80 } ,
567+ hittable : true ,
568+ } ,
569+ {
570+ index : 1 ,
571+ depth : 1 ,
572+ parentIndex : 0 ,
573+ type : 'XCUIElementTypeOther' ,
574+ label : 'Decorative group' ,
575+ rect : { x : 30 , y : 40 , width : 60 , height : 20 } ,
576+ hittable : false ,
577+ } ,
578+ ] ) ;
579+ }
580+
425581function snapshotWithOffscreenContent ( ) : SnapshotState {
426582 return makeSnapshotState ( [
427583 {
@@ -495,3 +651,9 @@ function createInteractionDevice(
495651 policy : localCommandPolicy ( ) ,
496652 } ) ;
497653}
654+
655+ async function clickRefE2 ( device : ReturnType < typeof createInteractionDevice > ) {
656+ return await device . interactions . click ( ref ( '@e2' ) , {
657+ session : 'default' ,
658+ } ) ;
659+ }
0 commit comments