Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
256 changes: 255 additions & 1 deletion test/integration/provider-scenarios/android-test-suite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,265 @@ test('Provider-backed integration Android Maestro replay uses fresh selector sna
world.adbCalls.find((call) => call.slice(0, 3).join(' ') === 'shell input swipe'),
['shell', 'input', 'swipe', '351', '390', '39', '390', '300'],
);
assert.equal(snapshots >= 2, true);
assert.equal(snapshots, 2);
},
);
});

test('Provider-backed integration Android Maestro replay test suite discovers YAML flows in directories', async () => {
let snapshots = 0;
await withProviderScenarioResource(
async () =>
await createAndroidSettingsWorld({
snapshotXml: () => {
snapshots += 1;
return androidMaestroReplayXml('[100,300][260,360]');
},
}),
async (world) => {
const client = world.daemon.client();
const suiteRoot = path.join(world.tempRoot, 'suite-maestro-directory');
fs.mkdirSync(suiteRoot, { recursive: true });
fs.writeFileSync(
path.join(suiteRoot, '01-visible.yaml'),
['appId: com.android.settings', '---', '- launchApp', '- assertVisible: Apps', ''].join(
'\n',
),
);
fs.writeFileSync(
path.join(suiteRoot, '02-tap.yml'),
['appId: com.android.settings', '---', '- tapOn: Search', ''].join('\n'),
);

const suite = await client.replay.test({
paths: [suiteRoot],
backend: 'maestro',
artifactsDir: path.join(suiteRoot, 'artifacts'),
timeoutMs: 30000,
...world.selection,
});

assert.equal(suite.total, 2, JSON.stringify(suite));
assert.equal(suite.executed, 2, JSON.stringify(suite));
assert.equal(suite.passed, 2, JSON.stringify(suite));
assert.equal(suite.failed, 0, JSON.stringify(suite));
assert.deepEqual(
world.adbCalls.find((call) => call.slice(0, 3).join(' ') === 'shell input tap'),
['shell', 'input', 'tap', '180', '330'],
);
assert.equal(snapshots, 2);
},
);
});

test('Provider-backed integration Android Maestro types after tapOn inputText without trailing Enter', async () => {
await withProviderScenarioResource(
async () => await createAndroidSettingsWorld({ nativeTextInjection: true }),
async (world) => {
const client = world.daemon.client();
const suiteRoot = path.join(world.tempRoot, 'suite-maestro-input');
fs.mkdirSync(suiteRoot, { recursive: true });
const flowPath = path.join(suiteRoot, 'input-only.yaml');
fs.writeFileSync(
flowPath,
[
'appId: com.android.settings',
'---',
'- launchApp',
'- tapOn: Search',
'- inputText: "Łódź café"',
'',
].join('\n'),
);

const suite = await client.replay.test({
paths: [flowPath],
backend: 'maestro',
artifactsDir: path.join(suiteRoot, 'artifacts'),
timeoutMs: 30000,
...world.selection,
});

assert.equal(suite.total, 1, JSON.stringify(suite));
assert.equal(suite.passed, 1, JSON.stringify(suite));
assert.equal(suite.failed, 0, JSON.stringify(suite));
assert.deepEqual(world.textInjectionCalls, [
{
action: 'type',
text: 'Łódź café',
delayMs: 0,
},
]);
assert.deepEqual(
world.adbCalls.find((call) => call.slice(0, 3).join(' ') === 'shell input tap'),
['shell', 'input', 'tap', '195', '52'],
);
assert.equal(
world.adbCalls.some(
(call) => call[0] === 'shell' && call[1] === 'input' && call[2] === 'text',
),
false,
JSON.stringify(world.adbCalls),
);
assert.equal(
world.adbCalls.some((call) => call.slice(0, 4).join(' ') === 'shell input keyevent ENTER'),
false,
JSON.stringify(world.adbCalls),
);
world.assertNoHostAdbCalls();
},
);
});

