From 156228295cf8fd589b9b12d3aee8775112d1d898 Mon Sep 17 00:00:00 2001 From: Aziz Becha Date: Thu, 30 Apr 2026 12:57:52 +0100 Subject: [PATCH] fix(ios): never default to a connected device on plain run-ios (#2765) When neither --device nor --udid nor --simulator is supplied, run-ios now always targets a simulator (booted preferred, otherwise the fallback simulator) and ignores any connected physical device. The previous default counted physical devices as "booted" and installed the app on the iPhone alongside (or instead of) a simulator, which contradicted the docs and was unavoidable in modern Xcode where Wi-Fi device pairing cannot be disabled. Physical devices are still reachable via --device / --udid. --- .../runCommand/__tests__/createRun.test.ts | 172 ++++++++++++++++++ .../src/commands/runCommand/createRun.ts | 58 +++--- 2 files changed, 196 insertions(+), 34 deletions(-) create mode 100644 packages/cli-platform-apple/src/commands/runCommand/__tests__/createRun.test.ts diff --git a/packages/cli-platform-apple/src/commands/runCommand/__tests__/createRun.test.ts b/packages/cli-platform-apple/src/commands/runCommand/__tests__/createRun.test.ts new file mode 100644 index 000000000..d4dd3d9ab --- /dev/null +++ b/packages/cli-platform-apple/src/commands/runCommand/__tests__/createRun.test.ts @@ -0,0 +1,172 @@ +/// +import {Config} from '@react-native-community/cli-types'; +import createRun from '../createRun'; +import listDevices from '../../../tools/listDevices'; +import {runOnSimulator} from '../runOnSimulator'; +import {runOnDevice} from '../runOnDevice'; +import {getXcodeProjectAndDir} from '../../buildCommand/getXcodeProjectAndDir'; +import {getConfiguration} from '../../buildCommand/getConfiguration'; +import {getFallbackSimulator} from '../getFallbackSimulator'; +import {Device} from '../../../types'; +import path from 'path'; + +const packageRoot = path.resolve(__dirname, '../../../../'); + +jest.mock('../../../tools/listDevices'); +jest.mock('../runOnSimulator'); +jest.mock('../runOnDevice'); +jest.mock('../../buildCommand/getXcodeProjectAndDir'); +jest.mock('../../buildCommand/getConfiguration'); +jest.mock('../getFallbackSimulator'); + +const fallbackSimulator: Device = { + name: 'iPhone 14', + udid: 'FALLBACK-SIM-UDID', + type: 'simulator', + state: 'Shutdown', + version: '17.0', +}; + +const bootedSimulator: Device = { + name: 'iPhone 17', + udid: 'BOOTED-SIM-UDID', + type: 'simulator', + state: 'Booted', + version: '26.2', +}; + +const physicalDevice: Device = { + name: 'Stefan’s iPhone 16', + udid: 'PHYSICAL-DEVICE-UDID', + type: 'device', +}; + +function buildCtx(): Config { + return { + root: packageRoot, + reactNativePath: '', + reactNativeVersion: 'unknown', + project: { + ios: { + sourceDir: packageRoot, + automaticPodsInstallation: false, + }, + }, + dependencies: {}, + } as unknown as Config; +} + +function buildArgs(overrides: Record = {}) { + return { + packager: false, + port: 8081, + terminal: undefined, + listDevices: false, + interactive: false, + onlyPods: false, + forcePods: false, + ...overrides, + } as any; +} + +describe('createRun no-flag default targeting (issue #2765)', () => { + let chdirSpy: jest.SpyInstance; + + beforeEach(() => { + jest.clearAllMocks(); + chdirSpy = jest.spyOn(process, 'chdir').mockImplementation(() => {}); + (getXcodeProjectAndDir as jest.Mock).mockReturnValue({ + xcodeProject: {name: 'Demo', isWorkspace: true}, + sourceDir: packageRoot, + }); + (getConfiguration as jest.Mock).mockResolvedValue({ + mode: 'Debug', + scheme: 'Demo', + }); + (getFallbackSimulator as jest.Mock).mockReturnValue(fallbackSimulator); + (runOnSimulator as jest.Mock).mockResolvedValue(undefined); + (runOnDevice as jest.Mock).mockResolvedValue(undefined); + }); + + afterEach(() => { + chdirSpy.mockRestore(); + }); + + test('connected iPhone + no booted simulator + no flags -> launches fallback simulator only', async () => { + (listDevices as jest.Mock).mockResolvedValue([physicalDevice]); + + await createRun({platformName: 'ios'})([], buildCtx(), buildArgs()); + + expect(runOnSimulator).toHaveBeenCalledTimes(1); + expect(runOnSimulator).toHaveBeenCalledWith( + expect.anything(), + 'ios', + 'Debug', + 'Demo', + expect.anything(), + fallbackSimulator, + ); + expect(runOnDevice).not.toHaveBeenCalled(); + }); + + test('connected iPhone + booted simulator + no flags -> launches booted simulator only', async () => { + (listDevices as jest.Mock).mockResolvedValue([ + physicalDevice, + bootedSimulator, + ]); + + await createRun({platformName: 'ios'})([], buildCtx(), buildArgs()); + + expect(runOnSimulator).toHaveBeenCalledTimes(1); + expect(runOnSimulator).toHaveBeenCalledWith( + expect.anything(), + 'ios', + 'Debug', + 'Demo', + expect.anything(), + bootedSimulator, + ); + expect(runOnDevice).not.toHaveBeenCalled(); + }); + + test('connected iPhone + --device "name" -> runs on the physical device', async () => { + (listDevices as jest.Mock).mockResolvedValue([ + physicalDevice, + bootedSimulator, + ]); + + await createRun({platformName: 'ios'})( + [], + buildCtx(), + buildArgs({device: physicalDevice.name}), + ); + + expect(runOnDevice).toHaveBeenCalledTimes(1); + expect(runOnDevice).toHaveBeenCalledWith( + physicalDevice, + 'ios', + 'Debug', + 'Demo', + expect.anything(), + expect.anything(), + ); + expect(runOnSimulator).not.toHaveBeenCalled(); + }); + + test('no devices, no flags -> launches fallback simulator', async () => { + (listDevices as jest.Mock).mockResolvedValue([fallbackSimulator]); + + await createRun({platformName: 'ios'})([], buildCtx(), buildArgs()); + + expect(runOnSimulator).toHaveBeenCalledTimes(1); + expect(runOnSimulator).toHaveBeenCalledWith( + expect.anything(), + 'ios', + 'Debug', + 'Demo', + expect.anything(), + fallbackSimulator, + ); + expect(runOnDevice).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/cli-platform-apple/src/commands/runCommand/createRun.ts b/packages/cli-platform-apple/src/commands/runCommand/createRun.ts index fa16ebe53..4a600b1da 100644 --- a/packages/cli-platform-apple/src/commands/runCommand/createRun.ts +++ b/packages/cli-platform-apple/src/commands/runCommand/createRun.ts @@ -266,48 +266,38 @@ const createRun = const bootedSimulators = devices.filter( ({state, type}) => state === 'Booted' && type === 'simulator', ); - const bootedDevices = devices.filter(({type}) => type === 'device'); // Physical devices here are always booted - const booted = [...bootedSimulators, ...bootedDevices]; + const connectedDevices = devices.filter(({type}) => type === 'device'); - if (booted.length === 0) { - logger.info( - 'No booted devices or simulators found. Launching first available simulator...', - ); - return runOnSimulator( - xcodeProject, - platformName, - mode, - scheme, - args, - fallbackSimulator, - ); - } - - logger.info(`Found booted ${booted.map(({name}) => name).join(', ')}`); + const targetSimulator = bootedSimulators[0] ?? fallbackSimulator; - for (const simulator of bootedSimulators) { - await runOnSimulator( - xcodeProject, - platformName, - mode, - scheme, - args, - simulator || fallbackSimulator, + if (bootedSimulators.length === 0) { + logger.info( + 'No booted simulators found. Launching first available simulator...', ); + } else { + logger.info(`Found booted ${targetSimulator.name}`); } - for (const device of bootedDevices) { - await runOnDevice( - device, - platformName, - mode, - scheme, - xcodeProject, - args, + if (connectedDevices.length > 0) { + logger.info( + `Ignoring connected ${ + connectedDevices.length === 1 ? 'device' : 'devices' + } (${connectedDevices + .map(({name}) => name) + .join( + ', ', + )}). Pass --device or --udid to install on a physical device.`, ); } - return; + return runOnSimulator( + xcodeProject, + platformName, + mode, + scheme, + args, + targetSimulator, + ); } if (args.device && args.udid) {