Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
275 changes: 275 additions & 0 deletions .claude/skills/writing-tests/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
---
name: writing-tests
description: Auto-loads when a session touches a test file under src/__tests__/ or a mock under src/__mocks__/. Encodes the auto-qa testing conventions established by RFC 0001 — pyramid layout, helpers, centralized mocks, smoke-vs-feature-area cadence, Ledger contract boundary, three-contract reducer pattern.
---

# Writing tests for hathor-wallet

This skill loads whenever you're editing or creating a test file in the
desktop wallet repo. It is the long-form companion to the rules in
[`CLAUDE.md`](../../../CLAUDE.md); the rules there are the short-form
authoritative version, and the patterns below are the depth and worked
examples that let you actually apply them.

## Pyramid layout

```
__tests__/
├── utils/ # L1 — pure functions, selectors
├── reducers/ # L1 — reducer three-contracts tests
├── sagas/ # L2 — saga integration (expectSaga + provide())
├── screens/ # L3 — component render + interaction
└── components/ # L3 — non-screen reusable component tests

src/test-utils/ # helpers used by tests (renderWithProviders, etc.)
src/__mocks__/ # manual mocks for npm packages
src/setupTests.js # CRA-conventional global setup; activates the mocks
```
Comment on lines +16 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language identifier to the directory-tree code fence.

This fence is missing a language tag (MD040). Use text to keep markdown lint clean.

Proposed fix
-```
+```text
 __tests__/
 ├── utils/        # L1 — pure functions, selectors
 ├── reducers/     # L1 — reducer three-contracts tests
 ├── sagas/        # L2 — saga integration (expectSaga + provide())
 ├── screens/      # L3 — component render + interaction
 └── components/   # L3 — non-screen reusable component tests
 
 src/test-utils/   # helpers used by tests (renderWithProviders, etc.)
 src/__mocks__/    # manual mocks for npm packages
 src/setupTests.js # CRA-conventional global setup; activates the mocks
</details>

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.22.1)</summary>

[warning] 16-16: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/writing-tests/SKILL.md around lines 16 - 27, The Markdown
code fence containing the directory tree is missing a language identifier;
update the opening fence from totext for the directory-tree block (the
fenced block that starts with "tests/" and lists
utils/reducers/sagas/screens/components and the src/* lines) so the linter MD040
is satisfied and the snippet is treated as plain text.


</details>

<!-- fingerprinting:phantom:poseidon:hawk -->

<!-- This is an auto-generated comment by CodeRabbit -->


Layer 4 (Playwright + Electron E2E) will live under a top-level `e2e/`
directory in PR 2; it is not in scope here.

## Reference smoke vs feature-area distinction

RFC 0001 splits the work into two kinds of PR.

**Reference smoke PRs** land one representative test per technique plus
the shared infrastructure (helpers, mocks, agent docs). PR 1 (the one
that introduced this skill) was the reference smoke for Layers 1–3:
eight tests, one for each of pure function, selector, reducer, saga,
saga with state reset, screen happy, screen error, screen navigation.

**Feature-area PRs** cover one slice of the wallet across all applicable
layers. Examples in flight or planned:

- New wallet creation / backup
- Lock / Unlock
- Send Tokens (incl. fee model)
- Custom Tokens (create / register / admin)
- Token Import
- Nano Contracts
- Reown / WalletConnect
- Ledger / Hardware Wallet (JS-mock layer only)

When you're writing a new test, identify which kind of PR you're in.
Reference-smoke PRs land tiny: one test per technique, sized to be a
template. Feature-area PRs cover a slice: as many tests as the feature
needs.

## Test patterns

The eight reference smoke tests in PR 1 are the canonical examples. Find
the smoke that matches what you're writing and copy its shape. Quick
index:

| Need to test … | Reference example |
|---|---|
| A pure function (no mocks, no state) | [`utils/tokens.test.js`](../../../src/__tests__/utils/tokens.test.js) |
| A function reading from Redux state | [`utils/selectors.test.js`](../../../src/__tests__/utils/selectors.test.js) |
| A reducer (three contracts) | [`reducers/reducer.wallet.test.js`](../../../src/__tests__/reducers/reducer.wallet.test.js) |
| A saga happy path | [`sagas/wallet.test.js`](../../../src/__tests__/sagas/wallet.test.js) |
| A saga with module-level state | [`sagas/modal.test.js`](../../../src/__tests__/sagas/modal.test.js) |
| A screen render (happy path) | [`screens/Welcome.test.jsx`](../../../src/__tests__/screens/Welcome.test.jsx) |
| A screen error / non-happy state | [`screens/SoftwareWalletWarning.test.jsx`](../../../src/__tests__/screens/SoftwareWalletWarning.test.jsx) |
| A screen navigation | [`screens/WalletType.test.jsx`](../../../src/__tests__/screens/WalletType.test.jsx) |

### renderWithProviders

```jsx
import { renderWithProviders } from '../../test-utils';
renderWithProviders(<MyScreen />, {
preloadedState: { /* partial state, merged with getInitialState() */ },
initialEntries: ['/some-route'], // MemoryRouter prop, defaults to ['/']
});
```

Returns the standard `@testing-library/react` render result plus
`{ store }` for tests that need to dispatch or read state mid-test.

### createTestStore

Use when you need the store reference but not a render — e.g., in saga
tests that you drive with `expectSaga`. Configures the production
middlewares (`redux-saga`, `redux-thunk`) and `serializableCheck: false`.

```js
import { createTestStore } from '../../test-utils';
const { store, sagaMiddleware } = createTestStore({ isOnline: true });
```

### mockNavigation

Two styles, depending on whether the test wants the shared spy or its
own:

- Shared spy: `import { mockNavigate, mockNavigationModule } from
'../../test-utils'` then `jest.mock('react-router-dom', () =>
mockNavigationModule())`.
- Test-local spy: declare `const mockNavigate = jest.fn();` (the `mock`
prefix is required for `babel-plugin-jest-hoist` to allow it inside the
`jest.mock` factory), then mock react-router-dom inline.

`WalletType.test.jsx` uses the test-local style; future feature-area PRs
that share a navigation spec across files can switch to the shared form.

## Centralized mocks

The mocks every test would otherwise re-declare live in `src/__mocks__/`
and are activated in `src/setupTests.js`. The list as of PR 1:

- `@hathor/wallet-lib` — dissolves the ESM-only-axios import chain that
Jest's default Babel transform cannot parse. Provides the constants
(NATIVE_TOKEN_UID, DECIMAL_PLACES, TokenVersion, WalletType,
PartialTxProposal, …) the wallet reads at module-load time.
- `@hathor/hathor-rpc-handler` — same import-chain concern; minimal stub.
- `@reown/walletkit`, `@walletconnect/{core,utils}` — these libraries
open WebSockets at import time. Mocked to no-ops.
- `@sentry/electron` — no-op telemetry.
- `@ledgerhq/hw-transport-node-hid` — scripted USB transport; the
in-file comment points contributors at hathor-ledger-app for the APDU
contract.
- `unleash-proxy-client` — fixed no-flags state; no HTTP polling.
- `./store/index` (mocked in setupTests.js) — stubs the global Redux
store to break a circular import chain that fires only on test entry.

If you find yourself needing to redeclare one of these in a test file,
think twice. Almost always the right move is to override the centralized
mock's specific shape locally:

```js
// In your test file, BEFORE other imports
jest.mock('@hathor/wallet-lib', () => {
const actual = jest.requireActual('@hathor/wallet-lib');
return { ...actual, swapService: { /* test-specific shape */ } };
});
```

Adding a competing mock for a module the central mocks already cover is
a code-review nit.

## The three-contract reducer pattern

When you write a reducer test, pin three independent contracts in
separate `describe` blocks:

1. **Initial state shape.** Enumerate the top-level keys the reducer
initializes, alphabetically sorted. Use `it.each(keys)('has top-level
key %s', ...)` so failures point at the specific missing key.
2. **Action type literals.** Verify the action-type constants resolve to
the expected string literals (not just "they're defined"). A rename
in `actions/index.js` that drops a literal surfaces as a clear
failure instead of a silent runtime no-op.
3. **Behaviour.** Dispatch each handled action and assert on the
resulting state. This is the bulk of feature-area reducer testing.

The reference example is `src/__tests__/reducers/reducer.wallet.test.js`.

The shape and action-type contracts are the safety net for the eventual
RTK-slices migration. Tests that assert only behaviour leave the
migration unprotected.

## Ledger JS-mock conventions

The Ledger transport mock at `src/__mocks__/@ledgerhq/hw-transport-node-hid.js`
and the fluent helper at `src/test-utils/ledgerTransportMock.js` mirror
the APDU response shapes documented in
[HathorNetwork/hathor-ledger-app](https://github.com/HathorNetwork/hathor-ledger-app).
When you write a Ledger-aware test, the mock is your contract surface.

**What JS-mock tests do certify:** the wallet's renderer + main-process
code reacts correctly *given* a Ledger response shape. PIN handling,
APDU encoding, error branches.

**What JS-mock tests do NOT certify:** Ledger firmware correctness, real
USB transport behaviour, hardware-wallet user-interaction flows.
Release validation for Ledger flows continues to live in `QA_LEDGER.md`
and requires real hardware.

When the real device behaviour disagrees with the mock, the source of
truth is hathor-ledger-app's pytest + Speculos suite, not this mock.
Update the mock to match. Do not "fix" the wallet code to match an
incorrect mock.

## Saga test patterns

Use `redux-saga-test-plan`'s `expectSaga` for integration tests. The
canonical pattern is:

```js
import { expectSaga } from 'redux-saga-test-plan';
import * as matchers from 'redux-saga-test-plan/matchers';

return expectSaga(mySaga, actionPayload)
.provide([
[matchers.call.fn(someEffect), 'mocked-return-value'],
[matchers.select(selector), 'mocked-state-slice'],
])
.put(someAction) // assert a dispatched action
.returns(expectedValue) // assert the saga's return value
.run();
```

`matchers.call.fn(someEffect)` matches any call to `someEffect`
regardless of argument values. For tighter assertions that include
argument values, use `matchers.call(someEffect, exactArg1, exactArg2)`.

When the saga under test has module-level state, the production code
exports a `*ForTesting` reset function. Call it in `beforeEach`:

```js
beforeEach(() => {
clearModalContextForTesting(); // resets src/sagas/modal.js's modalContext
});
```

If the saga you're testing keeps module-level state but lacks a
`*ForTesting` export, adding one is part of the feature-area PR. Keep
the reset function to one line and mark it with a JSDoc comment that
explains it's test-only.

## What NOT to test

Skip these in unit/integration/component tests; they belong to manual QA
or future Layer-4 E2E:

- Packaged-build behaviour (`electron-builder` output, code signing,
notarization).
- Real network conditions (3G, offline, timeouts) — see `QA.md`.
- OS-level dialogs (file pickers, notifications) — see `QA.md`.
- Visual regressions (pixel-level layout shifts) — out of scope.
- Real Ledger hardware — see `QA_LEDGER.md`.
- Auto-updater download/install — the wallet does not currently ship an
auto-updater; if one is added later, it stays a QA concern.

When you find yourself writing a test that requires one of the above to
work, the test is in the wrong layer. Move it to QA or wait for Layer 4.

## Diagnostic checklist when a test mysteriously fails

In order of likelihood for this codebase:

1. **"Cannot find module @hathor/wallet-lib" or similar import-chain
error.** A new wallet-lib export was added that the centralized mock
does not declare. Add the missing export to
`src/__mocks__/@hathor/wallet-lib.js`, including any constants the
wallet code reads at module-load time.

2. **"Cannot read properties of undefined" inside the mock chain.** A
module-level property you expected is missing. Same fix as above.

3. **A test passes locally but fails in CI.** Make sure you ran with
`CI=true npm test -- --watchAll=false`. The watch-mode default
behaves differently and silently swallows some failures.

4. **Module-level state from a previous test leaks forward.** Check
whether the saga under test has a `*ForTesting` reset function. If
so, call it in `beforeEach`. If not, add one.

5. **`jest.spyOn(obj, method)` fails with "Cannot spyOn on a primitive
value".** The mock returns `obj` as `undefined`. The fix is in the
mock file; see #1.

6. **A `jest.mock(...)` factory references an out-of-scope variable.**
Babel's `jest-hoist` plugin only allows variables prefixed with
`mock` (e.g., `mockNavigate`) inside the factory. Rename or move the
variable.
18 changes: 7 additions & 11 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,13 @@ jobs:
run: make check_po
- name: i18n
run: make i18n
# Tests are currently disabled because of an incompatibility between React 18 and
# React-Scripts v4.
# See related PRs about this:
# https://github.com/HathorNetwork/hathor-wallet/pull/416
# https://github.com/HathorNetwork/hathor-wallet/pull/449
# https://github.com/HathorNetwork/hathor-wallet/pull/567
# - name: Unit tests
# run: npm run test -- --coverage
# - name: Upload coverage
# # https://github.com/codecov/codecov-action/releases/tag/v3.1.4
# uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d
- name: Unit + integration + component tests
run: npm test -- --ci --coverage --watchAll=false --maxWorkers=2
env:
CI: true
- name: Upload coverage
# https://github.com/codecov/codecov-action/releases/tag/v3.1.4
uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d
Comment on lines +52 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What is the latest stable version of codecov/codecov-action GitHub Action and are there any security advisories for v3.1.4?

💡 Result:

Latest stable version of codecov/codecov-action (as of 2026-05-22): v6.0.0 [1][2]. Security advisories for v3.1.4: I did not find any GitHub Security Advisory (GHSA) entry or other official security advisory page specifically mentioning v3.1.4. The only v3.1.4 material I found is the changelog describing non-security fixes (typos, CLI option names, and dev dependency bumps) rather than a security issue [3][4]. If you want to be extra sure, search the repository’s “Security” → “Advisories” (or GitHub’s GHSA listing) for entries that mention “3.1.4” or “v3.1.4”; based on the sources above, no such advisory surfaced in my research.

Citations:


Update Codecov action pin (current major is v6.0.0)

codecov/codecov-action is pinned to eaaf4bedf32dbdc6b720b63067d99c4d77d6047d (v3.1.4 from 2023). The latest stable release is v6.0.0. I didn’t find a GHSA/security advisory specifically for v3.1.4, but this pin is several major versions behind—upgrade (still pinned to a commit) to pick up maintenance/bug fixes and any security hardening from newer releases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/main.yml around lines 52 - 54, The workflow step named
"Upload coverage" is pinned to an old commit for codecov/codecov-action
(v3.1.4); update the uses reference in that step to the current stable major
(for example codecov/codecov-action@v6.0.0) or to the commit hash corresponding
to the v6.0.0 release so you pick up the latest fixes and hardening while
keeping the action pinned; change the uses line in the "Upload coverage" step
accordingly and run a workflow test to confirm compatibility with the new
action.

- name: Start
run: npm start & npx wait-on http://localhost:3000
env:
Expand Down
48 changes: 48 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# AGENTS.md — Cross-tool entry point

This repository ships with conventions for AI coding assistants that work in
it (Claude Code, Gemini CLI, Codex, OpenAI Copilot, and any future
equivalents). The conventions are tool-neutral; the deeper guidance lives in
a separate skill file that loads on demand, and in a long-form testing guide
for both humans and agents.

## What to read first

| You are about to … | Open … |
|---|---|
| Add or modify a test (any layer) | [`.claude/skills/writing-tests/SKILL.md`](./.claude/skills/writing-tests/SKILL.md) |
| Look up a testing convention | [`docs/testing-guide.md`](./docs/testing-guide.md) |
| Configure Claude-Code-specific behaviour | [`CLAUDE.md`](./CLAUDE.md) |

## Repo-wide conventions

- **Conventional commits.** 50-char subject max; type prefixes (`feat:`,
`fix:`, `test:`, `chore:`, `docs:`, `ci:`). Keep the bodies short and
intent-focused.
- **No new emojis in code or comments.** They survive translation toolchains
inconsistently and add noise to git diffs. Existing emojis stay.
- **Do not edit the manual `QA*.md` checklists casually.** They are the
release-gating QA artefact. Updates to them should accompany the feature
that changes the manual flow, not appear in unrelated PRs.

## Testing-specific conventions (load the skill for depth)

- **Reference smoke vs feature-area distinction.** RFC 0001 splits PRs into
two flavours: "reference smoke" PRs land one representative test per
technique plus shared infrastructure; "feature-area" PRs cover one slice
of the wallet across all applicable layers. Do not mix the two in a single
PR.
- **Use the centralized mocks.** Shared package mocks are activated in
`src/setupTests.js` and implemented in `src/__mocks__/`. Do not redeclare a
mock in a test file when a centralized one exists; if you need a different
shape, override locally — never add a competing mock for the same module.
- **`*ForTesting` named exports** are part of the public test surface. Each
one resets module-level state that integration tests depend on for
isolation. Do not remove or rename one as part of an unrelated refactor.
- **JS-level Ledger mock is a contract mirror, not a reimplementation.**
When the real device behaviour disagrees with the mock, the source of
truth is [HathorNetwork/hathor-ledger-app](https://github.com/HathorNetwork/hathor-ledger-app).
Release validation for Ledger flows remains with `QA_LEDGER.md` and real
hardware.

For the full ruleset and worked examples, load the writing-tests skill.
Loading
Loading