test('Provider-backed integration Android Maestro preserves pressKey Enter after native fill', async () => {
await withProviderScenarioResource(
async () => await createAndroidSettingsWorld({ nativeTextInjection: true }),
async (world) => {
const client = world.daemon.client();
const suiteRoot = path.join(world.tempRoot, 'suite-maestro-input-submit');
fs.mkdirSync(suiteRoot, { recursive: true });
const flowPath = path.join(suiteRoot, 'input-submit.yaml');
fs.writeFileSync(
flowPath,
[
'appId: com.android.settings',
'---',
'- launchApp',
'- tapOn: Search',
'- inputText: "Łódź café"',
'- pressKey: Enter',
'',
].join('\n'),
);

const suite = await client.replay.test({
paths: [flowPath],
backend: 'maestro',
artifactsDir: path.join(suiteRoot, 'artifacts'),
timeoutMs: 30000,
...world.selection,
});

assert.equal(suite.total, 1, JSON.stringify(suite));
assert.equal(suite.passed, 1, JSON.stringify(suite));
assert.equal(suite.failed, 0, JSON.stringify(suite));
assert.deepEqual(world.textInjectionCalls, [
{
action: 'fill',
target: { x: 195, y: 52 },
text: 'Łódź café',
delayMs: 0,
},
]);
assert.equal(
world.adbCalls.some(
(call) => call[0] === 'shell' && call[1] === 'input' && call[2] === 'text',
),
false,
JSON.stringify(world.adbCalls),
);
assert.deepEqual(
world.adbCalls.find((call) => call.slice(0, 4).join(' ') === 'shell input keyevent ENTER'),
['shell', 'input', 'keyevent', 'ENTER'],
);
world.assertNoHostAdbCalls();
},
);
});

test('Provider-backed integration Android Maestro executes runFlow conditions and retry batches at runtime', async () => {
let snapshots = 0;
await withProviderScenarioResource(
async () =>
await createAndroidSettingsWorld({
snapshotXml: () => {
snapshots += 1;
return androidMaestroReplayXml('[100,300][260,360]');
},
}),
async (world) => {
const client = world.daemon.client();
const suiteRoot = path.join(world.tempRoot, 'suite-maestro-runtime-flow');
fs.mkdirSync(suiteRoot, { recursive: true });
const flowPath = path.join(suiteRoot, 'runtime-flow.yaml');
fs.writeFileSync(
flowPath,
[
'appId: com.android.settings',
'---',
'- launchApp',
'- runFlow:',
' when:',
' visible: Apps',
' commands:',
' - tapOn: Search',
'- retry:',
' maxRetries: 1',
' commands:',
' - assertVisible: Apps',
'',
].join('\n'),
);

const suite = await client.replay.test({
paths: [flowPath],
backend: 'maestro',
artifactsDir: path.join(suiteRoot, 'artifacts'),
timeoutMs: 30000,
...world.selection,
});

assert.equal(suite.total, 1, JSON.stringify(suite));
assert.equal(suite.passed, 1, JSON.stringify(suite));
assert.equal(suite.failed, 0, JSON.stringify(suite));
assert.deepEqual(
world.adbCalls.find((call) => call.slice(0, 3).join(' ') === 'shell input tap'),
['shell', 'input', 'tap', '180', '330'],
);
assert.equal(snapshots, 3);
},
);
});

test('Provider-backed integration Android Maestro optional tap misses without touching the device', async () => {
await withProviderScenarioResource(createAndroidSettingsWorld, async (world) => {
const client = world.daemon.client();
const suiteRoot = path.join(world.tempRoot, 'suite-maestro-optional');
fs.mkdirSync(suiteRoot, { recursive: true });
const flowPath = path.join(suiteRoot, 'optional-miss.yaml');
fs.writeFileSync(
flowPath,
[
'appId: com.android.settings',
'---',
'- launchApp',
'- tapOn:',
' text: Missing target',
' optional: true',
'- assertVisible: Apps',
'',
].join('\n'),
);

const suite = await client.replay.test({
paths: [flowPath],
backend: 'maestro',
artifactsDir: path.join(suiteRoot, 'artifacts'),
timeoutMs: 30000,
...world.selection,
});

assert.equal(suite.total, 1, JSON.stringify(suite));
assert.equal(suite.passed, 1, JSON.stringify(suite));
assert.equal(suite.failed, 0, JSON.stringify(suite));
assert.equal(
world.adbCalls.some((call) => call.slice(0, 3).join(' ') === 'shell input tap'),
false,
JSON.stringify(world.adbCalls),
);
});
});

function androidMaestroReplayXml(searchBounds: string): string {
return [
'<?xml version="1.0" encoding="UTF-8"?>',
Expand Down
Loading