@@ -20,31 +20,48 @@ const MAESTRO_ASSERTION_POLICY = {
2020 assertVisibleGraceMs : 1000 ,
2121 assertVisiblePollMs : 250 ,
2222 assertNotVisiblePollMs : 250 ,
23- assertNotVisibleTimeoutMs : 3000 ,
23+ defaultAssertNotVisibleTimeoutMs : 3000 ,
2424} as const ;
2525
26+ type MaestroVisibilitySample =
27+ | { visible : true ; response : DaemonResponse }
28+ | { visible : false ; response : DaemonResponse ; infrastructureFailure : boolean } ;
29+
2630export async function invokeMaestroAssertVisible ( params : {
2731 baseReq : ReplayBaseRequest ;
2832 positionals : string [ ] ;
2933 invoke : MaestroRuntimeInvoke ;
3034 scope ?: ReplayVarScope ;
3135} ) : Promise < DaemonResponse > {
32- const args = readAssertVisibleArgs ( params . positionals ) ;
36+ const args = readVisibilityAssertionArgs ( params . positionals , {
37+ command : 'assertVisible' ,
38+ defaultTimeoutMs : 5000 ,
39+ } ) ;
3340 if ( ! args . ok ) return args . response ;
3441
42+ // Native wait/is cannot replace this loop: wait only proves existence, while
43+ // is requires unique resolution and does not apply Maestro overlay filtering.
3544 const startedAt = Date . now ( ) ;
3645 const deadlineMs = args . timeoutMs + MAESTRO_ASSERTION_POLICY . assertVisibleGraceMs ;
3746 let lastResponse : DaemonResponse | undefined ;
3847 let capturedAfterDeadline = false ;
3948 while ( true ) {
4049 const captureStartedAt = Date . now ( ) ;
41- const attempt = await readAssertVisibleAttempt ( params , args . selector , startedAt ) ;
42- if ( attempt . done ) return attempt . response ;
43- lastResponse = attempt . response ;
50+ const sample = await readMaestroVisibilitySample ( params , args . selector , 'assertVisible' ) ;
51+ if ( sample . visible ) return visibleAssertionResponse ( sample . response , args . selector , startedAt ) ;
52+ if ( ! sample . visible && sample . infrastructureFailure ) return sample . response ;
53+ lastResponse = sample . response ;
4454
4555 const elapsedMs = Date . now ( ) - startedAt ;
4656 if ( elapsedMs >= deadlineMs ) {
47- if ( shouldCaptureOnceAfterDeadline ( capturedAfterDeadline , captureStartedAt , startedAt , deadlineMs ) ) {
57+ if (
58+ shouldCaptureOnceAfterDeadline (
59+ capturedAfterDeadline ,
60+ captureStartedAt ,
61+ startedAt ,
62+ deadlineMs ,
63+ )
64+ ) {
4865 capturedAfterDeadline = true ;
4966 continue ;
5067 }
@@ -62,42 +79,47 @@ export async function invokeMaestroAssertVisible(params: {
6279 ) ;
6380}
6481
65- function readAssertVisibleArgs (
82+ function readVisibilityAssertionArgs (
6683 positionals : string [ ] ,
67- ) :
68- | { ok : true ; selector : string ; timeoutMs : number }
69- | { ok : false ; response : DaemonResponse } {
70- const [ selector , timeoutValue = '5000' ] = positionals ;
84+ options : { command : string ; defaultTimeoutMs : number } ,
85+ ) : { ok : true ; selector : string ; timeoutMs : number } | { ok : false ; response : DaemonResponse } {
86+ const [ selector , timeoutValue = String ( options . defaultTimeoutMs ) ] = positionals ;
7187 if ( ! selector ) {
72- return { ok : false , response : errorResponse ( 'INVALID_ARGS' , 'assertVisible requires a selector.' ) } ;
88+ return {
89+ ok : false ,
90+ response : errorResponse ( 'INVALID_ARGS' , `${ options . command } requires a selector.` ) ,
91+ } ;
7392 }
7493 const timeoutMs = Number ( timeoutValue ) ;
7594 if ( ! Number . isFinite ( timeoutMs ) || timeoutMs < 0 ) {
7695 return {
7796 ok : false ,
78- response : errorResponse ( 'INVALID_ARGS' , 'assertVisible timeout must be a non-negative number.' ) ,
97+ response : errorResponse (
98+ 'INVALID_ARGS' ,
99+ `${ options . command } timeout must be a non-negative number.` ,
100+ ) ,
79101 } ;
80102 }
81103 return { ok : true , selector, timeoutMs } ;
82104}
83105
84- async function readAssertVisibleAttempt (
106+ async function readMaestroVisibilitySample (
85107 params : {
86108 baseReq : ReplayBaseRequest ;
87- positionals : string [ ] ;
88109 invoke : MaestroRuntimeInvoke ;
89110 scope ?: ReplayVarScope ;
90111 } ,
91112 selector : string ,
92- startedAt : number ,
93- ) : Promise < { done : true ; response : DaemonResponse } | { done : false ; response : DaemonResponse } > {
113+ command : string ,
114+ ) : Promise < MaestroVisibilitySample > {
94115 const response = await captureMaestroRawSnapshot ( params ) ;
95- if ( ! response . ok ) return { done : false , response } ;
116+ if ( ! response . ok ) return { visible : false , response, infrastructureFailure : true } ;
96117 const snapshot = readSnapshotState ( response . data ) ;
97118 if ( ! snapshot ) {
98119 return {
99- done : true ,
100- response : errorResponse ( 'COMMAND_FAILED' , 'Unable to read snapshot data for assertVisible.' ) ,
120+ visible : false ,
121+ response : errorResponse ( 'COMMAND_FAILED' , `Unable to read snapshot data for ${ command } .` ) ,
122+ infrastructureFailure : true ,
101123 } ;
102124 }
103125 const target = resolveVisibleMaestroNodeFromSnapshot (
@@ -107,10 +129,14 @@ async function readAssertVisibleAttempt(
107129 getSnapshotReferenceFrame ( snapshot ) ,
108130 ) ;
109131 if ( ! target . ok ) {
110- return { done : false , response : errorResponse ( 'COMMAND_FAILED' , target . message , { selector } ) } ;
132+ return {
133+ visible : false ,
134+ response : errorResponse ( 'COMMAND_FAILED' , target . message , { selector } ) ,
135+ infrastructureFailure : false ,
136+ } ;
111137 }
112138 return {
113- done : true ,
139+ visible : true ,
114140 response : {
115141 ok : true ,
116142 data : {
@@ -121,12 +147,27 @@ async function readAssertVisibleAttempt(
121147 nodeLabel : target . node . label ,
122148 nodeIdentifier : target . node . identifier ,
123149 rect : target . rect ,
124- waitedMs : Date . now ( ) - startedAt ,
125150 } ,
126151 } ,
127152 } ;
128153}
129154
155+ function visibleAssertionResponse (
156+ response : DaemonResponse ,
157+ selector : string ,
158+ startedAt : number ,
159+ ) : DaemonResponse {
160+ if ( ! response . ok ) return response ;
161+ return {
162+ ok : true ,
163+ data : {
164+ selector,
165+ ...response . data ,
166+ waitedMs : Date . now ( ) - startedAt ,
167+ } ,
168+ } ;
169+ }
170+
130171function shouldCaptureOnceAfterDeadline (
131172 capturedAfterDeadline : boolean ,
132173 captureStartedAt : number ,
@@ -141,99 +182,59 @@ export async function invokeMaestroAssertNotVisible(params: {
141182 positionals : string [ ] ;
142183 invoke : MaestroRuntimeInvoke ;
143184} ) : Promise < DaemonResponse > {
144- const [ selector ] = params . positionals ;
145- if ( ! selector ) {
146- return errorResponse ( 'INVALID_ARGS' , 'assertNotVisible requires a selector.' ) ;
147- }
185+ const args = readVisibilityAssertionArgs ( params . positionals , {
186+ command : 'assertNotVisible' ,
187+ defaultTimeoutMs : MAESTRO_ASSERTION_POLICY . defaultAssertNotVisibleTimeoutMs ,
188+ } ) ;
189+ if ( ! args . ok ) return args . response ;
190+
191+ // Native is hidden intentionally fails for absent selectors. Maestro
192+ // assertNotVisible treats absent and overlay-blocked targets as passing, so
193+ // this loop shares the visible resolver instead of delegating to native is.
148194 const startedAt = Date . now ( ) ;
149195 let hiddenSamples = 0 ;
150196 let lastVisibleResponse : DaemonResponse | undefined ;
151- while ( Date . now ( ) - startedAt <= MAESTRO_ASSERTION_POLICY . assertNotVisibleTimeoutMs ) {
152- const attempt = await readAssertNotVisibleAttempt ( params , selector ) ;
153- if ( attempt . visible ) {
197+ while ( Date . now ( ) - startedAt <= args . timeoutMs ) {
198+ const sample = await readMaestroVisibilitySample ( params , args . selector , 'assertNotVisible' ) ;
199+ if ( ! sample . visible && sample . infrastructureFailure ) return sample . response ;
200+ if ( sample . visible ) {
154201 hiddenSamples = 0 ;
155- lastVisibleResponse = attempt . response ;
156- } else if ( attempt . hidden ) {
202+ lastVisibleResponse = sample . response ;
203+ } else {
157204 hiddenSamples += 1 ;
158205 const waitedMs = Date . now ( ) - startedAt ;
159- if (
160- hiddenSamples >= 2 ||
161- waitedMs >= MAESTRO_ASSERTION_POLICY . assertNotVisibleTimeoutMs
162- ) {
206+ if ( hiddenSamples >= 2 || waitedMs >= args . timeoutMs ) {
163207 return {
164208 ok : true ,
165209 data : {
166210 pass : true ,
167- selector,
211+ selector : args . selector ,
168212 stableSamples : hiddenSamples ,
169213 waitedMs,
170- timeoutMs : MAESTRO_ASSERTION_POLICY . assertNotVisibleTimeoutMs ,
214+ timeoutMs : args . timeoutMs ,
171215 } ,
172216 } ;
173217 }
174- } else {
175- return attempt . response ;
176218 }
177219 await sleep ( MAESTRO_ASSERTION_POLICY . assertNotVisiblePollMs ) ;
178220 }
179- return errorResponse ( 'COMMAND_FAILED' , `Expected not visible but matched: ${ selector } ` , {
180- selector,
181- timeoutMs : MAESTRO_ASSERTION_POLICY . assertNotVisibleTimeoutMs ,
182- lastResponse : lastVisibleResponse ,
183- } ) ;
184- }
185-
186- async function readAssertNotVisibleAttempt (
187- params : {
188- baseReq : ReplayBaseRequest ;
189- positionals : string [ ] ;
190- invoke : MaestroRuntimeInvoke ;
191- } ,
192- selector : string ,
193- ) : Promise <
194- | { visible : true ; hidden : false ; response : DaemonResponse }
195- | { visible : false ; hidden : true ; response : DaemonResponse }
196- | { visible : false ; hidden : false ; response : DaemonResponse }
197- > {
198- const response = await captureMaestroRawSnapshot ( params ) ;
199- if ( ! response . ok ) return { visible : false , hidden : false , response } ;
200- const snapshot = readSnapshotState ( response . data ) ;
201- if ( ! snapshot ) {
221+ if ( hiddenSamples > 0 ) {
202222 return {
203- visible : false ,
204- hidden : false ,
205- response : errorResponse ( 'COMMAND_FAILED' , 'Unable to read snapshot data for assertNotVisible.' ) ,
206- } ;
207- }
208- const target = resolveVisibleMaestroNodeFromSnapshot (
209- snapshot ,
210- selector ,
211- readMaestroSelectorPlatform ( params . baseReq . flags ) ,
212- getSnapshotReferenceFrame ( snapshot ) ,
213- ) ;
214- if ( ! target . ok ) {
215- return {
216- visible : false ,
217- hidden : true ,
218- response : errorResponse ( 'COMMAND_FAILED' , target . message , { selector } ) ,
219- } ;
220- }
221- return {
222- visible : true ,
223- hidden : false ,
224- response : {
225223 ok : true ,
226224 data : {
227- selector,
228- matches : target . matches ,
229- nodeIndex : target . node . index ,
230- nodeType : target . node . type ,
231- nodeLabel : target . node . label ,
232- nodeIdentifier : target . node . identifier ,
233- rect : target . rect ,
225+ pass : true ,
226+ selector : args . selector ,
227+ stableSamples : hiddenSamples ,
228+ waitedMs : Date . now ( ) - startedAt ,
229+ timeoutMs : args . timeoutMs ,
234230 } ,
235- } ,
236- } ;
231+ } ;
232+ }
233+ return errorResponse ( 'COMMAND_FAILED' , `Expected not visible but matched: ${ args . selector } ` , {
234+ selector : args . selector ,
235+ timeoutMs : args . timeoutMs ,
236+ lastResponse : lastVisibleResponse ,
237+ } ) ;
237238}
238239
239240export async function invokeMaestroWaitForAnimationToEnd ( params : {
@@ -245,6 +246,8 @@ export async function invokeMaestroWaitForAnimationToEnd(params: {
245246 if ( ! Number . isFinite ( timeoutMs ) || timeoutMs < 0 ) {
246247 return errorResponse ( 'INVALID_ARGS' , 'waitForAnimationToEnd timeout must be a number.' ) ;
247248 }
249+ // There is no native wait/is equivalent for "animation has ended"; this is
250+ // snapshot stability polling by design.
248251 const startedAt = Date . now ( ) ;
249252 let previousSignature : string | undefined ;
250253 let lastResponse : DaemonResponse | undefined ;
0 commit comments