@@ -18,6 +18,154 @@ function truncate(str, maxLength) {
1818 return str ;
1919}
2020
21+ /**
22+ * Escapes a string for use in CSS selectors
23+ * @param {String } str - The string to escape
24+ * @returns {String } The escaped string
25+ */
26+ function escapeCSSSelector ( str ) {
27+ // Use the CSS.escape method if available
28+ if ( window . CSS && window . CSS . escape ) {
29+ return window . CSS . escape ( str ) ;
30+ }
31+ // Simple fallback for browsers that don't support CSS.escape
32+ return str
33+ . replace ( / [ ! " # $ % & ' ( ) * + , . / : ; < = > ? @ [ \\ \] ^ ` { | } ~ ] / g, '\\$&' )
34+ . replace ( / ^ \d / , '\\3$& ' ) ;
35+ }
36+ function generateSelectorWithShadow ( elm ) {
37+ const selectors = getShadowSelector ( elm ) ;
38+ if ( typeof selectors === 'string' ) {
39+ return selectors ;
40+ } else {
41+ // merge selectors of an array with ,
42+ return selectors . join ( ',' ) . replace ( / , $ / , '' ) ;
43+ }
44+ }
45+
46+ function getShadowSelector ( elm ) {
47+ if ( ! elm ) {
48+ return '' ;
49+ }
50+ let doc = ( elm . getRootNode && elm . getRootNode ( ) ) || document ;
51+ // Not a DOCUMENT_FRAGMENT - shadow DOM
52+ if ( doc . nodeType !== 11 ) {
53+ return getFullPathSelector ( elm ) ;
54+ }
55+
56+ const stack = [ ] ;
57+ while ( doc . nodeType === 11 ) {
58+ if ( ! doc . host ) {
59+ return '' ;
60+ }
61+ stack . unshift ( { elm, doc } ) ;
62+ elm = doc . host ;
63+ doc = elm . getRootNode ( ) ;
64+ }
65+
66+ stack . unshift ( { elm, doc } ) ;
67+ return stack . map ( item => getFullPathSelector ( item . elm ) ) ;
68+ }
69+
70+ function getFullPathSelector ( elm ) {
71+ if ( elm . nodeName === 'HTML' || elm . nodeName === 'BODY' ) {
72+ return elm . nodeName . toLowerCase ( ) ;
73+ }
74+
75+ if ( cache . get ( 'getFullPathSelector' ) === undefined ) {
76+ cache . set ( 'getFullPathSelector' , new WeakMap ( ) ) ;
77+ }
78+
79+ // Check cache first
80+ const sourceCache = cache . get ( 'getFullPathSelector' ) ;
81+ if ( sourceCache . has ( elm ) ) {
82+ return sourceCache . get ( elm ) ;
83+ }
84+
85+ const element = elm ;
86+ const names = [ ] ;
87+ while ( elm . parentElement && elm . nodeName !== 'BODY' ) {
88+ if ( sourceCache . has ( elm ) ) {
89+ names . unshift ( sourceCache . get ( elm ) ) ;
90+ break ;
91+ } else if ( elm . id ) {
92+ // Check if the ID is unique in the document before using it
93+ const escapedId = escapeCSSSelector ( elm . getAttribute ( 'id' ) ) ;
94+ const elementsWithSameId = document . querySelectorAll ( `#${ escapedId } ` ) ;
95+ if ( elementsWithSameId . length === 1 ) {
96+ // ID is unique, safe to use
97+ names . unshift ( '#' + escapedId ) ;
98+ break ;
99+ } else {
100+ // ID is not unique, fallback to position-based selector
101+ let c = 1 ;
102+ let e = elm ;
103+ for ( ; e . previousElementSibling ; e = e . previousElementSibling , c ++ ) {
104+ // Increment counter for each previous sibling
105+ }
106+ names . unshift ( `${ elm . nodeName . toLowerCase ( ) } :nth-child(${ c } )` ) ;
107+ }
108+ } else {
109+ let c = 1 ;
110+ let e = elm ;
111+ for ( ; e . previousElementSibling ; e = e . previousElementSibling , c ++ ) {
112+ // Increment counter for each previous sibling
113+ }
114+ names . unshift ( `${ elm . nodeName . toLowerCase ( ) } :nth-child(${ c } )` ) ;
115+ }
116+ elm = elm . parentElement ;
117+ }
118+
119+ const selector = names . join ( '>' ) ;
120+ sourceCache . set ( element , selector ) ;
121+ return selector ;
122+ }
123+
124+ function getSourceOpt ( element ) {
125+ if ( ! element ) {
126+ return '' ;
127+ }
128+
129+ // Initialize cache if needed
130+ if ( cache . get ( 'getSourceEfficient' ) === undefined ) {
131+ cache . set ( 'getSourceEfficient' , new WeakMap ( ) ) ;
132+ }
133+
134+ // Check cache first
135+ const sourceCache = cache . get ( 'getSourceEfficient' ) ;
136+ if ( sourceCache . has ( element ) ) {
137+ return sourceCache . get ( element ) ;
138+ }
139+
140+ // Compute value if not cached
141+ const tagName = element . nodeName ?. toLowerCase ( ) ;
142+ if ( ! tagName ) {
143+ return '' ;
144+ }
145+
146+ let result ;
147+ try {
148+ const attributes = Array . from ( element . attributes || [ ] )
149+ . filter ( attr => ! attr . name . startsWith ( 'data-percy-' ) )
150+ . map ( attr => `${ attr . name } ="${ attr . value } "` )
151+ . join ( ' ' ) ;
152+ const closingTag = element . children . length ? false : true ;
153+ if ( closingTag ) {
154+ result = `<${ tagName } ${ attributes } >${ element . textContent } </${ tagName } >` ;
155+ } else {
156+ result = attributes ? `<${ tagName } ${ attributes } >` : `<${ tagName } >` ;
157+ }
158+ result = truncate ( result , 300 ) ; // Truncate to 300 characters
159+ // Store in cache
160+ sourceCache . set ( element , result ) ;
161+ } catch ( e ) {
162+ // Handle potential errors (like accessing attributes on non-element nodes)
163+ result = `<${ tagName || 'unknown' } >` ;
164+ }
165+
166+ return result ;
167+ }
168+
21169function getSource ( element ) {
22170 if ( ! element ?. outerHTML ) {
23171 return '' ;
@@ -84,7 +232,11 @@ function DqElement(elm, options = null, spec = {}) {
84232 this . source = null ;
85233 // TODO: es-modules_audit
86234 if ( ! axe . _audit . noHtml ) {
87- this . source = this . spec . source ?? getSource ( this . _element ) ;
235+ if ( axe . _cache . get ( 'runTypeAOpt' ) ) {
236+ this . source = this . spec . source ?? getSourceOpt ( this . _element ) ;
237+ } else {
238+ this . source = this . spec . source ?? getSource ( this . _element ) ;
239+ }
88240 }
89241}
90242
@@ -94,6 +246,9 @@ DqElement.prototype = {
94246 * @return {String }
95247 */
96248 get selector ( ) {
249+ if ( axe . _cache . get ( 'runTypeAOpt' ) ) {
250+ return this . spec . selector || [ generateSelectorWithShadow ( this . element ) ] ;
251+ }
97252 return this . spec . selector || [ getSelector ( this . element , this . _options ) ] ;
98253 } ,
99254
0 commit comments