From harness-claude
Tests React Native apps with Jest, React Native Testing Library, and Detox for unit, integration, and E2E coverage. Covers mocking native modules, navigation, and async storage.
How this skill is triggered — by the user, by Claude, or both
Slash command
/harness-claude:mobile-testing-patternsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Test React Native apps with Jest, React Native Testing Library, and Detox for unit, integration, and E2E coverage
Test React Native apps with Jest, React Native Testing Library, and Detox for unit, integration, and E2E coverage
npm install -D @testing-library/react-native @testing-library/jest-native
import { render, screen, fireEvent, waitFor } from '@testing-library/react-native';
describe('LoginForm', () => {
it('shows error when email is empty', async () => {
render(<LoginForm onSubmit={jest.fn()} />);
fireEvent.press(screen.getByRole('button', { name: 'Log In' }));
await waitFor(() => {
expect(screen.getByText('Email is required')).toBeOnTheScreen();
});
});
it('calls onSubmit with credentials', async () => {
const onSubmit = jest.fn();
render(<LoginForm onSubmit={onSubmit} />);
fireEvent.changeText(screen.getByLabelText('Email'), '[email protected]');
fireEvent.changeText(screen.getByLabelText('Password'), 'password123');
fireEvent.press(screen.getByRole('button', { name: 'Log In' }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: '[email protected]',
password: 'password123',
});
});
});
});
// Best — queries that reflect accessibility
screen.getByRole('button', { name: 'Submit' });
screen.getByLabelText('Email address');
screen.getByText('Welcome back');
screen.getByPlaceholderText('Search...');
// Acceptable — test IDs for elements without accessible names
screen.getByTestId('avatar-image');
// jest.setup.ts
jest.mock('@react-native-async-storage/async-storage', () =>
require('@react-native-async-storage/async-storage/jest/async-storage-mock')
);
jest.mock('expo-secure-store', () => ({
getItemAsync: jest.fn(),
setItemAsync: jest.fn(),
deleteItemAsync: jest.fn(),
}));
jest.mock('expo-notifications', () => ({
getPermissionsAsync: jest.fn().mockResolvedValue({ status: 'granted' }),
requestPermissionsAsync: jest.fn().mockResolvedValue({ status: 'granted' }),
getExpoPushTokenAsync: jest.fn().mockResolvedValue({ data: 'ExponentPushToken[xxx]' }),
setNotificationHandler: jest.fn(),
}));
renderHook.import { renderHook, waitFor } from '@testing-library/react-native';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
describe('useOrders', () => {
it('returns orders from API', async () => {
fetchMock.mockResponseOnce(JSON.stringify([{ id: '1', total: 99.99 }]));
const { result } = renderHook(() => useOrders(), { wrapper: createWrapper() });
await waitFor(() => {
expect(result.current.data).toHaveLength(1);
expect(result.current.data![0].total).toBe(99.99);
});
});
});
import { NavigationContainer } from '@react-navigation/native';
function renderWithNavigation(component: React.ReactElement) {
return render(
<NavigationContainer>{component}</NavigationContainer>
);
}
it('navigates to detail screen on item press', async () => {
const { getByText } = renderWithNavigation(<OrderListScreen />);
fireEvent.press(getByText('Order #123'));
await waitFor(() => {
expect(getByText('Order Details')).toBeOnTheScreen();
});
});
npm install -D detox
npx detox init
// e2e/login.test.ts
describe('Login Flow', () => {
beforeAll(async () => {
await device.launchApp();
});
it('should login with valid credentials', async () => {
await element(by.label('Email')).typeText('[email protected]');
await element(by.label('Password')).typeText('password123');
await element(by.label('Log In')).tap();
await waitFor(element(by.text('Welcome back')))
.toBeVisible()
.withTimeout(5000);
});
it('should show error for invalid credentials', async () => {
await element(by.label('Email')).typeText('[email protected]');
await element(by.label('Password')).typeText('wrong');
await element(by.label('Log In')).tap();
await expect(element(by.text('Invalid credentials'))).toBeVisible();
});
});
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
const server = setupServer(
http.get(`${API_URL}/orders`, () => {
return HttpResponse.json([{ id: '1', total: 99.99, status: 'delivered' }]);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
src/
components/
OrderCard.tsx
OrderCard.test.tsx
screens/
OrderList.tsx
OrderList.test.tsx
hooks/
useOrders.ts
useOrders.test.ts
e2e/
login.test.ts
checkout.test.ts
Testing pyramid for React Native:
Mocking strategy: Mock at the boundary (native modules, network, storage), not internal implementation. Use MSW for network mocking instead of mocking fetch directly — it works at the network level and catches integration issues.
Snapshot testing: Use sparingly. Snapshots for complex components become unreadable and are approved without review. Prefer specific assertions (expect(screen.getByText('$99.99')).toBeOnTheScreen()) over snapshot matching.
Common mistakes:
getByTestId as the primary query (bypasses accessibility validation)waitFor, findByText)https://callstack.github.io/react-native-testing-library/
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeProvides Jest and React Native Testing Library patterns for testing React Native Web apps, including component testing, utilities, configuration, and best practices.
Writes, reviews, and fixes React Native component tests using @testing-library/react-native v13 (sync) and v14 (async). Covers render, queries, userEvent, fireEvent, waitFor, Jest matchers, and Expo setup.
Guides test-driven development workflow for React Native using Jest, React Native Testing Library, and Detox in Red-Green-Refactor cycle. For new features, bug fixes, refactoring.