Repository navigation
feat: e2e tests #1501
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: e2e tests #1501
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,188 @@ | ||||||
| name: e2e | ||||||
|
|
||||||
| on: | ||||||
| pull_request_target: | ||||||
| types: [opened, reopened, synchronize, labeled, unlabeled] | ||||||
| push: | ||||||
| branches: [main] | ||||||
| workflow_dispatch: | ||||||
|
|
||||||
| # one run per PR; a new push cancels the older one | ||||||
| concurrency: | ||||||
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | ||||||
| cancel-in-progress: true | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This also applies to
Suggested change
|
||||||
|
|
||||||
| # keep it read-only | ||||||
| permissions: | ||||||
| contents: read | ||||||
|
|
||||||
| jobs: | ||||||
| android: | ||||||
| name: e2e (Android) | ||||||
| # same-repo PRs - always run; forks - run only with the `e2e` label | ||||||
| if: >- | ||||||
| github.event_name != 'pull_request_target' || | ||||||
| github.event.pull_request.head.repo.full_name == github.repository || | ||||||
| contains(github.event.pull_request.labels.*.name, 'e2e') | ||||||
| runs-on: ubuntu-latest | ||||||
| timeout-minutes: 45 | ||||||
|
|
||||||
| steps: | ||||||
| - name: Checkout pull request head | ||||||
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2 | ||||||
| with: | ||||||
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | ||||||
|
|
||||||
| - name: Setup workspace | ||||||
| uses: ./.github/actions/setup-pnpm-node | ||||||
| with: | ||||||
| node-version: '24.18.0' | ||||||
|
|
||||||
| - name: Setup Java | ||||||
| uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # tag=v6.0.1 | ||||||
| with: | ||||||
| distribution: temurin | ||||||
| java-version: '17' | ||||||
|
|
||||||
| - name: Enable KVM | ||||||
| run: | | ||||||
| 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: Build workspace packages | ||||||
| run: pnpm --filter tester-federation-v2... build | ||||||
|
|
||||||
| # nohup/disown: the runner kills step processes at the end. /status wait: RNTA picks its bundle source once, never retries | ||||||
| - name: Start dev servers | ||||||
| working-directory: apps/tester-federation-v2 | ||||||
| env: | ||||||
| E2E: 'true' | ||||||
| run: | | ||||||
| nohup pnpm start:hostapp > /tmp/hostapp.log 2>&1 & | ||||||
| disown | ||||||
| nohup pnpm start:miniapp > /tmp/miniapp.log 2>&1 & | ||||||
| disown | ||||||
| timeout 180 sh -c 'until [ "$(curl -sf localhost:8081/status)" = "packager-status:running" ] && [ "$(curl -sf localhost:8082/status)" = "packager-status:running" ]; do sleep 2; done' | ||||||
|
|
||||||
| # ask for the bundles now so the servers compile them — they only build on first request | ||||||
| - name: Warm both compilers | ||||||
| working-directory: apps/tester-federation-v2 | ||||||
| run: | | ||||||
| curl -sf --max-time 300 -o /dev/null 'http://localhost:8081/android/index.bundle?platform=android&dev=true&lazy=true&minify=false&app=com.tester.federationV2&modulesOnly=false&runModule=true' | ||||||
| curl -sf --max-time 300 -o /dev/null 'http://localhost:8082/android/index.bundle?platform=android&dev=true&lazy=true&minify=false&app=com.tester.federationV2&modulesOnly=false&runModule=true' | ||||||
| echo 'Both bundles served; compilers are warm.' | ||||||
|
|
||||||
| - name: Build Android app | ||||||
| working-directory: apps/tester-federation-v2/android | ||||||
| run: ./gradlew :app:assembleDebug -PreactNativeArchitectures=x86_64 | ||||||
|
|
||||||
| # expose dev servers to the emulator, install the APK, run the tests | ||||||
| - name: Run e2e on an emulator | ||||||
| uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # tag=v2.38.0 | ||||||
| with: | ||||||
| api-level: 35 | ||||||
| target: google_apis | ||||||
| arch: x86_64 | ||||||
| profile: pixel_7 | ||||||
| working-directory: apps/tester-federation-v2 | ||||||
| script: | | ||||||
| adb shell settings put global hide_error_dialogs 1 | ||||||
| for i in 1 2 3; do adb shell "uiautomator dump /sdcard/d.xml >/dev/null && grep -q aerr_ /sdcard/d.xml" && { echo 'ANR dialog on screen, clearing it'; adb shell am force-stop com.google.android.apps.nexuslauncher; }; sleep 2; done | ||||||
| adb reverse tcp:8081 tcp:8081 | ||||||
| adb reverse tcp:8082 tcp:8082 | ||||||
| adb reverse --list | ||||||
| adb reverse --list | grep -q 'tcp:8081' || { echo '::error::adb reverse missing for tcp:8081'; exit 1; } | ||||||
| adb reverse --list | grep -q 'tcp:8082' || { echo '::error::adb reverse missing for tcp:8082'; exit 1; } | ||||||
| adb install -r -g android/app/build/outputs/apk/debug/app-debug.apk | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| pnpm exec e2e run --target android --reporter list,junit; ec=$?; adb logcat -d -v time > .e2e/logcat.txt 2>&1; exit $ec | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
env:
E2E_TELEMETRY_DISABLED: '1' |
||||||
|
|
||||||
| - name: Upload e2e artifacts | ||||||
| if: ${{ !cancelled() }} | ||||||
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # tag=v7.0.1 | ||||||
| with: | ||||||
| name: e2e-android | ||||||
| path: apps/tester-federation-v2/.e2e | ||||||
| include-hidden-files: true | ||||||
| if-no-files-found: error | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the job fails before the e2e step (Gradle, pod install, dev server timeout), Related: |
||||||
| retention-days: 7 | ||||||
|
|
||||||
| - name: Show dev server logs | ||||||
| if: ${{ !cancelled() }} | ||||||
| run: tail -n 80 /tmp/hostapp.log /tmp/miniapp.log | ||||||
|
|
||||||
| # dispatch + main only for now — no PR triggers until this lane has greens | ||||||
| ios: | ||||||
| name: e2e (iOS) | ||||||
| if: github.event_name != 'pull_request_target' | ||||||
| runs-on: macos-15 | ||||||
| timeout-minutes: 45 | ||||||
|
|
||||||
| steps: | ||||||
| - name: Checkout | ||||||
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2 | ||||||
| with: | ||||||
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | ||||||
|
|
||||||
| - name: Setup workspace | ||||||
| uses: ./.github/actions/setup-pnpm-node | ||||||
| with: | ||||||
| node-version: '24.18.0' | ||||||
|
|
||||||
| - name: Build workspace packages | ||||||
| run: pnpm --filter tester-federation-v2... build | ||||||
|
|
||||||
| # start both dev servers in the background, wait until both answer /status | ||||||
| - name: Start dev servers | ||||||
| working-directory: apps/tester-federation-v2 | ||||||
| env: | ||||||
| E2E: 'true' | ||||||
| run: | | ||||||
| nohup pnpm start:hostapp > /tmp/hostapp.log 2>&1 & | ||||||
| disown | ||||||
| nohup pnpm start:miniapp > /tmp/miniapp.log 2>&1 & | ||||||
| disown | ||||||
| for p in 8081 8082; do curl -sf --retry 90 --retry-delay 2 --retry-all-errors --retry-connrefused -o /dev/null "http://localhost:$p/status"; done | ||||||
|
|
||||||
| # ask for the iOS bundles now so the servers compile them — they only build on first request | ||||||
| - name: Warm both compilers | ||||||
| working-directory: apps/tester-federation-v2 | ||||||
| run: | | ||||||
| curl -sf --max-time 300 -o /dev/null 'http://localhost:8081/index.bundle?platform=ios&dev=true&lazy=true&minify=false&modulesOnly=false&runModule=true&app=com.tester.federationV2' | ||||||
| curl -sf --max-time 300 -o /dev/null 'http://localhost:8082/index.bundle?platform=ios&dev=true&lazy=true&minify=false&modulesOnly=false&runModule=true&app=com.tester.federationV2' | ||||||
| echo 'Both bundles served; compilers are warm.' | ||||||
|
|
||||||
| # match the runtime to the default Xcode's SDK (16.4 → iOS 18.x), not just newest available | ||||||
| - name: Boot simulator | ||||||
| run: | | ||||||
| RT=$(xcrun simctl list runtimes -j | jq -r '.runtimes | map(select(.isAvailable and (.identifier | startswith("com.apple.CoreSimulator.SimRuntime.iOS-18-")))) | sort_by(.version) | last | .identifier') | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Matching the runtime to the default Xcode SDK makes sense, but this is a hard failure point. If |
||||||
| echo "using $RT" | ||||||
| [ "$RT" != "null" ] && [ -n "$RT" ] | ||||||
| xcrun simctl create e2e "iPhone 16" "$RT" | ||||||
| xcrun simctl boot e2e | ||||||
| xcrun simctl bootstatus e2e -b | ||||||
|
|
||||||
| # pod install generates the Xcode project the workspace points at | ||||||
| - name: Build iOS app | ||||||
| working-directory: apps/tester-federation-v2/ios | ||||||
| run: pod install && xcodebuild -workspace ModuleFederationTesterV2.xcworkspace -scheme ReactTestApp -configuration Debug -destination 'generic/platform=iOS Simulator' -derivedDataPath build CODE_SIGNING_ALLOWED=NO ARCHS=arm64 build | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The app's own scripts use the Gemfile's CocoaPods (
Suggested change
|
||||||
|
|
||||||
| - name: Run e2e | ||||||
| working-directory: apps/tester-federation-v2 | ||||||
| run: | | ||||||
| xcrun simctl install booted ios/build/Build/Products/Debug-iphonesimulator/*.app | ||||||
| pnpm exec e2e run --target ios --reporter list,junit | ||||||
|
|
||||||
| - name: Upload e2e artifacts | ||||||
| if: ${{ !cancelled() }} | ||||||
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # tag=v7.0.1 | ||||||
| with: | ||||||
| name: e2e-ios | ||||||
| path: apps/tester-federation-v2/.e2e | ||||||
| include-hidden-files: true | ||||||
| if-no-files-found: error | ||||||
| retention-days: 7 | ||||||
|
|
||||||
| - name: Show dev server logs | ||||||
| if: ${{ !cancelled() }} | ||||||
| run: tail -n 80 /tmp/hostapp.log /tmp/miniapp.log | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -62,3 +62,6 @@ yarn-error.log | |
|
|
||
| # MF | ||
| **/@mf-types | ||
|
|
||
| # e2e | ||
| .e2e/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -99,6 +99,7 @@ export default Repack.defineRspackConfig((env) => { | |
| __WITH_PRELOAD__: | ||
| process.env.WITH_PRELOAD === 'true' || | ||
| process.env.WITH_PRELOAD === '1', | ||
| __E2E__: process.env.E2E === 'true', | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: |
||
| }), | ||
| ], | ||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { mobile } from '@e2e-dev/mobile'; | ||
| import type { E2EConfig } from 'e2e'; | ||
|
|
||
| export default { | ||
| tests: 'e2e/**/*.e2e.ts', | ||
| targets: [ | ||
| { | ||
| name: 'android', | ||
| engine: mobile({ platform: 'android' }), | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Without Pinning it in the config, with an env override for local runs, solved this for me: engine: mobile({ platform: 'android', device: process.env.E2E_DEVICE }),On iOS the job already names its simulator I'd also set |
||
| app: { bundleId: 'com.tester.federationV2' }, | ||
| }, | ||
| { | ||
| name: 'ios', | ||
| engine: mobile({ platform: 'ios' }), | ||
| app: { bundleId: 'com.tester.federationV2' }, | ||
| }, | ||
| ], | ||
| } satisfies E2EConfig; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import { test } from '@e2e-dev/mobile'; | ||
| import { expect } from 'e2e'; | ||
|
|
||
| test('the host app boots to Home', async ({ app, screen }) => { | ||
| await app.open(); | ||
|
|
||
| await expect(screen.getByTestId('home-details-button')).toBeVisible({ | ||
| timeout: 60_000, | ||
| }); | ||
| await expect(screen.getByTestId('home-gallery-button')).toBeVisible(); | ||
| }); | ||
|
|
||
| test('navigates from Home to Detail and back', async ({ app, screen }) => { | ||
| await app.open(); | ||
| await expect(screen.getByTestId('home-details-button')).toBeVisible({ | ||
| timeout: 60_000, | ||
| }); | ||
|
|
||
| await screen.getByTestId('home-details-button').tap(); | ||
| await expect(screen.getByTestId('detail-screen')).toBeVisible(); | ||
|
|
||
| await app.back(); | ||
| await expect(screen.getByTestId('home-details-button')).toBeVisible(); | ||
| await expect(screen.getByTestId('detail-screen')).toBeHidden(); | ||
| }); | ||
|
|
||
| test('Home opens the Gallery MiniApp', { | ||
| timeout: 120_000, | ||
| }, async ({ app, screen }) => { | ||
| await app.open(); | ||
| await expect(screen.getByTestId('home-gallery-button')).toBeVisible({ | ||
| timeout: 60_000, | ||
| }); | ||
|
|
||
| await screen.getByTestId('home-gallery-button').tap(); | ||
| await expect(screen.getByTestId('miniapp-gallery')).toBeVisible({ | ||
| timeout: 45_000, | ||
| }); | ||
| await expect(screen.getByText('Picture 1')).toBeVisible(); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,7 +9,8 @@ | |
| "pods:update": "(cd ios && bundle install && bundle exec pod update)", | ||
| "start:hostapp": "react-native webpack-start --config config.host-app.mts", | ||
| "start:miniapp": "react-native webpack-start --config config.mini-app.mts --port 8082", | ||
| "start:expo-host-widget": "react-native webpack-start --config config.expo-host-widget.mts --port 8083" | ||
| "start:expo-host-widget": "react-native webpack-start --config config.expo-host-widget.mts --port 8083", | ||
| "e2e": "pnpm exec e2e run" | ||
| }, | ||
| "dependencies": { | ||
| "@callstack/repack": "workspace:*", | ||
|
|
@@ -25,6 +26,7 @@ | |
| }, | ||
| "devDependencies": { | ||
| "@babel/core": "^7.29.7", | ||
| "@e2e-dev/mobile": "0.11.0", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two things about the new deps:
|
||
| "@react-native-community/cli": "catalog:testers", | ||
| "@react-native-community/cli-platform-android": "catalog:testers", | ||
| "@react-native-community/cli-platform-ios": "catalog:testers", | ||
|
|
@@ -36,6 +38,7 @@ | |
| "@swc/helpers": "catalog:", | ||
| "@types/jest": "^29.5.13", | ||
| "@types/react": "catalog:testers", | ||
| "e2e": "0.19.0", | ||
| "react-native-test-app": "catalog:testers", | ||
| "terser-webpack-plugin": "catalog:", | ||
| "typescript": "catalog:", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,15 @@ | ||
| import { ScriptManager } from '@callstack/repack/client'; | ||
| import { preloadRemote } from '@module-federation/runtime'; | ||
| import AsyncStorage from '@react-native-async-storage/async-storage'; | ||
| import { AppRegistry } from 'react-native'; | ||
| import { AppRegistry, LogBox } from 'react-native'; | ||
|
|
||
| import { components } from '../../app.json'; | ||
| import App from './App'; | ||
|
|
||
| if (__E2E__) { | ||
| LogBox.ignoreAllLogs(true); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This hides the symptom from #1499, but the cause is still there. The green run's Cause: the four MF configs pass import safeAreaPkg from 'react-native-safe-area-context/package.json' with { type: 'json' };
import screensPkg from 'react-native-screens/package.json' with { type: 'json' };
// ...
version: safeAreaPkg.version,
requiredVersion: safeAreaPkg.version,With that in place, this line can go entirely, or be narrowed to Also, |
||
| } | ||
|
|
||
| if (__WITH_PRELOAD__) { | ||
| // enable caching of scripts in the AsyncStorage | ||
| ScriptManager.shared.setStorage(AsyncStorage); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,7 +16,7 @@ class ErrorBoundary extends React.Component< | |
| render() { | ||
| if (this.state.hasError) { | ||
| return ( | ||
| <View style={styles.container}> | ||
| <View testID="miniapp-error" style={styles.container}> | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: nothing uses |
||
| <Text>Failed to load Mini App</Text> | ||
| </View> | ||
| ); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd use
pull_requesthere instead ofpull_request_target. The job uses no secrets and only needscontents: read, so the target trigger adds risk without enabling anything:e2eto a fork PR, every latersynchronizepush runs fork code with no fresh review. Withpull_request, GitHub's fork-workflow approval does this gating.setup-pnpm-nodepasses noversion, sopnpm/action-setupinstalls whateverpackageManagersays in the checked-out, fork-controlledpackage.json, against the fork's lockfile.pull_request_targetthe workflow file always comes frommain, so a PR's edits toe2e.ymlnever run on that PR. That includes this PR.head.sha. The defaultpull_requestcheckout uses the merge ref, which is the right thing for a merge gate.On cache poisoning, the concern from #1499: GitHub now gives untrusted triggers a read-only cache token for the default-branch scope, so this is mitigated by the platform. The points above still apply.
With
pull_request, I'd also:labeled/unlabeledfromtypes, since each label change currently starts a new runpaths:(e.g.apps/tester-federation-v2/**,packages/**,pnpm-lock.yamland this workflow file) so docs-only PRs don't spend emulator minutes