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
4 changes: 2 additions & 2 deletions src/__tests__/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,13 @@ test('apps.open resolves session device identifiers from open response', async (
app: 'Settings',
platform: 'ios',
relaunch: true,
noDeviceHub: true,
deviceHub: true,
});

assert.equal(setup.calls.length, 1);
assert.equal(setup.calls[0]?.command, 'open');
assert.deepEqual(setup.calls[0]?.positionals, ['Settings']);
assert.equal(setup.calls[0]?.flags?.noDeviceHub, true);
assert.equal(setup.calls[0]?.flags?.deviceHub, true);
assert.equal(result.identifiers.session, 'qa');
assert.equal(result.identifiers.deviceId, 'SIM-001');
assert.equal(result.identifiers.udid, 'SIM-001');
Expand Down
2 changes: 1 addition & 1 deletion src/client-normalizers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ export function buildFlags(options: InternalRequestOptions): CommandFlags {
relaunch: options.relaunch,
shutdown: options.shutdown,
saveScript: options.saveScript,
noDeviceHub: options.noDeviceHub,
deviceHub: options.deviceHub,
noRecord: options.noRecord,
backMode: options.backMode,
metroHost: options.metroHost,
Expand Down
4 changes: 2 additions & 2 deletions src/client-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export type AppOpenOptions = AgentDeviceRequestOverrides &
launchArgs?: string[];
relaunch?: boolean;
saveScript?: boolean | string;
noDeviceHub?: boolean;
deviceHub?: boolean;
noRecord?: boolean;
runtime?: SessionRuntimeHints;
};
Expand Down Expand Up @@ -884,7 +884,7 @@ export type InternalRequestOptions = AgentDeviceClientConfig &
relaunch?: boolean;
shutdown?: boolean;
saveScript?: boolean | string;
noDeviceHub?: boolean;
deviceHub?: boolean;
noRecord?: boolean;
backMode?: BackMode;
metroHost?: string;
Expand Down
2 changes: 1 addition & 1 deletion src/commands/cli-grammar/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const appCliReaders = {
launchArgs: flags.launchArgs,
relaunch: flags.relaunch,
saveScript: flags.saveScript,
noDeviceHub: flags.noDeviceHub,
deviceHub: flags.deviceHub,
noRecord: flags.noRecord,
}),
close: (positionals, flags) => ({
Expand Down
4 changes: 1 addition & 3 deletions src/commands/client-command-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,7 @@ export const clientCommandMetadata = [
),
relaunch: booleanField('Force relaunch.'),
saveScript: jsonSchemaField<boolean | string>({ oneOf: [booleanSchema(), stringSchema()] }),
noDeviceHub: booleanField(
'Skip Xcode Device Hub and use the standalone Simulator app when surfacing Apple simulators.',
),
deviceHub: booleanField('Use Xcode Device Hub when surfacing Apple simulators.'),
noRecord: booleanField('Do not record this action.'),
}),
defineClientCommandMetadata('close', {
Expand Down
6 changes: 3 additions & 3 deletions src/daemon-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export type OpenAppOptions = {
launchArgs?: NonNullable<DaemonRequest['flags']>['launchArgs'];
out?: NonNullable<DaemonRequest['flags']>['out'];
saveScript?: NonNullable<DaemonRequest['flags']>['saveScript'];
noDeviceHub?: NonNullable<DaemonRequest['flags']>['noDeviceHub'];
deviceHub?: NonNullable<DaemonRequest['flags']>['deviceHub'];
relaunch?: boolean;
runtime?: DaemonRequest['runtime'];
meta?: Omit<NonNullable<DaemonRequest['meta']>, 'uploadedArtifactId' | 'clientArtifactPaths'>;
Expand Down Expand Up @@ -226,7 +226,7 @@ export async function openApp(options: OpenAppOptions = {}): Promise<DaemonRespo
launchArgs,
out,
saveScript,
noDeviceHub,
deviceHub,
relaunch,
runtime,
meta,
Expand All @@ -249,7 +249,7 @@ export async function openApp(options: OpenAppOptions = {}): Promise<DaemonRespo
...(launchArgs !== undefined ? { launchArgs } : {}),
...(out !== undefined ? { out } : {}),
...(saveScript !== undefined ? { saveScript } : {}),
...(noDeviceHub !== undefined ? { noDeviceHub } : {}),
...(deviceHub !== undefined ? { deviceHub } : {}),
...(relaunch ? { relaunch: true } : {}),
},
...(runtime !== undefined ? { runtime } : {}),
Expand Down
6 changes: 3 additions & 3 deletions src/daemon/__tests__/device-ready.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,21 @@ test('ensureDeviceReady caches successful simulator readiness checks', async ()

expect(mockEnsureBootedSimulator).toHaveBeenCalledTimes(1);
expect(mockEnsureBootedSimulator).toHaveBeenCalledWith(device, {
deviceHub: undefined,
focusExisting: undefined,
preferStandalone: undefined,
});
});

test('ensureDeviceReady focuses cached simulator readiness checks when requested', async () => {
const device: DeviceInfo = { ...IOS_SIMULATOR, simulatorSetPath: '/tmp/simset-a' };

await ensureDeviceReady(device);
await ensureDeviceReady({ ...device }, { focusExisting: true, noDeviceHub: true });
await ensureDeviceReady({ ...device }, { deviceHub: true, focusExisting: true });

expect(mockEnsureBootedSimulator).toHaveBeenCalledTimes(2);
expect(mockEnsureBootedSimulator).toHaveBeenLastCalledWith(
{ ...device },
{ focusExisting: true, preferStandalone: true },
{ deviceHub: true, focusExisting: true },
);
});

Expand Down
4 changes: 2 additions & 2 deletions src/daemon/device-ready.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export const DEVICE_READY_CACHE_TTL_MS = 5_000;
const readyCache = new Map<string, number>();

export type DeviceReadyOptions = {
deviceHub?: boolean;
focusExisting?: boolean;
noDeviceHub?: boolean;
};

export async function ensureDeviceReady(
Expand All @@ -36,8 +36,8 @@ export async function ensureDeviceReady(
if (device.kind === 'simulator') {
const { ensureBootedSimulator } = await import('../platforms/ios/simulator.ts');
await ensureBootedSimulator(device, {
deviceHub: options.deviceHub,
focusExisting: options.focusExisting,
preferStandalone: options.noDeviceHub,
});
markDeviceReady(cacheKey);
return;
Expand Down
2 changes: 1 addition & 1 deletion src/daemon/handlers/session-open-prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ export async function prepareOpenCommandDetails(params: {
}): Promise<PreparedOpenCommandDetailsResult> {
const { req, sessionName, sessionStore, device, surface, openTarget, existingSession } = params;
await ensureDeviceReady(device, {
deviceHub: req.flags?.deviceHub === true,
focusExisting: true,
noDeviceHub: req.flags?.noDeviceHub === true,
});
const { appBundleId, appName } = await resolvePreparedOpenIdentity({
device,
Expand Down
186 changes: 103 additions & 83 deletions src/platforms/ios/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,41 @@ const mockRetryWithPolicy = vi.mocked(retryWithPolicy);
const mockRunIosRunnerCommand = vi.mocked(runIosRunnerCommand);
const mockEnsureBootedSimulator = vi.mocked(ensureBootedSimulator);
const mockOpenIosSimulatorApp = vi.mocked(openIosSimulatorApp);

type MockRunCmdResult = Awaited<ReturnType<typeof runCmd>>;
type MockRunCmdResponse = MockRunCmdResult | (() => MockRunCmdResult);

const OK_RESULT: MockRunCmdResult = { exitCode: 0, stdout: '', stderr: '' };

function mockRunCmdResponses(responses: Record<string, MockRunCmdResponse>): void {
mockRunCmd.mockImplementation(async (cmd, args) => {
const key = formatMockRunCmdCall(cmd, args);
const response = responses[key];
if (!response) throw new Error(`Unexpected command: ${key}`);
return typeof response === 'function' ? response() : response;
});
}

function formatMockRunCmdCall(cmd: string, args: string[]): string {
return `${cmd} ${args.join(' ')}`;
}

function simulatorListDevicesResult(state: string): MockRunCmdResult {
return {
exitCode: 0,
stdout: JSON.stringify({
devices: {
'com.apple.CoreSimulator.SimRuntime.iOS-18-6': [{ udid: 'sim-1', state }],
},
}),
stderr: '',
};
}

function simulatorStateSequence(...states: string[]): () => MockRunCmdResult {
let index = 0;
return () => simulatorListDevicesResult(states[index++] ?? states.at(-1) ?? 'Booted');
}
const mockPrepareStatusBarForScreenshot = vi.mocked(prepareStatusBarForScreenshot);

beforeEach(() => {
Expand Down Expand Up @@ -410,57 +445,49 @@ test('openIosApp custom scheme deep links on iOS devices require app bundle cont
);
});

test('ensureBootedSimulator opens Device Hub after cold boot when available', async () => {
let listCallCount = 0;
mockRunCmd.mockImplementation(async (cmd, args) => {
if (cmd === 'xcrun' && args.join(' ') === 'simctl list devices -j') {
listCallCount += 1;
const state = listCallCount === 1 ? 'Shutdown' : 'Booted';
return {
exitCode: 0,
stdout: JSON.stringify({
devices: {
'com.apple.CoreSimulator.SimRuntime.iOS-18-6': [{ udid: 'sim-1', state }],
},
}),
stderr: '',
};
}
if (cmd === 'xcrun' && args.join(' ') === 'simctl boot sim-1') {
return { exitCode: 0, stdout: '', stderr: '' };
}
if (cmd === 'xcrun' && args.join(' ') === 'simctl bootstatus sim-1 -b') {
return { exitCode: 0, stdout: '', stderr: '' };
}
if (cmd === 'open' && args.join(' ') === '-a Device Hub') {
return { exitCode: 0, stdout: '', stderr: '' };
}
throw new Error(`Unexpected command: ${cmd} ${args.join(' ')}`);
test('ensureBootedSimulator opens Simulator after cold boot by default', async () => {
mockRunCmdResponses({
'xcrun simctl list devices -j': simulatorStateSequence('Shutdown', 'Booted'),
'xcrun simctl boot sim-1': OK_RESULT,
'xcrun simctl bootstatus sim-1 -b': OK_RESULT,
'open -a Simulator': OK_RESULT,
});

await ensureBootedSimulator(IOS_TEST_SIMULATOR, { focusExisting: true });

assert.equal(
mockRunCmd.mock.calls.some(
([cmd, args]) => cmd === 'open' && args.join(' ') === '-a Device Hub',
([cmd, args]) => cmd === 'open' && args.join(' ') === '-a Simulator',
),
true,
);
});

test('openIosSimulatorApp falls back to Simulator when Device Hub is unavailable', async () => {
mockRunCmd.mockImplementation(async (cmd, args) => {
if (cmd === 'open' && args.join(' ') === '-a Device Hub') {
return { exitCode: 1, stdout: '', stderr: 'Unable to find application named Device Hub' };
}
if (cmd === 'open' && args.join(' ') === '-a Simulator') {
return { exitCode: 0, stdout: '', stderr: '' };
}
throw new Error(`Unexpected command: ${cmd} ${args.join(' ')}`);
test('openIosSimulatorApp opens Simulator by default', async () => {
mockRunCmdResponses({
'open -a Simulator': OK_RESULT,
});

await openIosSimulatorApp();

assert.deepEqual(
mockRunCmd.mock.calls.map(([cmd, args]) => [cmd, args.join(' ')]),
[['open', '-a Simulator']],
);
});

test('openIosSimulatorApp uses Device Hub when opted in and falls back to Simulator', async () => {
mockRunCmdResponses({
'open -a Device Hub': {
exitCode: 1,
stdout: '',
stderr: 'Unable to find application named Device Hub',
},
'open -a Simulator': OK_RESULT,
});

await openIosSimulatorApp({ deviceHub: true });

assert.deepEqual(
mockRunCmd.mock.calls.map(([cmd, args]) => [cmd, args.join(' ')]),
[
Expand All @@ -470,71 +497,60 @@ test('openIosSimulatorApp falls back to Simulator when Device Hub is unavailable
);
});

test('ensureBootedSimulator opens Device Hub when already booted and available', async () => {
mockRunCmd.mockImplementation(async (cmd, args) => {
if (cmd === 'xcrun' && args.join(' ') === 'simctl list devices -j') {
return {
exitCode: 0,
stdout: JSON.stringify({
devices: {
'com.apple.CoreSimulator.SimRuntime.iOS-18-6': [{ udid: 'sim-1', state: 'Booted' }],
},
}),
stderr: '',
};
}
if (cmd === 'open' && args.join(' ') === '-a Device Hub') {
return { exitCode: 0, stdout: '', stderr: '' };
}
throw new Error(`Unexpected command: ${cmd} ${args.join(' ')}`);
test('ensureBootedSimulator opens Simulator when already booted by default', async () => {
mockRunCmdResponses({
'xcrun simctl list devices -j': simulatorListDevicesResult('Booted'),
'open -a Simulator': OK_RESULT,
});

await ensureBootedSimulator(IOS_TEST_SIMULATOR, { focusExisting: true });

assert.deepEqual(
mockRunCmd.mock.calls.map(([cmd, args]) => [cmd, args.join(' ')]),
[
['xcrun', 'simctl list devices -j'],
['open', '-a Simulator'],
],
);
});

test('ensureBootedSimulator opens Device Hub without activation when already booted and opted in', async () => {
mockRunCmdResponses({
'xcrun simctl list devices -j': simulatorListDevicesResult('Booted'),
'open -g -a Device Hub': OK_RESULT,
});

await ensureBootedSimulator(IOS_TEST_SIMULATOR, { deviceHub: true, focusExisting: true });

assert.equal(
mockRunCmd.mock.calls.some(
([cmd, args]) => cmd === 'open' && args.join(' ') === '-a Device Hub',
([cmd, args]) => cmd === 'open' && args.join(' ') === '-g -a Device Hub',
),
true,
);
assert.equal(
mockRunCmd.mock.calls.some(
([cmd, args]) => cmd === 'open' && args.join(' ') === '-a Simulator',
([cmd, args]) => cmd === 'open' && args.join(' ') === '-g -a Simulator',
),
false,
);
});

test('ensureBootedSimulator honors standalone Simulator preference when already booted', async () => {
mockRunCmd.mockImplementation(async (cmd, args) => {
if (cmd === 'xcrun' && args.join(' ') === 'simctl list devices -j') {
return {
exitCode: 0,
stdout: JSON.stringify({
devices: {
'com.apple.CoreSimulator.SimRuntime.iOS-18-6': [{ udid: 'sim-1', state: 'Booted' }],
},
}),
stderr: '',
};
}
if (cmd === 'open' && args.join(' ') === '-a Simulator') {
return { exitCode: 0, stdout: '', stderr: '' };
}
throw new Error(`Unexpected command: ${cmd} ${args.join(' ')}`);
test('ensureBootedSimulator foregrounds Device Hub after cold boot when opted in', async () => {
mockRunCmdResponses({
'xcrun simctl list devices -j': simulatorStateSequence('Shutdown', 'Booted'),
'xcrun simctl boot sim-1': OK_RESULT,
'xcrun simctl bootstatus sim-1 -b': OK_RESULT,
'open -a Device Hub': OK_RESULT,
});

await ensureBootedSimulator(IOS_TEST_SIMULATOR, {
focusExisting: true,
preferStandalone: true,
});
await ensureBootedSimulator(IOS_TEST_SIMULATOR, { deviceHub: true, focusExisting: true });

assert.deepEqual(
mockRunCmd.mock.calls.map(([cmd, args]) => [cmd, args.join(' ')]),
[
['xcrun', 'simctl list devices -j'],
['open', '-a Simulator'],
],
assert.equal(
mockRunCmd.mock.calls.some(
([cmd, args]) => cmd === 'open' && args.join(' ') === '-a Device Hub',
),
true,
);
});

Expand Down Expand Up @@ -987,7 +1003,11 @@ test('screenshotIos retries simulator capture timeouts and eventually succeeds',
);
assert.equal(
logLines.filter(
(line) => line === '__OPEN__ -a Device Hub' || line === '__OPEN__ -a Simulator',
(line) =>
line === '__OPEN__ -a Device Hub' ||
line === '__OPEN__ -a Simulator' ||
line === '__OPEN__ -g -a Device Hub' ||
line === '__OPEN__ -g -a Simulator',
).length,
0,
'should not focus simulator host app while retrying screenshots',
Expand Down
Loading
Loading