Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5591834
chore: add react-native-harness dependencies for E2E testing
l2hyunwoo Dec 1, 2025
702464e
feat(e2e): add react-native-harness configuration
l2hyunwoo Dec 1, 2025
ee40a70
feat(e2e): add Jest configuration with harness preset
l2hyunwoo Dec 1, 2025
0e0ec44
feat(e2e): integrate harness with Metro bundler
l2hyunwoo Dec 1, 2025
83f3845
feat(e2e): add type definitions and validation helpers
l2hyunwoo Dec 1, 2025
62a79cb
test(e2e): add core device properties tests
l2hyunwoo Dec 1, 2025
2cb9b1a
test(e2e): add dynamic device state API tests
l2hyunwoo Dec 1, 2025
80427c9
test(e2e): add React hooks tests
l2hyunwoo Dec 1, 2025
3c2b056
test(e2e): add platform-specific API tests
l2hyunwoo Dec 1, 2025
a59557b
test(e2e): add edge case and Tier 3 type-only tests
l2hyunwoo Dec 1, 2025
fb0de54
ci: add E2E tests workflow with iOS and Android jobs
l2hyunwoo Dec 1, 2025
6b96db9
chore: update Android emulator to Pixel 9 API 35
l2hyunwoo Dec 1, 2025
2edfa1e
fix: lint task
l2hyunwoo Dec 1, 2025
e4ef041
ci: fix E2E tests runner compatibility
l2hyunwoo Dec 1, 2025
150b0ba
fix: address PR review feedback
l2hyunwoo Dec 1, 2025
3c80a54
chore: update yarn.lock
l2hyunwoo Dec 3, 2025
94252f4
fix: update harness config for CI compatibility
l2hyunwoo Dec 3, 2025
5e178d7
docs: add comments explaining CI vs local config behavior
l2hyunwoo Dec 3, 2025
bc73651
fix(ci): use awk instead of grep -oE for simulator ID extraction
l2hyunwoo Dec 3, 2025
46474f8
fix(ci): use working-directory for android-emulator-runner script
l2hyunwoo Dec 3, 2025
f936c19
fix(ci): add app build and install steps for E2E tests
l2hyunwoo Dec 3, 2025
e3b4a6a
fix(ci): use correct build commands for iOS and Android
l2hyunwoo Dec 3, 2025
fd585fc
fix(ci): enlarge memory disk size of android emulator
l2hyunwoo Dec 3, 2025
e263839
fix(ci): skip webview tests on ios
l2hyunwoo Dec 3, 2025
8899b84
ci: add describeIOS, describeAndroid for platform specific tests
l2hyunwoo Dec 3, 2025
3fa7c60
ci: add more time for timeout
l2hyunwoo Dec 3, 2025
180cdd2
ci: change app restart logic during ci
l2hyunwoo Dec 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
235 changes: 235 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
name: E2E Tests

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

concurrency:
group: e2e-${{ github.ref }}
cancel-in-progress: true

jobs:
e2e-ios:
name: E2E Tests (iOS)
runs-on: macos-15
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'

- name: Cache CocoaPods
uses: actions/cache@v4
with:
path: |
example/showcase/ios/Pods
~/Library/Caches/CocoaPods
key: pods-e2e-${{ runner.os }}-${{ hashFiles('**/Podfile.lock') }}
restore-keys: |
pods-e2e-${{ runner.os }}-

- name: Install dependencies
run: yarn install --immutable

- name: Build library
run: yarn prepare

- name: Install example dependencies
working-directory: example/showcase
run: yarn install --immutable

- name: Install Pods
working-directory: example/showcase/ios
run: pod install

- name: List available simulators
run: xcrun simctl list devices available

