From harness-claude
Provides patterns for testing React Native apps with Jest, React Native Testing Library, and Detox for unit, integration, and E2E coverage including native mocks and accessible queries.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Test React Native apps with Jest, React Native Testing Library, and Detox for unit, integration, and E2E coverage
Provides Jest and React Native Testing Library patterns for testing React Native Web apps, including component testing, utilities, configuration, and best practices.
Guides Test-Driven Development for React Native using Jest (jest-expo) and @testing-library/react-native. Use before implementing features, bugfixes, or refactors.
Guides Detox E2E testing for React Native apps including iOS/Android configuration, element matchers/actions/assertions, CI/CD integration, and synchronization debugging.
Share bugs, ideas, or general feedback.
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'), 'user@example.com');
fireEvent.changeText(screen.getByLabelText('Password'), 'password123');
fireEvent.press(screen.getByRole('button', { name: 'Log In' }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: 'user@example.com',
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('user@example.com');
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('wrong@example.com');
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/