Skip to content

Commit 4f862ef

Browse files
committed
fix: handle non-booted iOS simulator states
1 parent 5e6a200 commit 4f862ef

3 files changed

Lines changed: 130 additions & 27 deletions

File tree

packages/platform-ios/src/__tests__/instance.test.ts

Lines changed: 113 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import * as simctl from '../xcrun/simctl.js';
1111
import * as devicectl from '../xcrun/devicectl.js';
1212
import * as libimobiledevice from '../libimobiledevice.js';
1313
import { HarnessAppPathError } from '../errors.js';
14+
import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
15+
import { tmpdir } from 'node:os';
16+
import { join } from 'node:path';
1417

1518
const harnessConfig = {
1619
metroPort: DEFAULT_METRO_PORT,
@@ -201,8 +204,95 @@ describe('iOS platform instance dependency validation', () => {
201204
expect(shutdownSimulator).toHaveBeenCalledWith('sim-udid');
202205
});
203206

207+
it('waits for a simulator that is already booting', async () => {
208+
vi.spyOn(simctl, 'getSimulatorId').mockResolvedValue('sim-udid');
209+
vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booting');
210+
const bootSimulator = vi
211+
.spyOn(simctl, 'bootSimulator')
212+
.mockResolvedValue(undefined);
213+
const waitForBoot = vi
214+
.spyOn(simctl, 'waitForBoot')
215+
.mockResolvedValue(undefined);
216+
vi.spyOn(simctl, 'isAppInstalled').mockResolvedValue(true);
217+
vi.spyOn(simctl, 'applyHarnessJsLocationOverride').mockResolvedValue(
218+
undefined
219+
);
220+
vi.spyOn(simctl, 'stopApp').mockResolvedValue(undefined);
221+
vi.spyOn(simctl, 'clearHarnessJsLocationOverride').mockResolvedValue(
222+
undefined
223+
);
224+
const shutdownSimulator = vi
225+
.spyOn(simctl, 'shutdownSimulator')
226+
.mockResolvedValue(undefined);
227+
228+
const instance = await getAppleSimulatorPlatformInstance(
229+
{
230+
name: 'ios',
231+
device: {
232+
type: 'simulator',
233+
name: 'iPhone 16 Pro',
234+
systemVersion: '18.0',
235+
},
236+
bundleId: 'com.harnessplayground',
237+
},
238+
harnessConfig
239+
);
240+
241+
expect(bootSimulator).not.toHaveBeenCalled();
242+
expect(waitForBoot).toHaveBeenCalledWith('sim-udid');
243+
244+
await instance.dispose();
245+
246+
expect(shutdownSimulator).not.toHaveBeenCalled();
247+
});
248+
249+
it('boots and waits for other non-booted simulator states', async () => {
250+
vi.spyOn(simctl, 'getSimulatorId').mockResolvedValue('sim-udid');
251+
vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Creating');
252+
const bootSimulator = vi
253+
.spyOn(simctl, 'bootSimulator')
254+
.mockResolvedValue(undefined);
255+
const waitForBoot = vi
256+
.spyOn(simctl, 'waitForBoot')
257+
.mockResolvedValue(undefined);
258+
vi.spyOn(simctl, 'isAppInstalled').mockResolvedValue(true);
259+
vi.spyOn(simctl, 'applyHarnessJsLocationOverride').mockResolvedValue(
260+
undefined
261+
);
262+
vi.spyOn(simctl, 'stopApp').mockResolvedValue(undefined);
263+
vi.spyOn(simctl, 'clearHarnessJsLocationOverride').mockResolvedValue(
264+
undefined
265+
);
266+
const shutdownSimulator = vi
267+
.spyOn(simctl, 'shutdownSimulator')
268+
.mockResolvedValue(undefined);
269+
270+
const instance = await getAppleSimulatorPlatformInstance(
271+
{
272+
name: 'ios',
273+
device: {
274+
type: 'simulator',
275+
name: 'iPhone 16 Pro',
276+
systemVersion: '18.0',
277+
},
278+
bundleId: 'com.harnessplayground',
279+
},
280+
harnessConfig
281+
);
282+
283+
expect(bootSimulator).toHaveBeenCalledWith('sim-udid');
284+
expect(waitForBoot).toHaveBeenCalledWith('sim-udid');
285+
286+
await instance.dispose();
287+
288+
expect(shutdownSimulator).toHaveBeenCalledWith('sim-udid');
289+
});
290+
204291
it('installs the app from HARNESS_APP_PATH when missing', async () => {
205-
vi.stubEnv('HARNESS_APP_PATH', '/tmp/HarnessPlayground.app');
292+
const appDir = mkdtempSync(join(tmpdir(), 'rn-harness-ios-app-'));
293+
const bundlePath = join(appDir, 'HarnessPlayground.app');
294+
mkdirSync(bundlePath);
295+
vi.stubEnv('HARNESS_APP_PATH', bundlePath);
206296
vi.spyOn(simctl, 'getSimulatorId').mockResolvedValue('sim-udid');
207297
vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booted');
208298
vi.spyOn(simctl, 'isAppInstalled').mockResolvedValue(false);
@@ -212,30 +302,27 @@ describe('iOS platform instance dependency validation', () => {
212302
vi.spyOn(simctl, 'applyHarnessJsLocationOverride').mockResolvedValue(
213303
undefined
214304
);
215-
const existsSync = vi
216-
.spyOn(await import('node:fs'), 'existsSync')
217-
.mockReturnValue(true);
218305

219-
await expect(
220-
getAppleSimulatorPlatformInstance(
221-
{
222-
name: 'ios',
223-
device: {
224-
type: 'simulator',
225-
name: 'iPhone 16 Pro',
226-
systemVersion: '18.0',
306+
try {
307+
await expect(
308+
getAppleSimulatorPlatformInstance(
309+
{
310+
name: 'ios',
311+
device: {
312+
type: 'simulator',
313+
name: 'iPhone 16 Pro',
314+
systemVersion: '18.0',
315+
},
316+
bundleId: 'com.harnessplayground',
227317
},
228-
bundleId: 'com.harnessplayground',
229-
},
230-
harnessConfig
231-
)
232-
).resolves.toBeDefined();
318+
harnessConfig
319+
)
320+
).resolves.toBeDefined();
233321

234-
expect(existsSync).toHaveBeenCalledWith('/tmp/HarnessPlayground.app');
235-
expect(installApp).toHaveBeenCalledWith(
236-
'sim-udid',
237-
'/tmp/HarnessPlayground.app'
238-
);
322+
expect(installApp).toHaveBeenCalledWith('sim-udid', bundlePath);
323+
} finally {
324+
rmSync(appDir, { force: true, recursive: true });
325+
}
239326
});
240327

241328
it('throws a HarnessAppPathError when HARNESS_APP_PATH is missing', async () => {
@@ -260,11 +347,13 @@ describe('iOS platform instance dependency validation', () => {
260347
});
261348

262349
it('throws a HarnessAppPathError when HARNESS_APP_PATH points to a missing app', async () => {
263-
vi.stubEnv('HARNESS_APP_PATH', '/tmp/missing.app');
350+
vi.stubEnv(
351+
'HARNESS_APP_PATH',
352+
join(tmpdir(), 'rn-harness-ios-missing-app', 'Missing.app')
353+
);
264354
vi.spyOn(simctl, 'getSimulatorId').mockResolvedValue('sim-udid');
265355
vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booted');
266356
vi.spyOn(simctl, 'isAppInstalled').mockResolvedValue(false);
267-
vi.spyOn(await import('node:fs'), 'existsSync').mockReturnValue(false);
268357

269358
await expect(
270359
getAppleSimulatorPlatformInstance(

packages/platform-ios/src/instance.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,15 @@ export const getAppleSimulatorPlatformInstance = async (
5656
const simulatorStatus = await simctl.getSimulatorStatus(udid);
5757
let startedByHarness = false;
5858

59-
if (simulatorStatus === 'Shutdown') {
59+
if (
60+
!simctl.isBootedSimulatorStatus(simulatorStatus) &&
61+
!simctl.isBootingSimulatorStatus(simulatorStatus)
62+
) {
6063
await simctl.bootSimulator(udid);
6164
startedByHarness = true;
6265
}
6366

64-
if (simulatorStatus === 'Shutdown' || simulatorStatus === 'Booting') {
67+
if (!simctl.isBootedSimulatorStatus(simulatorStatus)) {
6568
await simctl.waitForBoot(udid);
6669
}
6770

packages/platform-ios/src/xcrun/simctl.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,18 @@ export const isAppInstalled = async (
209209
return appInfo !== null;
210210
};
211211

212-
export type AppleSimulatorState = 'Booted' | 'Booting' | 'Shutdown';
212+
export type AppleSimulatorState =
213+
| 'Booted'
214+
| 'Booting'
215+
| 'Shutdown'
216+
| (string & {});
217+
218+
export const isBootedSimulatorStatus = (status: AppleSimulatorState): boolean =>
219+
status === 'Booted';
220+
221+
export const isBootingSimulatorStatus = (
222+
status: AppleSimulatorState
223+
): boolean => status === 'Booting';
213224

214225
export type AppleSimulatorInfo = {
215226
name: string;

0 commit comments

Comments
 (0)