test: Add e2e tests using react-native-harness#41
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds comprehensive E2E testing infrastructure using react-native-harness for testing the react-native-nitro-device-info library on real devices/simulators. The implementation includes 68 E2E tests across 5 test suites with CI integration for automated testing on both iOS and Android platforms.
Key Changes
- E2E test infrastructure with react-native-harness configuration and 68 tests across core properties, dynamic state, hooks, platform-specific APIs, and edge cases
- Jest and Metro configuration for harness integration
- GitHub Actions CI workflow for automated E2E testing on iOS Simulator and Android Emulator
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| yarn.lock | Dependency updates for react-native-harness and related testing packages |
| example/showcase/package.json | Added E2E test scripts and harness dependencies |
| example/showcase/jest.config.js | Jest configuration for harness preset |
| example/showcase/metro.config.js | Metro configuration with harness integration |
| example/showcase/rn-harness.config.mjs | Harness runner configuration for iOS and Android |
| example/showcase/src/tests/types.ts | Type definitions and validation helpers for tests |
| example/showcase/src/tests/core-properties.harness.ts | Tests for core device properties |
| example/showcase/src/tests/dynamic-state.harness.ts | Tests for dynamic state APIs |
| example/showcase/src/tests/hooks.harness.tsx | Tests for React hooks |
| example/showcase/src/tests/platform-specific.harness.ts | Tests for platform-specific APIs |
| example/showcase/src/tests/edge-cases.harness.ts | Tests for edge cases and type verification |
| .github/workflows/e2e-tests.yml | CI workflow for automated E2E testing |
Comments suppressed due to low confidence (4)
example/showcase/src/tests/hooks.harness.tsx:9
- Unused import useState.
import React, { useEffect, useState } from 'react';
example/showcase/src/tests/hooks.harness.tsx:27
- Unused variable TestComponent.
const TestComponent = () => {
example/showcase/src/tests/hooks.harness.tsx:56
- Unused variable TestComponent.
const TestComponent = () => {
example/showcase/src/tests/hooks.harness.tsx:82
- Unused variable TestComponent.
const TestComponent = () => {
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // NoClassDefFoundError is expected on emulators without Google Play Services | ||
| expect(true).toBe(true); |
There was a problem hiding this comment.
The assertion expect(true).toBe(true) is a no-op that always passes. If the catch block is expected to execute in some cases (when Play Services is not available), consider either:
- Removing the assertion entirely since the test already passes by not throwing
- Adding a more meaningful assertion or comment explaining this is intentionally empty
- Using a jest matcher like
expect(() => DeviceInfoModule.getHasGms()).not.toThrow()outside the try-catch instead
| // NoClassDefFoundError is expected on emulators without Google Play Services | |
| expect(true).toBe(true); | |
| // NoClassDefFoundError is expected on emulators without Google Play Services. | |
| // This is an expected scenario; test passes if no unexpected error is thrown. |
| api-level: 35 | ||
| arch: x86_64 | ||
| profile: pixel_9 | ||
| target: google_apis |
There was a problem hiding this comment.
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.
| target: google_apis | |
| target: google_apis_playstore |
| }), | ||
| androidPlatform({ | ||
| name: 'android', | ||
| device: androidEmulator('Pixel_9_Pro'), |
There was a problem hiding this comment.
The harness config specifies androidEmulator('Pixel_9_Pro') (with underscore), but the CI workflow uses profile: pixel_9 (without _Pro suffix). This mismatch in device/emulator naming will likely cause issues.
Recommendation: Ensure the device name in the harness config matches what will be created/available in the CI environment. The android-emulator-runner action uses profile names, not AVD names, so these need to align.
| device: androidEmulator('Pixel_9_Pro'), | |
| device: androidEmulator('pixel_9'), |
| test('useBatteryLevel returns valid battery level', async () => { | ||
| let capturedLevel: number | null = null; | ||
| let renderCount = 0; | ||
|
|
||
| const TestComponent = () => { | ||
| const batteryLevel = useBatteryLevel(); | ||
| renderCount++; | ||
|
|
||
| useEffect(() => { | ||
| if (batteryLevel !== null && batteryLevel !== undefined) { | ||
| capturedLevel = batteryLevel; | ||
| } | ||
| }, [batteryLevel]); | ||
|
|
||
| return ( | ||
| <View testID="battery-test"> | ||
| <Text>{`Battery: ${batteryLevel}`}</Text> | ||
| </View> | ||
| ); | ||
| }; | ||
|
|
||
| // The component should render and capture the battery level | ||
| // In a real harness test, we would use render() and waitFor() | ||
| // For now, we verify the hook exists and can be called | ||
|
|
||
| expect(useBatteryLevel).toBeDefined(); | ||
| expect(typeof useBatteryLevel).toBe('function'); | ||
| }); |
There was a problem hiding this comment.
The test creates a TestComponent with hooks and state management but never actually renders it or validates the captured values. The test only verifies that the hook function exists, which doesn't test the actual hook behavior in a React component.
The comment acknowledges this ("For now, we verify the hook exists and can be called"), but for E2E tests, you should actually render the component and verify the values. Consider using the render utilities from react-native-harness to mount the component and assert on the captured values (e.g., capturedLevel should be between 0 and 1).
| test('useIsHeadphonesConnected returns boolean value', async () => { | ||
| let capturedValue: boolean | null = null; | ||
|
|
||
| const TestComponent = () => { | ||
| const isConnected = useIsHeadphonesConnected(); | ||
|
|
||
| useEffect(() => { | ||
| if (typeof isConnected === 'boolean') { | ||
| capturedValue = isConnected; | ||
| } | ||
| }, [isConnected]); | ||
|
|
||
| return ( | ||
| <View testID="headphones-test"> | ||
| <Text>{`Connected: ${isConnected}`}</Text> | ||
| </View> | ||
| ); | ||
| }; | ||
|
|
||
| expect(useIsHeadphonesConnected).toBeDefined(); | ||
| expect(typeof useIsHeadphonesConnected).toBe('function'); | ||
| }); |
There was a problem hiding this comment.
This test also creates a TestComponent but never renders it. The test only verifies the hook function exists.
Render the component and verify that capturedValue is actually a boolean as expected.
| echo "SIMULATOR_DEVICE_ID=$DEVICE_ID" >> $GITHUB_ENV | ||
|
|
There was a problem hiding this comment.
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.
| echo "SIMULATOR_DEVICE_ID=$DEVICE_ID" >> $GITHUB_ENV |
| runners: [ | ||
| applePlatform({ | ||
| name: 'ios', | ||
| device: appleSimulator('iPhone 16 Pro', '18.2'), |
There was a problem hiding this comment.
The harness config specifies "iPhone 16 Pro" with iOS 18.2, but the CI workflow is looking for "iPhone 15 Pro". This mismatch could cause the CI workflow to fail if the specified simulator is not available on the macOS runners.
Recommendation: Update the harness config to use "iPhone 15 Pro" to match what's available in the CI environment, or update the CI workflow to match the harness config (and verify the simulator is available in macos-14 runners).
| device: appleSimulator('iPhone 16 Pro', '18.2'), | |
| device: appleSimulator('iPhone 15 Pro', '18.2'), |
| * BCP 47 language tag pattern (simplified) | ||
| * Examples: "en", "en-US", "ko-KR", "zh-Hans-CN" | ||
| */ | ||
| export const BCP47_PATTERN = /^[a-z]{2,3}(-[A-Za-z]{2,8})*$/; |
There was a problem hiding this comment.
The BCP 47 regex pattern is too restrictive and will reject valid language tags. The pattern /^[a-z]{2,3}(-[A-Za-z]{2,8})*$/ requires all components after the language code to be exactly 2-8 characters, but:
- Script subtags (like "Hans", "Latn") are exactly 4 characters
- Region subtags are 2 characters (uppercase) or 3 digits
- Variant subtags can be 5-8 alphanumeric characters
For example, valid tags like "en-US" (uppercase region), "en-001" (numeric region), or "sr-Cyrl-RS" (script + region) would fail this pattern.
Recommendation: Use a more accurate BCP 47 pattern or consider using a library for validation. A better simplified pattern: /^[a-z]{2,3}(-[A-Z][a-z]{3})?(-([A-Z]{2}|[0-9]{3}))?(-[A-Za-z0-9]{5,8})*$/ or allow mixed case: /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/i
| export const BCP47_PATTERN = /^[a-z]{2,3}(-[A-Za-z]{2,8})*$/; | |
| export const BCP47_PATTERN = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/i; |
| test('usePowerState returns valid PowerState object', async () => { | ||
| let capturedState: ReturnType<typeof usePowerState> | null = null; | ||
|
|
||
| const TestComponent = () => { | ||
| const powerState = usePowerState(); | ||
|
|
||
| useEffect(() => { | ||
| if (powerState !== null && powerState !== undefined) { | ||
| capturedState = powerState; | ||
| } | ||
| }, [powerState]); | ||
|
|
||
| return ( | ||
| <View testID="power-state-test"> | ||
| <Text>{`State: ${JSON.stringify(powerState)}`}</Text> | ||
| </View> | ||
| ); | ||
| }; | ||
|
|
||
| expect(usePowerState).toBeDefined(); | ||
| expect(typeof usePowerState).toBe('function'); | ||
| }); |
There was a problem hiding this comment.
Similar to the useBatteryLevel test, this test creates a TestComponent but never renders it or validates the captured state. The test only checks that the hook function exists.
For a proper E2E test, render the component and verify that capturedState is a valid PowerState object using the isValidPowerState helper that was imported but never used.
2d51d04 to
dc7d6b1
Compare
84ab0a5 to
4460020
Compare
- Add react-native-harness and platform packages - Add jest-cli and @types/jest - Add use-sync-external-store for zustand compatibility - Add E2E test scripts (test:e2e, test:e2e:ios, test:e2e:android)
Configure iOS (iPhone 16 Pro) and Android (Pixel 7 API 34) runners for E2E test execution
Add withRnHarness wrapper to Metro configuration
Add helper functions for validating DeviceType, BatteryState, NavigationMode, PowerState, and BCP 47 language tags
Test deviceId, brand, systemName, systemVersion, model, deviceType, isTablet, uniqueId, manufacturer, version, buildNumber, bundleId, applicationName, isEmulator, and supportedAbis
Test getBatteryLevel, getPowerState, getUsedMemory, totalMemory, getFreeDiskStorage, totalDiskCapacity, and systemLanguage
Test useBatteryLevel, usePowerState, and useIsHeadphonesConnected hooks
Test iOS-specific (getBrightness, getHasNotch, getHasDynamicIsland), Android-specific (getHasGms, getHasHms, apiLevel, navigationMode), and cross-platform APIs (getFontScale, getIsLandscape, time-based)
Add tests for edge cases (no SIM, location disabled, simulator battery) and Tier 3 type-only validation for environment-dependent APIs.
Configure GitHub Actions workflow for automated E2E testing: - iOS job on macos-14 with simulator - Android job on ubuntu-latest with emulator - Test result reporting with artifacts - Triggers on push/PR to main branch
- Update CI workflow to use API level 35 and pixel_9 profile - Update local harness config to use Pixel_9_API_35 AVD - Update AVD cache key to match new API level
- Use macos-15 for iOS (Xcode 16.1+ required by RN 0.81.1) - Use pixel_6 profile for Android (pixel_9 unavailable in CI)
- Remove no-op expect(true).toBe(true) assertion in catch block - Fix Android emulator name mismatch (Pixel_9_Pro -> test) to match CI AVD
dc7d6b1 to
3c80a54
Compare
- Add CI environment detection via process.env.CI - Set bridgeTimeout to 180000ms in CI (60000ms locally) - Update iOS simulator to iOS 18.4 (available on macos-15 runners)
- Replace grep -oE (not reliable on macOS BSD grep) with awk - Update to search for iPhone 16 Pro first (available on macos-15) - Fallback to any iPhone if specific model not found
The multi-line script was being split into separate shell invocations, causing 'react-native-harness: not found'. Using working-directory input instead of cd in script.
Both iOS and Android E2E tests require the app to be built and installed before react-native-harness can run tests. iOS: - Add 'Build iOS App' step using react-native build-ios - Add 'Install iOS App' step using xcrun simctl install Android: - Add build and install commands in emulator-runner script - Use react-native build-android and adb install
iOS: - Use xcodebuild directly instead of 'react-native build-ios --simulator' which doesn't exist - Build for iphonesimulator SDK with Debug configuration Android: - Separate 'Build Android App' step using './gradlew assembleDebug' - This generates APK at the expected path for adb install - Previous 'react-native build-android' was creating AAB bundle instead
aac3f0e to
e3b4a6a
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 13 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Summary
Add comprehensive E2E testing infrastructure using react-native-harness for real device/simulator testing.
What's included
core-properties.harness.ts- Core device properties (deviceId, brand, model, etc.)dynamic-state.harness.ts- Dynamic APIs (battery, memory, storage)hooks.harness.tsx- React hooks (useBatteryLevel, usePowerState, useIsHeadphonesConnected)platform-specific.harness.ts- Platform-specific APIs with fallback validationedge-cases.harness.ts- Edge cases and Tier 3 type-only testsScreenshots