◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-03-14Advanced

Rork Max App Testing Guide — Jest, Detox E2E, and Quality Assurance

Add production-grade testing to Rork Max generated apps. Covers Jest/React Native Testing Library for unit tests, Detox for E2E tests, snapshot testing, and CI automation on GitHub Actions.

Rork585testing5JestDetoxE2EReact Native239

Setup and context: Why Testing Matters for Rork Max Apps

Rork Max accelerates mobile app development by generating React Native code from AI prompts and visual builders. However, AI-generated code introduces unique testing challenges: auto-generated components may have subtle bugs, dependencies shift versions rapidly, and integration points require careful validation. Production-grade apps demand comprehensive testing, even when generated.

The testing pyramid for mobile apps follows three layers:

  1. Unit Tests (70%) — Test individual components, hooks, and utilities in isolation
  2. Integration Tests (20%) — Test multiple components working together, API calls
  3. E2E Tests (10%) — Test complete user flows on real/simulated devices
ℹ️
Rork Max projects come pre-configured with Expo and Jest. This guide assumes you're starting from a generated Rork Max app structure (Expo managed workflow).

Part 1: Jest Setup for Unit Testing

Install Testing Dependencies

Your Rork Max project likely has Jest already, but ensure you have all necessary libraries:

npm install --save-dev @testing-library/react-native @testing-library/jest-native jest-expo babel-jest

Or with Yarn:

yarn add --dev @testing-library/react-native @testing-library/jest-native jest-expo babel-jest

Configure jest.config.js

Create or update jest.config.js in your project root:

module.exports = {
  preset: 'jest-expo',
  testEnvironment: 'node',
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  testPathIgnorePatterns: ['/node_modules/', '/android/', '/ios/'],
  collectCoverageFrom: [
    'app/**/*.{ts,tsx}',
    '!app/**/*.d.ts',
    '!app/**/index.ts',
  ],
  coverageThreshold: {
    global: {
      branches: 60,
      functions: 60,
      lines: 60,
      statements: 60,
    },
  },
};

Create jest.setup.js

import '@testing-library/jest-native/extend-expect';

Run Your First Test

Create a simple test file app/__tests__/App.test.tsx:

import React from 'react';
import { render, screen } from '@testing-library/react-native';
import App from '../App';
 
describe('App Component', () => {
  it('renders welcome message', () => {
    render(<App />);
    expect(screen.getByText(/Welcome/i)).toBeTruthy();
  });
});

Run tests:

npm test
⚠️
If you see "Cannot find module 'react-native'" errors, ensure `jest-expo` is the preset in jest.config.js. The `jest-expo` preset handles React Native mocking automatically.

Part 2: Component Unit Testing

Testing a Button Component

Most Rork Max apps include button components. Let's test a custom button:

// app/components/Button.tsx
import React from 'react';
import { TouchableOpacity, Text } from 'react-native';
 
interface ButtonProps {
  title: string;
  onPress: () => void;
  disabled?: boolean;
}
 
export const Button: React.FC<ButtonProps> = ({ title, onPress, disabled }) => (
  <TouchableOpacity onPress={onPress} disabled={disabled}>
    <Text>{title}</Text>
  </TouchableOpacity>
);

Now test it:

// app/__tests__/Button.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react-native';
import { Button } from '../components/Button';
 
describe('Button Component', () => {
  it('renders with correct title', () => {
    render(<Button title="Press me" onPress={jest.fn()} />);
    expect(screen.getByText('Press me')).toBeTruthy();
  });
 
  it('calls onPress when pressed', () => {
    const mockPress = jest.fn();
    render(<Button title="Press me" onPress={mockPress} />);
 
    const button = screen.getByRole('button', { name: /press me/i });
    fireEvent.press(button);
 
    expect(mockPress).toHaveBeenCalledTimes(1);
  });
 
  it('does not call onPress when disabled', () => {
    const mockPress = jest.fn();
    render(
      <Button title="Press me" onPress={mockPress} disabled={true} />
    );
 
    const button = screen.getByRole('button');
    fireEvent.press(button);
 
    expect(mockPress).not.toHaveBeenCalled();
  });
});

Testing Forms

Forms are critical in apps. Test form submission:

// app/__tests__/LoginForm.test.tsx
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react-native';
import { LoginForm } from '../components/LoginForm';
 
