RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
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.

Rork515testing5JestDetoxE2EReact Native209

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-07-15
The Comma That Fell to the Start of a Line: Rork, Quote Cards, and Japanese Line Breaking
React Native's Text component does not guarantee Japanese line-breaking rules. Here are the violation rates I measured, a WORD JOINER implementation that survives copy and VoiceOver, an onTextLayout audit harness, and how Rork Max (SwiftUI) differs.
Dev Tools2026-07-15
My Rork sleep timer faded out on time — but the sound didn't
Rork writes sleep-timer fades against the wall clock with setInterval. The numbers say 30 minutes, but the real audio fades early or cuts out abruptly. Here is how to drive the fade from the actual playback position, why a logarithmic curve sounds natural, and the honest limit of JS fades when the screen is locked.
Dev Tools2026-07-14
Designing Seams That Survive AI Regeneration in Rork
Every follow-up prompt to Rork can quietly wipe out logic you wrote by hand. Protecting it with prompts is a patch, not a fix. Here is how to separate generated code from code you own, and draw a boundary that regeneration cannot reach, with working Zustand and service-layer examples.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →