--- name: testing description: | Write unit and integration tests for Ledger Wallet apps. Use for Jest tests (Desktop/Mobile), MSW handlers and testing best practices. Applies to "*.test.*", "*.spec.*", "**/tests/**", "**/__tests__/**", "**/__integrations__/**", "**/jest-setup*" --- # Ledger Wallet Testing Skill > Jest + MSW + React Testing Library (Desktop) / React Native Testing Library (Mobile) > ❌ E2E tests → Use `e2e` skill --- ## Golden Rules 1. **`toBeVisible()` over `toBeInTheDocument()`** — Always. `toBeInTheDocument` only checks DOM presence; elements can be hidden. Use `toBeInTheDocument` only when testing explicitly hidden elements. 2. **Search before you create** — Before writing any mock, fixture, or helper, `rg` the codebase. If it exists, import it. If your new mock is reusable (2+ files), put it in a shared location. 3. **Mock external deps only** — Never mock child components. Test integration. 4. **One behavior per test** — Name: `it("should when ")`. 5. **Query priority**: `getByRole` > `getByLabelText` > `getByText` > `getByTestId` (last resort). 6. **Feature flags via store, never mocked** — Use `overriddenFeatureFlags` in `initialState.settings`. 7. **Use existing factories** — `genAccount()` from `@ledgerhq/ledger-wallet-framework/mocks/account`, `getCryptoCurrencyById()` from `@ledgerhq/live-common/currencies`. Never recreate account/currency data from scratch. --- ## Quick Reference ```bash pnpm test:jest "filename" # Run specific file pnpm test:jest # Run all tests pnpm test:jest --coverage # Coverage report # Coin modules (libs/coin-modules) pnpm coin: test # Unit tests pnpm coin: test "filename" # Specific file pnpm coin: test-integ # Integration tests (slow) ``` | Platform | Render Import | MSW Server | | -------- | ---------------------- | --------------------- | | Desktop | `tests/testSetup` | `tests/server.ts` | | Mobile | `@tests/test-renderer` | `__tests__/server.ts` | --- ## Test Template ```typescript import { render, screen, waitFor } from "tests/testSetup"; import { server } from "tests/server"; import { http, HttpResponse } from "msw"; jest.mock("@ledgerhq/live-common/someService"); describe("MyComponent", () => { beforeEach(() => jest.clearAllMocks()); it("should render title", () => { render(); expect(screen.getByText("Test")).toBeVisible(); }); it("should call onClick when button pressed", async () => { const onClick = jest.fn(); const { user } = render(); await user.click(screen.getByRole("button")); expect(onClick).toHaveBeenCalledTimes(1); }); it("should display fetched data", async () => { server.use(http.get("/api/data", () => HttpResponse.json({ name: "Bitcoin" }))); render(); await waitFor(() => expect(screen.getByText("Bitcoin")).toBeVisible()); }); }); ``` --- ## Testing Hooks ```typescript import { renderHook, act, waitFor } from "tests/testSetup"; const { result } = renderHook(() => useMyHook()); await act(async () => result.current.increment()); expect(result.current.value).toBe(1); ``` --- ## Redux Store **Desktop** — plain object merged with defaults: ```typescript render(, { initialState: { settings: { theme: "dark" }, accounts: { active: [mockAccount] }, }, }); ``` **Mobile** — function receiving default state: ```typescript render(, { overrideInitialState: (state: State) => ({ ...state, settings: { ...state.settings, blacklistedTokenIds: ["ethereum/erc20/usdt"] }, }), }); ``` **Feature flags** (both platforms): ```typescript render(, { initialState: { settings: { overriddenFeatureFlags: { myFlag: { enabled: true, params: { key: "value" } }, }, }, }, }); ``` --- ## Mocking Patterns ```typescript // External dependency jest.mock("@ledgerhq/live-common/account/index"); const mockedFn = jest.mocked(getMainAccount); mockedFn.mockReturnValue(mockAccount); // ViewModel (MVVM) jest.mock("../useMyViewModel", () => ({ useMyViewModel: jest.fn() })); jest.spyOn(UseMyViewModel, "useMyViewModel").mockImplementation(() => ({ data: mockData, isLoading: false, })); // MSW override for error case server.use(http.get("/api/data", () => HttpResponse.json({ error: "Not found" }, { status: 404 }))); ``` --- ## Shared Resources — Search Here First Before creating anything, check these locations: | Location | What's there | | ------------------------------------------------- | --------------------------------------------------------- | | `@ledgerhq/ledger-wallet-framework/mocks/account` | `genAccount()`, `genTokenAccount()` | | `@ledgerhq/live-common/currencies` | `getCryptoCurrencyById()`, `getTokenById()` | | `tests/handlers/` | MSW handlers (market, assets, countervalues, cryptoIcons) | | `tests/handlers/fixtures/` | JSON fixtures for API responses | | `tests/fixtures/` | `mockedAccountList`, `expectedCurrencyList` | | `tests/mocks/` | Mock components, assets, countervalues | ```bash # Run BEFORE writing new mocks rg "yourMockName|yourHelper" tests/ __tests__/ src/ libs/ --type ts ``` New reusable mocks go in `tests/mocks/`, new handlers in `tests/handlers/`, new fixtures in `tests/handlers/fixtures/`. --- ## Mock Anti-Patterns These cause flaky tests and mock cannibalization when running with parallel workers (`--maxWorkers=50%`). **Duplicate mocks** — Don't `jest.mock("module")` in a test file when it's already mocked in jest-setup (`apps/ledger-live-mobile/__tests__/jest-setup.js` or `apps/ledger-live-desktop/tests/jestSetup.js`). Use `jest.mocked(module.export).mockReturnValue(...)` in `beforeEach` instead. **Hooks at describe load time** — Never call `renderHook(...)` or access `.result.current` at the top level of a `describe` block. Move into `beforeEach` or inside each test — calling hooks outside of test callbacks crashes Jest parallel workers. **`jest.restoreAllMocks()`** — Never use this; it restores global mocks from jest-setup and breaks other tests. Use `jest.clearAllMocks()` or `mock.mockRestore()` for specific spies only. **Wrong `beforeEach` order** — Call `jest.clearAllMocks()` _before_ `mockReturnValue()` or other mock configuration — not after (calling clear after wipes the setup). --- ## Workflow ``` 1. Search for existing mocks → 2. Write test → 3. Run test → 4. Fix if fail → 5. Next file ``` Start with the simplest units (utils, hooks), then work up to components with side effects (API, navigation). This order catches foundational bugs early and builds confidence incrementally.