- name: Boot iOS Simulator
run: |
# Find iPhone 16 Pro first (available on macos-15), fallback to any iPhone
DEVICE_ID=$(xcrun simctl list devices available | grep "iPhone 16 Pro" | head -1 | awk -F '[()]' '{print $2}')
if [ -z "$DEVICE_ID" ]; then
DEVICE_ID=$(xcrun simctl list devices available | grep "iPhone" | head -1 | awk -F '[()]' '{print $2}')
fi
echo "Booting simulator: $DEVICE_ID"
xcrun simctl boot "$DEVICE_ID" || true
echo "SIMULATOR_DEVICE_ID=$DEVICE_ID" >> $GITHUB_ENV

Comment on lines +65 to +66
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SIMULATOR_DEVICE_ID environment variable is set but never used in subsequent steps. If this was intended for use by react-native-harness, consider documenting it or removing it if unused.

Suggested change
echo "SIMULATOR_DEVICE_ID=$DEVICE_ID" >> $GITHUB_ENV

Copilot uses AI. Check for mistakes.
- name: Build iOS App
working-directory: example/showcase
run: |
xcodebuild -workspace ios/NitroDeviceInfoExample.xcworkspace \
-scheme NitroDeviceInfoExample \
-configuration Debug \
-sdk iphonesimulator \
-destination "id=$SIMULATOR_DEVICE_ID" \
-derivedDataPath ios/build \
build

- name: Install iOS App
run: |
APP_PATH=$(find example/showcase/ios/build -name "*.app" -type d | head -1)
echo "Installing app from: $APP_PATH"
xcrun simctl install "$SIMULATOR_DEVICE_ID" "$APP_PATH"

- name: Run E2E Tests
working-directory: example/showcase
run: npx react-native-harness --harnessRunner ios
env:
CI: true

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: e2e-ios-results
path: |
example/showcase/jest-results.xml
example/showcase/coverage/
retention-days: 7

e2e-android:
name: E2E Tests (Android)
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Free Disk Space
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: false # Keep Android SDK
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true

- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'

- name: Setup Java
uses: actions/setup-java@v4
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: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-e2e-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
gradle-e2e-${{ runner.os }}-

- name: Cache AVD
uses: actions/cache@v4
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-api-30-${{ runner.os }}
restore-keys: |
avd-api-30-

- name: Install dependencies
run: yarn install --immutable

- name: Build library
run: yarn prepare

- name: Install example dependencies
working-directory: example/showcase
run: yarn install --immutable

- name: Build Android App
working-directory: example/showcase/android
run: ./gradlew assembleDebug

- name: Create AVD and run E2E tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 30
arch: x86_64
profile: pixel_4
target: google_apis
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The target: google_apis may not be compatible with API level 35. For newer API levels (34+), the recommended target is google_apis_playstore or default. Additionally, the test at line 64-72 of platform-specific.harness.ts expects Google Play Services (getHasGms) to potentially throw on emulators without GMS, but google_apis target should include Google APIs.

Recommendation: Verify that API 35 supports google_apis target, or use default or google_apis_playstore as the target.

Suggested change
target: google_apis
target: google_apis_playstore

Copilot uses AI. Check for mistakes.
disk-size: 2G
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -partition-size 2048
disable-animations: true
working-directory: example/showcase
script: |
# Install the Android app
adb install android/app/build/outputs/apk/debug/app-debug.apk
# Run E2E tests
npx react-native-harness --harnessRunner android
env:
CI: true

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: e2e-android-results
path: |
example/showcase/jest-results.xml
example/showcase/coverage/
retention-days: 7

report:
name: E2E Test Report
needs: [e2e-ios, e2e-android]
runs-on: ubuntu-latest
if: always()
steps:
- name: Download iOS results
uses: actions/download-artifact@v4
with:
name: e2e-ios-results
path: ios-results
continue-on-error: true

- name: Download Android results
uses: actions/download-artifact@v4
with:
name: e2e-android-results
path: android-results
continue-on-error: true

- name: Report Status
run: |
echo "## E2E Test Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY

if [ "${{ needs.e2e-ios.result }}" == "success" ]; then
echo "- iOS: ✅ Passed" >> $GITHUB_STEP_SUMMARY
else
echo "- iOS: ❌ Failed" >> $GITHUB_STEP_SUMMARY
fi

