diff --git a/.github/actions/build-android/action.yml b/.github/actions/build-android/action.yml index 11239d50ab49..6856221dc57d 100644 --- a/.github/actions/build-android/action.yml +++ b/.github/actions/build-android/action.yml @@ -34,7 +34,7 @@ runs: run: | if [[ "${{ inputs.release-type }}" == "dry-run" ]]; then # dry-run: we only build ARM64 to save time/resources. For release/nightlies the default is to build all archs. - export ORG_GRADLE_PROJECT_reactNativeArchitectures="arm64-v8a" + export ORG_GRADLE_PROJECT_reactNativeArchitectures="arm64-v8a,x86" # x86 is required for E2E testing TASKS="publishAllToMavenTempLocal build" elif [[ "${{ inputs.release-type }}" == "nightly" ]]; then # nightly: we set isSnapshot to true so artifacts are sent to the right repository on Maven Central. diff --git a/.github/actions/maestro-android/action.yml b/.github/actions/maestro-android/action.yml new file mode 100644 index 000000000000..2ef24e5b43d2 --- /dev/null +++ b/.github/actions/maestro-android/action.yml @@ -0,0 +1,70 @@ +name: Maestro E2E Android +description: Runs E2E Tests on iOS using Maestro +inputs: + app-path: + required: true + description: The path to the .apk file + app-id: + required: true + description: The id of the app to test + jsengine: + required: true + description: The js engine we are using + maestro-flow: + required: true + description: the folder that contains the maestro tests + install-java: + required: false + default: 'true' + description: whether this action has to install java 17 or not +runs: + using: composite + steps: + - name: Installing Maestro + shell: bash + run: curl -Ls "https://get.maestro.mobile.dev" | bash + - name: Set up JDK 17 + if: ${{ inputs.install-java == 'true' }} + uses: actions/setup-java@v2 + with: + java-version: '17' + distribution: 'zulu' + - name: Enable KVM group perms + shell: bash + run: | + # ubuntu machines have hardware acceleration available and when we try to create an emulator, the script pauses asking for user input + # These lines set the rules to reply automatically to that question and unblock the creation of the emulator. + # source: https://github.com/ReactiveCircus/android-emulator-runner?tab=readme-ov-file#running-hardware-accelerated-emulators-on-linux-runners + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - name: Run e2e tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 24 + arch: x86 + script: | + echo "Install APK from ${{ inputs.app-path }}" + adb install "${{ inputs.app-path }}" + + echo "Start recording to /sdcard/screen.mp4" + adb shell screenrecord /sdcard/screen.mp4 + + echo "Start testing ${{ inputs.maestro-flow }}" + $HOME/.maestro/bin/maestro test ${{ inputs.maestro-flow }} --format junit -e APP_ID=${{ inputs.app-id }} --debug-output /tmp/MaestroLogs + + echo "Stop recording. Saving to screen.mp4" + adb pull /sdcard/screen.mp4 + - name: Store tests result + uses: actions/upload-artifact@v3 + with: + name: e2e_android_${{ inputs.app-id }}_report_${{ inputs.jsengine }} + path: | + report.xml + screen.mp4 + - name: Store Logs + if: failure() && steps.run-tests.outcome == 'failure' + uses: actions/upload-artifact@v4 + with: + name: maestro-logs-android-${{ inputs.app-id }}-${{ inputs.jsengine }} + path: /tmp/MaestroLogs diff --git a/.github/actions/maestro-ios/action.yml b/.github/actions/maestro-ios/action.yml new file mode 100644 index 000000000000..8b6e86c5125f --- /dev/null +++ b/.github/actions/maestro-ios/action.yml @@ -0,0 +1,96 @@ +name: Maestro E2E iOS +description: Runs E2E Tests on iOS using Maestro +inputs: + app-path: + required: true + description: The path to the .app file + app-id: + required: true + description: The id of the app to test + jsengine: + required: true + description: The js engine we are using + maestro-flow: + required: true + description: the folder that contains the maestro tests +runs: + using: composite + steps: + - name: Installing Maestro + shell: bash + run: curl -Ls "https://get.maestro.mobile.dev" | bash + - name: Installing Maestro dependencies + shell: bash + run: | + brew tap facebook/fb + brew install facebook/fb/idb-companion jq + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: '17' + distribution: 'zulu' + - name: Run tests + id: run-tests + shell: bash + run: | + # Avoid exit from the job if one of the command returns an error. + # Maestro can fail in case of flakyness, we have some retry logic. + set +e + + echo "Launching iOS Simulator: iPhone 15 Pro" + xcrun simctl boot "iPhone 15 Pro" + + echo "Installing app on Simulator" + xcrun simctl install booted "${{ inputs.app-path }}" + + echo "Retrieving device UDID" + UDID=$(xcrun simctl list devices booted -j | jq -r '[.devices[]] | add | first | .udid') + echo "UDID is $UDID" + + echo "Bring simulator in foreground" + open -a simulator + + echo "Launch the app" + xcrun simctl launch $UDID ${{ inputs.app-id }} + + echo "Running tests with Maestro" + export MAESTRO_DRIVER_STARTUP_TIMEOUT=1500000 # 25 min. CI is extremely slow + + # Add retries for flakyness + MAX_ATTEMPTS=3 + CURR_ATTEMPT=0 + RESULT=1 + + while [[ $CURR_ATTEMPT -lt $MAX_ATTEMPTS ]] && [[ $RESULT -ne 0 ]]; do + CURR_ATTEMPT=$((CURR_ATTEMPT+1)) + echo "Attempt number $CURR_ATTEMPT" + + echo "Start video record using pid: video_record_${{ inputs.jsengine }}_$CURR_ATTEMPT.pid" + xcrun simctl io booted recordVideo video_record_$CURR_ATTEMPT.mov & echo $! > video_record_${{ inputs.jsengine }}_$CURR_ATTEMPT.pid + + echo '$HOME/.maestro/bin/maestro --udid=$UDID test ${{ inputs.maestro-flow }} --format junit -e APP_ID=${{ inputs.app-id }}' + $HOME/.maestro/bin/maestro --udid=$UDID test ${{ inputs.maestro-flow }} --format junit -e APP_ID=${{ inputs.app-id }} --debug-output /tmp/MaestroLogs + + RESULT=$? + + # Stop video + kill -SIGINT $(cat video_record_${{ inputs.jsengine }}_$CURR_ATTEMPT.pid) + done + + exit $RESULT + - name: Store video record + if: always() + uses: actions/upload-artifact@v4 + with: + name: e2e_ios_${{ inputs.app-id }}_report_${{ inputs.jsengine }} + path: | + video_record_1.mov + video_record_2.mov + video_record_3.mov + report.xml + - name: Store Logs + if: failure() && steps.run-tests.outcome == 'failure' + uses: actions/upload-artifact@v4 + with: + name: maestro-logs-${{ inputs.app-id }}-${{ inputs.jsengine }} + path: /tmp/MaestroLogs diff --git a/.github/actions/test-ios-rntester/action.yml b/.github/actions/test-ios-rntester/action.yml index 2a6642005fa8..2829309dd94a 100644 --- a/.github/actions/test-ios-rntester/action.yml +++ b/.github/actions/test-ios-rntester/action.yml @@ -28,6 +28,10 @@ inputs: react-native-version: description: The version of react-native required: true + run-e2e-tests: + description: Whether we want to run E2E tests or not + required: false + default: false runs: using: composite @@ -114,17 +118,32 @@ runs: bundle install bundle exec pod install - name: Build RNTester - if: ${{ inputs.run-unit-tests != 'true' }} + if: ${{ inputs.run-unit-tests != 'true' && inputs.run-e2e-tests == 'false' }} shell: bash run: | xcodebuild build \ -workspace packages/rn-tester/RNTesterPods.xcworkspace \ -scheme RNTester \ -sdk iphonesimulator + - name: Build RNTester (E2E Tests) + shell: bash + if: ${{ inputs.run-e2e-tests == 'true' }} + run: | + xcrun xcodebuild \ + -scheme "RNTester" \ + -workspace packages/rn-tester/RNTesterPods.xcworkspace \ + -configuration "Release" \ + -sdk "iphonesimulator" \ + -destination "generic/platform=iOS Simulator" \ + -derivedDataPath "/tmp/RNTesterBuild" + + echo "Print path to *.app file" + find "/tmp/RNTesterBuild" -type d -name "*.app" - name: "Run Tests: iOS Unit and Integration Tests" if: ${{ inputs.run-unit-tests == 'true' }} shell: bash run: yarn test-ios + - name: Zip Derived data folder if: ${{ inputs.run-unit-tests == 'true' }} shell: bash diff --git a/.github/workflows/test-all.yml b/.github/workflows/test-all.yml index fc88181c9647..a10e88367507 100644 --- a/.github/workflows/test-all.yml +++ b/.github/workflows/test-all.yml @@ -179,6 +179,40 @@ jobs: hermes-version: ${{ needs.prepare_hermes_workspace.outputs.hermes-version }} react-native-version: ${{ needs.prepare_hermes_workspace.outputs.react-native-version }} + test_e2e_ios_rntester: + runs-on: macos-13 + needs: + [build_apple_slices_hermes, prepare_hermes_workspace, build_hermes_macos] + env: + HERMES_WS_DIR: /tmp/hermes + HERMES_TARBALL_ARTIFACTS_DIR: /tmp/hermes/hermes-runtime-darwin + continue-on-error: true + strategy: + fail-fast: false + matrix: + jsengine: [Hermes, JSC] + architecture: [NewArch] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Run it + uses: ./.github/actions/test-ios-rntester + with: + jsengine: ${{ matrix.jsengine }} + architecture: ${{ matrix.architecture }} + run-unit-tests: "false" + use-frameworks: StaticLibraries + hermes-version: ${{ needs.prepare_hermes_workspace.outputs.hermes-version }} + react-native-version: ${{ needs.prepare_hermes_workspace.outputs.react-native-version }} + run-e2e-tests: "true" + - name: Run E2E Tests + uses: ./.github/actions/maestro-ios + with: + app-path: "/tmp/RNTesterBuild/Build/Products/Release-iphonesimulator/RNTester.app" + app-id: com.meta.RNTester.localDevelopment + jsengine: ${{ matrix.jsengine }} + maestro-flow: ./packages/rn-tester/.maestro/ + build_hermesc_linux: runs-on: ubuntu-latest needs: prepare_hermes_workspace @@ -231,6 +265,35 @@ jobs: with: release-type: ${{ needs.set_release_type.outputs.RELEASE_TYPE }} + test_e2e_android_rntester: + runs-on: ubuntu-latest + needs: [build_android] + strategy: + fail-fast: false + matrix: + jsengine: [hermes, jsc] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup node.js + uses: ./.github/actions/setup-node + - name: Install node dependencies + uses: ./.github/actions/yarn-install-with-cache + - name: Download APK + uses: actions/download-artifact@v4 + with: + name: rntester-${{ matrix.jsengine }}-release + path: ./packages/rn-tester/android/app/build/outputs/apk/${{ matrix.jsengine }}/release/ + - name: Print folder structure + run: ls -lR ./packages/rn-tester/android/app/build/outputs/apk/${{ matrix.jsengine }}/release/ + - name: Run E2E Tests + uses: ./.github/actions/maestro-android + with: + app-path: ./packages/rn-tester/android/app/build/outputs/apk/${{ matrix.jsengine }}/release/app-${{ matrix.jsengine }}-x86-release.apk + app-id: com.facebook.react.uiapp + jsengine: ${{ matrix.jsengine }} + maestro-flow: ./packages/rn-tester/.maestro/ + build_npm_package: runs-on: 8-core-ubuntu needs: diff --git a/packages/rn-tester/.maestro/start.yml b/packages/rn-tester/.maestro/start.yml new file mode 100644 index 000000000000..bb90cf08b106 --- /dev/null +++ b/packages/rn-tester/.maestro/start.yml @@ -0,0 +1,11 @@ +appId: ${APP_ID} # iOS: com.meta.RNTester.localDevelopment | Android: com.facebook.react.uiapp +--- +- launchApp +- assertVisible: "Components" +- scrollUntilVisible: + element: + id: "Modal" + direction: DOWN + speed: 60 +- tapOn: + id: "Modal" diff --git a/scripts/e2e/.maestro/start.yml b/scripts/e2e/.maestro/start.yml new file mode 100644 index 000000000000..55373006458d --- /dev/null +++ b/scripts/e2e/.maestro/start.yml @@ -0,0 +1,4 @@ +appId: ${APP_ID} # iOS: org.reactjs.native.example.RNTestProject | Android: com.rntestproject +--- +- launchApp +- assertVisible: "Step One"