@@ -98,10 +98,6 @@ async function testScenario(
9898 const scenario : ParsedScenario = scenarioUtilities . parseScenario (
9999 doc . body ,
100100 ) ;
101- const needlesById = new Map (
102- scenario . needles . map ( ( needle ) => [ needle . id , needle ] ) ,
103- ) ;
104-
105101 const result : ScenarioTestResult = {
106102 success : [ ] ,
107103 error : [ ] ,
@@ -115,9 +111,58 @@ async function testScenario(
115111 } ) ;
116112 }
117113
114+ const generatedByNeedle = new Map < string , string > ( ) ;
115+
116+ scenario . needles . forEach ( ( { id, elements, root } ) => {
117+ if ( elements . length === 0 ) {
118+ return ;
119+ }
120+ const generatedSelector = CssSelectorGenerator . getCssSelector (
121+ elements . length === 1 ? elements [ 0 ] : elements ,
122+ root
123+ ? { ...scenario . metadata . options , root }
124+ : scenario . metadata . options ,
125+ ) ;
126+ generatedByNeedle . set ( id , generatedSelector ) ;
127+
128+ // Every generated selector must resolve back to exactly the elements
129+ // it was generated for, searched from the same root the generator
130+ // used. This catches malformed and under-specified selectors that an
131+ // expected-string comparison cannot express.
132+ const searchRoot = ( root ?? elements [ 0 ] . getRootNode ( ) ) as ParentNode ;
133+ let matched : Element [ ] ;
134+ try {
135+ matched = Array . from ( searchRoot . querySelectorAll ( generatedSelector ) ) ;
136+ } catch {
137+ result . error . push ( {
138+ key : id ,
139+ expectation : "a valid selector" ,
140+ selector : generatedSelector ,
141+ } ) ;
142+ return ;
143+ }
144+
145+ const isExact =
146+ matched . length === elements . length &&
147+ elements . every ( ( element ) => matched . includes ( element ) ) ;
148+ if ( isExact ) {
149+ result . success . push ( {
150+ key : id ,
151+ expectation : "resolves to its own elements" ,
152+ selector : generatedSelector ,
153+ } ) ;
154+ } else {
155+ result . error . push ( {
156+ key : id ,
157+ expectation : `to resolve to its own ${ String ( elements . length ) } element(s)` ,
158+ selector : `${ generatedSelector } resolved to ${ String ( matched . length ) } ` ,
159+ } ) ;
160+ }
161+ } ) ;
162+
118163 scenario . expectations . forEach ( ( { needleId, selector, negative } ) => {
119- const needle = needlesById . get ( needleId ) ;
120- if ( ! needle || needle . elements . length === 0 ) {
164+ const generatedSelector = generatedByNeedle . get ( needleId ) ;
165+ if ( generatedSelector === undefined ) {
121166 result . error . push ( {
122167 key : selector ,
123168 expectation : selector ,
@@ -126,13 +171,6 @@ async function testScenario(
126171 return ;
127172 }
128173
129- const { elements, root } = needle ;
130- const generatedSelector = CssSelectorGenerator . getCssSelector (
131- elements . length === 1 ? elements [ 0 ] : elements ,
132- root
133- ? { ...scenario . metadata . options , root }
134- : scenario . metadata . options ,
135- ) ;
136174 const matches = selector === generatedSelector ;
137175 result [ matches !== negative ? "success" : "error" ] . push ( {
138176 expectation : negative ? `anything but ${ selector } ` : selector ,
0 commit comments