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)

Requirements Traceability Matrix

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 describe with the US ID
  • Test success, failure for every story
  • queryBy for elements that may be absent
  • Mock service calls with jest.spyOn
    • we should not hit real Supabase in tests
  • No test.only or describe.only in 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