Skip to content

Commit 5d68483

Browse files
authored
test: add Maestro provider integration guards (#620)
* test: add maestro provider integration guards * test: cover maestro provider input coalescing * test: align maestro input provider guard
1 parent 0932ad5 commit 5d68483

1 file changed

Lines changed: 255 additions & 1 deletion

File tree

test/integration/provider-scenarios/android-test-suite.test.ts

Lines changed: 255 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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+
129383
function androidMaestroReplayXml(searchBounds: string): string {
130384
return [
131385
'<?xml version="1.0" encoding="UTF-8"?>',

0 commit comments

Comments
 (0)