Component testing
Same testing philosophy on both platforms; the runner and rendering harness
differ. Read the shared conventions, then the section for the lib you're in
(derive it from the path — see the Libraries table in AGENTS.md).
Shared conventions (both platforms)
- Import test globals explicitly — do not rely on ambient globals.
- Prefer callback/handler spies for interactions and assert with
toHaveBeenCalledTimes, toHaveBeenCalledWith, not.toHaveBeenCalled.
- Cover the behaviour, not the implementation: rendering, interaction,
controlled state, callbacks, and the disabled state.
- Structure: a single top-level
describe('<ComponentName>'). Use flat
it('should …') cases for simple components; nest describe blocks by concern
(Rendering, Appearances, Sizes, Interactions, States, Styling) for
richer components, and it.each([...]) for variant/size matrices.
- No snapshot tests unless there's a clear reason.
React web (libs/ui-react)
Runner: Vitest + React Testing Library.
- Globals:
import { describe, it, expect, vi } from 'vitest'; and
import '@testing-library/jest-dom';.
- Render:
import { render, screen, fireEvent } from '@testing-library/react'; —
render directly with render(<Component />), no wrapper.
- Queries: prefer accessible ones (
getByRole, getByLabelText, getByText);
data-testid only when nothing accessible applies.
- Interactions:
fireEvent for simple click/change; userEvent from
@testing-library/user-event for typing / keyboard / focus.
- Spies:
vi.fn().
React Native (libs/ui-rnative)
Runner: Jest + React Native Testing Library.
- Globals:
import { describe, it, expect, jest } from '@jest/globals';.
- Render:
render, fireEvent, screen, waitFor from
@testing-library/react-native. Every render must be wrapped in a
ThemeProvider — components read theme tokens, so an unwrapped render
throws. Use a small TestWrapper:
import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
<ThemeProvider themes={ledgerLiveThemes} colorScheme='dark' locale='en'>
{children}
</ThemeProvider>
);
- Queries: target root elements via
testID; text when testID doesn't fit;
getByRole for semantic elements like switches.
- Interactions:
fireEvent.press, fireEvent.changeText(input, 'text'), and the
low-level fireEvent(element, '<eventName>') form for non-press events
(longPress, onError). Use waitFor for state that updates after an
interaction.
- Spies:
jest.fn().
Review checks
Rules verifiable from a diff.
| Check |
Applies to |
Detect |
Skip |
| Wrong runner API for the lib |
both |
jest.fn()/@jest/globals in a ui-react test; vi.fn()/vitest in a ui-rnative test |
— |
RN render not wrapped in ThemeProvider |
rnative |
render(<…/>) without a themed wrapper |
— |
data-testid/testID used where an accessible query fits |
both |
getByTestId on a role-bearing element |
root nodes with no accessible role |
| Snapshot test with no justification |
both |
toMatchSnapshot() |
intentional, commented snapshots |
| Missing coverage of the disabled/controlled path on an interactive component |
both |
no test touching disabled/controlled state |
non-interactive components |
1---2name: component-testing3description: Use when writing or editing component tests in libs/ui-react or libs/ui-rnative (and their visualization libs) — shared structure and coverage conventions, plus the per-platform runner: Vitest + React Testing Library on web, Jest + React Native Testing Library on RN. Load this before writing tests.4---56# Component testing78Same testing philosophy on both platforms; the runner and rendering harness9differ. Read the shared conventions, then the section for the lib you're in10(derive it from the path — see the `Libraries` table in `AGENTS.md`).1112## Shared conventions (both platforms)1314- **Import test globals explicitly** — do not rely on ambient globals.15- **Prefer callback/handler spies** for interactions and assert with16 `toHaveBeenCalledTimes`, `toHaveBeenCalledWith`, `not.toHaveBeenCalled`.17- **Cover the behaviour, not the implementation**: rendering, interaction,18 controlled state, callbacks, and the disabled state.19- **Structure**: a single top-level `describe('<ComponentName>')`. Use flat20 `it('should …')` cases for simple components; nest `describe` blocks by concern21 (`Rendering`, `Appearances`, `Sizes`, `Interactions`, `States`, `Styling`) for22 richer components, and `it.each([...])` for variant/size matrices.23- **No snapshot tests** unless there's a clear reason.2425## React web (`libs/ui-react`)2627Runner: **Vitest** + React Testing Library.2829- Globals: `import { describe, it, expect, vi } from 'vitest';` and30 `import '@testing-library/jest-dom';`.31- Render: `import { render, screen, fireEvent } from '@testing-library/react';` —32 render directly with `render(<Component />)`, **no wrapper**.33- Queries: prefer accessible ones (`getByRole`, `getByLabelText`, `getByText`);34 `data-testid` only when nothing accessible applies.35- Interactions: `fireEvent` for simple click/change; `userEvent` from36 `@testing-library/user-event` for typing / keyboard / focus.37- Spies: `vi.fn()`.3839## React Native (`libs/ui-rnative`)4041Runner: **Jest** + React Native Testing Library.4243- Globals: `import { describe, it, expect, jest } from '@jest/globals';`.44- Render: `render`, `fireEvent`, `screen`, `waitFor` from45 `@testing-library/react-native`. **Every render must be wrapped in a46 `ThemeProvider`** — components read theme tokens, so an unwrapped render47 throws. Use a small `TestWrapper`:4849```tsx50import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';51import { render, fireEvent, waitFor } from '@testing-library/react-native';52import { ThemeProvider } from '../ThemeProvider/ThemeProvider';5354const TestWrapper = ({ children }: { children: React.ReactNode }) => (55 <ThemeProvider themes={ledgerLiveThemes} colorScheme='dark' locale='en'>56 {children}57 </ThemeProvider>58);59```6061- Queries: target root elements via `testID`; text when `testID` doesn't fit;62 `getByRole` for semantic elements like switches.63- Interactions: `fireEvent.press`, `fireEvent.changeText(input, 'text')`, and the64 low-level `fireEvent(element, '<eventName>')` form for non-press events65 (`longPress`, `onError`). Use `waitFor` for state that updates after an66 interaction.67- Spies: `jest.fn()`.6869## Review checks7071Rules verifiable from a diff.7273| Check | Applies to | Detect | Skip |74| --- | --- | --- | --- |75| Wrong runner API for the lib | both | `jest.fn()`/`@jest/globals` in a `ui-react` test; `vi.fn()`/`vitest` in a `ui-rnative` test | — |76| RN render not wrapped in `ThemeProvider` | rnative | `render(<…/>)` without a themed wrapper | — |77| `data-testid`/`testID` used where an accessible query fits | both | `getByTestId` on a role-bearing element | root nodes with no accessible role |78| Snapshot test with no justification | both | `toMatchSnapshot()` | intentional, commented snapshots |79| Missing coverage of the disabled/controlled path on an interactive component | both | no test touching `disabled`/controlled state | non-interactive components |