Testing
We are using Jest + React Native Testing Library
- All Tests live under fieldnotes-ai-app/__tests__/
Structure
__tests__/
├── acceptance/ # User-story acceptance tests (primary)
├── integration/ # Service-level tests (no real Supabase)
├── unit/ # Pure unit tests (e.g., filter configs)
├── accessibility/ # A11y tests using react-native-accessibility-engine
├── fixtures/ # Shared test data (e.g., emergencyContact.ts)
└── utils/testUtils.tsx # renderWithProviders, mock users, helpers
__mocks__/svgMock.js # SVG file mock for Jest
Running Tests
npm test
npm run test:watch
npm run test:coverage
npm test -- specific-file.test.tsx
npm test -- --clearCache
npm test -- --verbose
Requirements Traceability Matrix
RTM Dashboard (Visual)
The raw traceability data is stored in traceability-matrix.csv.
Writing Acceptance Tests
Label every describe with the user story ID. Test both success and failure.
/**
* US-01.01 — Secure Login as Practitioner
*/
import React from 'react';
import { renderWithProviders, waitFor, fireEvent } from '@/__tests__/utils/testUtils';
import LoginScreen from '@/app/login';
describe('US-01.01: Practitioner Login', () => {
test('redirects to dashboard on valid credentials', async () => {
const { getByPlaceholderText, getByText } = renderWithProviders(<LoginScreen />);
fireEvent.changeText(getByPlaceholderText(/email/i), 'worker@example.com');
fireEvent.changeText(getByPlaceholderText(/password/i), 'correctpassword');
fireEvent.press(getByText(/sign in/i));
await waitFor(() => {
expect(mockRouter.replace).toHaveBeenCalledWith('/(practitioner-tabs)/home');
});
});
test('shows error on invalid credentials', async () => {
const { getByPlaceholderText, getByText, findByText } = renderWithProviders(<LoginScreen />);
fireEvent.changeText(getByPlaceholderText(/email/i), 'wrong@example.com');
fireEvent.changeText(getByPlaceholderText(/password/i), 'wrong');
fireEvent.press(getByText(/sign in/i));
expect(await findByText(/invalid credentials/i)).toBeTruthy();
});
});
Test Utilities
// renderWithProviders options
renderWithProviders(<Screen />); // unauthenticated
renderWithProviders(<Screen />, { authenticated: true, user: mockUser }); // practitioner
renderWithProviders(<Screen />, { authenticated: true, user: mockAdmin }); // admin
// mock objects
export const mockUser = {
id: 'practitioner-uuid',
email: 'practitioner@example.com',
user_metadata: { role: 'practitioner' },
};
export const mockAdmin = {
id: 'admin-uuid',
email: 'admin@example.com',
user_metadata: { role: 'admin' },
};
Common Patterns
// async state
await waitFor(() => expect(getByText(/loaded/i)).toBeTruthy());
// loading state
expect(queryByTestId('loading-spinner')).toBeTruthy();
// empty state
jest.spyOn(caseService, 'getCases').mockResolvedValueOnce([]);
expect(await findByText(/no cases/i)).toBeTruthy();
// error state
jest.spyOn(caseService, 'getCases').mockRejectedValueOnce(new Error('fail'));
expect(await findByText(/failed/i)).toBeTruthy();
// SOS (US-04.01)
fireEvent.press(getByTestId('start-recording-button'));
await waitFor(() => expect(getByTestId('sos-button')).toBeTruthy());
fireEvent.press(getByTestId('sos-button'));
await waitFor(() => {
expect(mockRouter.replace).toHaveBeenCalledWith('/(practitioner-tabs)/home');
});
Query / Event Reference
getByText(/text/i) // throws if not found
queryByText(/text/i) // null if not found
findByText(/text/i) // async, waits
getByPlaceholderText(/email/i)
getByTestId('id')
getByRole('button', { name: /submit/i })
fireEvent.press(el)
fireEvent.changeText(el, 'value')
Rules
- Label every
describewith the US ID - Test success, failure for every story
queryByfor elements that may be absent- Mock service calls with
jest.spyOn- we should not hit real Supabase in tests
- No
test.onlyordescribe.onlyin committed code - No order-dependent tests
Coverage Targets
npm run test:coverage
# a HTML report will be generated at coverage/lcov-report/index.html
Debugging
test.only('just this', () => {}); // isolate
test.skip('skip this', () => {});
const { debug } = renderWithProviders(<Screen />);
debug(); // print component tree
await waitFor(() => ..., { timeout: 8000 }); // extend timeout