@@ -48,9 +48,47 @@ type AriaRef = {
4848
4949let lastRef = 0 ;
5050
51- export type AriaTreeOptions = { forAI ?: boolean , refPrefix ?: string , refs ?: boolean , visibleOnly ?: boolean } ;
51+ export type AriaTreeOptions = {
52+ mode : 'ai' | 'expect' | 'codegen' | 'autoexpect' ;
53+ refPrefix ?: string ;
54+ } ;
55+
56+ type InternalOptions = {
57+ visibility : 'aria' | 'ariaOrVisible' | 'ariaAndVisible' ,
58+ refs : 'all' | 'interactable' | 'none' ,
59+ refPrefix ?: string ,
60+ includeGenericRole ?: boolean ,
61+ renderCursorPointer ?: boolean ,
62+ renderActive ?: boolean ,
63+ renderStringsAsRegex ?: boolean ,
64+ } ;
5265
53- export function generateAriaTree ( rootElement : Element , options ?: AriaTreeOptions ) : AriaSnapshot {
66+ function toInternalOptions ( options : AriaTreeOptions ) : InternalOptions {
67+ if ( options . mode === 'ai' ) {
68+ // For AI consumption.
69+ return {
70+ visibility : 'ariaOrVisible' ,
71+ refs : 'interactable' ,
72+ refPrefix : options . refPrefix ,
73+ includeGenericRole : true ,
74+ renderActive : true ,
75+ renderCursorPointer : true ,
76+ } ;
77+ }
78+ if ( options . mode === 'autoexpect' ) {
79+ // To auto-generate assertions on visible elements.
80+ return { visibility : 'ariaAndVisible' , refs : 'all' } ;
81+ }
82+ if ( options . mode === 'codegen' ) {
83+ // To generate aria assertion with regex heurisitcs.
84+ return { visibility : 'aria' , refs : 'none' , renderStringsAsRegex : true } ;
85+ }
86+ // To match aria snapshot.
87+ return { visibility : 'aria' , refs : 'none' } ;
88+ }
89+
90+ export function generateAriaTree ( rootElement : Element , publicOptions : AriaTreeOptions ) : AriaSnapshot {
91+ const options = toInternalOptions ( publicOptions ) ;
5492 const visited = new Set < Node > ( ) ;
5593
5694 const snapshot : AriaSnapshot = {
@@ -79,8 +117,16 @@ export function generateAriaTree(rootElement: Element, options?: AriaTreeOptions
79117 return ;
80118
81119 const element = node as Element ;
82- const isElementHiddenForAria = roleUtils . isElementHiddenForAria ( element ) ;
83- if ( isElementHiddenForAria && ! options ?. forAI )
120+ const isElementVisibleForAria = ! roleUtils . isElementHiddenForAria ( element ) ;
121+ let visible = isElementVisibleForAria ;
122+ if ( options . visibility === 'ariaOrVisible' )
123+ visible = isElementVisibleForAria || isElementVisible ( element ) ;
124+ if ( options . visibility === 'ariaAndVisible' )
125+ visible = isElementVisibleForAria && isElementVisible ( element ) ;
126+
127+ // Optimization: if we only consider aria visibility, we can skip child elements because
128+ // they will not be visible for aria as well.
129+ if ( options . visibility === 'aria' && ! visible )
84130 return ;
85131
86132 const ariaChildren : Element [ ] = [ ] ;
@@ -93,7 +139,6 @@ export function generateAriaTree(rootElement: Element, options?: AriaTreeOptions
93139 }
94140 }
95141
96- const visible = options ?. visibleOnly ? isElementVisible ( element ) : ! isElementHiddenForAria || isElementVisible ( element ) ;
97142 const childAriaNode = visible ? toAriaNode ( element , options ) : null ;
98143 if ( childAriaNode ) {
99144 if ( childAriaNode . ref ) {
@@ -157,36 +202,39 @@ export function generateAriaTree(rootElement: Element, options?: AriaTreeOptions
157202 return snapshot ;
158203}
159204
160- function ariaRef ( element : Element , role : string , name : string , options ?: AriaTreeOptions ) : string | undefined {
161- if ( ! options ?. forAI && ! options ?. refs )
162- return undefined ;
205+ function computeAriaRef ( ariaNode : AriaNode , options : InternalOptions ) {
206+ if ( options . refs === 'none' )
207+ return ;
208+ if ( options . refs === 'interactable' && ( ! ariaNode . box . visible || ! ariaNode . receivesPointerEvents ) )
209+ return ;
163210
164211 let ariaRef : AriaRef | undefined ;
165- ariaRef = ( element as any ) . _ariaRef ;
166- if ( ! ariaRef || ariaRef . role !== role || ariaRef . name !== name ) {
167- ariaRef = { role, name, ref : ( options ? .refPrefix ?? '' ) + 'e' + ( ++ lastRef ) } ;
168- ( element as any ) . _ariaRef = ariaRef ;
212+ ariaRef = ( ariaNode . element as any ) . _ariaRef ;
213+ if ( ! ariaRef || ariaRef . role !== ariaNode . role || ariaRef . name !== ariaNode . name ) {
214+ ariaRef = { role : ariaNode . role , name : ariaNode . name , ref : ( options . refPrefix ?? '' ) + 'e' + ( ++ lastRef ) } ;
215+ ( ariaNode . element as any ) . _ariaRef = ariaRef ;
169216 }
170- return ariaRef . ref ;
217+ ariaNode . ref = ariaRef . ref ;
171218}
172219
173- function toAriaNode ( element : Element , options ?: AriaTreeOptions ) : AriaNode | null {
220+ function toAriaNode ( element : Element , options : InternalOptions ) : AriaNode | null {
174221 const active = element . ownerDocument . activeElement === element ;
175222 if ( element . nodeName === 'IFRAME' ) {
176- return {
223+ const ariaNode : AriaNode = {
177224 role : 'iframe' ,
178225 name : '' ,
179- ref : ariaRef ( element , 'iframe' , '' , options ) ,
180226 children : [ ] ,
181227 props : { } ,
182228 element,
183229 box : box ( element ) ,
184230 receivesPointerEvents : true ,
185231 active
186232 } ;
233+ computeAriaRef ( ariaNode , options ) ;
234+ return ariaNode ;
187235 }
188236
189- const defaultRole = options ?. forAI ? 'generic' : null ;
237+ const defaultRole = options . includeGenericRole ? 'generic' : null ;
190238 const role = roleUtils . getAriaRole ( element ) ?? defaultRole ;
191239 if ( ! role || role === 'presentation' || role === 'none' )
192240 return null ;
@@ -197,14 +245,14 @@ function toAriaNode(element: Element, options?: AriaTreeOptions): AriaNode | nul
197245 const result : AriaNode = {
198246 role,
199247 name,
200- ref : ariaRef ( element , role , name , options ) ,
201248 children : [ ] ,
202249 props : { } ,
203250 element,
204251 box : box ( element ) ,
205252 receivesPointerEvents,
206253 active
207254 } ;
255+ computeAriaRef ( result , options ) ;
208256
209257 if ( roleUtils . kAriaCheckedRoles . includes ( role ) )
210258 result . checked = roleUtils . getAriaChecked ( element ) ;
@@ -245,7 +293,7 @@ function normalizeGenericRoles(node: AriaNode) {
245293 }
246294
247295 // Only remove generic that encloses one element, logical grouping still makes sense, even if it is not ref-able.
248- const removeSelf = node . role === 'generic' && result . length <= 1 && result . every ( c => typeof c !== 'string' && receivesPointerEvents ( c ) ) ;
296+ const removeSelf = node . role === 'generic' && result . length <= 1 && result . every ( c => typeof c !== 'string' && ! ! c . ref ) ;
249297 if ( removeSelf )
250298 return result ;
251299 node . children = result ;
@@ -308,20 +356,20 @@ export type MatcherReceived = {
308356 regex : string ;
309357} ;
310358
311- export function matchesAriaTree ( rootElement : Element , template : AriaTemplateNode ) : { matches : AriaNode [ ] , received : MatcherReceived } {
312- const snapshot = generateAriaTree ( rootElement ) ;
359+ export function matchesExpectAriaTemplate ( rootElement : Element , template : AriaTemplateNode ) : { matches : AriaNode [ ] , received : MatcherReceived } {
360+ const snapshot = generateAriaTree ( rootElement , { mode : 'expect' } ) ;
313361 const matches = matchesNodeDeep ( snapshot . root , template , false , false ) ;
314362 return {
315363 matches,
316364 received : {
317- raw : renderAriaTree ( snapshot , { mode : 'raw ' } ) ,
318- regex : renderAriaTree ( snapshot , { mode : 'regex ' } ) ,
365+ raw : renderAriaTree ( snapshot , { mode : 'expect ' } ) ,
366+ regex : renderAriaTree ( snapshot , { mode : 'codegen ' } ) ,
319367 }
320368 } ;
321369}
322370
323- export function getAllByAria ( rootElement : Element , template : AriaTemplateNode ) : Element [ ] {
324- const root = generateAriaTree ( rootElement ) . root ;
371+ export function getAllElementsMatchingExpectAriaTemplate ( rootElement : Element , template : AriaTemplateNode ) : Element [ ] {
372+ const root = generateAriaTree ( rootElement , { mode : 'expect' } ) . root ;
325373 const matches = matchesNodeDeep ( root , template , true , false ) ;
326374 return matches . map ( n => n . element ) ;
327375}
@@ -411,10 +459,11 @@ function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode, collectAll:
411459 return results ;
412460}
413461
414- export function renderAriaTree ( ariaSnapshot : AriaSnapshot , options ?: { mode ?: 'raw' | 'regex' , forAI ?: boolean , refs ?: boolean } ) : string {
462+ export function renderAriaTree ( ariaSnapshot : AriaSnapshot , publicOptions : AriaTreeOptions ) : string {
463+ const options = toInternalOptions ( publicOptions ) ;
415464 const lines : string [ ] = [ ] ;
416- const includeText = options ?. mode === 'regex' ? textContributesInfo : ( ) => true ;
417- const renderString = options ?. mode === 'regex' ? convertToBestGuessRegex : ( str : string ) => str ;
465+ const includeText = options . renderStringsAsRegex ? textContributesInfo : ( ) => true ;
466+ const renderString = options . renderStringsAsRegex ? convertToBestGuessRegex : ( str : string ) => str ;
418467 const visit = ( ariaNode : AriaNode | string , parentAriaNode : AriaNode | null , indent : string ) => {
419468 if ( typeof ariaNode === 'string' ) {
420469 if ( parentAriaNode && ! includeText ( parentAriaNode , ariaNode ) )
@@ -442,7 +491,7 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, options?: { mode?: 'r
442491 key += ` [disabled]` ;
443492 if ( ariaNode . expanded )
444493 key += ` [expanded]` ;
445- if ( ariaNode . active && options ?. forAI )
494+ if ( ariaNode . active && options . renderActive )
446495 key += ` [active]` ;
447496 if ( ariaNode . level )
448497 key += ` [level=${ ariaNode . level } ]` ;
@@ -453,10 +502,9 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, options?: { mode?: 'r
453502 if ( ariaNode . selected === true )
454503 key += ` [selected]` ;
455504
456- const includeRef = ( options ?. forAI && receivesPointerEvents ( ariaNode ) ) || options ?. refs ;
457- if ( includeRef && ariaNode . ref ) {
505+ if ( ariaNode . ref ) {
458506 key += ` [ref=${ ariaNode . ref } ]` ;
459- if ( hasPointerCursor ( ariaNode ) )
507+ if ( options . renderCursorPointer && hasPointerCursor ( ariaNode ) )
460508 key += ' [cursor=pointer]' ;
461509 }
462510
@@ -548,10 +596,6 @@ function textContributesInfo(node: AriaNode, text: string): boolean {
548596 return filtered . trim ( ) . length / text . length > 0.1 ;
549597}
550598
551- function receivesPointerEvents ( ariaNode : AriaNode ) : boolean {
552- return ariaNode . box . visible && ariaNode . receivesPointerEvents ;
553- }
554-
555599function hasPointerCursor ( ariaNode : AriaNode ) : boolean {
556600 return ariaNode . box . style ?. cursor === 'pointer' ;
557601}
0 commit comments