describe('LoginForm Component', () => {
  it('submits form with email and password', async () => {
    const mockSubmit = jest.fn();
    render(<LoginForm onSubmit={mockSubmit} />);
 
    const emailInput = screen.getByPlaceholderText('Email');
    const passwordInput = screen.getByPlaceholderText('Password');
    const submitButton = screen.getByText('Sign In');
 
    fireEvent.changeText(emailInput, 'user@example.com');
    fireEvent.changeText(passwordInput, 'password123');
    fireEvent.press(submitButton);
 
    await waitFor(() => {
      expect(mockSubmit).toHaveBeenCalledWith({
        email: 'user@example.com',
        password: 'password123',
      });
    });
  });
 
  it('shows error when email is invalid', async () => {
    render(<LoginForm onSubmit={jest.fn()} />);
 
    const emailInput = screen.getByPlaceholderText('Email');
    fireEvent.changeText(emailInput, 'invalid-email');
 
    await waitFor(() => {
      expect(screen.getByText(/invalid email/i)).toBeTruthy();
    });
  });
});

Part 3: Hook Testing with renderHook

Custom hooks are common in React Native apps. Test them with renderHook:

// app/hooks/useAuth.ts
import { useState, useCallback } from 'react';
 
export const useAuth = () => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);
 
  const login = useCallback(async (email: string, password: string) => {
    setLoading(true);
    try {
      // API call
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ email, password }),
      });
      const data = await response.json();
      setUser(data.user);
    } finally {
      setLoading(false);
    }
  }, []);
 
  const logout = useCallback(() => {
    setUser(null);
  }, []);
 
  return { user, loading, login, logout };
};

Test the hook:

// app/__tests__/useAuth.test.ts
import { renderHook, act, waitFor } from '@testing-library/react-native';
import { useAuth } from '../hooks/useAuth';
 
describe('useAuth Hook', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });
 
  it('initializes with null user and false loading', () => {
    const { result } = renderHook(() => useAuth());
 
    expect(result.current.user).toBeNull();
    expect(result.current.loading).toBe(false);
  });
 
  it('sets loading to true during login', async () => {
    const { result } = renderHook(() => useAuth());
 
    act(() => {
      result.current.login('user@example.com', 'password123');
    });
 
    expect(result.current.loading).toBe(true);
  });
 
  it('updates user after successful login', async () => {
    const mockUser = { id: '123', email: 'user@example.com' };
 
    global.fetch = jest.fn(() =>
      Promise.resolve({
        json: () => Promise.resolve({ user: mockUser }),
      })
    ) as jest.Mock;
 
    const { result } = renderHook(() => useAuth());
 
    await act(async () => {
      await result.current.login('user@example.com', 'password123');
    });
 
    await waitFor(() => {
      expect(result.current.user).toEqual(mockUser);
      expect(result.current.loading).toBe(false);
    });
  });
 
  it('clears user on logout', () => {
    const { result } = renderHook(() => useAuth());
 
    act(() => {
      result.current.logout();
    });
 
    expect(result.current.user).toBeNull();
  });
});

Part 4: API and Service Mocking

Mocking external services (like Supabase) is essential. Use jest.mock():

// app/services/supabaseClient.ts
import { createClient } from '@supabase/supabase-js';
 
export const supabase = createClient(
  process.env.EXPO_PUBLIC_SUPABASE_URL!,
  process.env.EXPO_PUBLIC_SUPABASE_KEY!
);

Mock it in tests:

// app/__tests__/supabaseService.test.ts
import { supabase } from '../services/supabaseClient';
 
jest.mock('../services/supabaseClient', () => ({
  supabase: {
    from: jest.fn(),
  },
}));
 
describe('Supabase Service', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });
 
  it('fetches posts successfully', async () => {
    const mockPosts = [
      { id: 1, title: 'Post 1' },
      { id: 2, title: 'Post 2' },
    ];
 
    (supabase.from as jest.Mock).mockReturnValue({
      select: jest.fn().mockResolvedValue({
        data: mockPosts,
        error: null,
      }),
    });
 
    const { data, error } = await supabase
      .from('posts')
      .select('*');
 
    expect(data).toEqual(mockPosts);
    expect(error).toBeNull();
  });
 
  it('handles errors gracefully', async () => {
    const mockError = { message: 'Network error' };
 
    (supabase.from as jest.Mock).mockReturnValue({
      select: jest.fn().mockResolvedValue({
        data: null,
        error: mockError,
      }),
    });
 
    const { data, error } = await supabase
      .from('posts')
      .select('*');
 
    expect(data).toBeNull();
    expect(error).toEqual(mockError);
  });
});
ℹ️
Always mock external services to avoid network calls during tests. Use `jest.clearAllMocks()` in `beforeEach` to reset mock state between tests.

Part 5: Snapshot Testing

Snapshot tests capture component output and detect regressions:

// app/__tests__/ProfileCard.snapshot.test.tsx
import React from 'react';
import { render } from '@testing-library/react-native';
import { ProfileCard } from '../components/ProfileCard';
 
