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
29 changes: 28 additions & 1 deletion test/integration/provider-scenarios/ios-lifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import assert from 'node:assert/strict';
import path from 'node:path';
import { test } from 'vitest';
import { assertFlatToolCall } from './assertions.ts';
import { assertFlatToolCall, assertRpcOk } from './assertions.ts';
import {
createIosBottomTabsSnapshotWorld,
createIosPhysicalReinstallWorld,
createIosSettingsWorld,
createIosSettledClickWorld,
IOS_SETTLED_CLICK_TAP,
} from './ios-world.ts';
import { runProviderScenario } from './scenario.ts';
import {
Expand Down Expand Up @@ -336,6 +338,31 @@ test(
PARALLEL_PROVIDER_SCENARIO_TIMEOUT_MS,
);

test(
'Provider-backed integration iOS click right after an unobservable open taps the settled point',
async () => {
await withProviderScenarioResource(
createIosSettledClickWorld,
async ({ daemon, runnerTranscript }) => {
const flags = { platform: 'ios', udid: PROVIDER_SCENARIO_IOS_SIMULATOR.id } as const;
const open = assertRpcOk<{ timing?: { postOpenObservation?: string } }>(
await daemon.callCommand('open', ['com.apple.Preferences'], flags),
);
assert.equal(open.timing?.postOpenObservation, 'unobservable');

assertRpcOk(await daemon.callCommand('click', ['label="General"'], flags));
const taps = runnerTranscript.calls.filter((call) => call.command === 'ios.runner.tap');
assert.deepEqual(
taps.map((call) => call.request),
[IOS_SETTLED_CLICK_TAP],
);
runnerTranscript.assertComplete();
},
);
},
PARALLEL_PROVIDER_SCENARIO_TIMEOUT_MS,
);

test('Provider-backed integration iOS physical reinstall uses scripted devicectl provider', async () => {
await withProviderScenarioResource(
createIosPhysicalReinstallWorld,
Expand Down
94 changes: 94 additions & 0 deletions test/integration/provider-scenarios/ios-world.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,69 @@ export async function createIosBottomTabsSnapshotWorld(): Promise<IosBottomTabsS
};
}

type IosSettledClickWorld = {
daemon: ProviderScenarioHarness;
runnerTranscript: ProviderScenarioTranscript;
close: () => Promise<void>;
};

// Centre of the settled `General` cell (y=406, height 44): the point the click must tap.
export const IOS_SETTLED_CLICK_TAP = {
command: 'tap',
x: 201,
y: 428,
synthesized: true,
appBundleId: 'com.apple.Preferences',
} as const;

export async function createIosSettledClickWorld(): Promise<IosSettledClickWorld> {
// The list is still sliding in on the first read after open and rests 87 points lower on the
// next two. The quiet-window loop must consume all three before the click resolves its target.
const runnerTranscript = createProviderTranscript([
generalCellSnapshot({ y: 319 }),
generalCellSnapshot({ y: 406 }),
generalCellSnapshot({ y: 406 }),
{
command: 'ios.runner.tap',
deviceId: PROVIDER_SCENARIO_IOS_SIMULATOR.id,
platform: 'apple',
result: { tapped: true },
},
]);
const appleRunnerProvider = createAppleRunnerProviderFromTranscript(
runnerTranscript,
'ios.runner',
);
const appleTool = createRecordingAppleToolProvider({
simctl: async (args) => {
const listDevices = simctlListDevicesResult(
args,
'com.apple.CoreSimulator.SimRuntime.iOS-18-0',
[{ name: 'iPhone 15', udid: 'sim-1' }],
);
if (listDevices) {
return listDevices;
}
return { stdout: '', stderr: '', exitCode: 0 };
},
});
const daemon = await createProviderScenarioHarness({
appleRunnerProvider: () => appleRunnerProvider,
appleToolProvider: () => appleTool.provider,
deviceInventoryProvider: async () => [PROVIDER_SCENARIO_IOS_SIMULATOR],
});
let closed = false;
return {
daemon,
runnerTranscript,
close: async () => {
if (closed) return;
closed = true;
await daemon.close();
},
};
}

export async function createIosPhysicalReinstallWorld(): Promise<IosPhysicalReinstallWorld> {
const appleTool = createRecordingAppleToolProvider({
devicectl: async (args) => {
Expand Down Expand Up @@ -393,6 +456,37 @@ function runnerSnapshot() {
};
}

function generalCellSnapshot(options: { y: number }) {
return {
command: 'ios.runner.snapshot',
deviceId: PROVIDER_SCENARIO_IOS_SIMULATOR.id,
platform: 'apple' as const,
result: {
nodes: [
{
index: 0,
type: 'XCUIElementTypeApplication',
label: 'Settings',
identifier: 'com.apple.Preferences',
rect: IOS_SETTINGS_VIEWPORT,
enabled: true,
hittable: true,
},
{
index: 1,
type: 'XCUIElementTypeCell',
label: 'General',
identifier: 'General',
rect: { x: 16, y: options.y, width: 370, height: 44 },
enabled: true,
hittable: true,
},
],
truncated: false,
},
};
}

function runnerGestureViewport() {
return {
command: 'ios.runner.gestureViewport',
Expand Down
Loading