Skip to content

Commit ea69ebd

Browse files
authored
refactor: converge Maestro input handling (#623)
* refactor: converge Maestro input handling * fix: preserve Maestro optional input semantics
1 parent 7909290 commit ea69ebd

3 files changed

Lines changed: 86 additions & 19 deletions

File tree

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

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ env:
9090
assert.equal(parsed.actions[3]?.flags.intervalMs, 150);
9191
assert.equal(parsed.actions[4]?.flags.holdMs, 3000);
9292
assert.equal(parsed.actions[1]?.flags.maestro?.allowNonHittableCoordinateFallback, true);
93-
assert.equal(parsed.actions[6]?.flags?.maestro?.allowNonHittableCoordinateFallback, undefined);
93+
assert.equal(parsed.actions[6]?.flags?.maestro?.allowNonHittableCoordinateFallback, true);
9494
});
9595

9696
test('parseMaestroReplayFlow maps iOS openLink through the app id when available', () => {
@@ -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 keeps tapOn inputText without Enter on Maestro path', () => {
227227
const parsed = parseMaestroReplayFlow(`appId: com.callstack.agentdevicelab
228228
---
229229
- tapOn:
@@ -238,7 +238,29 @@ test('parseMaestroReplayFlow marks tapOn before inputText for snapshot tap focus
238238
['type', ['Saved list']],
239239
],
240240
);
241-
assert.equal(parsed.actions[0]?.flags?.maestro?.allowNonHittableCoordinateFallback, undefined);
241+
assert.deepEqual(parsed.actionLines, [3, 5]);
242+
assert.equal(parsed.actions[0]?.flags?.maestro?.allowNonHittableCoordinateFallback, true);
243+
});
244+
245+
test('parseMaestroReplayFlow preserves optional tapOn before inputText without Enter', () => {
246+
const parsed = parseMaestroReplayFlow(`appId: com.callstack.agentdevicelab
247+
---
248+
- tapOn:
249+
id: editableNameInput
250+
optional: true
251+
- inputText: Saved list
252+
`);
253+
254+
assert.deepEqual(
255+
parsed.actions.map((entry) => [entry.command, entry.positionals]),
256+
[
257+
['__maestroTapOn', ['id="editableNameInput"']],
258+
['type', ['Saved list']],
259+
],
260+
);
261+
assert.deepEqual(parsed.actionLines, [3, 6]);
262+
assert.equal(parsed.actions[0]?.flags?.maestro?.optional, true);
263+
assert.equal(parsed.actions[0]?.flags?.maestro?.allowNonHittableCoordinateFallback, true);
242264
});
243265

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

306+
test('parseMaestroReplayFlow maps focused input commands to native type and keyboard actions', () => {
307+
const parsed = parseMaestroReplayFlow(`appId: com.callstack.agentdevicelab
308+
---
309+
- inputText: hello
310+
- eraseText:
311+
charactersToErase: 3
312+
- pasteText: pasted
313+
- pressKey: Return
314+
`);
315+
316+
assert.deepEqual(
317+
parsed.actions.map((entry) => [entry.command, entry.positionals]),
318+
[
319+
['type', ['hello']],
320+
['type', ['\b'.repeat(3)]],
321+
['type', ['pasted']],
322+
['__maestroPressEnter', []],
323+
],
324+
);
325+
});
326+
284327
test('parseMaestroReplayFlow rejects relative runScript paths without source path', () => {
285328
assert.throws(
286329
() =>

src/compat/maestro/replay-flow.ts

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -104,19 +104,12 @@ function optimizeTypedAfterTap(
104104
actionLines: number[],
105105
index: number,
106106
): { actions: SessionAction[]; actionLines: number[]; consumed: number } | null {
107-
const action = actions[index]!;
108-
const nextAction = actions[index + 1];
109-
const typedAfterTap = readPlainTypeText(nextAction);
110-
const tapSelector = readPlainMaestroTapSelector(action);
111-
if (!nextAction || typedAfterTap === null || tapSelector === null) return null;
112-
const line = actionLines[index] ?? 1;
107+
const candidate = readTypedAfterTapCandidate(actions, actionLines, index);
108+
if (!candidate) return null;
109+
const { action, nextAction, pressEnterAction, tapSelector, typedAfterTap, line } = candidate;
113110
if (!isLikelyTextEntrySelector(tapSelector)) {
114111
return { actions: [clearMaestroNonHittableTap(action)], actionLines: [line], consumed: 1 };
115112
}
116-
const pressEnterAction = actions[index + 2];
117-
if (pressEnterAction?.command !== MAESTRO_RUNTIME_COMMAND.pressEnter) {
118-
return { actions: [clearMaestroNonHittableTap(action)], actionLines: [line], consumed: 1 };
119-
}
120113
return {
121114
actions: [
122115
{
@@ -137,6 +130,36 @@ function optimizeTypedAfterTap(
137130
};
138131
}
139132

133+
function readTypedAfterTapCandidate(
134+
actions: SessionAction[],
135+
actionLines: number[],
136+
index: number,
137+
): {
138+
action: SessionAction;
139+
nextAction: SessionAction;
140+
pressEnterAction: SessionAction;
141+
tapSelector: string;
142+
typedAfterTap: string;
143+
line: number;
144+
} | null {
145+
const action = actions[index]!;
146+
const nextAction = actions[index + 1];
147+
const pressEnterAction = actions[index + 2];
148+
if (pressEnterAction?.command !== MAESTRO_RUNTIME_COMMAND.pressEnter) return null;
149+
if (action.flags?.maestro?.optional === true) return null;
150+
const typedAfterTap = readPlainTypeText(nextAction);
151+
const tapSelector = readPlainMaestroTapSelector(action);
152+
if (!nextAction || typedAfterTap === null || tapSelector === null) return null;
153+
return {
154+
action,
155+
nextAction,
156+
pressEnterAction,
157+
tapSelector,
158+
typedAfterTap,
159+
line: actionLines[index] ?? 1,
160+
};
161+
}
162+
140163
function clearMaestroNonHittableTap(action: SessionAction): SessionAction {
141164
const maestro = { ...(action.flags?.maestro ?? {}) };
142165
delete maestro.allowNonHittableCoordinateFallback;

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

Lines changed: 7 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',
@@ -1257,6 +1257,7 @@ test('runReplayScriptFile keeps Maestro text-entry tapOn on the snapshot path',
12571257
'- tapOn:',
12581258
' id: editableNameInput',
12591259
'- inputText: Saved list',
1260+
'- pressKey: Enter',
12601261
'',
12611262
].join('\n'),
12621263
flags: { replayBackend: 'maestro' },
@@ -1284,12 +1285,12 @@ test('runReplayScriptFile keeps Maestro text-entry tapOn on the snapshot path',
12841285
assert.deepEqual(
12851286
calls.map((call) => [call.command, call.positionals]),
12861287
[
1287-
['snapshot', []],
1288-
['click', ['120', '120']],
1289-
['type', ['Saved list']],
1288+
['wait', ['id="editableNameInput"', '30000']],
1289+
['fill', ['id="editableNameInput"', 'Saved list']],
1290+
['keyboard', ['enter']],
12901291
],
12911292
);
1292-
assert.equal(calls[0]?.flags?.noRecord, true);
1293+
assert.equal(calls[1]?.flags?.maestro?.allowNonHittableCoordinateFallback, true);
12931294
});
12941295

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

0 commit comments

Comments
 (0)