@@ -74,6 +74,97 @@ test('resolveSelectorChain falls back when first selector is ambiguous', () => {
7474 assert . equal ( resolved . node . ref , 'e2' ) ;
7575} ) ;
7676
77+ test ( 'resolveSelectorChain keeps strict ambiguity behavior by default' , ( ) => {
78+ const chain = parseSelectorChain ( 'label="Continue"' ) ;
79+ const resolved = resolveSelectorChain ( nodes , chain , {
80+ platform : 'ios' ,
81+ requireRect : true ,
82+ requireUnique : true ,
83+ } ) ;
84+ assert . equal ( resolved , null ) ;
85+ } ) ;
86+
87+ test ( 'resolveSelectorChain disambiguates to deeper/smaller matching node when enabled' , ( ) => {
88+ const disambiguationNodes : SnapshotState [ 'nodes' ] = [
89+ {
90+ ref : 'e1' ,
91+ index : 0 ,
92+ type : 'Other' ,
93+ label : 'Press me' ,
94+ rect : { x : 0 , y : 0 , width : 300 , height : 300 } ,
95+ depth : 1 ,
96+ enabled : true ,
97+ hittable : true ,
98+ } ,
99+ {
100+ ref : 'e2' ,
101+ index : 1 ,
102+ type : 'Other' ,
103+ label : 'Press me' ,
104+ rect : { x : 10 , y : 10 , width : 100 , height : 20 } ,
105+ depth : 2 ,
106+ enabled : true ,
107+ hittable : true ,
108+ } ,
109+ ] ;
110+ const chain = parseSelectorChain ( 'role="other" label="Press me" || label="Press me"' ) ;
111+ const resolved = resolveSelectorChain ( disambiguationNodes , chain , {
112+ platform : 'ios' ,
113+ requireRect : true ,
114+ requireUnique : true ,
115+ disambiguateAmbiguous : true ,
116+ } ) ;
117+ assert . ok ( resolved ) ;
118+ assert . equal ( resolved . node . ref , 'e2' ) ;
119+ assert . equal ( resolved . matches , 2 ) ;
120+ } ) ;
121+
122+ test ( 'resolveSelectorChain disambiguation tie falls back to next selector' , ( ) => {
123+ const tieNodes : SnapshotState [ 'nodes' ] = [
124+ {
125+ ref : 'e1' ,
126+ index : 0 ,
127+ type : 'Other' ,
128+ label : 'Press me' ,
129+ rect : { x : 0 , y : 0 , width : 100 , height : 20 } ,
130+ depth : 2 ,
131+ enabled : true ,
132+ hittable : true ,
133+ } ,
134+ {
135+ ref : 'e2' ,
136+ index : 1 ,
137+ type : 'Other' ,
138+ label : 'Press me' ,
139+ rect : { x : 0 , y : 40 , width : 100 , height : 20 } ,
140+ depth : 2 ,
141+ enabled : true ,
142+ hittable : true ,
143+ } ,
144+ {
145+ ref : 'e3' ,
146+ index : 2 ,
147+ type : 'Other' ,
148+ label : 'Press me' ,
149+ identifier : 'press_me_unique' ,
150+ rect : { x : 0 , y : 80 , width : 100 , height : 20 } ,
151+ depth : 2 ,
152+ enabled : true ,
153+ hittable : true ,
154+ } ,
155+ ] ;
156+ const chain = parseSelectorChain ( 'label="Press me" || id="press_me_unique"' ) ;
157+ const resolved = resolveSelectorChain ( tieNodes , chain , {
158+ platform : 'ios' ,
159+ requireRect : true ,
160+ requireUnique : true ,
161+ disambiguateAmbiguous : true ,
162+ } ) ;
163+ assert . ok ( resolved ) ;
164+ assert . equal ( resolved . selectorIndex , 1 ) ;
165+ assert . equal ( resolved . node . ref , 'e3' ) ;
166+ } ) ;
167+
77168test ( 'findSelectorChainMatch returns first matching selector for existence checks' , ( ) => {
78169 const chain = parseSelectorChain ( 'label="Continue" || id=auth_continue' ) ;
79170 const match = findSelectorChainMatch ( nodes , chain , {
@@ -91,12 +182,31 @@ test('splitSelectorFromArgs extracts selector prefix and trailing value', () =>
91182 assert . deepEqual ( split . rest , [ 'qa@example.com' ] ) ;
92183} ) ;
93184
185+ test ( 'splitSelectorFromArgs prefers trailing token for value when requested' , ( ) => {
186+ const split = splitSelectorFromArgs ( [ 'label="Filter"' , 'visible=true' ] , { preferTrailingValue : true } ) ;
187+ assert . ok ( split ) ;
188+ assert . equal ( split . selectorExpression , 'label="Filter"' ) ;
189+ assert . deepEqual ( split . rest , [ 'visible=true' ] ) ;
190+ } ) ;
191+
192+ test ( 'splitSelectorFromArgs keeps full selector when trailing value preference is disabled' , ( ) => {
193+ const split = splitSelectorFromArgs ( [ 'label="Filter"' , 'visible=true' ] ) ;
194+ assert . ok ( split ) ;
195+ assert . equal ( split . selectorExpression , 'label="Filter" visible=true' ) ;
196+ assert . deepEqual ( split . rest , [ ] ) ;
197+ } ) ;
198+
94199test ( 'parseSelectorChain rejects unknown keys and malformed quotes' , ( ) => {
95200 assert . throws ( ( ) => parseSelectorChain ( 'foo=bar' ) , / U n k n o w n s e l e c t o r k e y / i) ;
96201 assert . throws ( ( ) => parseSelectorChain ( 'label="unclosed' ) , / U n c l o s e d q u o t e / i) ;
97202 assert . throws ( ( ) => parseSelectorChain ( '' ) , / c a n n o t b e e m p t y / i) ;
98203} ) ;
99204
205+ test ( 'parseSelectorChain handles quoted values ending in escaped backslashes' , ( ) => {
206+ const chain = parseSelectorChain ( 'label="path\\\\" || id=auth_continue' ) ;
207+ assert . equal ( chain . selectors . length , 2 ) ;
208+ } ) ;
209+
100210test ( 'isSelectorToken only accepts known keys for key=value tokens' , ( ) => {
101211 assert . equal ( isSelectorToken ( 'id=foo' ) , true ) ;
102212 assert . equal ( isSelectorToken ( 'editable=true' ) , true ) ;
@@ -126,3 +236,26 @@ test('buildSelectorChainForNode prefers id and adds editable for fill action', (
126236 assert . ok ( chain . some ( ( entry ) => entry . includes ( 'id=' ) ) ) ;
127237 assert . ok ( chain . some ( ( entry ) => entry . includes ( 'editable=true' ) ) ) ;
128238} ) ;
239+
240+ test ( 'role selector normalization matches Android class names by leaf type' , ( ) => {
241+ const androidNodes : SnapshotState [ 'nodes' ] = [
242+ {
243+ ref : 'a1' ,
244+ index : 0 ,
245+ type : 'android.widget.Button' ,
246+ label : 'Continue' ,
247+ identifier : 'auth_continue' ,
248+ rect : { x : 0 , y : 0 , width : 120 , height : 44 } ,
249+ enabled : true ,
250+ hittable : true ,
251+ } ,
252+ ] ;
253+ const chain = parseSelectorChain ( 'role=button label="Continue"' ) ;
254+ const resolved = resolveSelectorChain ( androidNodes , chain , {
255+ platform : 'android' ,
256+ requireRect : true ,
257+ requireUnique : true ,
258+ } ) ;
259+ assert . ok ( resolved ) ;
260+ assert . equal ( resolved . node . ref , 'a1' ) ;
261+ } ) ;
0 commit comments