Files
callstack__agent-device/test/integration/ios-simulator-e2e/live-automation-scenario.ts
Michał Pierzchała 611858103e fix(ios): harden Bluesky-class interaction reliability (#1588)
* fix: type into focused iOS inputs without AX

* fix: fill AX-hostile iOS text inputs

* fix: keep scrolling containers from stealing taps

* fix: stop agents after explicit task success

* chore: format benchmark guidance

* fix(ios): preserve fill semantics across fast paths

* test: retire direct selector fill expectations

* test: assert runtime selector fill evidence

* fix(ios): preserve verified and Maestro fill paths

* refactor(ios): isolate synthesized text entry

* fix(client): preserve open diagnostic paths

* fix(ios): expose structured text entry route

* fix(packaging): strip text entry policy tests
2026-08-05 08:02:44 +02:00

231 lines
9.5 KiB
TypeScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { PUBLIC_COMMANDS } from '../../../src/command-catalog.ts';
import {
assertElementText,
assertElementTextAfterScrolling,
assertJsonContains,
assertWaitText,
} from './live-assertions.ts';
import { clearStateLaunchUrlMaestroFlow } from './live-fixtures.ts';
import { type LiveContext, runStep, verifyBehavior, verifyCommand } from './live-harness.ts';
const C = PUBLIC_COMMANDS;
const FIXTURE_HOME_TITLE = 'Agent Device Tester';
const AUTOMATION_DEEP_LINK =
'agent-device-test-app:///automation?event=cold.start&payload=%7B%22source%22%3A%22deep-link%22%7D';
async function observeFixtureHome(context: LiveContext) {
await assertWaitText(context, FIXTURE_HOME_TITLE);
const snapshot = await runStep(context, 'capture fixture home', [
'snapshot',
'-i',
'-s',
FIXTURE_HOME_TITLE,
]);
const nodes = Array.isArray(snapshot.json?.data?.nodes) ? snapshot.json.data.nodes : [];
assert.ok(
nodes.some((node: { label?: unknown }) => node.label === FIXTURE_HOME_TITLE),
`home snapshot nodes should expose ${FIXTURE_HOME_TITLE}: ${JSON.stringify(snapshot.json)}`,
);
return snapshot;
}
export async function assertAutomationInput(context: LiveContext): Promise<void> {
const opened = await runStep(context, 'cold launch fixture', [
'open',
context.appId,
'--relaunch',
]);
assertJsonContains(opened, context.appId, 'open response should retain fixture identity');
const runnerLogPath = opened.json?.data?.runnerLogPath;
assert.equal(
typeof runnerLogPath,
'string',
`open response should expose the authoritative runner log path: ${JSON.stringify(opened.json)}`,
);
context.runnerLogPath = runnerLogPath;
if (context.tier === 'full') {
await runStep(context, 'normalize simulator orientation', ['orientation', 'portrait']);
await runStep(context, 'normalize simulator appearance', ['settings', 'appearance', 'light']);
}
await observeFixtureHome(context);
verifyCommand(context, C.snapshot, 'interactive fixture tree exposes its stable title');
verifyCommand(context, C.open, 'cold launch exposes the fixture UI through snapshot and wait');
await openAutomationDeepLink(context, 'cold launch fixture through a deep link');
await acceptDeepLinkConfirmationIfPresent(context);
await assertWaitText(context, 'Automation lab');
await assertElementText(context, 'id="automation-event-name"', 'cold.start');
await assertElementText(context, 'id="automation-event-payload"', '{"source":"deep-link"}');
await assertClearStateLaunchUrl(context);
await runStep(context, 'navigate onward from cold deep link', [
'click',
'id="automation-continue-catalog"',
]);
await runStep(context, 'wait for exact catalog destination', [
'wait',
'id="catalog-title"',
'10000',
]);
verifyBehavior(
context,
'cold-start-deep-link-navigation',
'launchApp(clearState) removed a seeded data canary, rendered the stored deep route and payload, and continued into the fixture catalog',
);
await runStep(context, 'wait for settings tab target', ['wait', 'label="Settings"', '10000']);
await runStep(context, 'open settings tab', ['click', 'label="Settings"']);
await assertWaitText(context, 'Settings');
await runStep(context, 'open automation route', ['click', 'id="open-automation-lab"']);
await assertWaitText(context, 'Automation lab');
verifyCommand(context, C.click, 'selector click opens the automation route');
await runStep(context, 'open fixture sheet', ['click', 'id="automation-open-sheet"']);
await assertWaitText(context, 'Automation sheet');
await runStep(context, 'close fixture sheet', ['click', 'id="automation-close-sheet"']);
await assertWaitText(context, 'Automation lab');
await runStep(context, 'restore automation route top after sheet', ['scroll', 'top']);
verifyBehavior(
context,
'modal-open-close',
'page-sheet modal exposed structural content and returned to the automation route',
);
const heading = await runStep(context, 'read automation heading', [
'get',
'text',
'text="Automation lab"',
]);
assertJsonContains(heading, 'Automation lab', 'get text should return automation heading');
verifyCommand(context, C.get, 'get returns exact automation heading text');
const visible = await runStep(context, 'assert automation heading visible', [
'is',
'visible',
'id="automation-title"',
]);
assert.equal(visible.json?.data?.pass, true, JSON.stringify(visible.json));
verifyCommand(context, C.is, 'visible predicate passes for the automation heading');
const found = await runStep(context, 'find automation heading', [
'find',
'text',
'Automation lab',
'exists',
]);
assert.equal(found.json?.data?.found, true, JSON.stringify(found.json));
verifyCommand(context, C.find, 'find reports the automation heading');
await assertElementTextAfterScrolling(context, 'id="automation-press"', 'Press canary');
const pressVisible = await runStep(context, 'assert automation-press is visible', [
'is',
'visible',
'id="automation-press"',
]);
assert.equal(pressVisible.json?.data?.pass, true, JSON.stringify(pressVisible.json));
await runStep(context, 'press semantic canary', ['press', 'id="automation-press"']);
await assertWaitText(context, 'Last input: press');
verifyCommand(context, C.press, 'semantic press changes the durable fixture canary');
await assertElementTextAfterScrolling(context, 'id="automation-longpress"', 'Long press canary');
const longPressVisible = await runStep(context, 'assert automation-longpress is visible', [
'is',
'visible',
'id="automation-longpress"',
]);
assert.equal(longPressVisible.json?.data?.pass, true, JSON.stringify(longPressVisible.json));
await runStep(context, 'long press semantic canary', [
'longpress',
'id="automation-longpress"',
'800',
]);
await assertWaitText(context, 'Long presses: 1');
verifyCommand(context, C.longPress, '800ms hold increments the long-press counter');
await runStep(context, 'scroll native alert canary into view', ['scroll', 'down', '1']);
await runStep(context, 'open native alert', ['click', 'id="automation-open-alert"']);
const alert = await runStep(context, 'wait for native alert', ['alert', 'wait', '5000']);
assertJsonContains(alert, 'Automation confirmation', 'alert wait should return fixture alert');
await runStep(context, 'inspect native alert', ['alert', 'get']);
await runStep(context, 'dismiss native alert', ['alert', 'dismiss']);
await assertWaitText(context, 'Alert result: cancelled');
await runStep(context, 'reopen native alert', ['click', 'id="automation-open-alert"']);
await runStep(context, 'accept native alert', ['alert', 'accept']);
await assertWaitText(context, 'Alert result: accepted');
verifyCommand(context, C.alert, 'alert wait/get/dismiss/accept produce both fixture outcomes');
await runStep(context, 'return from automation route', ['back']);
await assertWaitText(context, 'Settings');
verifyCommand(context, C.back, 'back returns from automation route to Settings');
}
async function assertClearStateLaunchUrl(context: LiveContext): Promise<void> {
const prepared = await runStep(context, 'prepare fixture data clear canary', [
'settings',
'clear-app-state',
context.appId,
]);
const containerPath = prepared.json?.data?.containerPath;
assert.ok(
typeof containerPath === 'string' && path.isAbsolute(containerPath),
`clear-state response should expose an absolute data container: ${JSON.stringify(prepared.json)}`,
);
const canaryPath = path.join(containerPath, 'Documents', 'agent-device-clear-state-canary.txt');
fs.mkdirSync(path.dirname(canaryPath), { recursive: true });
fs.writeFileSync(canaryPath, 'must be removed by launchApp(clearState: true)\n');
assert.equal(fs.existsSync(canaryPath), true, `failed to seed clear-state canary: ${canaryPath}`);
const flowPath = path.join(context.artifactDir, 'clear-state-launch-url.yaml');
fs.writeFileSync(flowPath, clearStateLaunchUrlMaestroFlow(context.appId));
const replay = await runStep(context, 'launch clear-state fixture through stored URL', [
'replay',
flowPath,
'--maestro',
]);
assert.equal(replay.json?.data?.replayed, 3, JSON.stringify(replay.json));
assert.equal(
fs.existsSync(canaryPath),
false,
`launchApp(clearState: true) retained data canary: ${canaryPath}`,
);
await assertElementText(context, 'id="automation-event-name"', 'cold.start');
await assertElementText(context, 'id="automation-event-payload"', '{"source":"deep-link"}');
}
async function acceptDeepLinkConfirmationIfPresent(context: LiveContext): Promise<void> {
const destination = await runStep(
context,
'wait for deep-link destination before inspecting system UI',
['wait', 'text', 'Automation lab', '2500'],
{ allowFailure: true },
);
if (destination.status === 0) return;
const alert = await runStep(context, 'inspect delayed deep-link system alert', ['alert', 'get']);
const alertInfo = alert.json?.data;
assert.match(String(alertInfo?.message), /^Open in\b/, JSON.stringify(alert.json));
assert.ok(
Array.isArray(alertInfo?.items) && alertInfo.items.includes('Open'),
JSON.stringify(alert.json),
);
await runStep(context, 'accept deep-link confirmation', ['alert', 'accept']);
}
async function openAutomationDeepLink(context: LiveContext, step: string): Promise<void> {
await runStep(context, step, [
'open',
context.appId,
'--relaunch',
'--launch-url',
AUTOMATION_DEEP_LINK,
]);
}