@@ -4,7 +4,7 @@ import { parseSelectorChain } from '../../daemon/selectors.ts';
44import { matchesSelector } from '../../daemon/selectors-match.ts' ;
55import { evaluateIsPredicate } from '../../utils/selector-is-predicates.ts' ;
66import { normalizeText } from '../../utils/finders.ts' ;
7- import { extractNodeText } from '../../utils/snapshot-processing.ts' ;
7+ import { extractNodeText , normalizeType } from '../../utils/snapshot-processing.ts' ;
88import type { TouchReferenceFrame } from '../../daemon/touch-reference-frame.ts' ;
99import type { DaemonRequest } from '../../daemon/types.ts' ;
1010import type { Selector , SelectorTerm } from '../../daemon/selectors-parse.ts' ;
@@ -74,6 +74,8 @@ export function resolveMaestroNodeFromSnapshot(
7474 options . index ,
7575 extractMaestroVisibleTextQuery ( selector ) ,
7676 frame ,
77+ false ,
78+ true ,
7779 ) ;
7880 if ( ! target ) {
7981 const index = options . index ?? 0 ;
@@ -93,7 +95,15 @@ export function resolveMaestroFuzzyTextNodeFromSnapshot(
9395 frame : TouchReferenceFrame | undefined ,
9496) : { ok : true ; node : SnapshotNode ; rect : Rect } | { ok : false ; message : string } {
9597 const matches = findMaestroFuzzyTextMatches ( snapshot , query ) ;
96- const target = selectMaestroSnapshotMatch ( snapshot . nodes , matches , undefined , query , frame ) ;
98+ const target = selectMaestroSnapshotMatch (
99+ snapshot . nodes ,
100+ matches ,
101+ undefined ,
102+ query ,
103+ frame ,
104+ false ,
105+ true ,
106+ ) ;
97107 if ( ! target ) {
98108 return { ok : false , message : `Maestro fuzzy text did not match: ${ query } ` } ;
99109 }
@@ -308,6 +318,7 @@ function selectMaestroSnapshotMatch(
308318 visibleTextQuery : string | null ,
309319 frame : TouchReferenceFrame | undefined ,
310320 requireOnScreen = false ,
321+ promoteTapTarget = false ,
311322) : { node : SnapshotNode ; rect : Rect } | null {
312323 const nodeByIndex = buildSnapshotNodeByIndex ( nodes ) ;
313324 const resolved = matches
@@ -321,12 +332,24 @@ function selectMaestroSnapshotMatch(
321332 ? preferOnScreenMatches ( resolved , frame , requireOnScreen )
322333 : resolved ;
323334 if ( index !== undefined ) {
324- return candidates [ index ] ?? null ;
335+ return promoteMaestroSnapshotMatch (
336+ nodes ,
337+ candidates [ index ] ?? null ,
338+ nodeByIndex ,
339+ promoteTapTarget ,
340+ frame ,
341+ ) ;
325342 }
326343 const sorted = candidates . sort ( ( left , right ) =>
327344 compareMaestroSnapshotMatches ( left , right , visibleTextQuery ) ,
328345 ) ;
329- return sorted [ 0 ] ?? null ;
346+ return promoteMaestroSnapshotMatch (
347+ nodes ,
348+ sorted [ 0 ] ?? null ,
349+ nodeByIndex ,
350+ promoteTapTarget ,
351+ frame ,
352+ ) ;
330353}
331354
332355function preferOnScreenMatches (
@@ -407,7 +430,78 @@ function rectArea(rect: Rect): number {
407430}
408431
409432function maestroTapTargetTypeRank ( node : SnapshotNode ) : number {
410- return MAESTRO_TAP_TARGET_TYPE_RANK . get ( node . type ?. toLowerCase ( ) ?? '' ) ?? 3 ;
433+ return MAESTRO_TAP_TARGET_TYPE_RANK . get ( normalizeType ( node . type ?? '' ) ) ?? 3 ;
434+ }
435+
436+ function promoteMaestroSnapshotMatch (
437+ nodes : SnapshotState [ 'nodes' ] ,
438+ match : MaestroResolvedSnapshotMatch | null ,
439+ nodeByIndex : SnapshotNodeByIndex ,
440+ promoteTapTarget : boolean ,
441+ frame : TouchReferenceFrame | undefined ,
442+ ) : { node : SnapshotNode ; rect : Rect } | null {
443+ if ( ! match ) return null ;
444+ if ( ! promoteTapTarget ) {
445+ return { node : match . node , rect : match . rect } ;
446+ }
447+ const ancestor = findMaestroTapAncestor ( nodes , match , nodeByIndex , frame ) ;
448+ return ancestor ?? { node : match . node , rect : match . rect } ;
449+ }
450+
451+ function findMaestroTapAncestor (
452+ nodes : SnapshotState [ 'nodes' ] ,
453+ match : MaestroResolvedSnapshotMatch ,
454+ nodeByIndex : SnapshotNodeByIndex ,
455+ frame : TouchReferenceFrame | undefined ,
456+ ) : { node : SnapshotNode ; rect : Rect } | null {
457+ if ( isActionableMaestroTapTarget ( match . node ) ) return null ;
458+ return findSnapshotAncestor ( nodes , match . node , nodeByIndex , ( ancestor ) => {
459+ if ( ! isActionableMaestroTapTarget ( ancestor ) ) return null ;
460+ const ancestorRect = resolveNodeRect ( nodes , ancestor , nodeByIndex ) ;
461+ if ( ! ancestorRect || ! isUsefulMaestroTapAncestorRect ( match . rect , ancestorRect . rect , frame ) ) {
462+ return null ;
463+ }
464+ return { node : ancestor , rect : ancestorRect . rect } ;
465+ } ) ;
466+ }
467+
468+ function isActionableMaestroTapTarget ( node : SnapshotNode ) : boolean {
469+ const type = normalizeType ( node . type ?? '' ) ;
470+ return (
471+ node . hittable === true ||
472+ type === 'button' ||
473+ type === 'link' ||
474+ type === 'cell' ||
475+ type === 'textfield' ||
476+ type === 'searchfield' ||
477+ type === 'switch' ||
478+ type === 'slider'
479+ ) ;
480+ }
481+
482+ function isUsefulMaestroTapAncestorRect (
483+ matchRect : Rect ,
484+ ancestorRect : Rect ,
485+ frame : TouchReferenceFrame | undefined ,
486+ ) : boolean {
487+ if ( ! rectContains ( ancestorRect , matchRect ) ) return false ;
488+ const ancestorArea = rectArea ( ancestorRect ) ;
489+ const matchArea = rectArea ( matchRect ) ;
490+ if ( matchArea > 0 && ancestorArea > matchArea * 30 ) return false ;
491+ if ( frame ) {
492+ const frameArea = frame . referenceWidth * frame . referenceHeight ;
493+ if ( frameArea > 0 && ancestorArea > frameArea * 0.5 ) return false ;
494+ }
495+ return true ;
496+ }
497+
498+ function rectContains ( container : Rect , child : Rect ) : boolean {
499+ return (
500+ child . x >= container . x &&
501+ child . y >= container . y &&
502+ child . x + child . width <= container . x + container . width &&
503+ child . y + child . height <= container . y + container . height
504+ ) ;
411505}
412506
413507function maestroVisibleTextMatchRank ( node : SnapshotNode , query : string ) : number {
0 commit comments