Skip to content

test: Add e2e tests using react-native-harness#41

Merged
l2hyunwoo merged 27 commits intomainfrom
test/harness-e2e-tests
Dec 3, 2025
Merged

test: Add e2e tests using react-native-harness#41
l2hyunwoo merged 27 commits intomainfrom
test/harness-e2e-tests

Conversation

@l2hyunwoo
Copy link
Owner

@l2hyunwoo l2hyunwoo commented Dec 1, 2025

Summary

Add comprehensive E2E testing infrastructure using react-native-harness for real device/simulator testing.

What's included

  • Test Infrastructure: Configure react-native-harness with iOS Simulator (iPhone 16 Pro) and Android Emulator (Pixel 9 API 35) runners
  • 68 E2E Tests across 5 test suites:
    • 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 validation
    • edge-cases.harness.ts - Edge cases and Tier 3 type-only tests
    • CI Integration: GitHub Actions workflow for automated E2E testing on PRs and pushes to main

Screenshots

스크린샷 2025-12-03 오후 5 30 04

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines 70 to 71
// NoClassDefFoundError is expected on emulators without Google Play Services
expect(true).toBe(true);
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 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:

  1. Removing the assertion entirely since the test already passes by not throwing
  2. Adding a more meaningful assertion or comment explaining this is intentionally empty
  3. Using a jest matcher like expect(() => DeviceInfoModule.getHasGms()).not.toThrow() outside the try-catch instead
Suggested change
// 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.

Copilot uses AI. Check for mistakes.
api-level: 35
arch: x86_64
profile: pixel_9
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.
}),
androidPlatform({
name: 'android',
device: androidEmulator('Pixel_9_Pro'),
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 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.

Suggested change
device: androidEmulator('Pixel_9_Pro'),
device: androidEmulator('pixel_9'),

Copilot uses AI. Check for mistakes.
Comment on lines 23 to 50
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');
});
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 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).

Copilot uses AI. Check for mistakes.
Comment on lines 79 to 100
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');
});
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.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +66
echo "SIMULATOR_DEVICE_ID=$DEVICE_ID" >> $GITHUB_ENV

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.
runners: [
applePlatform({
name: 'ios',
device: appleSimulator('iPhone 16 Pro', '18.2'),
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 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).

Suggested change
device: appleSimulator('iPhone 16 Pro', '18.2'),
device: appleSimulator('iPhone 15 Pro', '18.2'),

Copilot uses AI. Check for mistakes.
* 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})*$/;
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 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:

  1. Script subtags (like "Hans", "Latn") are exactly 4 characters
  2. Region subtags are 2 characters (uppercase) or 3 digits
  3. 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

Suggested change
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;

Copilot uses AI. Check for mistakes.
Comment on lines 53 to 74
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');
});
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.

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.

Copilot uses AI. Check for mistakes.
@l2hyunwoo l2hyunwoo marked this pull request as draft December 1, 2025 16:38
@l2hyunwoo l2hyunwoo force-pushed the test/harness-e2e-tests branch 2 times, most recently from 2d51d04 to dc7d6b1 Compare December 2, 2025 01:23
@l2hyunwoo l2hyunwoo force-pushed the main branch 6 times, most recently from 84ab0a5 to 4460020 Compare December 3, 2025 02:42
- 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
@l2hyunwoo l2hyunwoo force-pushed the test/harness-e2e-tests branch from dc7d6b1 to 3c80a54 Compare December 3, 2025 02:53
- 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
@l2hyunwoo l2hyunwoo force-pushed the test/harness-e2e-tests branch from aac3f0e to e3b4a6a Compare December 3, 2025 06:13
@l2hyunwoo l2hyunwoo requested a review from Copilot December 3, 2025 08:09
@l2hyunwoo l2hyunwoo marked this pull request as ready for review December 3, 2025 08:09
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

@l2hyunwoo l2hyunwoo merged commit 51228ba into main Dec 3, 2025
16 checks passed
@l2hyunwoo l2hyunwoo deleted the test/harness-e2e-tests branch December 3, 2025 08:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants