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) {