A selector containing a hex escape with its terminating whitespace works when it is the last compound in the selector, but silently returns no matches (no SyntaxError) as soon as a combinator follows. Browsers match all of these.
This is exactly the form CSS.escape() produces for identifiers starting with a digit (CSS.escape('2o') === '\\32 o'), so any code that composes selectors with CSS.escape and runs them under jsdom gets silently-empty results.
Repro
const { JSDOM } = require('jsdom');
const document = new JSDOM('<div id="2o"><i>a</i></div>').window.document;
document.querySelectorAll('div#\\32 o'); // 1 ✅
document.querySelectorAll('#\\32 o i'); // 0 ❌ Chrome: 1
document.querySelectorAll('div#\\32 o i'); // 0 ❌ Chrome: 1
document.querySelectorAll('div#\\32 o > i'); // 0 ❌ Chrome: 1
Single-character escapes are unaffected — with <div id=":2o"><i>b</i></div>:
document.querySelectorAll('#\\3a 2o'); // 1 ✅
document.querySelectorAll('#\\3a 2o i'); // 0 ❌ Chrome: 1
document.querySelectorAll('#\\:2o i'); // 1 ✅ (char escape + combinator is fine)
So the failure is specific to hex escapes, whose whitespace terminator presumably collides with descendant-combinator whitespace handling somewhere between normalization and the compiled matcher.
Expected
Per CSS Syntax §4.3.7 (consume an escaped code point), the whitespace following a hex escape is consumed as part of the escape, so #\32 o i is id 2o + descendant i. Chrome 148 matches 1 for all selectors above.
Versions
Reproduced on 2.2.16, 2.2.23 (npm latest), and master HEAD (1e457d1), via jsdom 26.1.0's querySelectorAll (which delegates to nwsapi).
Context
Found while debugging the :scope escaping issue (#156, #153) on Gmail's colon-prefixed ids — a CSS.escape-based workaround produced these hex-escape selectors and still returned empty results, which is how we isolated this. Happy to test a fix or provide more cases.
A selector containing a hex escape with its terminating whitespace works when it is the last compound in the selector, but silently returns no matches (no
SyntaxError) as soon as a combinator follows. Browsers match all of these.This is exactly the form
CSS.escape()produces for identifiers starting with a digit (CSS.escape('2o') === '\\32 o'), so any code that composes selectors withCSS.escapeand runs them under jsdom gets silently-empty results.Repro
Single-character escapes are unaffected — with
<div id=":2o"><i>b</i></div>:So the failure is specific to hex escapes, whose whitespace terminator presumably collides with descendant-combinator whitespace handling somewhere between normalization and the compiled matcher.
Expected
Per CSS Syntax §4.3.7 (consume an escaped code point), the whitespace following a hex escape is consumed as part of the escape, so
#\32 o iis id2o+ descendanti. Chrome 148 matches1for all selectors above.Versions
Reproduced on 2.2.16, 2.2.23 (npm latest), and master HEAD (1e457d1), via jsdom 26.1.0's
querySelectorAll(which delegates to nwsapi).Context
Found while debugging the
:scopeescaping issue (#156, #153) on Gmail's colon-prefixed ids — aCSS.escape-based workaround produced these hex-escape selectors and still returned empty results, which is how we isolated this. Happy to test a fix or provide more cases.