@@ -121,11 +121,265 @@ test('Provider-backed integration Android Maestro replay uses fresh selector sna
121121 world . adbCalls . find ( ( call ) => call . slice ( 0 , 3 ) . join ( ' ' ) === 'shell input swipe' ) ,
122122 [ 'shell' , 'input' , 'swipe' , '351' , '390' , '39' , '390' , '300' ] ,
123123 ) ;
124- assert . equal ( snapshots >= 2 , true ) ;
124+ assert . equal ( snapshots , 2 ) ;
125125 } ,
126126 ) ;
127127} ) ;
128128
129+ test ( 'Provider-backed integration Android Maestro replay test suite discovers YAML flows in directories' , async ( ) => {
130+ let snapshots = 0 ;
131+ await withProviderScenarioResource (
132+ async ( ) =>
133+ await createAndroidSettingsWorld ( {
134+ snapshotXml : ( ) => {
135+ snapshots += 1 ;
136+ return androidMaestroReplayXml ( '[100,300][260,360]' ) ;
137+ } ,
138+ } ) ,
139+ async ( world ) => {
140+ const client = world . daemon . client ( ) ;
141+ const suiteRoot = path . join ( world . tempRoot , 'suite-maestro-directory' ) ;
142+ fs . mkdirSync ( suiteRoot , { recursive : true } ) ;
143+ fs . writeFileSync (
144+ path . join ( suiteRoot , '01-visible.yaml' ) ,
145+ [ 'appId: com.android.settings' , '---' , '- launchApp' , '- assertVisible: Apps' , '' ] . join (
146+ '\n' ,
147+ ) ,
148+ ) ;
149+ fs . writeFileSync (
150+ path . join ( suiteRoot , '02-tap.yml' ) ,
151+ [ 'appId: com.android.settings' , '---' , '- tapOn: Search' , '' ] . join ( '\n' ) ,
152+ ) ;
153+
154+ const suite = await client . replay . test ( {
155+ paths : [ suiteRoot ] ,
156+ backend : 'maestro' ,
157+ artifactsDir : path . join ( suiteRoot , 'artifacts' ) ,
158+ timeoutMs : 30000 ,
159+ ...world . selection ,
160+ } ) ;
161+
162+ assert . equal ( suite . total , 2 , JSON . stringify ( suite ) ) ;
163+ assert . equal ( suite . executed , 2 , JSON . stringify ( suite ) ) ;
164+ assert . equal ( suite . passed , 2 , JSON . stringify ( suite ) ) ;
165+ assert . equal ( suite . failed , 0 , JSON . stringify ( suite ) ) ;
166+ assert . deepEqual (
167+ world . adbCalls . find ( ( call ) => call . slice ( 0 , 3 ) . join ( ' ' ) === 'shell input tap' ) ,
168+ [ 'shell' , 'input' , 'tap' , '180' , '330' ] ,
169+ ) ;
170+ assert . equal ( snapshots , 2 ) ;
171+ } ,
172+ ) ;
173+ } ) ;
174+
175+ test ( 'Provider-backed integration Android Maestro types after tapOn inputText without trailing Enter' , async ( ) => {
176+ await withProviderScenarioResource (
177+ async ( ) => await createAndroidSettingsWorld ( { nativeTextInjection : true } ) ,
178+ async ( world ) => {
179+ const client = world . daemon . client ( ) ;
180+ const suiteRoot = path . join ( world . tempRoot , 'suite-maestro-input' ) ;
181+ fs . mkdirSync ( suiteRoot , { recursive : true } ) ;
182+ const flowPath = path . join ( suiteRoot , 'input-only.yaml' ) ;
183+ fs . writeFileSync (
184+ flowPath ,
185+ [
186+ 'appId: com.android.settings' ,
187+ '---' ,
188+ '- launchApp' ,
189+ '- tapOn: Search' ,
190+ '- inputText: "Łódź café"' ,
191+ '' ,
192+ ] . join ( '\n' ) ,
193+ ) ;
194+
195+ const suite = await client . replay . test ( {
196+ paths : [ flowPath ] ,
197+ backend : 'maestro' ,
198+ artifactsDir : path . join ( suiteRoot , 'artifacts' ) ,
199+ timeoutMs : 30000 ,
200+ ...world . selection ,
201+ } ) ;
202+
203+ assert . equal ( suite . total , 1 , JSON . stringify ( suite ) ) ;
204+ assert . equal ( suite . passed , 1 , JSON . stringify ( suite ) ) ;
205+ assert . equal ( suite . failed , 0 , JSON . stringify ( suite ) ) ;
206+ assert . deepEqual ( world . textInjectionCalls , [
207+ {
208+ action : 'type' ,
209+ text : 'Łódź café' ,
210+ delayMs : 0 ,
211+ } ,
212+ ] ) ;
213+ assert . deepEqual (
214+ world . adbCalls . find ( ( call ) => call . slice ( 0 , 3 ) . join ( ' ' ) === 'shell input tap' ) ,
215+ [ 'shell' , 'input' , 'tap' , '195' , '52' ] ,
216+ ) ;
217+ assert . equal (
218+ world . adbCalls . some (
219+ ( call ) => call [ 0 ] === 'shell' && call [ 1 ] === 'input' && call [ 2 ] === 'text' ,
220+ ) ,
221+ false ,
222+ JSON . stringify ( world . adbCalls ) ,
223+ ) ;
224+ assert . equal (
225+ world . adbCalls . some ( ( call ) => call . slice ( 0 , 4 ) . join ( ' ' ) === 'shell input keyevent ENTER' ) ,
226+ false ,
227+ JSON . stringify ( world . adbCalls ) ,
228+ ) ;
229+ world . assertNoHostAdbCalls ( ) ;
230+ } ,
231+ ) ;
232+ } ) ;
233+
234+ test ( 'Provider-backed integration Android Maestro preserves pressKey Enter after native fill' , async ( ) => {
235+ await withProviderScenarioResource (
236+ async ( ) => await createAndroidSettingsWorld ( { nativeTextInjection : true } ) ,
237+ async ( world ) => {
238+ const client = world . daemon . client ( ) ;
239+ const suiteRoot = path . join ( world . tempRoot , 'suite-maestro-input-submit' ) ;
240+ fs . mkdirSync ( suiteRoot , { recursive : true } ) ;
241+ const flowPath = path . join ( suiteRoot , 'input-submit.yaml' ) ;
242+ fs . writeFileSync (
243+ flowPath ,
244+ [
245+ 'appId: com.android.settings' ,
246+ '---' ,
247+ '- launchApp' ,
248+ '- tapOn: Search' ,
249+ '- inputText: "Łódź café"' ,
250+ '- pressKey: Enter' ,
251+ '' ,
252+ ] . join ( '\n' ) ,
253+ ) ;
254+
255+ const suite = await client . replay . test ( {
256+ paths : [ flowPath ] ,
257+ backend : 'maestro' ,
258+ artifactsDir : path . join ( suiteRoot , 'artifacts' ) ,
259+ timeoutMs : 30000 ,
260+ ...world . selection ,
261+ } ) ;
262+
263+ assert . equal ( suite . total , 1 , JSON . stringify ( suite ) ) ;
264+ assert . equal ( suite . passed , 1 , JSON . stringify ( suite ) ) ;
265+ assert . equal ( suite . failed , 0 , JSON . stringify ( suite ) ) ;
266+ assert . deepEqual ( world . textInjectionCalls , [
267+ {
268+ action : 'fill' ,
269+ target : { x : 195 , y : 52 } ,
270+ text : 'Łódź café' ,
271+ delayMs : 0 ,
272+ } ,
273+ ] ) ;
274+ assert . equal (
275+ world . adbCalls . some (
276+ ( call ) => call [ 0 ] === 'shell' && call [ 1 ] === 'input' && call [ 2 ] === 'text' ,
277+ ) ,
278+ false ,
279+ JSON . stringify ( world . adbCalls ) ,
280+ ) ;
281+ assert . deepEqual (
282+ world . adbCalls . find ( ( call ) => call . slice ( 0 , 4 ) . join ( ' ' ) === 'shell input keyevent ENTER' ) ,
283+ [ 'shell' , 'input' , 'keyevent' , 'ENTER' ] ,
284+ ) ;
285+ world . assertNoHostAdbCalls ( ) ;
286+ } ,
287+ ) ;
288+ } ) ;
289+
290+ test ( 'Provider-backed integration Android Maestro executes runFlow conditions and retry batches at runtime' , async ( ) => {
291+ let snapshots = 0 ;
292+ await withProviderScenarioResource (
293+ async ( ) =>
294+ await createAndroidSettingsWorld ( {
295+ snapshotXml : ( ) => {
296+ snapshots += 1 ;
297+ return androidMaestroReplayXml ( '[100,300][260,360]' ) ;
298+ } ,
299+ } ) ,
300+ async ( world ) => {
301+ const client = world . daemon . client ( ) ;
302+ const suiteRoot = path . join ( world . tempRoot , 'suite-maestro-runtime-flow' ) ;
303+ fs . mkdirSync ( suiteRoot , { recursive : true } ) ;
304+ const flowPath = path . join ( suiteRoot , 'runtime-flow.yaml' ) ;
305+ fs . writeFileSync (
306+ flowPath ,
307+ [
308+ 'appId: com.android.settings' ,
309+ '---' ,
310+ '- launchApp' ,
311+ '- runFlow:' ,
312+ ' when:' ,
313+ ' visible: Apps' ,
314+ ' commands:' ,
315+ ' - tapOn: Search' ,
316+ '- retry:' ,
317+ ' maxRetries: 1' ,
318+ ' commands:' ,
319+ ' - assertVisible: Apps' ,
320+ '' ,
321+ ] . join ( '\n' ) ,
322+ ) ;
323+
324+ const suite = await client . replay . test ( {
325+ paths : [ flowPath ] ,
326+ backend : 'maestro' ,
327+ artifactsDir : path . join ( suiteRoot , 'artifacts' ) ,
328+ timeoutMs : 30000 ,
329+ ...world . selection ,
330+ } ) ;
331+
332+ assert . equal ( suite . total , 1 , JSON . stringify ( suite ) ) ;
333+ assert . equal ( suite . passed , 1 , JSON . stringify ( suite ) ) ;
334+ assert . equal ( suite . failed , 0 , JSON . stringify ( suite ) ) ;
335+ assert . deepEqual (
336+ world . adbCalls . find ( ( call ) => call . slice ( 0 , 3 ) . join ( ' ' ) === 'shell input tap' ) ,
337+ [ 'shell' , 'input' , 'tap' , '180' , '330' ] ,
338+ ) ;
339+ assert . equal ( snapshots , 3 ) ;
340+ } ,
341+ ) ;
342+ } ) ;
343+
344+ test ( 'Provider-backed integration Android Maestro optional tap misses without touching the device' , async ( ) => {
345+ await withProviderScenarioResource ( createAndroidSettingsWorld , async ( world ) => {
346+ const client = world . daemon . client ( ) ;
347+ const suiteRoot = path . join ( world . tempRoot , 'suite-maestro-optional' ) ;
348+ fs . mkdirSync ( suiteRoot , { recursive : true } ) ;
349+ const flowPath = path . join ( suiteRoot , 'optional-miss.yaml' ) ;
350+ fs . writeFileSync (
351+ flowPath ,
352+ [
353+ 'appId: com.android.settings' ,
354+ '---' ,
355+ '- launchApp' ,
356+ '- tapOn:' ,
357+ ' text: Missing target' ,
358+ ' optional: true' ,
359+ '- assertVisible: Apps' ,
360+ '' ,
361+ ] . join ( '\n' ) ,
362+ ) ;
363+
364+ const suite = await client . replay . test ( {
365+ paths : [ flowPath ] ,
366+ backend : 'maestro' ,
367+ artifactsDir : path . join ( suiteRoot , 'artifacts' ) ,
368+ timeoutMs : 30000 ,
369+ ...world . selection ,
370+ } ) ;
371+
372+ assert . equal ( suite . total , 1 , JSON . stringify ( suite ) ) ;
373+ assert . equal ( suite . passed , 1 , JSON . stringify ( suite ) ) ;
374+ assert . equal ( suite . failed , 0 , JSON . stringify ( suite ) ) ;
375+ assert . equal (
376+ world . adbCalls . some ( ( call ) => call . slice ( 0 , 3 ) . join ( ' ' ) === 'shell input tap' ) ,
377+ false ,
378+ JSON . stringify ( world . adbCalls ) ,
379+ ) ;
380+ } ) ;
381+ } ) ;
382+
129383function androidMaestroReplayXml ( searchBounds : string ) : string {
130384 return [
131385 '<?xml version="1.0" encoding="UTF-8"?>' ,
0 commit comments