describe('ProfileCard Snapshot', () => {
  it('renders correctly', () => {
    const tree = render(
      <ProfileCard
        name="John Doe"
        bio="Developer"
        avatar="https://example.com/avatar.jpg"
      />
    ).toJSON();
 
    expect(tree).toMatchSnapshot();
  });
});

When you run npm test -- -u, Jest saves a snapshot file (ProfileCard.snapshot.test.tsx.snap). On subsequent runs, Jest compares output to the snapshot.

When to use snapshots:

  • ✅ UI components with stable structure
  • ✅ Regression detection for design changes

When to avoid:

  • ❌ Dynamic content (timestamps, IDs)
  • ❌ Frequently changing UI

Part 6: Detox E2E Setup

Detox tests apps end-to-end on real devices or simulators. Install:

npm install --save-dev detox-cli detox detox-build-configuration

Configure .detoxrc.js

Create .detoxrc.js in your project root:

module.exports = {
  testRunner: 'jest',
  apps: {
    ios: {
      type: 'ios.app',
      binaryPath: 'artifacts/build/Build/Products/Release-iphonesimulator/YourApp.app',
      build: 'xcodebuild -workspace ios/YourApp.xcworkspace -scheme YourApp -configuration Release -sdk iphonesimulator -derivedDataPath artifacts/build',
    },
  },
  configurations: {
    'ios.sim.debug': {
      device: {
        type: 'iPhone 14',
      },
      app: 'ios',
    },
  },
  testRunner: 'jest',
};

Build the App

detox build-framework-cache
detox build-app --configuration ios.sim.debug

Run Your First E2E Test

Create e2e/firstTest.e2e.js:

describe('First E2E Test', () => {
  beforeAll(async () => {
    await device.launchApp();
  });
 
  beforeEach(async () => {
    await device.reloadReactNative();
  });
 
  it('should display welcome screen', async () => {
    await expect(element(by.text('Welcome'))).toBeVisible();
  });
});

Run the test:

detox test e2e/firstTest.e2e.js --configuration ios.sim.debug
⚠️
Detox requires Xcode and iOS simulators on macOS. For Android testing, use `android.emu.debug` configuration. Detox setup can be complex; refer to the official Detox docs for platform-specific issues.

Part 7: Detox E2E Test Examples

Login Flow Test

// e2e/loginFlow.e2e.js
describe('Login Flow', () => {
  beforeAll(async () => {
    await device.launchApp();
  });
 
  beforeEach(async () => {
    await device.reloadReactNative();
  });
 
  it('should successfully log in with valid credentials', async () => {
    // Navigate to login screen
    await element(by.id('loginButton')).multiTap();
 
    // Enter email
    await element(by.id('emailInput')).typeText('user@example.com');
 
    // Enter password
    await element(by.id('passwordInput')).typeText('password123');
 
    // Tap sign in button
    await element(by.id('signInButton')).multiTap();
 
    // Verify successful login by checking for dashboard
    await waitFor(element(by.text('Dashboard')))
      .toBeVisible()
      .withTimeout(5000);
  });
 
  it('should show error with invalid email', async () => {
    await element(by.id('loginButton')).multiTap();
    await element(by.id('emailInput')).typeText('invalid-email');
    await element(by.id('signInButton')).multiTap();
 
    await expect(element(by.text('Invalid email'))).toBeVisible();
  });
});

Navigation Flow Test

// e2e/navigation.e2e.js
describe('Navigation Flow', () => {
  beforeAll(async () => {
    await device.launchApp();
  });
 
  it('should navigate between tabs', async () => {
    // Start on home tab
    await expect(element(by.text('Home'))).toBeVisible();
 
    // Tap profile tab
    await element(by.id('profileTab')).multiTap();
    await expect(element(by.text('Profile'))).toBeVisible();
 
    // Tap settings tab
    await element(by.id('settingsTab')).multiTap();
    await expect(element(by.text('Settings'))).toBeVisible();
 
    // Navigate back to home
    await element(by.id('homeTab')).multiTap();
    await expect(element(by.text('Home'))).toBeVisible();
  });
});

Form Submission Test

// e2e/formSubmission.e2e.js
describe('Form Submission', () => {
  beforeAll(async () => {
    await device.launchApp();
  });
 
  it('should submit form with all fields filled', async () => {
    // Navigate to form
    await element(by.id('openFormButton')).multiTap();
 
    // Fill form fields
    await element(by.id('nameInput')).typeText('John Doe');
    await element(by.id('emailInput')).typeText('john@example.com');
    await element(by.id('messageInput')).typeText('Hello World');
 
    // Submit
    await element(by.id('submitButton')).multiTap();
 
    // Verify success message
    await waitFor(element(by.text('Form submitted successfully')))
      .toBeVisible()
      .withTimeout(5000);
  });
 
  it('should show validation error if required field is empty', async () => {
    await element(by.id('openFormButton')).multiTap();
    await element(by.id('emailInput')).typeText('john@example.com');
    await element(by.id('submitButton')).multiTap();
 
    await expect(element(by.text('Name is required'))).toBeVisible();
  });
});

