Ledger Wallet Testing Skill
Jest + MSW + React Testing Library (Desktop) / React Native Testing Library (Mobile) ❌ E2E tests → Use
e2eskill
Golden Rules
toBeVisible()overtoBeInTheDocument()— Always.toBeInTheDocumentonly checks DOM presence; elements can be hidden. UsetoBeInTheDocumentonly when testing explicitly hidden elements.- Search before you create — Before writing any mock, fixture, or helper,
rgthe codebase. If it exists, import it. If your new mock is reusable (2+ files), put it in a shared location. - Mock external deps only — Never mock child components. Test integration.
- Network requests via MSW, never
fetch— New test code must not mockfetch; use Mock Service Worker handlers instead. - One behavior per test — Name:
it("should <behavior> when <condition>"). - Query priority:
getByRole>getByLabelText>getByText>getByTestId(last resort). - Feature flags via store, never mocked — Use
overriddenFeatureFlagsininitialState.settings. - 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
bashpnpm 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:<name> test # Unit tests pnpm coin:<name> test "filename" # Specific file pnpm coin:<name> 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
typescriptimport { 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(<MyComponent title="Test" />); expect(screen.getByText("Test")).toBeVisible(); }); it("should call onClick when button pressed", async () => { const onClick = jest.fn(); const { user } = render(<MyComponent onClick={onClick} />); 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(<MyComponent />); await waitFor(() => expect(screen.getByText("Bitcoin")).toBeVisible()); }); });
Testing Hooks
typescriptimport { 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:
typescriptrender(<MyComponent />, { initialState: { settings: { theme: "dark" }, accounts: { active: [mockAccount] }, }, });
Mobile — function receiving default state:
typescriptrender(<MyScreen />, { overrideInitialState: (state: State) => ({ ...state, settings: { ...state.settings, blacklistedTokenIds: ["ethereum/erc20/usdt"] }, }), });
Feature flags (both platforms):
typescriptrender(<MyComponent />, { 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.


