Skip to content

Commit b46dfbb

Browse files
committed
refactor: converge Maestro input handling
1 parent f74f4e0 commit b46dfbb

4 files changed

Lines changed: 95 additions & 20 deletions

File tree

src/compat/maestro/__tests__/replay-flow.test.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ test('parseMaestroReplayFlow keeps focused inputText and pressKey Enter as separ
223223
assert.deepEqual(parsed.actionLines, [3, 4, 5]);
224224
});
225225

226-
test('parseMaestroReplayFlow marks tapOn before inputText for snapshot tap focus', () => {
226+
test('parseMaestroReplayFlow coalesces tapOn inputText through native fill', () => {
227227
const parsed = parseMaestroReplayFlow(`appId: com.callstack.agentdevicelab
228228
---
229229
- tapOn:
@@ -234,11 +234,12 @@ test('parseMaestroReplayFlow marks tapOn before inputText for snapshot tap focus
234234
assert.deepEqual(
235235
parsed.actions.map((entry) => [entry.command, entry.positionals]),
236236
[
237-
['__maestroTapOn', ['id="editableNameInput"']],
238-
['type', ['Saved list']],
237+
['wait', ['id="editableNameInput"', '30000']],
238+
['fill', ['id="editableNameInput"', 'Saved list']],
239239
],
240240
);
241-
assert.equal(parsed.actions[0]?.flags?.maestro?.allowNonHittableCoordinateFallback, undefined);
241+
assert.deepEqual(parsed.actionLines, [3, 3]);
242+
assert.equal(parsed.actions[1]?.flags?.maestro?.allowNonHittableCoordinateFallback, true);
242243
});
243244

244245
test('parseMaestroReplayFlow coalesces tapOn inputText while preserving pressKey Enter submit', () => {
@@ -281,6 +282,27 @@ test('parseMaestroReplayFlow does not coalesce text entry for non-input-looking
281282
assert.equal(parsed.actions[0]?.flags?.maestro?.allowNonHittableCoordinateFallback, undefined);
282283
});
283284

285+
test('parseMaestroReplayFlow maps focused input commands to native type and keyboard actions', () => {
286+
const parsed = parseMaestroReplayFlow(`appId: com.callstack.agentdevicelab
287+
---
288+
- inputText: hello
289+
- eraseText:
290+
charactersToErase: 3
291+
- pasteText: pasted
292+
- pressKey: Return
293+
`);
294+
295+
assert.deepEqual(
296+
parsed.actions.map((entry) => [entry.command, entry.positionals]),
297+
[
298+
['type', ['hello']],
299+
['type', ['\b'.repeat(3)]],
300+
['type', ['pasted']],
301+
['__maestroPressEnter', []],
302+
],
303+
);
304+
});
305+
284306
test('parseMaestroReplayFlow rejects relative runScript paths without source path', () => {
285307
assert.throws(
286308
() =>
@@ -658,10 +680,10 @@ test('parseMaestroReplayFlow parses the test-app Maestro suite fixture', () => {
658680
'__maestroAssertVisible',
659681
'__maestroTapOn',
660682
'__maestroAssertVisible',
661-
'__maestroTapOn',
662-
'type',
663-
'__maestroTapOn',
664-
'type',
683+
'wait',
684+
'fill',
685+
'wait',
686+
'fill',
665687
'__maestroTapOn',
666688
'__maestroAssertVisible',
667689
'__maestroAssertVisible',

src/compat/maestro/replay-flow.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,7 @@ function optimizeTypedAfterTap(
114114
return { actions: [clearMaestroNonHittableTap(action)], actionLines: [line], consumed: 1 };
115115
}
116116
const pressEnterAction = actions[index + 2];
117-
if (pressEnterAction?.command !== MAESTRO_RUNTIME_COMMAND.pressEnter) {
118-
return { actions: [clearMaestroNonHittableTap(action)], actionLines: [line], consumed: 1 };
119-
}
117+
const shouldKeepEnter = pressEnterAction?.command === MAESTRO_RUNTIME_COMMAND.pressEnter;
120118
return {
121119
actions: [
122120
{
@@ -130,10 +128,10 @@ function optimizeTypedAfterTap(
130128
positionals: [tapSelector, typedAfterTap],
131129
flags: action.flags,
132130
},
133-
pressEnterAction,
131+
...(shouldKeepEnter ? [pressEnterAction] : []),
134132
],
135-
actionLines: [line, line, actionLines[index + 2] ?? line],
136-
consumed: 3,
133+
actionLines: [line, line, ...(shouldKeepEnter ? [actionLines[index + 2] ?? line] : [])],
134+
consumed: shouldKeepEnter ? 3 : 2,
137135
};
138136
}
139137

src/daemon/handlers/__tests__/session-replay-vars.test.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const LOC = { file: 'test.ad', line: 1 };
2727
type CapturedInvocation = {
2828
command: string;
2929
positionals?: string[];
30-
flags?: Record<string, unknown>;
30+
flags?: CommandFlags;
3131
};
3232

3333
async function runReplayFixture(params: {
@@ -1247,7 +1247,7 @@ test('runReplayScriptFile lets snapshot id tap handle Maestro one-point edge con
12471247
);
12481248
});
12491249

1250-
test('runReplayScriptFile keeps Maestro text-entry tapOn on the snapshot path', async () => {
1250+
test('runReplayScriptFile coalesces Maestro text-entry tapOn into native fill', async () => {
12511251
const calls: CapturedInvocation[] = [];
12521252
const { response } = await runReplayFixture({
12531253
label: 'maestro-tap-input-text-snapshot',
@@ -1284,12 +1284,11 @@ test('runReplayScriptFile keeps Maestro text-entry tapOn on the snapshot path',
12841284
assert.deepEqual(
12851285
calls.map((call) => [call.command, call.positionals]),
12861286
[
1287-
['snapshot', []],
1288-
['click', ['120', '120']],
1289-
['type', ['Saved list']],
1287+
['wait', ['id="editableNameInput"', '30000']],
1288+
['fill', ['id="editableNameInput"', 'Saved list']],
12901289
],
12911290
);
1292-
assert.equal(calls[0]?.flags?.noRecord, true);
1291+
assert.equal(calls[1]?.flags?.maestro?.allowNonHittableCoordinateFallback, true);
12931292
});
12941293

12951294
test('runReplayScriptFile resolves Maestro swipe.label from a labeled element rect', async () => {

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,62 @@ test('Provider-backed integration Android Maestro replay uses fresh selector sna
126126
);
127127
});
128128

129+
test('Provider-backed integration Android Maestro coalesces tapOn inputText and pressKey Enter through native paths', async () => {
130+
await withProviderScenarioResource(
131+
async () => await createAndroidSettingsWorld({ nativeTextInjection: true }),
132+
async (world) => {
133+
const client = world.daemon.client();
134+
const suiteRoot = path.join(world.tempRoot, 'suite-maestro-input');
135+
fs.mkdirSync(suiteRoot, { recursive: true });
136+
const flowPath = path.join(suiteRoot, 'input-submit.yaml');
137+
fs.writeFileSync(
138+
flowPath,
139+
[
140+
'appId: com.android.settings',
141+
'---',
142+
'- launchApp',
143+
'- tapOn: Search',
144+
'- inputText: "Łódź café"',
145+
'- pressKey: Enter',
146+
'',
147+
].join('\n'),
148+
);
149+
150+
const suite = await client.replay.test({
151+
paths: [flowPath],
152+
backend: 'maestro',
153+
artifactsDir: path.join(suiteRoot, 'artifacts'),
154+
timeoutMs: 30000,
155+
...world.selection,
156+
});
157+
158+
assert.equal(suite.total, 1, JSON.stringify(suite));
159+
assert.equal(suite.passed, 1, JSON.stringify(suite));
160+
assert.equal(suite.failed, 0, JSON.stringify(suite));
161+
assert.deepEqual(world.textInjectionCalls, [
162+
{
163+
action: 'fill',
164+
target: { x: 195, y: 52 },
165+
text: 'Łódź café',
166+
delayMs: 0,
167+
},
168+
]);
169+
assert.equal(
170+
world.adbCalls.some(
171+
(call) => call[0] === 'shell' && call[1] === 'input' && call[2] === 'text',
172+
),
173+
false,
174+
JSON.stringify(world.adbCalls),
175+
);
176+
assert.deepEqual(
177+
world.adbCalls.find((call) => call.slice(0, 4).join(' ') === 'shell input keyevent ENTER'),
178+
['shell', 'input', 'keyevent', 'ENTER'],
179+
);
180+
world.assertNoHostAdbCalls();
181+
},
182+
);
183+
});
184+
129185
function androidMaestroReplayXml(searchBounds: string): string {
130186
return [
131187
'<?xml version="1.0" encoding="UTF-8"?>',

0 commit comments

Comments
 (0)