Part 8: Coverage Reporting

Generate Coverage Reports

Run Jest with coverage:

npm test -- --coverage

This generates coverage reports in coverage/ directory. Coverage metrics include:

  • Statements: Percentage of statements executed
  • Branches: Percentage of conditional branches tested
  • Functions: Percentage of functions called
  • Lines: Percentage of lines executed

Set Coverage Thresholds

In jest.config.js, set minimum thresholds:

coverageThreshold: {
  global: {
    branches: 70,
    functions: 75,
    lines: 80,
    statements: 80,
  },
  './app/services/': {
    branches: 90,
    functions: 90,
    lines: 90,
    statements: 90,
  },
};

If coverage falls below thresholds, Jest exits with code 1, failing the build.

View Coverage Report in Browser

After running coverage, open coverage/lcov-report/index.html in a browser to see detailed, interactive coverage reports.


Part 9: GitHub Actions CI Integration

Create .github/workflows/test.yml

name: Jest Tests
 
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]
 
jobs:
  test:
    runs-on: ubuntu-latest
 
    strategy:
      matrix:
        node-version: [18.x, 20.x]
 
    steps:
      - uses: actions/checkout@v4
 
      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
 
      - name: Install dependencies
        run: npm ci
 
      - name: Run tests
        run: npm test -- --coverage
 
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info
          flags: unittests
          name: codecov-umbrella
          fail_ci_if_error: false

Run on Every PR

The workflow automatically runs on pull requests. If tests fail, the PR shows a red X. If tests pass, it shows a green checkmark.

ℹ️
Add a badge to your README to show test status:
![Tests](https://github.com/YOUR_ORG/YOUR_REPO/workflows/Jest%20Tests/badge.svg)
```</div></div>
 
---
 
## Part 10: E2E Tests in CI
 
Running Detox in CI requires Android/iOS build tools. Here's a minimal example for iOS:
 
```yaml
name: Detox E2E Tests
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
jobs:
  test:
    runs-on: macos-latest
 
    steps:
      - uses: actions/checkout@v4
 
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'
          cache: 'npm'
 
      - name: Install dependencies
        run: npm ci
 
      - name: Build Detox
        run: |
          detox build-framework-cache
          detox build-app --configuration ios.sim.debug
 
      - name: Run E2E tests
        run: detox test e2e --configuration ios.sim.debug --cleanup
⚠️
macOS runners cost more than Linux. Consider running E2E tests only on main branch, not every PR, to reduce CI costs.

Summary: A Winning Testing Strategy for Rork Max

To ensure your Rork Max app is production-ready:

  1. Unit Tests (Jest): Cover components, hooks, and utilities. Aim for 80%+ line coverage.
  2. Component Tests: Test user interactions (button clicks, form submissions).
  3. Mocking: Mock external APIs and services to isolate logic.
  4. E2E Tests (Detox): Test critical user flows (login, payments, core features).
  5. CI/CD: Automate testing on every PR with GitHub Actions.
  6. Coverage Reports: Track and enforce coverage thresholds.

AI-generated code isn't perfect, but comprehensive testing catches issues early. Start with Jest unit tests, add E2E tests for critical paths, and iterate.

Next steps:

  • Set up Jest in your Rork Max project
  • Write tests for your most critical components
  • Integrate GitHub Actions for continuous testing
  • Gradually increase coverage as your app grows

Happy testing!

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ Dev Tools2026-09-04
EAS secret visibility does not keep a value out of your app — deciding prefix and visibility separately
The EXPO_PUBLIC_ prefix decides what ships inside your app; EAS visibility decides who can read it. Why stacking them blanks a value on OTA updates, and how to check your build.
⬡ Dev Tools2026-08-22
Every bulk replace exited zero. The damage was in the lines I did not delete
Run a bulk replace over generated code and the breakage lands on the neighbouring lines, not the matched ones. Here is what broke in a live project, and a dependency-free guard that checks the invariants a replace must preserve.
⬡ Dev Tools2026-08-14
Find the native edits expo prebuild will erase before you upgrade to SDK 57
Expo SDK 57 makes expo prebuild clear and regenerate ios and android by default. Here is how to audit your hand edits first, move them into config plugins, and why 57.0.9 matters for Reanimated apps.