Testing Navigation Flows in React Native — testing React Native apps with Detox and @testing-library/react-native.
Testing Navigation Flows in React Native addresses the unique challenges of mobile application testing. Mobile apps have device-specific behaviors (touch gestures, orientation, system permissions), platform differences (iOS vs Android), and performance constraints that web testing frameworks do not cover.
1Layer Tool Speed Fidelity
2Unit Jest + @testing-library/rn Fast Low
3Component Jest + @testing-library/rn Fast Medium
4E2E (gray-box) Detox Slow High
5E2E (black-box) Appium Slow High
6Performance Flashlight + Detox Slow High1# Install @testing-library/react-native
2npm install --save-dev @testing-library/react-native @testing-library/jest-native
3
4# Install Detox
5npm install --save-dev detox
6npx detox init
7
8# Build and run Detox tests:
9npx detox build --configuration ios.sim.debug
10npx detox test --configuration ios.sim.debug1import { render, screen, fireEvent } from '@testing-library/react-native';
2
3test('renders and handles interaction', () => {
4 const onPress = jest.fn();
5 render(<EnrollButton courseId="c1" onEnroll={onPress} />);
6
7 const button = screen.getByRole('button', { name: /enroll/i });
8 fireEvent.press(button);
9 expect(onPress).toHaveBeenCalledWith('c1');
10});
11
12test('shows loading state during enrollment', async () => {
13 render(<EnrollButton courseId="c1" isLoading onEnroll={jest.fn()} />);
14 expect(screen.getByRole('progressbar')).toBeTruthy();
15 expect(screen.getByRole('button')).toBeDisabled();
16});1import { device, element, by, expect as detoxExpect } from 'detox';
2
3describe('Login E2E', () => {
4 beforeAll(async () => await device.launchApp({ newInstance: true }));
5
6 it('completes login flow', async () => {
7 await element(by.id('email-input')).typeText('[email protected]');
8 await element(by.id('password-input')).typeText('Pass123!');
9 await element(by.id('password-input')).tapReturnKey();
10 await detoxExpect(element(by.id('dashboard-screen'))).toBeVisible();
11 });
12
13 it('shows error on invalid credentials', async () => {
14 await device.reloadReactNative();
15 await element(by.id('email-input')).typeText('[email protected]');
16 await element(by.id('password-input')).typeText('WrongPass');
17 await element(by.id('login-button')).tap();
18 await detoxExpect(element(by.id('error-message'))).toBeVisible();
19 });
20});1// Test orientation changes
2await device.setOrientation('landscape');
3await detoxExpect(element(by.id('courses-list'))).toBeVisible();
4await device.setOrientation('portrait');
5
6// Test biometric authentication
7await device.setBiometricEnrollment(true);
8await element(by.id('use-face-id-button')).tap();
9await device.matchFace();
10await detoxExpect(element(by.id('dashboard-screen'))).toBeVisible();
11
12// Take screenshot for visual comparison
13await device.takeScreenshot('login-screen');
14
15// Test with slow network (3G simulation)
16await device.setStatusBar({ network: 'hsdpa' }); // 3G-ish
17await element(by.id('courses-tab')).tap();
18// Verify loading state appears
19await detoxExpect(element(by.id('loading-spinner'))).toBeVisible();1# GitHub Actions for React Native
2jobs:
3 unit-tests:
4 runs-on: ubuntu-latest
5 steps:
6 - uses: actions/checkout@v4
7 - run: npm ci && npm test -- --ci --coverage
8
9 e2e-ios:
10 runs-on: macos-latest # Required for iOS simulator
11 steps:
12 - uses: actions/checkout@v4
13 - run: npm ci
14 - run: cd ios && pod install
15 - run: npx detox build --configuration ios.sim.debug
16 - run: npx detox test --configuration ios.sim.debug --headless
17 - uses: actions/upload-artifact@v4
18 if: always()
19 with: { name: detox-results, path: artifacts/ }macOS runners cost ~10x Ubuntu runners. Strategy:
Working code for testing navigation flows in react native in a React Native application.
1// Testing navigation flows in React Native with Detox
2import { device, element, by, expect as detoxExpect } from 'detox';
3
4describe('Navigation flow', () => {
5 beforeAll(async () => {
6 await device.launchApp({ newInstance: true });
7 });
8
9 it('navigates through the onboarding flow', async () => {
10 await detoxExpect(element(by.id('onboarding-screen-1'))).toBeVisible();
11 await element(by.id('next-button')).tap();
12 await detoxExpect(element(by.id('onboarding-screen-2'))).toBeVisible();
13 await element(by.id('next-button')).tap();
14 await detoxExpect(element(by.id('onboarding-screen-3'))).toBeVisible();
15 await element(by.id('get-started-button')).tap();
16 await detoxExpect(element(by.id('login-screen'))).toBeVisible();
17 });
18
19 it('bottom tab navigation switches screens', async () => {
20 // Login first...
21 await element(by.id('home-tab')).tap();
22 await detoxExpect(element(by.id('home-screen'))).toBeVisible();
23 await element(by.id('courses-tab')).tap();
24 await detoxExpect(element(by.id('courses-screen'))).toBeVisible();
25 await element(by.id('profile-tab')).tap();
26 await detoxExpect(element(by.id('profile-screen'))).toBeVisible();
27 });
28
29 it('back navigation works correctly', async () => {
30 await element(by.id('courses-tab')).tap();
31 await element(by.id('course-card')).atIndex(0).tap();
32 await detoxExpect(element(by.id('course-detail-screen'))).toBeVisible();
33 await device.pressBack(); // Android
34 // iOS: await element(by.id('back-button')).tap();
35 await detoxExpect(element(by.id('courses-screen'))).toBeVisible();
36 });
37});This implementation demonstrates testing navigation flows in react native following React Native testing best practices. Unit tests with @testing-library/react-native run fast in CI without a device. Detox E2E tests run on simulators/emulators for user flow verification. The combination provides coverage at all layers while keeping CI costs manageable.
Continue with the next mobile testing topic to build comprehensive coverage for your React Native application.
Your progress is saved in this browser, on this device only.