1- import test from 'node:test' ;
1+ import test , { type TestContext } from 'node:test' ;
22import assert from 'node:assert/strict' ;
33import fs from 'node:fs' ;
44import os from 'node:os' ;
@@ -37,6 +37,44 @@ const INCREMENT_NODE = {
3737 depth : 0 ,
3838} ;
3939
40+ async function runFindClickScenario ( options : {
41+ positionals : string [ ] ;
42+ nodes : Array < Record < string , unknown > > ;
43+ invoke ?: ( req : DaemonRequest ) => Promise < Record < string , unknown > > ;
44+ } ) : Promise < { response : NonNullable < Awaited < ReturnType < typeof handleFindCommands > > > ; invokeCalls : DaemonRequest [ ] } > {
45+ const sessionStore = makeSessionStore ( ) ;
46+ const sessionName = 'default' ;
47+ sessionStore . set ( sessionName , makeSession ( sessionName ) ) ;
48+
49+ const invokeCalls : DaemonRequest [ ] = [ ] ;
50+ const response = await handleFindCommands ( {
51+ req : {
52+ token : 't' ,
53+ session : sessionName ,
54+ command : 'find' ,
55+ positionals : options . positionals ,
56+ flags : { } ,
57+ } ,
58+ sessionName,
59+ logPath : '/tmp/test.log' ,
60+ sessionStore,
61+ invoke : async ( req ) => {
62+ invokeCalls . push ( req ) ;
63+ const data = options . invoke ? await options . invoke ( req ) : { } ;
64+ return { ok : true , data } ;
65+ } ,
66+ dispatch : async ( _device , command ) => {
67+ if ( command === 'snapshot' ) {
68+ return { nodes : options . nodes } ;
69+ }
70+ return { } ;
71+ } ,
72+ } ) ;
73+
74+ assert . ok ( response , 'expected a response' ) ;
75+ return { response, invokeCalls } ;
76+ }
77+
4078test ( 'parseFindArgs defaults to click with any locator' , ( ) => {
4179 const parsed = parseFindArgs ( [ 'Login' ] ) ;
4280 assert . equal ( parsed . locator , 'any' ) ;
@@ -132,63 +170,7 @@ test('parseFindArgs with bare locator yields empty query', () => {
132170 assert . equal ( parsed . action , 'click' ) ;
133171} ) ;
134172
135- test ( 'handleFindCommands click returns deterministic matched-target metadata' , async ( ) => {
136- const sessionStore = makeSessionStore ( ) ;
137- const sessionName = 'default' ;
138- sessionStore . set ( sessionName , makeSession ( sessionName ) ) ;
139-
140- const invokeCalls : DaemonRequest [ ] = [ ] ;
141- const response = await handleFindCommands ( {
142- req : {
143- token : 't' ,
144- session : sessionName ,
145- command : 'find' ,
146- positionals : [ 'Increment' , 'click' ] ,
147- flags : { } ,
148- } ,
149- sessionName,
150- logPath : '/tmp/test.log' ,
151- sessionStore,
152- invoke : async ( req ) => {
153- invokeCalls . push ( req ) ;
154- // Simulate runner returning non-deterministic platform data that should not bleed through
155- return { ok : true , data : { platformSpecificRef : 'XCUIElementTypeApplication' , x : 0 , y : 0 } } ;
156- } ,
157- dispatch : async ( _device , command ) => {
158- if ( command === 'snapshot' ) {
159- return { nodes : [ INCREMENT_NODE ] } ;
160- }
161- return { } ;
162- } ,
163- } ) ;
164-
165- assert . ok ( response , 'expected a response' ) ;
166- assert . ok ( response . ok , 'expected success' ) ;
167- const data = response . data as Record < string , unknown > ;
168-
169- // Deterministic matched-target metadata
170- assert . equal ( data . ref , '@e1' , 'ref must match the resolved snapshot node' ) ;
171- assert . equal ( data . locator , 'any' , 'locator must reflect the find strategy' ) ;
172- assert . equal ( data . query , 'Increment' , 'query must reflect the search term' ) ;
173- assert . equal ( data . x , 100 , 'x must be derived from the matched node rect center' ) ;
174- assert . equal ( data . y , 50 , 'y must be derived from the matched node rect center' ) ;
175-
176- // Strict key set — no platform-specific fields may leak through
177- assert . deepEqual ( Object . keys ( data ) . sort ( ) , [ 'locator' , 'query' , 'ref' , 'x' , 'y' ] ) ;
178-
179- // invoke was called with the resolved ref
180- assert . equal ( invokeCalls . length , 1 ) ;
181- assert . equal ( invokeCalls [ 0 ] . positionals ?. [ 0 ] , '@e1' ) ;
182- } ) ;
183-
184- test ( 'handleFindCommands click response contains exactly the deterministic key set (fallback: no rect on resolved node)' , async ( ) => {
185- const sessionStore = makeSessionStore ( ) ;
186- const sessionName = 'default' ;
187- sessionStore . set ( sessionName , makeSession ( sessionName ) ) ;
188-
189- // Parent is hittable but has no rect — resolving through it loses coordinates.
190- // Child has a rect (satisfies requireRect) but is not hittable, so findNearestHittableAncestor
191- // walks up to the parent, which has no rect → fallback path: x/y are absent from response.
173+ test ( 'handleFindCommands click returns deterministic metadata across locator variants' , async ( t : TestContext ) => {
192174 const hittableParentNoRect = { index : 0 , type : 'View' , hittable : true , depth : 0 } ;
193175 const nonHittableChildWithRect = {
194176 index : 1 ,
@@ -200,55 +182,67 @@ test('handleFindCommands click response contains exactly the deterministic key s
200182 parentIndex : 0 ,
201183 } ;
202184
203- const response = await handleFindCommands ( {
204- req : {
205- token : 't' ,
206- session : sessionName ,
207- command : 'find' ,
185+ const scenarios : Array < {
186+ label : string ;
187+ positionals : string [ ] ;
188+ nodes : Array < Record < string , unknown > > ;
189+ invoke ?: ( req : DaemonRequest ) => Promise < Record < string , unknown > > ;
190+ expectedKeys : string [ ] ;
191+ expectedLocator : string ;
192+ expectedQuery : string ;
193+ expectedCoordinates ?: { x : number ; y : number } ;
194+ } > = [
195+ {
196+ label : 'returns deterministic matched-target metadata' ,
208197 positionals : [ 'Increment' , 'click' ] ,
209- flags : { } ,
198+ nodes : [ INCREMENT_NODE ] ,
199+ invoke : async ( ) => ( { platformSpecificRef : 'XCUIElementTypeApplication' , x : 0 , y : 0 } ) ,
200+ expectedKeys : [ 'locator' , 'query' , 'ref' , 'x' , 'y' ] ,
201+ expectedLocator : 'any' ,
202+ expectedQuery : 'Increment' ,
203+ expectedCoordinates : { x : 100 , y : 50 } ,
210204 } ,
211- sessionName,
212- logPath : '/tmp/test.log' ,
213- sessionStore,
214- invoke : async ( ) => ( { ok : true , data : { platformSpecificRef : 'XCUIElementTypeView' } } ) ,
215- dispatch : async ( _device , command ) => {
216- if ( command === 'snapshot' ) return { nodes : [ hittableParentNoRect , nonHittableChildWithRect ] } ;
217- return { } ;
205+ {
206+ label : 'falls back to deterministic key set when resolved node has no rect' ,
207+ positionals : [ 'Increment' , 'click' ] ,
208+ nodes : [ hittableParentNoRect , nonHittableChildWithRect ] ,
209+ invoke : async ( ) => ( { platformSpecificRef : 'XCUIElementTypeView' } ) ,
210+ expectedKeys : [ 'locator' , 'query' , 'ref' ] ,
211+ expectedLocator : 'any' ,
212+ expectedQuery : 'Increment' ,
218213 } ,
219- } ) ;
220-
221- assert . ok ( response ?. ok ) ;
222- const data = response ! . data as Record < string , unknown > ;
223- assert . deepEqual ( Object . keys ( data ) . sort ( ) , [ 'locator' , 'query' , 'ref' ] ) ;
224- } ) ;
225-
226- test ( 'handleFindCommands click with explicit label locator returns locator in metadata' , async ( ) => {
227- const sessionStore = makeSessionStore ( ) ;
228- const sessionName = 'default' ;
229- sessionStore . set ( sessionName , makeSession ( sessionName ) ) ;
230-
231- const response = await handleFindCommands ( {
232- req : {
233- token : 't' ,
234- session : sessionName ,
235- command : 'find' ,
214+ {
215+ label : 'keeps explicit label locator in metadata' ,
236216 positionals : [ 'label' , 'Increment' , 'click' ] ,
237- flags : { } ,
217+ nodes : [ INCREMENT_NODE ] ,
218+ expectedKeys : [ 'locator' , 'query' , 'ref' , 'x' , 'y' ] ,
219+ expectedLocator : 'label' ,
220+ expectedQuery : 'Increment' ,
221+ expectedCoordinates : { x : 100 , y : 50 } ,
238222 } ,
239- sessionName,
240- logPath : '/tmp/test.log' ,
241- sessionStore,
242- invoke : async ( ) => ( { ok : true , data : { } } ) ,
243- dispatch : async ( _device , command ) => {
244- if ( command === 'snapshot' ) return { nodes : [ INCREMENT_NODE ] } ;
245- return { } ;
246- } ,
247- } ) ;
223+ ] ;
224+
225+ for ( const scenario of scenarios ) {
226+ await t . test ( scenario . label , async ( ) => {
227+ const { response, invokeCalls } = await runFindClickScenario ( scenario ) ;
228+ assert . ok ( response . ok , 'expected success' ) ;
229+
230+ const data = response . data as Record < string , unknown > ;
231+ assert . deepEqual ( Object . keys ( data ) . sort ( ) , scenario . expectedKeys ) ;
232+ assert . equal ( data . ref , '@e1' , 'ref must match the resolved snapshot node' ) ;
233+ assert . equal ( data . locator , scenario . expectedLocator ) ;
234+ assert . equal ( data . query , scenario . expectedQuery ) ;
235+
236+ if ( scenario . expectedCoordinates ) {
237+ assert . equal ( data . x , scenario . expectedCoordinates . x ) ;
238+ assert . equal ( data . y , scenario . expectedCoordinates . y ) ;
239+ } else {
240+ assert . equal ( Object . hasOwn ( data , 'x' ) , false ) ;
241+ assert . equal ( Object . hasOwn ( data , 'y' ) , false ) ;
242+ }
248243
249- assert . ok ( response ?. ok ) ;
250- const data = response ! . data as Record < string , unknown > ;
251- assert . deepEqual ( Object . keys ( data ) . sort ( ) , [ 'locator' , 'query' , 'ref' , 'x' , 'y' ] ) ;
252- assert . equal ( data . locator , 'label' ) ;
253- assert . equal ( data . query , 'Increment' ) ;
244+ assert . equal ( invokeCalls . length , 1 ) ;
245+ assert . equal ( invokeCalls [ 0 ] . positionals ?. [ 0 ] , '@e1' ) ;
246+ } ) ;
247+ }
254248} ) ;
0 commit comments