diff --git a/src/compat/maestro/__tests__/replay-flow.test.ts b/src/compat/maestro/__tests__/replay-flow.test.ts index 7542d487e..ad55885c5 100644 --- a/src/compat/maestro/__tests__/replay-flow.test.ts +++ b/src/compat/maestro/__tests__/replay-flow.test.ts @@ -90,7 +90,7 @@ env: assert.equal(parsed.actions[3]?.flags.intervalMs, 150); assert.equal(parsed.actions[4]?.flags.holdMs, 3000); assert.equal(parsed.actions[1]?.flags.maestro?.allowNonHittableCoordinateFallback, true); - assert.equal(parsed.actions[6]?.flags?.maestro?.allowNonHittableCoordinateFallback, undefined); + assert.equal(parsed.actions[6]?.flags?.maestro?.allowNonHittableCoordinateFallback, true); }); 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 assert.deepEqual(parsed.actionLines, [3, 4, 5]); }); -test('parseMaestroReplayFlow marks tapOn before inputText for snapshot tap focus', () => { +test('parseMaestroReplayFlow keeps tapOn inputText without Enter on Maestro path', () => { const parsed = parseMaestroReplayFlow(`appId: com.callstack.agentdevicelab --- - tapOn: @@ -238,7 +238,29 @@ test('parseMaestroReplayFlow marks tapOn before inputText for snapshot tap focus ['type', ['Saved list']], ], ); - assert.equal(parsed.actions[0]?.flags?.maestro?.allowNonHittableCoordinateFallback, undefined); + assert.deepEqual(parsed.actionLines, [3, 5]); + assert.equal(parsed.actions[0]?.flags?.maestro?.allowNonHittableCoordinateFallback, true); +}); + +test('parseMaestroReplayFlow preserves optional tapOn before inputText without Enter', () => { + const parsed = parseMaestroReplayFlow(`appId: com.callstack.agentdevicelab +--- +- tapOn: + id: editableNameInput + optional: true +- inputText: Saved list +`); + + assert.deepEqual( + parsed.actions.map((entry) => [entry.command, entry.positionals]), + [ + ['__maestroTapOn', ['id="editableNameInput"']], + ['type', ['Saved list']], + ], + ); + assert.deepEqual(parsed.actionLines, [3, 6]); + assert.equal(parsed.actions[0]?.flags?.maestro?.optional, true); + assert.equal(parsed.actions[0]?.flags?.maestro?.allowNonHittableCoordinateFallback, true); }); 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 assert.equal(parsed.actions[0]?.flags?.maestro?.allowNonHittableCoordinateFallback, undefined); }); +test('parseMaestroReplayFlow maps focused input commands to native type and keyboard actions', () => { + const parsed = parseMaestroReplayFlow(`appId: com.callstack.agentdevicelab +--- +- inputText: hello +- eraseText: + charactersToErase: 3 +- pasteText: pasted +- pressKey: Return +`); + + assert.deepEqual( + parsed.actions.map((entry) => [entry.command, entry.positionals]), + [ + ['type', ['hello']], + ['type', ['\b'.repeat(3)]], + ['type', ['pasted']], + ['__maestroPressEnter', []], + ], + ); +}); + test('parseMaestroReplayFlow rejects relative runScript paths without source path', () => { assert.throws( () => diff --git a/src/compat/maestro/replay-flow.ts b/src/compat/maestro/replay-flow.ts index 60211a7e4..05aa9f6a8 100644 --- a/src/compat/maestro/replay-flow.ts +++ b/src/compat/maestro/replay-flow.ts @@ -104,19 +104,12 @@ function optimizeTypedAfterTap( actionLines: number[], index: number, ): { actions: SessionAction[]; actionLines: number[]; consumed: number } | null { - const action = actions[index]!; - const nextAction = actions[index + 1]; - const typedAfterTap = readPlainTypeText(nextAction); - const tapSelector = readPlainMaestroTapSelector(action); - if (!nextAction || typedAfterTap === null || tapSelector === null) return null; - const line = actionLines[index] ?? 1; + const candidate = readTypedAfterTapCandidate(actions, actionLines, index); + if (!candidate) return null; + const { action, nextAction, pressEnterAction, tapSelector, typedAfterTap, line } = candidate; if (!isLikelyTextEntrySelector(tapSelector)) { return { actions: [clearMaestroNonHittableTap(action)], actionLines: [line], consumed: 1 }; } - const pressEnterAction = actions[index + 2]; - if (pressEnterAction?.command !== MAESTRO_RUNTIME_COMMAND.pressEnter) { - return { actions: [clearMaestroNonHittableTap(action)], actionLines: [line], consumed: 1 }; - } return { actions: [ { @@ -137,6 +130,36 @@ function optimizeTypedAfterTap( }; } +function readTypedAfterTapCandidate( + actions: SessionAction[], + actionLines: number[], + index: number, +): { + action: SessionAction; + nextAction: SessionAction; + pressEnterAction: SessionAction; + tapSelector: string; + typedAfterTap: string; + line: number; +} | null { + const action = actions[index]!; + const nextAction = actions[index + 1]; + const pressEnterAction = actions[index + 2]; + if (pressEnterAction?.command !== MAESTRO_RUNTIME_COMMAND.pressEnter) return null; + if (action.flags?.maestro?.optional === true) return null; + const typedAfterTap = readPlainTypeText(nextAction); + const tapSelector = readPlainMaestroTapSelector(action); + if (!nextAction || typedAfterTap === null || tapSelector === null) return null; + return { + action, + nextAction, + pressEnterAction, + tapSelector, + typedAfterTap, + line: actionLines[index] ?? 1, + }; +} + function clearMaestroNonHittableTap(action: SessionAction): SessionAction { const maestro = { ...(action.flags?.maestro ?? {}) }; delete maestro.allowNonHittableCoordinateFallback; diff --git a/src/daemon/handlers/__tests__/session-replay-vars.test.ts b/src/daemon/handlers/__tests__/session-replay-vars.test.ts index cb4eaa557..2ca387c51 100644 --- a/src/daemon/handlers/__tests__/session-replay-vars.test.ts +++ b/src/daemon/handlers/__tests__/session-replay-vars.test.ts @@ -27,7 +27,7 @@ const LOC = { file: 'test.ad', line: 1 }; type CapturedInvocation = { command: string; positionals?: string[]; - flags?: Record; + flags?: CommandFlags; }; async function runReplayFixture(params: { @@ -1247,7 +1247,7 @@ test('runReplayScriptFile lets snapshot id tap handle Maestro one-point edge con ); }); -test('runReplayScriptFile keeps Maestro text-entry tapOn on the snapshot path', async () => { +test('runReplayScriptFile coalesces Maestro text-entry tapOn into native fill', async () => { const calls: CapturedInvocation[] = []; const { response } = await runReplayFixture({ label: 'maestro-tap-input-text-snapshot', @@ -1257,6 +1257,7 @@ test('runReplayScriptFile keeps Maestro text-entry tapOn on the snapshot path', '- tapOn:', ' id: editableNameInput', '- inputText: Saved list', + '- pressKey: Enter', '', ].join('\n'), flags: { replayBackend: 'maestro' }, @@ -1284,12 +1285,12 @@ test('runReplayScriptFile keeps Maestro text-entry tapOn on the snapshot path', assert.deepEqual( calls.map((call) => [call.command, call.positionals]), [ - ['snapshot', []], - ['click', ['120', '120']], - ['type', ['Saved list']], + ['wait', ['id="editableNameInput"', '30000']], + ['fill', ['id="editableNameInput"', 'Saved list']], + ['keyboard', ['enter']], ], ); - assert.equal(calls[0]?.flags?.noRecord, true); + assert.equal(calls[1]?.flags?.maestro?.allowNonHittableCoordinateFallback, true); }); test('runReplayScriptFile resolves Maestro swipe.label from a labeled element rect', async () => {