From 7cc0e64a8906fc31b7fb4587ada1a7cca40a0359 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Tue, 18 Aug 2026 07:42:50 -0700 Subject: [PATCH] Use macOS 26 runners for iOS E2E tests (#57993) Summary: Moves the RNTester and template app iOS E2E jobs from `macos-15-large` to `macos-26-large`. The new runner image ships with Xcode 26, which is now required by `idb-companion`, and the jobs use the image default Xcode so its hosted simulator platform is available. The Maestro runner discovers an available iPhone Pro simulator and boots it by UDID instead of depending on a device name tied to an older runner image. - `test_e2e_ios_templateapp / test (Debug)` failed before the E2E tests because the explicitly selected Xcode 26.0.1 installation did not include the iOS Simulator platform. Removed the explicit Xcode selection from both E2E workflows so `macos-26-large` uses its default Xcode 26.6 installation and hosted simulator setup. - The RNTester E2E attempts then installed `idb-companion` successfully but failed to boot the hardcoded `iPhone 16 Pro`, which is not present on the macOS 26 image. Updated the Maestro iOS runner to discover an available iPhone Pro from the newest installed runtime and boot it by UDID, with a unit test covering selection across runtimes. [INTERNAL] [FIXED] - Run iOS E2E tests on macOS 26 runners with Xcode 26. Pull Request resolved: https://github.com/react/react-native/pull/57993 Test Plan: - `git diff --check` - `yarn prettier --check .github/workflow-scripts/maestro-ios.js .github/workflow-scripts/__tests__/maestro-ios-test.js .github/workflows/e2e-ios-rntester.yml .github/workflows/e2e-ios-templateapp.yml` - `ruby -ryaml -e "ARGV.each { |path| YAML.parse_file(path) }" .github/workflows/e2e-ios-rntester.yml .github/workflows/e2e-ios-templateapp.yml` - `node --check .github/workflow-scripts/maestro-ios.js` - `yarn jest .github/workflow-scripts/__tests__/maestro-ios-test.js --runInBand --config '{"testEnvironment":"node","transform":{}}'` Reviewed By: alanleedev, mdvacca Differential Revision: D116460922 Pulled By: cipolleschi fbshipit-source-id: e769f017b874c9da76d63f19727c358719dbdbc6 (cherry picked from commit 2dd6d4b482c2512d3e92fbaf0a10a9f8ac055e5d) --- .../__tests__/maestro-ios-test.js | 41 +++++++++ .github/workflow-scripts/maestro-ios.js | 83 +++++++++++-------- .github/workflows/e2e-ios-rntester.yml | 4 +- .github/workflows/e2e-ios-templateapp.yml | 4 +- 4 files changed, 90 insertions(+), 42 deletions(-) create mode 100644 .github/workflow-scripts/__tests__/maestro-ios-test.js diff --git a/.github/workflow-scripts/__tests__/maestro-ios-test.js b/.github/workflow-scripts/__tests__/maestro-ios-test.js new file mode 100644 index 000000000000..e77c0c05cfd0 --- /dev/null +++ b/.github/workflow-scripts/__tests__/maestro-ios-test.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +jest.mock('child_process', () => ({ + execSync: jest.fn(), + spawn: jest.fn(), +})); +const childProcess = require('child_process'); + +const {findAvailableSimulator} = require('../maestro-ios'); + +describe('Maestro iOS runner', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('selects an iPhone Pro simulator from the latest runtime', () => { + childProcess.execSync.mockReturnValue( + JSON.stringify({ + devices: { + 'iOS 18.5': [{name: 'iPhone 16 Pro', udid: 'old-pro'}], + 'iOS 26.5': [ + {name: 'iPhone 17 Pro Max', udid: 'new-pro-max'}, + {name: 'iPhone 17 Pro', udid: 'new-pro'}, + ], + }, + }), + ); + + expect(findAvailableSimulator()).toEqual({ + name: 'iPhone 17 Pro', + udid: 'new-pro', + }); + }); +}); diff --git a/.github/workflow-scripts/maestro-ios.js b/.github/workflow-scripts/maestro-ios.js index bad5f8a35248..0fcae5a54b89 100644 --- a/.github/workflow-scripts/maestro-ios.js +++ b/.github/workflow-scripts/maestro-ios.js @@ -23,25 +23,28 @@ node maestro-android.js /^iPhone .* Pro$/.test(device.name)); -const APP_PATH = args[0]; -const APP_ID = args[1]; -const MAESTRO_FLOW = args[2]; -const JS_ENGINE = args[3]; -const IS_DEBUG = args[4] === 'Debug'; -const WORKING_DIRECTORY = args[5]; + if (simulator == null) { + throw new Error('Unable to find an available iPhone Pro simulator'); + } -const MAX_ATTEMPTS = 5; + return simulator; +} -function launchSimulator(simulatorName) { - console.log(`Launching simulator ${simulatorName}`); +function launchSimulator(simulator) { + console.log(`Launching simulator ${simulator.name} (${simulator.udid})`); try { - childProcess.execSync(`xcrun simctl boot "${simulatorName}"`); + childProcess.execSync(`xcrun simctl boot "${simulator.udid}"`); } catch (error) { if ( !error.message.includes('Unable to boot device in current state: Booted') @@ -56,14 +59,6 @@ function installAppOnSimulator(appPath) { childProcess.execSync(`xcrun simctl install booted "${appPath}"`); } -function extractSimulatorUDID() { - console.log('Retrieving device UDID'); - const command = `xcrun simctl list devices booted -j | jq -r '[.devices[]] | add | first | .udid'`; - const udid = String(childProcess.execSync(command)).trim(); - console.log(`UDID is ${udid}`); - return udid; -} - function bringSimulatorInForeground() { console.log('Bringing simulator in foreground'); childProcess.execSync('open -a simulator'); @@ -151,26 +146,42 @@ function executeTestsWithRetries( } } -async function main() { +async function main(args = process.argv.slice(2)) { + if (args.length !== 6) { + throw new Error(`Invalid number of arguments.\n${usage}`); + } + + const appPath = args[0]; + const appId = args[1]; + const maestroFlow = args[2]; + const jsengine = args[3]; + const isDebug = args[4] === 'Debug'; + const workingDirectory = args[5]; + console.info('\n=============================='); console.info('Running tests for iOS with the following parameters:'); - console.info(`APP_PATH: ${APP_PATH}`); - console.info(`APP_ID: ${APP_ID}`); - console.info(`MAESTRO_FLOW: ${MAESTRO_FLOW}`); - console.info(`JS_ENGINE: ${JS_ENGINE}`); - console.info(`IS_DEBUG: ${IS_DEBUG}`); - console.info(`WORKING_DIRECTORY: ${WORKING_DIRECTORY}`); + console.info(`APP_PATH: ${appPath}`); + console.info(`APP_ID: ${appId}`); + console.info(`MAESTRO_FLOW: ${maestroFlow}`); + console.info(`JS_ENGINE: ${jsengine}`); + console.info(`IS_DEBUG: ${isDebug}`); + console.info(`WORKING_DIRECTORY: ${workingDirectory}`); console.info('==============================\n'); - const simulatorName = 'iPhone 16 Pro'; - launchSimulator(simulatorName); - installAppOnSimulator(APP_PATH); - const udid = extractSimulatorUDID(); + const simulator = findAvailableSimulator(); + launchSimulator(simulator); + installAppOnSimulator(appPath); bringSimulatorInForeground(); - await launchAppOnSimulator(APP_ID, udid, IS_DEBUG); - executeTestsWithRetries(APP_ID, udid, MAESTRO_FLOW, JS_ENGINE, 1); + await launchAppOnSimulator(appId, simulator.udid, isDebug); + executeTestsWithRetries(appId, simulator.udid, maestroFlow, jsengine, 1); console.log('Test finished'); process.exit(0); } -main(); +if (require.main === module) { + main(); +} + +module.exports = { + findAvailableSimulator, +}; diff --git a/.github/workflows/e2e-ios-rntester.yml b/.github/workflows/e2e-ios-rntester.yml index 941711c111e9..999af94846f7 100644 --- a/.github/workflows/e2e-ios-rntester.yml +++ b/.github/workflows/e2e-ios-rntester.yml @@ -16,7 +16,7 @@ on: jobs: test: - runs-on: macos-15-large + runs-on: macos-26-large outputs: status: ${{ steps.report-status.outputs.status }} strategy: @@ -35,8 +35,6 @@ jobs: path: /tmp/RNTesterBuild/RNTester.app - name: Check downloaded folder content run: ls -lR /tmp/RNTesterBuild - - name: Setup xcode - uses: ./.github/actions/setup-xcode - name: Run E2E Tests id: run-tests continue-on-error: true diff --git a/.github/workflows/e2e-ios-templateapp.yml b/.github/workflows/e2e-ios-templateapp.yml index dd4761471ca7..b203e9c84b24 100644 --- a/.github/workflows/e2e-ios-templateapp.yml +++ b/.github/workflows/e2e-ios-templateapp.yml @@ -16,7 +16,7 @@ on: jobs: test: - runs-on: macos-15-large + runs-on: macos-26-large outputs: status: ${{ steps.report-status.outputs.status }} strategy: @@ -26,8 +26,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 - - name: Setup xcode - uses: ./.github/actions/setup-xcode - name: Setup node.js uses: ./.github/actions/setup-node - name: Run yarn