if [ "${{ needs.e2e-android.result }}" == "success" ]; then
echo "- Android: ✅ Passed" >> $GITHUB_STEP_SUMMARY
else
echo "- Android: ❌ Failed" >> $GITHUB_STEP_SUMMARY
fi
4 changes: 4 additions & 0 deletions example/showcase/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
preset: 'react-native-harness',
testMatch: ['**/__tests__/**/*.harness.(ts|tsx)'],
};
5 changes: 4 additions & 1 deletion example/showcase/metro.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const path = require('path');
const { getDefaultConfig } = require('@react-native/metro-config');
const { withMetroConfig } = require('react-native-monorepo-config');
const { withRnHarness } = require('react-native-harness/metro');

const root = path.resolve(__dirname, '../..');

Expand All @@ -10,7 +11,9 @@ const root = path.resolve(__dirname, '../..');
*
* @type {import('metro-config').MetroConfig}
*/
module.exports = withMetroConfig(getDefaultConfig(__dirname), {
const baseConfig = withMetroConfig(getDefaultConfig(__dirname), {
root,
dirname: __dirname,
});

module.exports = withRnHarness(baseConfig);
13 changes: 11 additions & 2 deletions example/showcase/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@
"start": "react-native start",
"build:android": "react-native build-android --extra-params \"--no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a\"",
"build:ios": "react-native build-ios --mode Debug",
"pod": "cd ios && pod install"
"pod": "cd ios && pod install",
"test:e2e": "react-native-harness",
"test:e2e:ios": "react-native-harness --harnessRunner ios",
"test:e2e:android": "react-native-harness --harnessRunner android"
},
"dependencies": {
"react": "19.1.0",
"react-native": "0.81.1",
"react-native-nitro-modules": "^0.31.2"
"react-native-nitro-modules": "^0.31.2",
"use-sync-external-store": "^1.4.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",
Expand All @@ -22,11 +26,16 @@
"@react-native-community/cli": "20.0.0",
"@react-native-community/cli-platform-android": "20.0.0",
"@react-native-community/cli-platform-ios": "20.0.0",
"@react-native-harness/platform-android": "^1.0.0-alpha.18",
"@react-native-harness/platform-apple": "^1.0.0-alpha.18",
"@react-native/babel-preset": "0.81.1",
"@react-native/metro-config": "0.81.1",
"@react-native/typescript-config": "0.81.1",
"@types/jest": "^29.5.14",
"@types/react": "^19.1.0",
"jest-cli": "^29.7.0",
"react-native-builder-bob": "^0.40.13",
"react-native-harness": "^1.0.0-alpha.18",
"react-native-monorepo-config": "^0.1.9"
},
"engines": {
Expand Down
40 changes: 40 additions & 0 deletions example/showcase/rn-harness.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {
androidPlatform,
androidEmulator,
} from '@react-native-harness/platform-android';
import {
applePlatform,
appleSimulator,
} from '@react-native-harness/platform-apple';

// CI environment detection - GitHub Actions sets CI=true
const isCI = process.env.CI === 'true';

/** @type {import('react-native-harness').HarnessConfig} */
const config = {
entryPoint: './index.js',
appRegistryComponentName: 'NitroDeviceInfoExample',
// CI: 300s timeout for slower Metro startup and simulator response on shared runners
// Local: 60s timeout for faster feedback during development
bridgeTimeout: isCI ? 300000 : 60000,
// CI: Disable app restart between test files to avoid simulator timeout issues
// Local: Enable for better test isolation during development
resetEnvironmentBetweenTestFiles: !isCI,

runners: [
applePlatform({
name: 'ios',
device: appleSimulator('iPhone 16 Pro', '18.4'),
bundleId: 'nitrodeviceinfo.example',
}),
androidPlatform({
name: 'android',
device: androidEmulator('test'),
bundleId: 'nitrodeviceinfo.example',
}),
],

defaultRunner: 'ios',
};

export default config;
Loading
Loading