1414 * limitations under the License.
1515 */
1616
17- import { ariaPropsEqual } from '@isomorphic/ariaSnapshot' ;
17+ import * as aria from '@isomorphic/ariaSnapshot' ;
1818import { escapeRegExp , longestCommonSubstring , normalizeWhiteSpace } from '@isomorphic/stringUtils' ;
1919
2020import { computeBox , getElementComputedStyle , isElementVisible } from './domUtils' ;
2121import * as roleUtils from './roleUtils' ;
2222import { yamlEscapeKeyIfNeeded , yamlEscapeValueIfNeeded } from './yaml' ;
2323
24- import type { AriaProps , AriaRegex , AriaTextValue , AriaRole , AriaTemplateNode } from '@isomorphic/ariaSnapshot' ;
25- import type { Box } from './domUtils' ;
26-
27- // Note: please keep in sync with ariaNodesEqual() below.
28- export type AriaNode = AriaProps & {
29- role : AriaRole | 'fragment' | 'iframe' ;
30- name : string ;
31- ref ?: string ;
32- children : ( AriaNode | string ) [ ] ;
33- element : Element ;
34- box : Box ;
35- receivesPointerEvents : boolean ;
36- props : Record < string , string > ;
37- } ;
38-
39- function ariaNodesEqual ( a : AriaNode , b : AriaNode ) : boolean {
40- if ( a . role !== b . role || a . name !== b . name )
41- return false ;
42- if ( ! ariaPropsEqual ( a , b ) || hasPointerCursor ( a ) !== hasPointerCursor ( b ) )
43- return false ;
44- const aKeys = Object . keys ( a . props ) ;
45- const bKeys = Object . keys ( b . props ) ;
46- return aKeys . length === bKeys . length && aKeys . every ( k => a . props [ k ] === b . props [ k ] ) ;
47- }
48-
4924export type AriaSnapshot = {
50- root : AriaNode ;
25+ root : aria . AriaNode ;
5126 elements : Map < string , Element > ;
5227 refs : Map < Element , string > ;
5328 iframeRefs : string [ ] ;
@@ -105,13 +80,14 @@ export function generateAriaTree(rootElement: Element, publicOptions: AriaTreeOp
10580 const visited = new Set < Node > ( ) ;
10681
10782 const snapshot : AriaSnapshot = {
108- root : { role : 'fragment' , name : '' , children : [ ] , element : rootElement , props : { } , box : computeBox ( rootElement ) , receivesPointerEvents : true } ,
83+ root : { role : 'fragment' , name : '' , children : [ ] , props : { } , box : computeBox ( rootElement ) , receivesPointerEvents : true } ,
10984 elements : new Map < string , Element > ( ) ,
11085 refs : new Map < Element , string > ( ) ,
11186 iframeRefs : [ ] ,
11287 } ;
88+ setAriaNodeElement ( snapshot . root , rootElement ) ;
11389
114- const visit = ( ariaNode : AriaNode , node : Node , parentElementVisible : boolean ) => {
90+ const visit = ( ariaNode : aria . AriaNode , node : Node , parentElementVisible : boolean ) => {
11591 if ( visited . has ( node ) )
11692 return ;
11793 visited . add ( node ) ;
@@ -166,7 +142,7 @@ export function generateAriaTree(rootElement: Element, publicOptions: AriaTreeOp
166142 processElement ( childAriaNode || ariaNode , element , ariaChildren , visible ) ;
167143 } ;
168144
169- function processElement ( ariaNode : AriaNode , element : Element , ariaChildren : Element [ ] , parentElementVisible : boolean ) {
145+ function processElement ( ariaNode : aria . AriaNode , element : Element , ariaChildren : Element [ ] , parentElementVisible : boolean ) {
170146 // Surround every element with spaces for the sake of concatenated text nodes.
171147 const display = getElementComputedStyle ( element ) ?. display || 'inline' ;
172148 const treatAsBlock = ( display !== 'inline' || element . nodeName === 'BR' ) ? ' ' : '' ;
@@ -223,34 +199,34 @@ export function generateAriaTree(rootElement: Element, publicOptions: AriaTreeOp
223199 return snapshot ;
224200}
225201
226- function computeAriaRef ( ariaNode : AriaNode , options : InternalOptions ) {
202+ function computeAriaRef ( ariaNode : aria . AriaNode , options : InternalOptions ) {
227203 if ( options . refs === 'none' )
228204 return ;
229205 if ( options . refs === 'interactable' && ( ! ariaNode . box . visible || ! ariaNode . receivesPointerEvents ) )
230206 return ;
231207
232- let ariaRef : AriaRef | undefined ;
233- ariaRef = ( ariaNode . element as any ) . _ariaRef ;
208+ const element = ariaNodeElement ( ariaNode ) ;
209+ let ariaRef = ( element as any ) . _ariaRef as AriaRef | undefined ;
234210 if ( ! ariaRef || ariaRef . role !== ariaNode . role || ariaRef . name !== ariaNode . name ) {
235211 ariaRef = { role : ariaNode . role , name : ariaNode . name , ref : ( options . refPrefix ?? '' ) + 'e' + ( ++ lastRef ) } ;
236- ( ariaNode . element as any ) . _ariaRef = ariaRef ;
212+ ( element as any ) . _ariaRef = ariaRef ;
237213 }
238214 ariaNode . ref = ariaRef . ref ;
239215}
240216
241- function toAriaNode ( element : Element , options : InternalOptions ) : AriaNode | null {
217+ function toAriaNode ( element : Element , options : InternalOptions ) : aria . AriaNode | null {
242218 const active = element . ownerDocument . activeElement === element ;
243219 if ( element . nodeName === 'IFRAME' ) {
244- const ariaNode : AriaNode = {
220+ const ariaNode : aria . AriaNode = {
245221 role : 'iframe' ,
246222 name : '' ,
247223 children : [ ] ,
248224 props : { } ,
249- element,
250225 box : computeBox ( element ) ,
251226 receivesPointerEvents : true ,
252227 active
253228 } ;
229+ setAriaNodeElement ( ariaNode , element ) ;
254230 computeAriaRef ( ariaNode , options ) ;
255231 return ariaNode ;
256232 }
@@ -267,16 +243,16 @@ function toAriaNode(element: Element, options: InternalOptions): AriaNode | null
267243 if ( role === 'generic' && box . inline && element . childNodes . length === 1 && element . childNodes [ 0 ] . nodeType === Node . TEXT_NODE )
268244 return null ;
269245
270- const result : AriaNode = {
246+ const result : aria . AriaNode = {
271247 role,
272248 name,
273249 children : [ ] ,
274250 props : { } ,
275- element,
276251 box,
277252 receivesPointerEvents,
278253 active
279254 } ;
255+ setAriaNodeElement ( result , element ) ;
280256 computeAriaRef ( result , options ) ;
281257
282258 if ( roleUtils . kAriaCheckedRoles . includes ( role ) )
@@ -305,9 +281,9 @@ function toAriaNode(element: Element, options: InternalOptions): AriaNode | null
305281 return result ;
306282}
307283
308- function normalizeGenericRoles ( node : AriaNode ) {
309- const normalizeChildren = ( node : AriaNode ) => {
310- const result : ( AriaNode | string ) [ ] = [ ] ;
284+ function normalizeGenericRoles ( node : aria . AriaNode ) {
285+ const normalizeChildren = ( node : aria . AriaNode ) => {
286+ const result : ( aria . AriaNode | string ) [ ] = [ ] ;
311287 for ( const child of node . children || [ ] ) {
312288 if ( typeof child === 'string' ) {
313289 result . push ( child ) ;
@@ -328,8 +304,8 @@ function normalizeGenericRoles(node: AriaNode) {
328304 normalizeChildren ( node ) ;
329305}
330306
331- function normalizeStringChildren ( rootA11yNode : AriaNode ) {
332- const flushChildren = ( buffer : string [ ] , normalizedChildren : ( AriaNode | string ) [ ] ) => {
307+ function normalizeStringChildren ( rootA11yNode : aria . AriaNode ) {
308+ const flushChildren = ( buffer : string [ ] , normalizedChildren : ( aria . AriaNode | string ) [ ] ) => {
333309 if ( ! buffer . length )
334310 return ;
335311 const text = normalizeWhiteSpace ( buffer . join ( '' ) ) ;
@@ -338,8 +314,8 @@ function normalizeStringChildren(rootA11yNode: AriaNode) {
338314 buffer . length = 0 ;
339315 } ;
340316
341- const visit = ( ariaNode : AriaNode ) => {
342- const normalizedChildren : ( AriaNode | string ) [ ] = [ ] ;
317+ const visit = ( ariaNode : aria . AriaNode ) => {
318+ const normalizedChildren : ( aria . AriaNode | string ) [ ] = [ ] ;
343319 const buffer : string [ ] = [ ] ;
344320 for ( const child of ariaNode . children || [ ] ) {
345321 if ( typeof child === 'string' ) {
@@ -358,7 +334,7 @@ function normalizeStringChildren(rootA11yNode: AriaNode) {
358334 visit ( rootA11yNode ) ;
359335}
360336
361- function matchesStringOrRegex ( text : string , template : AriaRegex | string | undefined ) : boolean {
337+ function matchesStringOrRegex ( text : string , template : aria . AriaRegex | string | undefined ) : boolean {
362338 if ( ! template )
363339 return true ;
364340 if ( ! text )
@@ -368,7 +344,7 @@ function matchesStringOrRegex(text: string, template: AriaRegex | string | undef
368344 return ! ! text . match ( new RegExp ( template . pattern ) ) ;
369345}
370346
371- function matchesTextValue ( text : string , template : AriaTextValue | undefined ) {
347+ function matchesTextValue ( text : string , template : aria . AriaTextValue | undefined ) {
372348 if ( ! template ?. normalized )
373349 return true ;
374350 if ( ! text )
@@ -387,7 +363,7 @@ function matchesTextValue(text: string, template: AriaTextValue | undefined) {
387363
388364const cachedRegexSymbol = Symbol ( 'cachedRegex' ) ;
389365
390- function cachedRegex ( template : AriaTextValue ) : RegExp | null {
366+ function cachedRegex ( template : aria . AriaTextValue ) : RegExp | null {
391367 if ( ( template as any ) [ cachedRegexSymbol ] !== undefined )
392368 return ( template as any ) [ cachedRegexSymbol ] ;
393369
@@ -408,7 +384,7 @@ export type MatcherReceived = {
408384 regex : string ;
409385} ;
410386
411- export function matchesExpectAriaTemplate ( rootElement : Element , template : AriaTemplateNode ) : { matches : AriaNode [ ] , received : MatcherReceived } {
387+ export function matchesExpectAriaTemplate ( rootElement : Element , template : aria . AriaTemplateNode ) : { matches : aria . AriaNode [ ] , received : MatcherReceived } {
412388 const snapshot = generateAriaTree ( rootElement , { mode : 'expect' } ) ;
413389 const matches = matchesNodeDeep ( snapshot . root , template , false , false ) ;
414390 return {
@@ -420,13 +396,13 @@ export function matchesExpectAriaTemplate(rootElement: Element, template: AriaTe
420396 } ;
421397}
422398
423- export function getAllElementsMatchingExpectAriaTemplate ( rootElement : Element , template : AriaTemplateNode ) : Element [ ] {
399+ export function getAllElementsMatchingExpectAriaTemplate ( rootElement : Element , template : aria . AriaTemplateNode ) : Element [ ] {
424400 const root = generateAriaTree ( rootElement , { mode : 'expect' } ) . root ;
425401 const matches = matchesNodeDeep ( root , template , true , false ) ;
426- return matches . map ( n => n . element ) ;
402+ return matches . map ( n => ariaNodeElement ( n ) ) ;
427403}
428404
429- function matchesNode ( node : AriaNode | string , template : AriaTemplateNode , isDeepEqual : boolean ) : boolean {
405+ function matchesNode ( node : aria . AriaNode | string , template : aria . AriaTemplateNode , isDeepEqual : boolean ) : boolean {
430406 if ( typeof node === 'string' && template . kind === 'text' )
431407 return matchesTextValue ( node , template . text ) ;
432408
@@ -462,7 +438,7 @@ function matchesNode(node: AriaNode | string, template: AriaTemplateNode, isDeep
462438 return containsList ( node . children || [ ] , template . children || [ ] ) ;
463439}
464440
465- function listEqual ( children : ( AriaNode | string ) [ ] , template : AriaTemplateNode [ ] , isDeepEqual : boolean ) : boolean {
441+ function listEqual ( children : ( aria . AriaNode | string ) [ ] , template : aria . AriaTemplateNode [ ] , isDeepEqual : boolean ) : boolean {
466442 if ( template . length !== children . length )
467443 return false ;
468444 for ( let i = 0 ; i < template . length ; ++ i ) {
@@ -472,7 +448,7 @@ function listEqual(children: (AriaNode | string)[], template: AriaTemplateNode[]
472448 return true ;
473449}
474450
475- function containsList ( children : ( AriaNode | string ) [ ] , template : AriaTemplateNode [ ] ) : boolean {
451+ function containsList ( children : ( aria . AriaNode | string ) [ ] , template : aria . AriaTemplateNode [ ] ) : boolean {
476452 if ( template . length > children . length )
477453 return false ;
478454 const cc = children . slice ( ) ;
@@ -490,9 +466,9 @@ function containsList(children: (AriaNode | string)[], template: AriaTemplateNod
490466 return true ;
491467}
492468
493- function matchesNodeDeep ( root : AriaNode , template : AriaTemplateNode , collectAll : boolean , isDeepEqual : boolean ) : AriaNode [ ] {
494- const results : AriaNode [ ] = [ ] ;
495- const visit = ( node : AriaNode | string , parent : AriaNode | null ) : boolean => {
469+ function matchesNodeDeep ( root : aria . AriaNode , template : aria . AriaTemplateNode , collectAll : boolean , isDeepEqual : boolean ) : aria . AriaNode [ ] {
470+ const results : aria . AriaNode [ ] = [ ] ;
471+ const visit = ( node : aria . AriaNode | string , parent : aria . AriaNode | null ) : boolean => {
496472 if ( matchesNode ( node , template , isDeepEqual ) ) {
497473 const result = typeof node === 'string' ? parent : node ;
498474 if ( result )
@@ -511,7 +487,7 @@ function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode, collectAll:
511487 return results ;
512488}
513489
514- function buildByRefMap ( root : AriaNode | undefined , map : Map < string | undefined , AriaNode > = new Map ( ) ) : Map < string | undefined , AriaNode > {
490+ function buildByRefMap ( root : aria . AriaNode | undefined , map : Map < string | undefined , aria . AriaNode > = new Map ( ) ) : Map < string | undefined , aria . AriaNode > {
515491 if ( root ?. ref )
516492 map . set ( root . ref , root ) ;
517493 for ( const child of root ?. children || [ ] ) {
@@ -521,13 +497,13 @@ function buildByRefMap(root: AriaNode | undefined, map: Map<string | undefined,
521497 return map ;
522498}
523499
524- function compareSnapshots ( ariaSnapshot : AriaSnapshot , previousSnapshot : AriaSnapshot | undefined ) : Map < AriaNode , 'skip' | 'same' | 'changed' > {
500+ function compareSnapshots ( ariaSnapshot : AriaSnapshot , previousSnapshot : AriaSnapshot | undefined ) : Map < aria . AriaNode , 'skip' | 'same' | 'changed' > {
525501 const previousByRef = buildByRefMap ( previousSnapshot ?. root ) ;
526- const result = new Map < AriaNode , 'skip' | 'same' | 'changed' > ( ) ;
502+ const result = new Map < aria . AriaNode , 'skip' | 'same' | 'changed' > ( ) ;
527503
528504 // Returns whether ariaNode is the same as previousNode.
529- const visit = ( ariaNode : AriaNode , previousNode : AriaNode | undefined ) : boolean => {
530- let same : boolean = ariaNode . children . length === previousNode ?. children . length && ariaNodesEqual ( ariaNode , previousNode ) ;
505+ const visit = ( ariaNode : aria . AriaNode , previousNode : aria . AriaNode | undefined ) : boolean => {
506+ let same : boolean = ariaNode . children . length === previousNode ?. children . length && aria . ariaNodesEqual ( ariaNode , previousNode ) ;
531507 let canBeSkipped = same ;
532508
533509 for ( let childIndex = 0 ; childIndex < ariaNode . children . length ; childIndex ++ ) {
@@ -558,10 +534,10 @@ function compareSnapshots(ariaSnapshot: AriaSnapshot, previousSnapshot: AriaSnap
558534}
559535
560536// Chooses only the changed parts of the snapshot and returns them as new roots.
561- function filterSnapshotDiff ( nodes : ( AriaNode | string ) [ ] , statusMap : Map < AriaNode , 'skip' | 'same' | 'changed' > ) : ( AriaNode | string ) [ ] {
562- const result : ( AriaNode | string ) [ ] = [ ] ;
537+ function filterSnapshotDiff ( nodes : ( aria . AriaNode | string ) [ ] , statusMap : Map < aria . AriaNode , 'skip' | 'same' | 'changed' > ) : ( aria . AriaNode | string ) [ ] {
538+ const result : ( aria . AriaNode | string ) [ ] = [ ] ;
563539
564- const visit = ( ariaNode : AriaNode ) => {
540+ const visit = ( ariaNode : aria . AriaNode ) => {
565541 const status = statusMap . get ( ariaNode ) ;
566542 if ( status === 'same' ) {
567543 // No need to render unchanged root at all.
@@ -605,7 +581,7 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTr
605581 lines . push ( indent + '- text: ' + escaped ) ;
606582 } ;
607583
608- const createKey = ( ariaNode : AriaNode , renderCursorPointer : boolean ) : string => {
584+ const createKey = ( ariaNode : aria . AriaNode , renderCursorPointer : boolean ) : string => {
609585 let key = ariaNode . role ;
610586 // Yaml has a limit of 1024 characters per key, and we leave some space for role and attributes.
611587 if ( ariaNode . name && ariaNode . name . length <= 900 ) {
@@ -636,17 +612,17 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTr
636612
637613 if ( ariaNode . ref ) {
638614 key += ` [ref=${ ariaNode . ref } ]` ;
639- if ( renderCursorPointer && hasPointerCursor ( ariaNode ) )
615+ if ( renderCursorPointer && aria . hasPointerCursor ( ariaNode ) )
640616 key += ' [cursor=pointer]' ;
641617 }
642618 return key ;
643619 } ;
644620
645- const getSingleInlinedTextChild = ( ariaNode : AriaNode | undefined ) : string | undefined => {
621+ const getSingleInlinedTextChild = ( ariaNode : aria . AriaNode | undefined ) : string | undefined => {
646622 return ariaNode ?. children . length === 1 && typeof ariaNode . children [ 0 ] === 'string' && ! Object . keys ( ariaNode . props ) . length ? ariaNode . children [ 0 ] : undefined ;
647623 } ;
648624
649- const visit = ( ariaNode : AriaNode , indent : string , renderCursorPointer : boolean ) => {
625+ const visit = ( ariaNode : aria . AriaNode , indent : string , renderCursorPointer : boolean ) => {
650626 // Replace the whole subtree with a single reference when possible.
651627 if ( statusMap . get ( ariaNode ) === 'same' && ariaNode . ref ) {
652628 lines . push ( indent + `- ref=${ ariaNode . ref } [unchanged]` ) ;
@@ -675,7 +651,7 @@ export function renderAriaTree(ariaSnapshot: AriaSnapshot, publicOptions: AriaTr
675651 lines . push ( indent + ' - /' + name + ': ' + yamlEscapeValueIfNeeded ( value ) ) ;
676652
677653 const childIndent = indent + ' ' ;
678- const inCursorPointer = ! ! ariaNode . ref && renderCursorPointer && hasPointerCursor ( ariaNode ) ;
654+ const inCursorPointer = ! ! ariaNode . ref && renderCursorPointer && aria . hasPointerCursor ( ariaNode ) ;
679655 for ( const child of ariaNode . children ) {
680656 if ( typeof child === 'string' )
681657 visitText ( includeText ( ariaNode , child ) ? child : '' , childIndent ) ;
@@ -734,7 +710,7 @@ function convertToBestGuessRegex(text: string): string {
734710 return String ( new RegExp ( pattern ) ) ;
735711}
736712
737- function textContributesInfo ( node : AriaNode , text : string ) : boolean {
713+ function textContributesInfo ( node : aria . AriaNode , text : string ) : boolean {
738714 if ( ! text . length )
739715 return false ;
740716
@@ -752,6 +728,17 @@ function textContributesInfo(node: AriaNode, text: string): boolean {
752728 return filtered . trim ( ) . length / text . length > 0.1 ;
753729}
754730
755- function hasPointerCursor ( ariaNode : AriaNode ) : boolean {
756- return ariaNode . box . cursor === 'pointer' ;
731+ const elementSymbol = Symbol ( 'element' ) ;
732+
733+ function ariaNodeElement ( ariaNode : aria . AriaNode ) : Element {
734+ return ( ariaNode as any ) [ elementSymbol ] ;
735+ }
736+
737+ function setAriaNodeElement ( ariaNode : aria . AriaNode , element : Element ) {
738+ ( ariaNode as any ) [ elementSymbol ] = element ;
739+ }
740+
741+ export function findNewElement ( from : aria . AriaNode | undefined , to : aria . AriaNode ) : Element | undefined {
742+ const node = aria . findNewNode ( from , to ) ;
743+ return node ? ariaNodeElement ( node ) : undefined ;
757744}
0 commit comments