component-testing
Use when writing or editing component tests in libs/ui-react or libs/ui-rnative — shared structure and coverage conventions, plus the per-platform runner: Vitest + React Testing Library on web, Jest +
SKILL.md.claude/skills/component-testing/SKILL.md
GitHubComponent 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 flatit('should …')cases for simple components; nestdescribeblocks by concern (Rendering,Appearances,Sizes,Interactions,States,Styling) for richer components, andit.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';andimport '@testing-library/jest-dom';. - Render:
import { render, screen, fireEvent } from '@testing-library/react';— render directly withrender(<Component />), no wrapper. - Queries: prefer accessible ones (
getByRole,getByLabelText,getByText);data-testidonly when nothing accessible applies. - Interactions:
fireEventfor simple click/change;userEventfrom@testing-library/user-eventfor 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,waitForfrom@testing-library/react-native. Every render must be wrapped in aThemeProvider— components read theme tokens, so an unwrapped render throws. Use a smallTestWrapper:
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 whentestIDdoesn't fit;getByRolefor semantic elements like switches. - Interactions:
fireEvent.press,fireEvent.changeText(input, 'text'), and the low-levelfireEvent(element, '<eventName>')form for non-press events (longPress,onError). UsewaitForfor 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 |