diff --git a/.github/actions/e2e-ios/action.yml b/.github/actions/e2e-ios/action.yml new file mode 100644 index 00000000..5264288a --- /dev/null +++ b/.github/actions/e2e-ios/action.yml @@ -0,0 +1,101 @@ +name: iOS E2E (Detox) +description: Build and run Detox iOS end-to-end tests for a brownfield example app + +inputs: + app-path: + description: 'Path to the app workspace (e.g. apps/RNApp)' + required: true + + run-expo-prebuild: + description: 'Run brownfield codegen and Expo iOS prebuild before pod install' + required: false + default: 'false' + + run-brownfield-codegen: + description: 'Run brownfield codegen before building (required for RNApp)' + required: false + default: 'false' + + artifact-name: + description: 'Name prefix for Detox artifacts uploaded on failure' + required: false + default: 'detox' + +runs: + using: composite + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + + - name: Setup + uses: ./.github/actions/setup + + - name: Prepare iOS environment + uses: ./.github/actions/prepare-ios + + - name: Install applesimutils + run: | + brew tap wix/brew + brew install applesimutils + shell: bash + + - name: Brownfield codegen + if: inputs.run-brownfield-codegen == 'true' + run: yarn codegen + working-directory: ${{ inputs.app-path }} + shell: bash + + - name: Expo iOS prebuild + if: inputs.run-expo-prebuild == 'true' + run: | + node ../../packages/cli/dist/main.js codegen + yarn expo prebuild --platform ios --no-install + working-directory: ${{ inputs.app-path }} + shell: bash + + - name: Restore Pods cache + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5 + with: + path: ${{ inputs.app-path }}/ios/Pods + key: ${{ runner.os }}-e2e-ios-pods-${{ inputs.app-path }}-${{ hashFiles(format('{0}/ios/Podfile.lock', inputs.app-path)) }} + restore-keys: | + ${{ runner.os }}-e2e-ios-pods-${{ inputs.app-path }}- + + - name: Install CocoaPods + env: + # RNApp + RNScreens: prebuilt React-Core can fail Debug simulator linking. + RCT_USE_PREBUILT_RNCORE: ${{ inputs.app-path == 'apps/RNApp' && '0' || '' }} + run: pod install + working-directory: ${{ inputs.app-path }}/ios + shell: bash + + - name: Restore Detox build cache + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5 + with: + path: ${{ inputs.app-path }}/ios/build + key: ${{ runner.os }}-e2e-ios-build-${{ inputs.app-path }}-${{ hashFiles(format('{0}/ios/Podfile.lock', inputs.app-path), format('{0}/ios/*.xcodeproj/project.pbxproj', inputs.app-path)) }} + restore-keys: | + ${{ runner.os }}-e2e-ios-build-${{ inputs.app-path }}- + + - name: Install Detox iOS artifacts + run: node node_modules/detox/scripts/postinstall.js + working-directory: ${{ inputs.app-path }} + shell: bash + + - name: Detox build (iOS Simulator) + run: yarn e2e:build:ios + working-directory: ${{ inputs.app-path }} + shell: bash + + - name: Detox test (iOS Simulator) + run: yarn e2e:test:ios + working-directory: ${{ inputs.app-path }} + shell: bash + + - name: Upload Detox artifacts on failure + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: ${{ inputs.artifact-name }}-ios + path: ${{ inputs.app-path }}/artifacts + if-no-files-found: ignore diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 0ad48b82..b74ae79b 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -17,6 +17,10 @@ runs: cache: 'yarn' - name: Install dependencies + env: + # Monorepo has detox in multiple workspaces; parallel postinstalls race on + # $HOME/Library/Detox/ios/framework. E2E jobs run postinstall once later. + DETOX_DISABLE_POSTINSTALL: '1' run: yarn install shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c728c56e..2c2cf53d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,11 +5,16 @@ on: branches: [main] pull_request: branches: [main] + workflow_dispatch: concurrency: - group: pr-${{ github.event.pull_request.number }} + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +permissions: + contents: read + actions: write + jobs: filter: name: Detect changed paths @@ -35,10 +40,13 @@ jobs: - 'packages/**' rnapp: - 'apps/RNApp/**' + - 'apps/brownfield-example-shared-tests/**' expo54: - 'apps/ExpoApp54/**' + - 'apps/brownfield-example-shared-tests/**' expo55: - 'apps/ExpoApp55/**' + - 'apps/brownfield-example-shared-tests/**' androidapp: - 'apps/AndroidApp/**' appleapp: @@ -220,3 +228,64 @@ jobs: with: variant: expo${{ matrix.version }} rn-project-path: apps/ExpoApp${{ matrix.version }} + + e2e-ios-rnapp: + name: E2E iOS (RNApp) + runs-on: macos-26 + timeout-minutes: 90 + permissions: + contents: read + actions: write + needs: [filter, build-lint] + if: | + always() && + ( + needs.filter.outputs.rnapp == 'true' || + needs.filter.outputs.packages == 'true' || + needs.filter.outputs.ci == 'true' + ) && + (needs.build-lint.result == 'success' || needs.build-lint.result == 'skipped') + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + + - name: Run Detox E2E (RNApp) + uses: ./.github/actions/e2e-ios + with: + app-path: apps/RNApp + artifact-name: detox-rnapp + run-brownfield-codegen: 'true' + + e2e-ios-expo: + name: E2E iOS (Expo ${{ matrix.version }}) + runs-on: macos-26 + timeout-minutes: 90 + permissions: + contents: read + actions: write + needs: [filter, build-lint] + if: | + always() && + ( + needs.filter.outputs.expo54 == 'true' || + needs.filter.outputs.expo55 == 'true' || + needs.filter.outputs.packages == 'true' || + needs.filter.outputs.ci == 'true' + ) && + (needs.build-lint.result == 'success' || needs.build-lint.result == 'skipped') + strategy: + fail-fast: false + matrix: + include: + - version: '54' + - version: '55' + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + + - name: Run Detox E2E (Expo ${{ matrix.version }}) + uses: ./.github/actions/e2e-ios + with: + app-path: apps/ExpoApp${{ matrix.version }} + artifact-name: detox-expo${{ matrix.version }} + run-expo-prebuild: 'true' diff --git a/apps/ExpoApp54/.detoxrc.cjs b/apps/ExpoApp54/.detoxrc.cjs new file mode 100644 index 00000000..d3093290 --- /dev/null +++ b/apps/ExpoApp54/.detoxrc.cjs @@ -0,0 +1,43 @@ +const { + getIosSimulatorDeviceType, +} = require('../brownfield-example-shared-tests/detox-ios-simulator-device.cjs'); + +/** + * Requires a native tree from `expo prebuild` / `expo run:ios` (ios/ + Pods). + * @type {Detox.DetoxConfig} + */ +module.exports = { + testRunner: { + $0: 'jest', + args: { + config: 'e2e/jest.config.cjs', + _: ['e2e'], + }, + jest: { + setupTimeout: 300000, + }, + }, + apps: { + 'ios.debug': { + type: 'ios.app', + binaryPath: + 'ios/build/Build/Products/Debug-iphonesimulator/ExpoApp54.app', + build: + 'xcodebuild -workspace ios/ExpoApp54.xcworkspace -scheme ExpoApp54 -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build', + }, + }, + devices: { + 'ios.sim': { + type: 'ios.simulator', + device: { + type: getIosSimulatorDeviceType(), + }, + }, + }, + configurations: { + 'ios.sim.debug': { + device: 'ios.sim', + app: 'ios.debug', + }, + }, +}; diff --git a/apps/ExpoApp54/.gitignore b/apps/ExpoApp54/.gitignore index f8c6c2e8..2eed6de9 100644 --- a/apps/ExpoApp54/.gitignore +++ b/apps/ExpoApp54/.gitignore @@ -38,6 +38,9 @@ yarn-error.* app-example +# Detox +artifacts/ + # generated native folders /ios /android diff --git a/apps/ExpoApp54/RNApp.tsx b/apps/ExpoApp54/RNApp.tsx index 5f771364..5802333c 100644 --- a/apps/ExpoApp54/RNApp.tsx +++ b/apps/ExpoApp54/RNApp.tsx @@ -2,6 +2,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { Button, StyleSheet, Text, View } from 'react-native'; import BrownfieldNavigation from '@callstack/brownfield-navigation'; +import PostMessageTab from './app/(tabs)/postMessage'; import Counter from './components/counter'; import { checkAndFetchUpdate } from './utils/expo-rn-updates'; @@ -13,29 +14,35 @@ type RNAppProps = { export default function RNApp({ nativeOsVersionLabel }: RNAppProps) { return ( - Expo React Native Brownfield + + Expo React Native Brownfield - {nativeOsVersionLabel ? ( - - {nativeOsVersionLabel} - - ) : null} + {nativeOsVersionLabel ? ( + + {nativeOsVersionLabel} + + ) : null} - - + + -