Skip to content
Merged
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, expect, test } from 'vitest';
import type { MaestroDaemonOperationRequest } from '../daemon-runtime-public-operation.ts';
import { createDaemonMaestroRuntimePort } from '../daemon-runtime-port.ts';
import { makeRuntimeEnvelope, makeDependencies } from './daemon-runtime-port-fixtures.ts';

type TextCommand =
| { kind: 'inputText'; source: { line: number }; text: string }
| { kind: 'eraseText'; source: { line: number }; charactersToErase?: number };

async function typeRequestFor(
platform: 'android' | 'ios',
command: TextCommand,
): Promise<MaestroDaemonOperationRequest> {
const requests: MaestroDaemonOperationRequest[] = [];
const port = createDaemonMaestroRuntimePort({
...makeRuntimeEnvelope({ flags: { platform, replayBackend: 'maestro' } }),
invoke: async (request) => {
requests.push(request);
return request.command === 'snapshot'
? {
ok: true,
data: {
createdAt: requests.length,
nodes: [
{ index: 0, type: 'Application', rect: { x: 0, y: 0, width: 400, height: 800 } },
],
},
}
: { ok: true, data: {} };
},
dependencies: makeDependencies(),
platform,
});
await port.execute({
command,
generation: 0,
env: {},
invalidateObservation() {},
});
const request = requests.find((candidate) => candidate.command === 'type');
if (!request) throw new Error(`${command.kind} did not reach the type command`);
return request;
}

const MAESTRO_2_5_1_ANDROID_INPUT_TEXT_DELAY_MS = 75;
const INPUT_TEXT: TextCommand = { kind: 'inputText', source: { line: 2 }, text: 'hello' };
const ERASE_TEXT: TextCommand = { kind: 'eraseText', source: { line: 2 }, charactersToErase: 3 };

describe('Maestro inputText pacing', () => {
test('Android types with the pinned Maestro per-character delay', async () => {
const request = await typeRequestFor('android', INPUT_TEXT);
expect(request.positionals).toEqual(['hello']);
expect(request.flags?.delayMs).toBe(MAESTRO_2_5_1_ANDROID_INPUT_TEXT_DELAY_MS);
});

test.each(['android', 'ios'] as const)(
'%s eraseText sends backspaces without a delay',
async (platform) => {
const request = await typeRequestFor(platform, ERASE_TEXT);
expect(request.positionals).toEqual(['\b\b\b']);
expect(request.flags).not.toHaveProperty('delayMs');
},
);

test('iOS types without a delay', async () => {
const request = await typeRequestFor('ios', INPUT_TEXT);
expect(request.flags).not.toHaveProperty('delayMs');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ describe('Maestro public operation projection', () => {
operation: { kind: 'typeText', text: 'hello' },
expected: { command: 'type', positionals: ['hello'] },
},
{
operation: { kind: 'typeText', text: 'hello', delayMs: 75 },
expected: { command: 'type', positionals: ['hello'], flags: { delayMs: 75 } },
},
{
operation: { kind: 'gestureViewport' },
expected: { command: 'runtime', positionals: ['gesture-viewport'] },
Expand Down
13 changes: 11 additions & 2 deletions packages/maestro/src/daemon-port/daemon-runtime-port.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,20 @@ function createDaemonMaestroRuntimeParts(options: CreateDaemonMaestroRuntimeOper
applied.push(describePermissionMutation(mutation));
}
};
const inputTextDelayMs =
platform === 'android'
? MAESTRO_RUNTIME_ADAPTER_POLICY.androidInputTextCharacterDelayMs
: undefined;
const typeTextAndSettle = async (
text: string,
context: MaestroRuntimeOperationContext,
delayMs?: number,
): Promise<void> => {
registerDiagnosticSensitiveValue(text);
await invokeMutation({ kind: 'typeText', text }, context);
await invokeMutation(
{ kind: 'typeText', text, ...(delayMs === undefined ? {} : { delayMs }) },
context,
);
const stable = await waitForTypedSnapshotStability({
timeoutMs: MAESTRO_RUNTIME_ADAPTER_POLICY.settleTimeoutMs,
context,
Expand Down Expand Up @@ -262,7 +270,8 @@ function createDaemonMaestroRuntimeParts(options: CreateDaemonMaestroRuntimeOper
);
return data ? { data } : undefined;
},
inputText: async (input, context) => await typeTextAndSettle(input.text, context),
inputText: async (input, context) =>
await typeTextAndSettle(input.text, context, inputTextDelayMs),
eraseText: async (input, context) =>
await typeTextAndSettle(
'\b'.repeat(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export type MaestroPublicOperation =
mode?: 'full' | 'limited';
}
| { kind: 'openLink'; appId?: string; link: string; prewarmRunner: boolean }
| { kind: 'typeText'; text: string }
| { kind: 'typeText'; text: string; delayMs?: number }
| {
kind: 'clickSelector';
selector: MaestroDispatchSelector;
Expand Down Expand Up @@ -288,7 +288,11 @@ function projectInputOperation(operation: MaestroInputOperation): MaestroDaemonO
case 'gestureViewport':
return { command: 'runtime', positionals: ['gesture-viewport'] };
case 'typeText':
return { command: 'type', positionals: [operation.text] };
return {
command: 'type',
positionals: [operation.text],
...(operation.delayMs === undefined ? {} : { flags: { delayMs: operation.delayMs } }),
};
case 'clickSelector':
return projectSelectorClick(operation);
case 'clickPoint':
Expand Down
1 change: 1 addition & 0 deletions packages/maestro/src/internal/compatibility-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const MAESTRO_COMPATIBILITY_PRESETS = {
scrollUntilVisibleSpeed: 40,
scrollUntilVisiblePercentage: 100,
eraseTextMaxCharacters: 50,
androidInputTextCharacterDelayMs: 75,
},
// ScreenshotUtils.waitForAppToSettle and MAX_TIMEOUT_WAIT_TO_SETTLE_MS.
observation: {
Expand Down
2 changes: 2 additions & 0 deletions packages/maestro/src/internal/facade-runtime-port.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export const MAESTRO_RUNTIME_ADAPTER_POLICY = {
retryTapMaxAttempts: MAESTRO_COMPATIBILITY_PRESETS.command.retryTapMaxAttempts,
longPressDurationMs: MAESTRO_COMPATIBILITY_PRESETS.command.longPressDurationMs,
eraseTextMaxCharacters: MAESTRO_COMPATIBILITY_PRESETS.command.eraseTextMaxCharacters,
androidInputTextCharacterDelayMs:
MAESTRO_COMPATIBILITY_PRESETS.command.androidInputTextCharacterDelayMs,
scrollUntilVisiblePercentage: MAESTRO_COMPATIBILITY_PRESETS.command.scrollUntilVisiblePercentage,
animationWaitTimeoutMs: MAESTRO_COMPATIBILITY_PRESETS.command.waitForAnimationToEndTimeoutMs,
animationDifferencePercent:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ test(
{
action: 'type',
text: 'Łódź café',
delayMs: 0,
delayMs: 75,
},
]);
assert.deepEqual(
Expand Down Expand Up @@ -313,7 +313,7 @@ test(
{
action: 'type',
text: 'Łódź café',
delayMs: 0,
delayMs: 75,
},
]);
assert.equal(
Expand Down