1414 * limitations under the License.
1515 */
1616
17- import { cssEscape , escapeForAttributeSelector , escapeForTextSelector , escapeRegExp , quoteCSSAttributeValue } from '@isomorphic/stringUtils' ;
17+ import { escapeForAttributeSelector , escapeForTextSelector , escapeRegExp , quoteCSSAttributeValue } from '@isomorphic/stringUtils' ;
1818
1919import { closestCrossShadow , isElementVisible , isInsideScope , parentElementOrShadowHost } from './domUtils' ;
2020import { beginAriaCaches , endAriaCaches , getAriaRole , getElementAccessibleName } from './roleUtils' ;
@@ -245,13 +245,13 @@ function buildNoTextCandidates(injectedScript: InjectedScript, element: Element,
245245 candidates . push ( { engine : 'css' , selector : makeSelectorForId ( idAttr ) , score : kCSSIdScore } ) ;
246246 }
247247
248- candidates . push ( { engine : 'css' , selector : cssEscape ( element . nodeName . toLowerCase ( ) ) , score : kCSSTagNameScore } ) ;
248+ candidates . push ( { engine : 'css' , selector : escapeNodeName ( element ) , score : kCSSTagNameScore } ) ;
249249 }
250250
251251 if ( element . nodeName === 'IFRAME' ) {
252252 for ( const attribute of [ 'name' , 'title' ] ) {
253253 if ( element . getAttribute ( attribute ) )
254- candidates . push ( { engine : 'css' , selector : `${ cssEscape ( element . nodeName . toLowerCase ( ) ) } [${ attribute } =${ quoteCSSAttributeValue ( element . getAttribute ( attribute ) ! ) } ]` , score : kIframeByAttributeScore } ) ;
254+ candidates . push ( { engine : 'css' , selector : `${ escapeNodeName ( element ) } [${ attribute } =${ quoteCSSAttributeValue ( element . getAttribute ( attribute ) ! ) } ]` , score : kIframeByAttributeScore } ) ;
255255 }
256256
257257 // Locate by testId via CSS selector.
@@ -288,15 +288,15 @@ function buildNoTextCandidates(injectedScript: InjectedScript, element: Element,
288288 candidates . push ( { engine : 'internal:role' , selector : ariaRole , score : kRoleWithoutNameScore } ) ;
289289
290290 if ( element . getAttribute ( 'name' ) && [ 'BUTTON' , 'FORM' , 'FIELDSET' , 'FRAME' , 'IFRAME' , 'INPUT' , 'KEYGEN' , 'OBJECT' , 'OUTPUT' , 'SELECT' , 'TEXTAREA' , 'MAP' , 'META' , 'PARAM' ] . includes ( element . nodeName ) )
291- candidates . push ( { engine : 'css' , selector : `${ cssEscape ( element . nodeName . toLowerCase ( ) ) } [name=${ quoteCSSAttributeValue ( element . getAttribute ( 'name' ) ! ) } ]` , score : kCSSInputTypeNameScore } ) ;
291+ candidates . push ( { engine : 'css' , selector : `${ escapeNodeName ( element ) } [name=${ quoteCSSAttributeValue ( element . getAttribute ( 'name' ) ! ) } ]` , score : kCSSInputTypeNameScore } ) ;
292292
293293 if ( [ 'INPUT' , 'TEXTAREA' ] . includes ( element . nodeName ) && element . getAttribute ( 'type' ) !== 'hidden' ) {
294294 if ( element . getAttribute ( 'type' ) )
295- candidates . push ( { engine : 'css' , selector : `${ cssEscape ( element . nodeName . toLowerCase ( ) ) } [type=${ quoteCSSAttributeValue ( element . getAttribute ( 'type' ) ! ) } ]` , score : kCSSInputTypeNameScore } ) ;
295+ candidates . push ( { engine : 'css' , selector : `${ escapeNodeName ( element ) } [type=${ quoteCSSAttributeValue ( element . getAttribute ( 'type' ) ! ) } ]` , score : kCSSInputTypeNameScore } ) ;
296296 }
297297
298298 if ( [ 'INPUT' , 'TEXTAREA' , 'SELECT' ] . includes ( element . nodeName ) && element . getAttribute ( 'type' ) !== 'hidden' )
299- candidates . push ( { engine : 'css' , selector : cssEscape ( element . nodeName . toLowerCase ( ) ) , score : kCSSInputTypeNameScore + 1 } ) ;
299+ candidates . push ( { engine : 'css' , selector : escapeNodeName ( element ) , score : kCSSInputTypeNameScore + 1 } ) ;
300300
301301 penalizeScoreForLength ( [ candidates ] ) ;
302302 return candidates ;
@@ -330,7 +330,7 @@ function buildTextCandidates(injectedScript: InjectedScript, element: Element, i
330330 for ( const alternative of textAlternatives )
331331 candidates . push ( [ { engine : 'internal:text' , selector : escapeForTextSelector ( alternative . text , false ) , score : kTextScore - alternative . scoreBonus } ] ) ;
332332 }
333- const cssToken : SelectorToken = { engine : 'css' , selector : cssEscape ( element . nodeName . toLowerCase ( ) ) , score : kCSSTagNameScore } ;
333+ const cssToken : SelectorToken = { engine : 'css' , selector : escapeNodeName ( element ) , score : kCSSTagNameScore } ;
334334 for ( const alternative of textAlternatives )
335335 candidates . push ( [ cssToken , { engine : 'internal:has-text' , selector : escapeForTextSelector ( alternative . text , false ) , score : kTextScore - alternative . scoreBonus } ] ) ;
336336 if ( text . length <= 80 ) {
@@ -363,7 +363,7 @@ function buildTextCandidates(injectedScript: InjectedScript, element: Element, i
363363}
364364
365365function makeSelectorForId ( id : string ) {
366- return / ^ [ a - z A - Z ] [ a - z A - Z 0 - 9 \- \_ ] + $ / . test ( id ) ? '#' + id : `[id=" ${ cssEscape ( id ) } " ]` ;
366+ return / ^ [ a - z A - Z ] [ a - z A - Z 0 - 9 \- \_ ] + $ / . test ( id ) ? '#' + id : `[id=${ quoteCSSAttributeValue ( id ) } ]` ;
367367}
368368
369369function hasCSSIdToken ( tokens : SelectorToken [ ] ) {
@@ -395,8 +395,6 @@ function cssFallback(injectedScript: InjectedScript, targetElement: Element, opt
395395 }
396396
397397 for ( let element : Element | undefined = targetElement ; element && element !== root ; element = parentElementOrShadowHost ( element ) ) {
398- const nodeName = element . nodeName . toLowerCase ( ) ;
399-
400398 let bestTokenForLevel : string = '' ;
401399
402400 // Element ID is the strongest signal, use it.
@@ -411,9 +409,9 @@ function cssFallback(injectedScript: InjectedScript, targetElement: Element, opt
411409 const parent = element . parentNode as ( Element | ShadowRoot ) ;
412410
413411 // Combine class names until unique.
414- const classes = [ ...element . classList ] ;
412+ const classes = [ ...element . classList ] . map ( escapeClassName ) ;
415413 for ( let i = 0 ; i < classes . length ; ++ i ) {
416- const token = '.' + cssEscape ( classes . slice ( 0 , i + 1 ) . join ( '.' ) ) ;
414+ const token = '.' + classes . slice ( 0 , i + 1 ) . join ( '.' ) ;
417415 const selector = uniqueCSSSelector ( token ) ;
418416 if ( selector )
419417 return makeStrict ( selector ) ;
@@ -428,15 +426,16 @@ function cssFallback(injectedScript: InjectedScript, targetElement: Element, opt
428426 // Ordinal is the weakest signal.
429427 if ( parent ) {
430428 const siblings = [ ...parent . children ] ;
431- const sameTagSiblings = siblings . filter ( sibling => ( sibling ) . nodeName . toLowerCase ( ) === nodeName ) ;
432- const token = sameTagSiblings . indexOf ( element ) === 0 ? cssEscape ( nodeName ) : `${ cssEscape ( nodeName ) } :nth-child(${ 1 + siblings . indexOf ( element ) } )` ;
429+ const nodeName = element . nodeName ;
430+ const sameTagSiblings = siblings . filter ( sibling => sibling . nodeName === nodeName ) ;
431+ const token = sameTagSiblings . indexOf ( element ) === 0 ? escapeNodeName ( element ) : `${ escapeNodeName ( element ) } :nth-child(${ 1 + siblings . indexOf ( element ) } )` ;
433432 const selector = uniqueCSSSelector ( token ) ;
434433 if ( selector )
435434 return makeStrict ( selector ) ;
436435 if ( ! bestTokenForLevel )
437436 bestTokenForLevel = token ;
438437 } else if ( ! bestTokenForLevel ) {
439- bestTokenForLevel = cssEscape ( nodeName ) ;
438+ bestTokenForLevel = escapeNodeName ( element ) ;
440439 }
441440 tokens . unshift ( bestTokenForLevel ) ;
442441 }
@@ -572,3 +571,13 @@ function suitableTextAlternatives(text: string) {
572571
573572 return result ;
574573}
574+
575+ function escapeNodeName ( node : Node ) : string {
576+ // We are escaping it for document.querySelectorAll, not for usage in CSS file.
577+ return node . nodeName . toLocaleLowerCase ( ) . replace ( / [: \. ] / g, char => '\\' + char ) ;
578+ }
579+
580+ function escapeClassName ( className : string ) : string {
581+ // We are escaping it for document.querySelectorAll, not for usage in CSS file.
582+ return className . replace ( / [: \. ] / g, char => '\\' + char ) ;
583+ }
0 commit comments