diff --git a/.nx/version-plans/overlap-ios-xctest-agent-build-with-simulator-boot.md b/.nx/version-plans/overlap-ios-xctest-agent-build-with-simulator-boot.md new file mode 100644 index 00000000..e4bc8f30 --- /dev/null +++ b/.nx/version-plans/overlap-ios-xctest-agent-build-with-simulator-boot.md @@ -0,0 +1,5 @@ +--- +__default__: patch +--- + +The iOS XCTest permission agent now starts building while the simulator boots instead of waiting for the boot to finish first, cutting startup time for simulator runs that need permission auto-acceptance—especially on a cold build cache. diff --git a/apps/playground/rn-harness.config.mjs b/apps/playground/rn-harness.config.mjs index bb4badfe..006e4d27 100644 --- a/apps/playground/rn-harness.config.mjs +++ b/apps/playground/rn-harness.config.mjs @@ -114,7 +114,7 @@ export default { }), ], defaultRunner: 'android', - platformReadyTimeout: 300000, + platformReadyTimeout: 480000, bridgeTimeout: 120000, testTimeout: 10000, diff --git a/packages/platform-ios/src/__tests__/instance-xctest-agent.test.ts b/packages/platform-ios/src/__tests__/instance-xctest-agent.test.ts index 16ebd952..0fe7237b 100644 --- a/packages/platform-ios/src/__tests__/instance-xctest-agent.test.ts +++ b/packages/platform-ios/src/__tests__/instance-xctest-agent.test.ts @@ -85,8 +85,11 @@ describe('iOS XCTest agent runner integration', () => { }, signal: initSignal, }); - expect(mocks.prepare).not.toHaveBeenCalled(); + expect(mocks.prepare).toHaveBeenCalledTimes(1); expect(mocks.ensureStarted).toHaveBeenCalledTimes(1); + expect( + mocks.prepare.mock.invocationCallOrder[0] + ).toBeLessThan(mocks.ensureStarted.mock.invocationCallOrder[0]); expect(mocks.dispose).toHaveBeenCalledTimes(1); }); @@ -168,6 +171,7 @@ describe('iOS XCTest agent runner integration', () => { ); expect(mocks.createXCTestAgentController).not.toHaveBeenCalled(); + expect(mocks.prepare).not.toHaveBeenCalled(); expect(mocks.ensureStarted).not.toHaveBeenCalled(); }); }); diff --git a/packages/platform-ios/src/instance.ts b/packages/platform-ios/src/instance.ts index 227ca48e..e055ace2 100644 --- a/packages/platform-ios/src/instance.ts +++ b/packages/platform-ios/src/instance.ts @@ -74,6 +74,33 @@ export const getAppleSimulatorPlatformInstance = async ( throw new DeviceNotFoundError(getDeviceName(config.device)); } + const xctestAgent = permissionsEnabled + ? createXCTestAgentController({ + appBundleId: config.bundleId, + target: { + kind: 'simulator', + id: udid, + }, + capabilities: [createPermissionPromptAutoAcceptCapability()], + signal: init.signal, + }) + : null; + + // The XCTest agent build (build-for-testing, destination + // "generic/platform=iOS Simulator") does not need a booted device, so + // kick it off now to overlap with the boot/waitForBoot sequence below. + // It is awaited (not raced) inside the try block further down so build + // errors are still surfaced; the .catch here only prevents an + // unhandled-rejection warning if boot fails first and this promise is + // left to settle unobserved in the meantime. + const preparePromise = xctestAgent?.prepare(); + preparePromise?.catch((error: unknown) => { + iosInstanceLogger.debug( + 'XCTest agent prepare failed while overlapping with simulator boot (surfaced later): %s', + error + ); + }); + const simulatorStatus = await simctl.getSimulatorStatus(udid); let startedByHarness = false; @@ -127,20 +154,9 @@ export const getAppleSimulatorPlatformInstance = async ( `localhost:${harnessConfig.metroPort}` ); - const xctestAgent = permissionsEnabled - ? createXCTestAgentController({ - appBundleId: config.bundleId, - target: { - kind: 'simulator', - id: udid, - }, - capabilities: [createPermissionPromptAutoAcceptCapability()], - signal: init.signal, - }) - : null; - let agentStarted = false; try { + await preparePromise; await xctestAgent?.ensureStarted(); agentStarted = true; } finally {