mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
feat: support standalone Maestro clearState command (#2366)
* feat: support standalone Maestro clearState command Accept '- clearState' / '- clearState: <appId>' in Maestro YAML flows. Unlike launchApp.clearState (clear-then-open), the standalone form clears app state without relaunching, projecting to 'settings clear-app-state' on the daemon. Covers the Rocket.Chat login-with-deeplink helper, which previously failed with 'Maestro command "clearState" is not supported'. * test(maestro): cover standalone clearState with authored corpus flow Replace the UNVERIFIED_COMMANDS exemption with an authored clear-state flow exercising default and explicit appIds, plus the regenerated upstream parser fixture proving Maestro compatibility. Live iOS Simulator evidence (iPhone 16, com.apple.mobilesafari): - marker files in the data container, then replay '- clearState' (default) and '- clearState: <appId>' (explicit) via 'replay --maestro'; both replay 1/1, wipe the container, and leave MobileSafari not running (no reopen).
This commit is contained in:
@@ -475,6 +475,27 @@ describe('parseMaestroProgram', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('parses standalone clearState with an explicit or config app id', () => {
|
||||
const program = parseMaestroProgram(
|
||||
`appId: example.app
|
||||
---
|
||||
- clearState: example.app
|
||||
- clearState
|
||||
`,
|
||||
{ sourcePath: '/flows/clear.yaml' },
|
||||
);
|
||||
|
||||
assert.deepEqual(program.commands[0], {
|
||||
kind: 'clearState',
|
||||
source: { path: '/flows/clear.yaml', line: 3 },
|
||||
appId: 'example.app',
|
||||
});
|
||||
assert.deepEqual(program.commands[1], {
|
||||
kind: 'clearState',
|
||||
source: { path: '/flows/clear.yaml', line: 4 },
|
||||
});
|
||||
});
|
||||
|
||||
test('preserves source paths for unsupported and malformed flows', () => {
|
||||
const sourcePath = '/flows/includes/child.yaml';
|
||||
assert.throws(
|
||||
|
||||
@@ -59,6 +59,7 @@ export function makeOperations(
|
||||
resolveGestureViewport: async () => ({ x: 0, y: 0, width: 402, height: 874 }),
|
||||
launchApp: noOp,
|
||||
stopApp: noOp,
|
||||
clearState: noOp,
|
||||
openLink: noOp,
|
||||
tapOn: noOp,
|
||||
doubleTapOn: noOp,
|
||||
|
||||
@@ -122,6 +122,28 @@ describe('MaestroRuntimePort', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('dispatches standalone clearState with an explicit or config app id', async () => {
|
||||
const calls: RecordedCall[] = [];
|
||||
const operations = makeOperations({
|
||||
clearState: vi.fn(async (input, context) => record(calls, 'clearState', input, context)),
|
||||
});
|
||||
const program = parseMaestroProgram(
|
||||
[
|
||||
'appId: com.example.checkout',
|
||||
'---',
|
||||
'- clearState: com.example.checkout',
|
||||
'- clearState',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
const result = await executeMaestroProgram(program, createMaestroRuntimePort(operations));
|
||||
|
||||
expect(result).toMatchObject({ executed: 2, skipped: 0 });
|
||||
expect(calls.map(({ kind }) => kind)).toEqual(['clearState', 'clearState']);
|
||||
expect(calls[0]).toMatchObject({ input: { appId: 'com.example.checkout' } });
|
||||
expect(calls[1]).toMatchObject({ input: { appId: 'com.example.checkout' } });
|
||||
});
|
||||
|
||||
test('preserves observation validity after visual waits and scripts', async () => {
|
||||
const waitInvalidation = vi.fn();
|
||||
const scriptInvalidation = vi.fn();
|
||||
|
||||
@@ -77,6 +77,7 @@ export type CanonicalCommand =
|
||||
| { kind: 'takeScreenshot' }
|
||||
| { kind: 'waitForAnimationToEnd'; timeout?: number | string }
|
||||
| { kind: 'stopApp' }
|
||||
| { kind: 'clearState'; appId?: string }
|
||||
| { kind: 'repeat'; times: string | number }
|
||||
| { kind: 'retry'; maxRetries?: string | number }
|
||||
| { kind: 'runFlow'; label?: string; source: 'file' | 'commands' }
|
||||
@@ -92,6 +93,27 @@ const UPSTREAM_CONFIG_TYPES = new Set(['ApplyConfigurationCommand', 'DefineVaria
|
||||
|
||||
type UpstreamCommand = { type: string; fields: Record<string, unknown> };
|
||||
|
||||
function canonicalizeUpstreamLifecycleCommand(
|
||||
command: UpstreamCommand,
|
||||
): CanonicalCommand | undefined {
|
||||
const f = command.fields;
|
||||
switch (command.type) {
|
||||
case 'LaunchAppCommand':
|
||||
return dropUndefined({
|
||||
kind: 'launchApp' as const,
|
||||
appId: str(f.appId),
|
||||
clearState: bool(f.clearState),
|
||||
stopApp: bool(f.stopApp),
|
||||
});
|
||||
case 'StopAppCommand':
|
||||
return { kind: 'stopApp' };
|
||||
case 'ClearStateCommand':
|
||||
return dropUndefined({ kind: 'clearState' as const, appId: str(f.appId) });
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function canonicalizeUpstreamFlow(commands: UpstreamCommand[]): CanonicalCommand[] {
|
||||
return commands
|
||||
.filter((command) => !UPSTREAM_CONFIG_TYPES.has(command.type))
|
||||
@@ -99,15 +121,10 @@ export function canonicalizeUpstreamFlow(commands: UpstreamCommand[]): Canonical
|
||||
}
|
||||
|
||||
function canonicalizeUpstreamCommand(command: UpstreamCommand): CanonicalCommand {
|
||||
const lifecycle = canonicalizeUpstreamLifecycleCommand(command);
|
||||
if (lifecycle) return lifecycle;
|
||||
const f = command.fields;
|
||||
switch (command.type) {
|
||||
case 'LaunchAppCommand':
|
||||
return dropUndefined({
|
||||
kind: 'launchApp',
|
||||
appId: str(f.appId),
|
||||
clearState: bool(f.clearState),
|
||||
stopApp: bool(f.stopApp),
|
||||
});
|
||||
case 'TapOnElementCommand': {
|
||||
const repeat = asRecord(f.repeat);
|
||||
return canonicalTap({
|
||||
@@ -196,8 +213,6 @@ function canonicalizeUpstreamCommand(command: UpstreamCommand): CanonicalCommand
|
||||
kind: 'waitForAnimationToEnd',
|
||||
timeout: numLike(f.timeout) ?? str(f.timeout),
|
||||
});
|
||||
case 'StopAppCommand':
|
||||
return { kind: 'stopApp' };
|
||||
case 'RepeatCommand':
|
||||
return { kind: 'repeat', times: numLike(f.times) ?? str(f.times) ?? '' };
|
||||
case 'RetryCommand':
|
||||
@@ -302,8 +317,19 @@ export function canonicalizeAgentCommands(
|
||||
return program.commands.map((command) => canonicalizeAgentCommand(command, program.config));
|
||||
}
|
||||
|
||||
function canonicalizeAgentCommand(
|
||||
command: MaestroCommand,
|
||||
type AgentLifecycleCommand = Extract<
|
||||
MaestroCommand,
|
||||
{ kind: 'launchApp' | 'stopApp' | 'clearState' }
|
||||
>;
|
||||
|
||||
function isAgentLifecycleCommand(command: MaestroCommand): command is AgentLifecycleCommand {
|
||||
return (
|
||||
command.kind === 'launchApp' || command.kind === 'stopApp' || command.kind === 'clearState'
|
||||
);
|
||||
}
|
||||
|
||||
function canonicalizeAgentLifecycleCommand(
|
||||
command: AgentLifecycleCommand,
|
||||
config: MaestroProgram['config'],
|
||||
): CanonicalCommand {
|
||||
switch (command.kind) {
|
||||
@@ -314,6 +340,19 @@ function canonicalizeAgentCommand(
|
||||
clearState: command.clearState,
|
||||
stopApp: command.stopApp,
|
||||
});
|
||||
case 'stopApp':
|
||||
return { kind: 'stopApp' };
|
||||
case 'clearState':
|
||||
return dropUndefined({ kind: 'clearState', appId: command.appId ?? config.appId });
|
||||
}
|
||||
}
|
||||
|
||||
function canonicalizeAgentCommand(
|
||||
command: MaestroCommand,
|
||||
config: MaestroProgram['config'],
|
||||
): CanonicalCommand {
|
||||
if (isAgentLifecycleCommand(command)) return canonicalizeAgentLifecycleCommand(command, config);
|
||||
switch (command.kind) {
|
||||
case 'tapOn': {
|
||||
const repeat = numLike(command.repeat) ?? 1;
|
||||
const repeatIsNumber = typeof repeat === 'number';
|
||||
@@ -408,8 +447,6 @@ function canonicalizeAgentCommand(
|
||||
return { kind: 'takeScreenshot' };
|
||||
case 'waitForAnimationToEnd':
|
||||
return dropUndefined({ kind: 'waitForAnimationToEnd', timeout: numLike(command.timeout) });
|
||||
case 'stopApp':
|
||||
return { kind: 'stopApp' };
|
||||
case 'repeat':
|
||||
return { kind: 'repeat', times: numLike(command.times) ?? str(command.times) ?? '' };
|
||||
case 'retry':
|
||||
|
||||
@@ -3,6 +3,7 @@ import { stripUndefined } from './shared.ts';
|
||||
import type {
|
||||
MaestroAssertTrueCommand,
|
||||
MaestroBackCommand,
|
||||
MaestroClearStateCommand,
|
||||
MaestroCommand,
|
||||
MaestroEraseTextCommand,
|
||||
MaestroExtendedWaitUntilCommand,
|
||||
@@ -122,6 +123,7 @@ const COMMAND_VALUE_PARSERS: Readonly<Record<string, CommandValueParser>> = {
|
||||
back: parseBack,
|
||||
waitForAnimationToEnd: parseWaitForAnimationToEnd,
|
||||
stopApp: parseStopApp,
|
||||
clearState: parseClearState,
|
||||
runScript: parseMaestroRunScriptCommand,
|
||||
runFlow: (value, node, context) =>
|
||||
parseMaestroRunFlowCommand(value, node, context, parseMaestroCommandList),
|
||||
@@ -447,6 +449,16 @@ function parseStopApp(
|
||||
return { kind: 'stopApp', source, appId: readRequiredString(value, 'stopApp', context) };
|
||||
}
|
||||
|
||||
function parseClearState(
|
||||
value: Node | null,
|
||||
commandNode: Node,
|
||||
context: MaestroProgramParseContext,
|
||||
): MaestroClearStateCommand {
|
||||
const source = sourceAt(commandNode, context);
|
||||
if (isNullNode(value)) return { kind: 'clearState', source };
|
||||
return { kind: 'clearState', source, appId: readRequiredString(value, 'clearState', context) };
|
||||
}
|
||||
|
||||
function parseLaunchArguments(
|
||||
node: Node | null | undefined,
|
||||
name: string,
|
||||
|
||||
@@ -207,6 +207,12 @@ export type MaestroStopAppCommand = {
|
||||
appId?: string;
|
||||
};
|
||||
|
||||
export type MaestroClearStateCommand = {
|
||||
kind: 'clearState';
|
||||
source: MaestroSourceLocation;
|
||||
appId?: string;
|
||||
};
|
||||
|
||||
export type MaestroRunScriptCommand = {
|
||||
kind: 'runScript';
|
||||
source: MaestroSourceLocation;
|
||||
@@ -265,6 +271,7 @@ export type MaestroCommand =
|
||||
| MaestroBackCommand
|
||||
| MaestroWaitForAnimationToEndCommand
|
||||
| MaestroStopAppCommand
|
||||
| MaestroClearStateCommand
|
||||
| MaestroRunScriptCommand
|
||||
| MaestroRunFlowCommand
|
||||
| MaestroRepeatCommand
|
||||
|
||||
@@ -29,7 +29,9 @@ type MaestroCommandOf<K extends MaestroRuntimeCommand['kind']> = Extract<
|
||||
{ kind: K }
|
||||
>;
|
||||
|
||||
type MaestroLifecycleCommand = MaestroCommandOf<'launchApp' | 'stopApp' | 'openLink'>;
|
||||
type MaestroLifecycleCommand = MaestroCommandOf<
|
||||
'launchApp' | 'stopApp' | 'clearState' | 'openLink'
|
||||
>;
|
||||
type MaestroTargetCommand = MaestroCommandOf<'tapOn' | 'doubleTapOn' | 'longPressOn'>;
|
||||
type MaestroTextCommand = MaestroCommandOf<'inputText' | 'eraseText'>;
|
||||
type MaestroNavigationCommand = MaestroCommandOf<
|
||||
@@ -53,6 +55,7 @@ type MaestroRuntimeCommandHandlers = {
|
||||
const MAESTRO_RUNTIME_COMMAND_HANDLERS = {
|
||||
launchApp: executeLifecycleCommand,
|
||||
stopApp: executeLifecycleCommand,
|
||||
clearState: executeLifecycleCommand,
|
||||
openLink: executeLifecycleCommand,
|
||||
tapOn: executeTargetCommand,
|
||||
doubleTapOn: executeTargetCommand,
|
||||
@@ -77,6 +80,7 @@ const MAESTRO_RUNTIME_COMMAND_HANDLERS = {
|
||||
const MAESTRO_COMMAND_REQUIRES_SETTLED_PREDECESSOR = {
|
||||
launchApp: true,
|
||||
stopApp: true,
|
||||
clearState: true,
|
||||
openLink: true,
|
||||
tapOn: true,
|
||||
doubleTapOn: true,
|
||||
@@ -142,6 +146,13 @@ async function executeLifecycleCommand(
|
||||
context,
|
||||
'invalidate',
|
||||
);
|
||||
case 'clearState':
|
||||
return await invokeOperation(
|
||||
operations.clearState,
|
||||
{ appId: command.appId ?? request.appId },
|
||||
context,
|
||||
'invalidate',
|
||||
);
|
||||
case 'openLink':
|
||||
return await invokeOperation(
|
||||
operations.openLink,
|
||||
|
||||
@@ -123,6 +123,7 @@ export type MaestroRuntimeOperations = {
|
||||
readonly launchArguments?: MaestroLaunchArguments;
|
||||
}>;
|
||||
readonly stopApp: MaestroRuntimeOperation<{ readonly appId?: string }>;
|
||||
readonly clearState: MaestroRuntimeOperation<{ readonly appId?: string }>;
|
||||
readonly openLink: MaestroRuntimeOperation<{ readonly link: string }>;
|
||||
|
||||
readonly tapOn: MaestroRuntimeOperation<{
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
export const MAESTRO_COMPAT_SUPPORTED_CAPABILITIES = [
|
||||
'Flows: launchApp; runFlow file/inline with platform, visibility, and limited boolean conditions; onFlowStart/onFlowComplete; repeat.times and retry.',
|
||||
'Interactions: tapOn, doubleTapOn, longPressOn, inputText on the focused element, eraseText, openLink, hideKeyboard, basic pressKey, and back; selector targets poll until available and support recursive index, childOf, above, below, leftOf, rightOf, containsChild, containsDescendants, points, and optional; outer command labels are metadata, not target selectors.',
|
||||
'Assertions and navigation: assertVisible, assertNotVisible, assertTrue (literal values and ${VAR} lookups only; "", "false", "0", "null", and "undefined" are falsy, everything else is truthy), extendedWaitUntil, scroll, scrollUntilVisible, absolute/percentage/target swipe, takeScreenshot, waitForAnimationToEnd, and stopApp.',
|
||||
'Assertions and navigation: assertVisible, assertNotVisible, assertTrue (literal values and ${VAR} lookups only; "", "false", "0", "null", and "undefined" are falsy, everything else is truthy), extendedWaitUntil, scroll, scrollUntilVisible, absolute/percentage/target swipe, takeScreenshot, waitForAnimationToEnd, clearState, and stopApp.',
|
||||
'Scripts: ordered runScript file/env scripts with http.post, json, and output variables.',
|
||||
] as const;
|
||||
|
||||
export const MAESTRO_COMPAT_LIMITATIONS = [
|
||||
'Runtime: iOS and Android only; launchApp.clearState supports Android and iOS simulators, launch arguments are Apple-only, and standalone device utility/state commands are unsupported.',
|
||||
'Runtime: iOS and Android only; launchApp.clearState and standalone clearState support Android and iOS simulators, launch arguments are Apple-only, and other standalone device utility/state commands are unsupported.',
|
||||
'Expressions: when.true supports boolean literals and maestro.platform comparisons; assertTrue supports literal values and ${VAR} lookups only; repeat.while, evalScript, and broader JavaScript expressions are unsupported.',
|
||||
'Environment: flow env is the default, AD_VAR_* overrides it, and CLI -e KEY=VALUE wins over both.',
|
||||
'Failure diagnostics: resolved targets and runFlow paths are rendered, while inputText payloads remain hidden; do not place secrets in diagnostic identifiers.',
|
||||
|
||||
@@ -22,6 +22,8 @@ function validMaestroCommand(pick: number, salt: number): string[] {
|
||||
() => ['- back'],
|
||||
() => ['- hideKeyboard'],
|
||||
() => ['- stopApp'],
|
||||
() => ['- clearState'],
|
||||
() => [`- clearState: ${text}`],
|
||||
() => ['- scroll'],
|
||||
() => ['- waitForAnimationToEnd'],
|
||||
() => ['- eraseText'],
|
||||
|
||||
@@ -54,6 +54,8 @@ const NOTES = {
|
||||
'Coverage: above, below, leftOf, and rightOf recursively across target, assertion, wait, scroll, and swipe commands.',
|
||||
'authored/numeric-variable-wait':
|
||||
'Coverage: waitForAnimationToEnd timeout accepts a ${VAR} token and projects identically through the canonical model.',
|
||||
'authored/clear-state':
|
||||
'Coverage: standalone clearState with default and explicit appId (no upstream flow exercises it).',
|
||||
'invalid/bad-swipe-direction': 'Lenient-guard: unknown SwipeDirection enum value.',
|
||||
'invalid/unknown-command': 'Lenient-guard: unknown command name (tapOn typo).',
|
||||
'invalid/malformed-selector': 'Lenient-guard: selector given as a sequence.',
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
appId: com.example.app
|
||||
---
|
||||
- clearState
|
||||
- clearState: another.app
|
||||
@@ -464,6 +464,14 @@
|
||||
"note": "Lenient-guard: unknown field inside a selector map."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "authored/clear-state",
|
||||
"file": "authored/clear-state.yaml",
|
||||
"origin": {
|
||||
"kind": "authored",
|
||||
"note": "Coverage: standalone clearState with default and explicit appId (no upstream flow exercises it)."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "authored/doubletap",
|
||||
"file": "authored/doubletap.yaml",
|
||||
|
||||
@@ -3219,6 +3219,45 @@
|
||||
"message": "Unknown Property: bogusField"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "authored/clear-state",
|
||||
"file": "authored/clear-state.yaml",
|
||||
"status": "parsed",
|
||||
"commands": [
|
||||
{
|
||||
"type": "ApplyConfigurationCommand",
|
||||
"fields": {
|
||||
"config": {
|
||||
"appId": "com.example.app",
|
||||
"name": null,
|
||||
"tags": [],
|
||||
"ext": {},
|
||||
"onFlowStart": null,
|
||||
"onFlowComplete": null,
|
||||
"properties": {}
|
||||
},
|
||||
"label": null,
|
||||
"optional": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ClearStateCommand",
|
||||
"fields": {
|
||||
"appId": "com.example.app",
|
||||
"label": null,
|
||||
"optional": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ClearStateCommand",
|
||||
"fields": {
|
||||
"appId": "another.app",
|
||||
"label": null,
|
||||
"optional": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "authored/doubletap",
|
||||
"file": "authored/doubletap.yaml",
|
||||
@@ -4976,5 +5015,5 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"contentHash": "857715bbddad493e6242decf7d8f83a0d7ca2b1d7a1add871fa6b5c544ca29cb"
|
||||
"contentHash": "3fab6cb48d4ac7ec2640faa223b86a1dd58439d1e181ce442a76a5f754efc725"
|
||||
}
|
||||
|
||||
@@ -129,6 +129,45 @@ test('delegates lifecycle and coordinate gestures through public daemon commands
|
||||
]);
|
||||
});
|
||||
|
||||
test('projects standalone clearState to settings without opening the app', async () => {
|
||||
const requests: DaemonRequest[] = [];
|
||||
const invoke: DaemonInvokeFn = async (request) => {
|
||||
requests.push(request);
|
||||
return { ok: true, data: {} };
|
||||
};
|
||||
const port = createDaemonMaestroRuntimePort({
|
||||
baseReq: makeBaseRequest({ flags: { platform: 'android', replayBackend: 'maestro' } }),
|
||||
invoke,
|
||||
dependencies: makeDependencies(),
|
||||
platform: 'android',
|
||||
});
|
||||
|
||||
await port.execute({
|
||||
command: { kind: 'clearState', source: { line: 2 }, appId: 'com.example.app' },
|
||||
generation: 0,
|
||||
env: {},
|
||||
invalidateObservation() {},
|
||||
});
|
||||
await port.execute({
|
||||
command: { kind: 'clearState', source: { line: 3 } },
|
||||
generation: 1,
|
||||
env: {},
|
||||
appId: 'com.example.session',
|
||||
invalidateObservation() {},
|
||||
});
|
||||
|
||||
expect(requests).toEqual([
|
||||
expect.objectContaining({
|
||||
command: 'settings',
|
||||
positionals: ['clear-app-state', 'com.example.app'],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
command: 'settings',
|
||||
positionals: ['clear-app-state', 'com.example.session'],
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test('uses the direct viewport without snapshot and pairs it with the nested gesture request', async () => {
|
||||
const requests: DaemonRequest[] = [];
|
||||
const viewport = { x: 10, y: 20, width: 400, height: 800 };
|
||||
|
||||
@@ -48,6 +48,14 @@ describe('Maestro public operation projection', () => {
|
||||
operation: { kind: 'stopApp' },
|
||||
expected: { command: 'close', positionals: [], internal: { closeAppOnly: true } },
|
||||
},
|
||||
{
|
||||
operation: { kind: 'clearState', appId: 'com.example' },
|
||||
expected: { command: 'settings', positionals: ['clear-app-state', 'com.example'] },
|
||||
},
|
||||
{
|
||||
operation: { kind: 'clearState' },
|
||||
expected: { command: 'settings', positionals: ['clear-app-state'] },
|
||||
},
|
||||
{
|
||||
operation: {
|
||||
kind: 'openLink',
|
||||
|
||||
@@ -132,6 +132,10 @@ function createDaemonMaestroRuntimeParts(options: CreateDaemonMaestroRuntimeOper
|
||||
const appId = input.appId ?? context.appId;
|
||||
await invokeMutation({ kind: 'stopApp', ...(appId ? { appId } : {}) }, context);
|
||||
},
|
||||
clearState: async (input, context) => {
|
||||
const appId = input.appId ?? context.appId;
|
||||
await invokeMutation({ kind: 'clearState', ...(appId ? { appId } : {}) }, context);
|
||||
},
|
||||
openLink: async (input, context) => {
|
||||
await invokeMutation(
|
||||
{
|
||||
|
||||
@@ -20,6 +20,7 @@ export type MaestroPublicOperation =
|
||||
launchArgs: string[];
|
||||
}
|
||||
| { kind: 'stopApp'; appId?: string }
|
||||
| { kind: 'clearState'; appId?: string }
|
||||
| { kind: 'openLink'; appId?: string; link: string; prewarmRunner: boolean }
|
||||
| { kind: 'typeText'; text: string }
|
||||
| {
|
||||
@@ -45,6 +46,7 @@ export type ProjectedMaestroPublicOperation = Pick<DaemonRequest, 'command' | 'p
|
||||
export function projectMaestroPublicOperation(
|
||||
operation: MaestroPublicOperation,
|
||||
): ProjectedMaestroPublicOperation {
|
||||
if (operation.kind === 'clearState') return projectClearState(operation);
|
||||
if (isAppOperation(operation)) return projectAppOperation(operation);
|
||||
if (isCaptureOperation(operation)) return projectCaptureOperation(operation);
|
||||
return projectInputOperation(operation);
|
||||
@@ -96,6 +98,15 @@ function projectStopApp(
|
||||
};
|
||||
}
|
||||
|
||||
function projectClearState(
|
||||
operation: Extract<MaestroPublicOperation, { kind: 'clearState' }>,
|
||||
): ProjectedMaestroPublicOperation {
|
||||
return {
|
||||
command: 'settings',
|
||||
positionals: operation.appId ? ['clear-app-state', operation.appId] : ['clear-app-state'],
|
||||
};
|
||||
}
|
||||
|
||||
function projectOpenLink(
|
||||
operation: Extract<MaestroAppOperation, { kind: 'openLink' }>,
|
||||
): ProjectedMaestroPublicOperation {
|
||||
@@ -108,7 +119,7 @@ function projectOpenLink(
|
||||
|
||||
type MaestroInputOperation = Exclude<
|
||||
MaestroPublicOperation,
|
||||
MaestroAppOperation | MaestroCaptureOperation
|
||||
MaestroAppOperation | MaestroCaptureOperation | { kind: 'clearState' }
|
||||
>;
|
||||
|
||||
function projectInputOperation(operation: MaestroInputOperation): ProjectedMaestroPublicOperation {
|
||||
|
||||
@@ -72,12 +72,12 @@ Supported subset:
|
||||
|
||||
- Flows: `launchApp`; `runFlow` file/inline with platform, visibility, and limited boolean conditions; `onFlowStart`/`onFlowComplete`; `repeat.times` and retry.
|
||||
- Interactions: `tapOn`, `doubleTapOn`, `longPressOn`, `inputText` on the focused element, `eraseText`, `openLink`, `hideKeyboard`, basic `pressKey`, and `back`; selector targets poll until available and support recursive `index`, `childOf`, `above`, `below`, `leftOf`, `rightOf`, `containsChild`, `containsDescendants`, points, and `optional`; outer command labels are metadata, not target selectors.
|
||||
- Assertions and navigation: `assertVisible`, `assertNotVisible`, `assertTrue` (literal values and `${VAR}` lookups only; `""`, `"false"`, `"0"`, `"null"`, and `"undefined"` are falsy, everything else is truthy), `extendedWaitUntil`, `scroll`, `scrollUntilVisible`, absolute/percentage/target `swipe`, `takeScreenshot`, `waitForAnimationToEnd`, and `stopApp`.
|
||||
- Assertions and navigation: `assertVisible`, `assertNotVisible`, `assertTrue` (literal values and `${VAR}` lookups only; `""`, `"false"`, `"0"`, `"null"`, and `"undefined"` are falsy, everything else is truthy), `extendedWaitUntil`, `scroll`, `scrollUntilVisible`, absolute/percentage/target `swipe`, `takeScreenshot`, `waitForAnimationToEnd`, `clearState`, and `stopApp`.
|
||||
- Scripts: ordered `runScript` file/env scripts with `http.post`, `json`, and `output` variables.
|
||||
|
||||
Boundaries:
|
||||
|
||||
- Runtime: iOS and Android only; `launchApp.clearState` supports Android and iOS simulators, launch arguments are Apple-only, and standalone device utility/state commands are unsupported.
|
||||
- Runtime: iOS and Android only; `launchApp.clearState` and standalone `clearState` support Android and iOS simulators, launch arguments are Apple-only, and other standalone device utility/state commands are unsupported.
|
||||
- Expressions: `when.true` supports boolean literals and `maestro.platform` comparisons; `assertTrue` supports literal values and `${VAR}` lookups only; `repeat.while`, `evalScript`, and broader JavaScript expressions are unsupported.
|
||||
- Environment: flow `env` is the default, `AD_VAR_*` overrides it, and CLI `-e KEY=VALUE` wins over both.
|
||||
- Failure diagnostics: resolved targets and `runFlow` paths are rendered, while `inputText` payloads remain hidden; do not place secrets in diagnostic identifiers.
|
||||
|
||||
Reference in New Issue
Block a user