Skip to content

feat: Automated test suite — unit, component#863

Open
tuliomir wants to merge 20 commits into
masterfrom
feat/automated-test-suite-layers-1-3
Open

feat: Automated test suite — unit, component#863
tuliomir wants to merge 20 commits into
masterfrom
feat/automated-test-suite-layers-1-3

Conversation

@tuliomir

@tuliomir tuliomir commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Introduces the first three layers of the automated test suite for hathor-wallet-mobile, scoped from RFC #110 (HathorNetwork/rfcs#110) where the multi-layer testing pyramid is designed and motivated:

  • Layer 1 — Unit (38 tests): utils, reducer.wallet lifecycle, reducer.reown pending-requests slice.
  • Layer 2 — Integration (3 tests): wallet start/reset sagas via redux-saga-test-plan.
  • Layer 3 — Component (16 tests): InitWallet and BackupWords screens via @testing-library/react-native.

Layer 4 (E2E) is intentionally excluded from this PR — it is the largest and most contentious layer (framework choice, native build pipeline, CI cost) and will land separately so reviewers can evaluate Layers 1-3 on their own merits. See RFC #110 for the full rationale and the Layer 4 evaluation work.

What's in

  • New test deps: @testing-library/react-native, @testing-library/jest-native, redux-saga-test-plan, jest-circus.
  • Test infrastructure: __tests__/helpers/{getInitialState,mockNavigation,mockStore,renderWithProviders} shared across specs.
  • jestMockSetup.js: refreshed mocks for @react-native-firebase/{messaging,app}, @hathor/unleash-client (replacing the removed unleash-proxy-client), react-native-camera-kit, @walletconnect/react-native-compat, and wallet-lib sub-path imports.
  • package.json jest config: testPathIgnorePatterns for helpers/ and the legacy nanoContracts/fixtures.js; @walletconnect and @testing-library added to transformIgnorePatterns.
  • Pre-existing test failures fixed (made the suite green for the first time): App.test.tsx (legacy_createStore), historyNanoContract.test.js (saga lock ordering), registerNanoContract.test.js (blueprint fetch steps).
  • Tiny production change: clearLoadingLocksForTesting exported from src/sagas/nanoContract.js purely for Layer 2 test isolation.
  • Slice-refactor safety net (in __tests__/reducers/): behavior + initial-state-shape + action-type contract layers, scoped to the wallet/tokens/reset surface plus the new state.reown.pendingRequests handler. Lets the upcoming RTK-slices migration of src/reducers/reducer.js be reviewed primarily by the test diff.
  • Agent / contributor onboarding for tests (added per AC below):
    • AGENTS.md and CLAUDE.md at the repo root — concise repo conventions for any AI coding agent (cross-tool format).
    • .claude/skills/writing-tests/SKILL.md — Claude Code skill that auto-loads on test work and encodes the conventions surfaced by repeated reviews so each new test PR doesn't re-fight the same nits.
    • docs/testing-guide.md (renamed from the old testing-ci-plan.md): rich human-readable reference covering the four-layer pyramid, per-PR test checklist, design principles, and future-proofing patterns.

What's NOT in

  • No E2E tests, no Detox, no Maestro, no e2e/ or .maestro/ directory.
  • No production code changes for E2E compatibility (the Pressable / ToggleSwitch / NumPad-testID rework lives on a separate Layer 4 branch).
  • No CI workflow yet — the appendix in docs/testing-guide.md shows the snippet to add to .github/workflows/main.yml once this lands.

Out-of-scope fix bundled in

__tests__/sagas/reown.test.js (added on master by #857) was failing the moment this branch ran the full suite. Both the test and src/sagas/reown.js are byte-identical to master — the test was off-by-one in counting gen.next() returns and missed the saga's getPendingSessionRequests re-fetch after put(reownReject()). The bug pre-dated this PR; it just wasn't visible because master CI doesn't currently run jest. Fixed in 4f4e9d3 and labelled in the commit message; happy to extract to a master-only PR if preferred.

Acceptance criteria

  • Running npm test on a fresh clone reports zero failures and Layer 1-3 deliverable suites are green.
  • No production behavior changes other than the clearLoadingLocksForTesting test-only export in src/sagas/nanoContract.js.
  • Future test work is low-friction for any AI agent. The repo ships with AGENTS.md (cross-tool entry point), CLAUDE.md (Claude Code pointer), .claude/skills/writing-tests/SKILL.md (auto-loads on test work), and docs/testing-guide.md (long-form reference). A new contributor — human or agent — can add a test that follows the repo's contracts and review conventions without prior session context.

Design rationale

See RFC #110 for: layer definitions and scope boundaries, dependency choices (RTL vs Enzyme, redux-saga-test-plan vs manual gen.next() driving for sagas), file-organization conventions, and the rationale for deferring Layer 4 (E2E) to a separate review cycle.

Diff size breakdown

Category Insertions %
Tests 1406 58%
package-lock.json + package.json 468 19%
Test docs 375 15%
Test helpers 119 5%
jestMockSetup.js 61 3%
Production code 7 0%
Total 2436 100%

Summary by CodeRabbit

  • Tests

    • Large expansion of test suites across reducers, sagas, and screens; added test helpers for mocked navigation, test stores, initial state retrieval, and standardized render-with-providers; deterministic UI and saga integration tests; utilities to control test-side locks/fixtures.
  • Chores

    • Revamped Jest setup and mocks; updated dev testing tooling and test discovery/transform config.
  • Documentation

    • New contributor testing guide, agent/skill docs, and test-authoring conventions.

tuliomir and others added 4 commits April 28, 2026 11:31
Establishes a multi-layered test infrastructure for the wallet creation
workflow as a proof of concept for the full test suite.

Layer 1 (Unit): utils, reducer wallet lifecycle — 38 tests
Layer 2 (Integration): wallet start/reset sagas — 3 tests
Layer 3 (Component): InitWallet, BackupWords screens — 16 tests

New dependencies: @testing-library/react-native, redux-saga-test-plan
Fixes stale mocks in jestMockSetup.js (unleash, removed modules)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix historyNanoContract tests to match current saga flow (locking mechanism,
  new step ordering). Add clearLoadingLocksForTesting export for test isolation.
- Fix registerNanoContract tests for getBlueprintId/getBlueprintInformation saga steps
- Fix App.test.tsx redux mock (legacy_createStore)
- Add mocks: react-native-camera-kit, @walletconnect/react-native-compat,
  @react-native-firebase/app, wallet-lib sub-path imports (axiosWrapper)
- Add testPathIgnorePatterns for helpers/, fixtures, and e2e/
- Result: 9 suites passing, 83 tests, 0 failures

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
jest-circus@^30 transitively pulls unrs-resolver, which has an
install script. LavaMoat allow-scripts requires every such package
to be explicitly enumerated; default it to disallowed (false) like
the other entries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
reown.test.js (added on master by 8db34bb) drove its generators with
the wrong gen.next() arity — it forgot that gen.next(arg) returns the
NEXT yield, not the previous one. The 3 broken tests effectively read
yields off-by-one and missed the saga's getPendingSessionRequests
re-fetch after put(reownReject). Pre-existing master bug exposed once
this branch ran the test for the first time.

Fixes:
- rejectSinglePendingRequest "rejects request and refreshes list":
  capture respondSessionRequest from gen.next(mockReownClient) (one
  fewer gen.next call).
- rejectAllPendingRequests with 3 pending: gen.next(pending) already
  yields respond #1, so only 2 more bare gen.next calls cover #2/#3;
  added the missing re-fetch step and feed [] to it.
- rejectAllPendingRequests with empty pending: gen.next([]) directly
  yields put(reownReject); added the missing re-fetch step.

Renamed "clears list" → "resyncs list" to reflect the saga's actual
contract (it re-reads pending after rejection, not blindly setting []).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tuliomir tuliomir added the tests label Apr 28, 2026
@tuliomir tuliomir self-assigned this Apr 28, 2026
@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds extensive test infrastructure and many new tests: helper utilities for store/navigation/rendering/initial state, expanded Jest mocks and config, numerous reducer/saga/screen/utils tests, a test-only saga utility export, and contributor-facing testing documentation and guidance files.

Changes

Cohort / File(s) Summary
Test Helpers
__tests__/helpers/mockNavigation.ts, __tests__/helpers/mockStore.ts, __tests__/helpers/renderWithProviders.tsx, __tests__/helpers/getInitialState.js
New helpers: mock navigation/route, test Redux store creator merging reducer initial state with preloadedState, RTL render wrapper providing Redux and optional NavigationContainer, and helper to obtain root reducer initial state.
Jest Setup & Mocks
jestMockSetup.js, __tests__/App.test.tsx
Reworked Jest setup: firebase messaging/app mocks, new HathorUnleashClient mock, additional component/subpath compatibility mocks, and redux Jest mock now returns a stubbed store (including legacy_createStore). __tests__/App.test.tsx mock expanded to return a store-like object.
Jest Config & DevDeps
package.json
Added devDependencies for testing libs and redux-saga-test-plan; updated jest.transformIgnorePatterns allowlist and added testPathIgnorePatterns.
Reducer Tests
__tests__/reducers/reducer.wallet.test.ts, __tests__/reducers/reducer.reown.test.ts
Added comprehensive wallet and reown reducer test suites covering initial-state, transitions, immutability, exact action-type strings, and pending-requests behavior.
Saga Tests & Fixtures
__tests__/sagas/..., __tests__/sagas/nanoContracts/fixtures.js, src/sagas/nanoContract.js
Updated/added saga tests: history/register/reown/wallet suites refactored for new control flows, added fixture fields, and exported clearLoadingLocksForTesting() to reset internal loading locks for test isolation.
Component & Util Tests
__tests__/screens/BackupWords.test.tsx, __tests__/screens/InitWallet.test.tsx, __tests__/utils.test.ts
New screen/component tests with deterministic mocks and asset stubs; utility function tests for numeric/formatting helpers.
Docs & Test Guidance
docs/testing-guide.md, .claude/skills/writing-tests/SKILL.md, AGENTS.md, CLAUDE.md
New contributor-facing testing guide, repo test-writing skill, and agent docs codifying testing patterns, reducer contracts, and contributor workflow.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • pedroferreira1
  • r4mmer
  • raul-oliveira

Poem

🐰 I hopped through mocks and spun a test-y thread,
Stashed stores and routes where eager cases tread.
I tamed shuffled words and soothed unruly sagas,
Cleared locks and logs for calmer CI dramas.
Hooray — the burrow’s safe; the tests now tread ahead!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'feat: Automated test suite — unit, component' is partially related to the changeset but omits a major layer (integration sagas) and undersells the scope, which includes test infrastructure, documentation, and production changes. Consider updating the title to reflect all three test layers and key infrastructure additions, e.g. 'feat: Automated test suite — unit, integration, component + test infrastructure' or 'feat: Add Layers 1–3 test suite and testing guide'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/automated-test-suite-layers-1-3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (2)
__tests__/screens/BackupWords.test.tsx (2)

162-185: Early return silently passes test without assertions.

If wrongWord === correctWord, the test returns early at line 174 without any assertions. While this won't happen with the current test data ('abandon' !== 'able'), this pattern is fragile. Consider using expect.assertions(n) or throwing an error instead of silently passing.

♻️ Suggested improvement
   it('shows failure modal when wrong word is selected', async () => {
     const { getByText, queryByText } = render(
       <BackupWords navigation={mockNavigation} route={mockRoute} />
     );
 
     // Select a WRONG word (not the one at position FIXED_INDEXES[0])
     const correctWord = TEST_WORDS_ARR[FIXED_INDEXES[0] - 1];
     // Find any option that is NOT the correct word
     // Options are around index (FIXED_INDEXES[0]-1), which is 2. Options: indexes 0-4
     const wrongWord = TEST_WORDS_ARR[0]; // 'abandon' — not 'able'
-    if (wrongWord === correctWord) {
-      // Fallback if they happen to be the same
-      return;
-    }
+    // Ensure we actually have a wrong word to test with
+    expect(wrongWord).not.toBe(correctWord);
 
     await act(async () => {
       fireEvent.press(getByText(wrongWord));
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/screens/BackupWords.test.tsx` around lines 162 - 185, The test
silently returns if wrongWord === correctWord which can mask failures; instead
ensure the test always asserts by replacing the early return with a guard that
fails the test (e.g., throw new Error(...) or use
expect(wrongWord).not.toEqual(correctWord())) or declare expect.assertions(1)
and then proceed; update the logic around wrongWord, correctWord, TEST_WORDS_ARR
and FIXED_INDEXES in the 'shows failure modal when wrong word is selected' test
to either pick a guaranteed different wrongWord or explicitly fail when they
match so the test always performs the modal assertion.

206-232: Test lacks assertions for dismiss behavior.

This test verifies the failure modal appears but doesn't actually test the onDismiss → goBack behavior described in the comments. Consider either completing the test by simulating the dismiss and asserting navigation.goBack was called, or renaming the test to accurately reflect what it tests (e.g., 'shows failure modal for wrong selection').

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/screens/BackupWords.test.tsx` around lines 206 - 232, The test
"failure modal dismiss navigates back" currently only asserts the failure modal
appears; update it to actually simulate the dismiss and assert navigation.goBack
was called (or rename the test to reflect it only shows the modal). After the
waitFor that verifies queryByText('Wrong word.') is truthy, trigger the
FeedbackModal/BackdropModal dismiss (e.g., fireEvent.press on the backdrop
element found by testID like 'backdrop' or the modal's dismiss button, or call
the FeedbackModal's onDismiss handler via the rendered tree) and then
expect(mockNavigation.goBack).toHaveBeenCalled(); if you cannot simulate dismiss
reliably, rename the it(...) description to 'shows failure modal for wrong
selection' to match the current assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@__tests__/helpers/mockNavigation.ts`:
- Around line 46-67: The helper setupNavigationMocks currently calls jest.mock()
at runtime inside the function (wrapping the mocks for
'@react-navigation/native' and '../../src/hooks/navigation'), but jest.mock
calls are hoisted and must live at module scope; move the mocking out of
setupNavigationMocks: export createMockNavigation and createMockRoute (or export
mockNav and mockRoute factories) and perform jest.mock(...) at the top level in
tests (or at the top of this module if you want shared defaults) so
useNavigation/useRoute/useParams are mocked before modules are imported;
alternatively, change the helper to only return mock objects and update tests to
call jest.mock(...) at module scope using those mocks (refer to
setupNavigationMocks, createMockNavigation, createMockRoute, and the mocked
hooks useNavigation/useRoute/useParams).

In `@__tests__/sagas/nanoContracts/historyNanoContract.test.js`:
- Around line 91-95: The test fails because it compares two distinct Error
instances with toStrictEqual; capture the Error instance produced earlier and
reuse it in the expectation (or assert on its shape instead of instance
identity). Locate the generator advance that yields the error (the variable used
when creating the error at line 86) and use that same error variable in the
expect against put(onExceptionCaptured(...)) or change the assertion to match
error.message/type (e.g.,
expect(put(onExceptionCaptured(expect.objectContaining({message: 'network
error'})), false)) so the comparison does not rely on Error reference equality.

In `@__tests__/sagas/wallet.test.ts`:
- Around line 109-110: The test is asserting against the outer mockWallet object
(mockWallet.stop) which is unrelated to the saga run with state.wallet = null,
making the assertion vacuously true; instead assert that the saga did not yield
a call effect to wallet.stop by using expectSaga's effect matchers (e.g.,
replace expect(mockWallet.stop).not.toHaveBeenCalled() with
expectSaga(...).not.call.fn(mockWallet.stop) or use .not.call()/.not.call.fn()
to ensure no call effect to wallet.stop was yielded), targeting the wallet.stop
function and the expectSaga invocation that runs the saga under test.

In `@jestMockSetup.js`:
- Around line 61-72: The mock currently only provides a default callable but
tests import named exports; update the mock to export named symbols alongside
the default factory: export getMessaging (or a getMessaging factory returning
the same object), and named functions/values getToken, deleteToken,
hasPermission, requestPermission, getInitialNotification, onMessage,
onNotificationOpenedApp, setBackgroundMessageHandler,
isDeviceRegisteredForRemoteMessages, registerDeviceForRemoteMessages, and
AuthorizationStatus so imports in src/sagas/pushNotification.js and
src/workers/backgroundListeners.js resolve; keep existing jest.fn()
implementations and Promise.resolve behaviors and ensure the default export
still returns the same object.

In `@package.json`:
- Around line 120-121: The package.json lists "jest": "29.7.0" but
"jest-circus": "^30.3.0", causing a major-version mismatch; update the
jest-circus dependency to match Jest's major/minor (for example change
"jest-circus" to "29.7.0" or "^29.7.0") so both jest and jest-circus use the
same 29.x release and avoid compatibility issues.

---

Nitpick comments:
In `@__tests__/screens/BackupWords.test.tsx`:
- Around line 162-185: The test silently returns if wrongWord === correctWord
which can mask failures; instead ensure the test always asserts by replacing the
early return with a guard that fails the test (e.g., throw new Error(...) or use
expect(wrongWord).not.toEqual(correctWord())) or declare expect.assertions(1)
and then proceed; update the logic around wrongWord, correctWord, TEST_WORDS_ARR
and FIXED_INDEXES in the 'shows failure modal when wrong word is selected' test
to either pick a guaranteed different wrongWord or explicitly fail when they
match so the test always performs the modal assertion.
- Around line 206-232: The test "failure modal dismiss navigates back" currently
only asserts the failure modal appears; update it to actually simulate the
dismiss and assert navigation.goBack was called (or rename the test to reflect
it only shows the modal). After the waitFor that verifies queryByText('Wrong
word.') is truthy, trigger the FeedbackModal/BackdropModal dismiss (e.g.,
fireEvent.press on the backdrop element found by testID like 'backdrop' or the
modal's dismiss button, or call the FeedbackModal's onDismiss handler via the
rendered tree) and then expect(mockNavigation.goBack).toHaveBeenCalled(); if you
cannot simulate dismiss reliably, rename the it(...) description to 'shows
failure modal for wrong selection' to match the current assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b6c0652c-a995-493f-b1e2-6fc492754973

📥 Commits

Reviewing files that changed from the base of the PR and between 8db34bb and 4f4e9d3.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (16)
  • __tests__/App.test.tsx
  • __tests__/helpers/mockNavigation.ts
  • __tests__/helpers/mockStore.ts
  • __tests__/helpers/renderWithProviders.tsx
  • __tests__/reducers/reducer.wallet.test.ts
  • __tests__/sagas/nanoContracts/historyNanoContract.test.js
  • __tests__/sagas/nanoContracts/registerNanoContract.test.js
  • __tests__/sagas/reown.test.js
  • __tests__/sagas/wallet.test.ts
  • __tests__/screens/BackupWords.test.tsx
  • __tests__/screens/InitWallet.test.tsx
  • __tests__/utils.test.ts
  • docs/testing-ci-plan.md
  • jestMockSetup.js
  • package.json
  • src/sagas/nanoContract.js

Comment thread __tests__/helpers/mockNavigation.ts Outdated
Comment thread __tests__/sagas/nanoContracts/historyNanoContract.test.js Outdated
Comment thread __tests__/sagas/wallet.test.ts Outdated
Comment thread jestMockSetup.js
Comment on lines +61 to +72
jest.mock('@react-native-firebase/messaging', () => () => ({
getToken: jest.fn(() => Promise.resolve('mock-token')),
deleteToken: jest.fn(() => Promise.resolve()),
requestPermission: jest.fn(() => Promise.resolve(1)),
getInitialNotification: jest.fn(() => Promise.resolve(null)),
onMessage: jest.fn(() => jest.fn()),
onNotificationOpenedApp: jest.fn(() => jest.fn()),
setBackgroundMessageHandler: jest.fn(),
isDeviceRegisteredForRemoteMessages: true,
registerDeviceForRemoteMessages: jest.fn(() => Promise.resolve()),
AuthorizationStatus: { AUTHORIZED: 1, NOT_DETERMINED: -1, DENIED: 0, PROVISIONAL: 2 },
}));

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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Imports from `@react-native-firebase/messaging` =="
rg -n "from '@react-native-firebase/messaging'" -C2

echo
echo "== Named symbols referenced in app/tests =="
rg -nP "\b(getMessaging|getToken|deleteToken|hasPermission|requestPermission|onMessage|registerDeviceForRemoteMessages|AuthorizationStatus)\b" src __tests__

Repository: HathorNetwork/hathor-wallet-mobile

Length of output: 2922


Mock must export named symbols used by Firebase Messaging consumers.

The mock currently exposes only a default callable factory, but src/sagas/pushNotification.js and src/workers/backgroundListeners.js import named exports like getMessaging, getToken, deleteToken, hasPermission, requestPermission, onMessage, registerDeviceForRemoteMessages, setBackgroundMessageHandler, and AuthorizationStatus. These will be undefined in tests.

✅ Suggested fix (export named API alongside default callable)
-jest.mock('@react-native-firebase/messaging', () => () => ({
-  getToken: jest.fn(() => Promise.resolve('mock-token')),
-  deleteToken: jest.fn(() => Promise.resolve()),
-  requestPermission: jest.fn(() => Promise.resolve(1)),
-  getInitialNotification: jest.fn(() => Promise.resolve(null)),
-  onMessage: jest.fn(() => jest.fn()),
-  onNotificationOpenedApp: jest.fn(() => jest.fn()),
-  setBackgroundMessageHandler: jest.fn(),
-  isDeviceRegisteredForRemoteMessages: true,
-  registerDeviceForRemoteMessages: jest.fn(() => Promise.resolve()),
-  AuthorizationStatus: { AUTHORIZED: 1, NOT_DETERMINED: -1, DENIED: 0, PROVISIONAL: 2 },
-}));
+jest.mock('@react-native-firebase/messaging', () => {
+  const AuthorizationStatus = {
+    AUTHORIZED: 1,
+    NOT_DETERMINED: -1,
+    DENIED: 0,
+    PROVISIONAL: 2,
+    EPHEMERAL: 3,
+  };
+
+  const instance = {
+    getToken: jest.fn(() => Promise.resolve('mock-token')),
+    deleteToken: jest.fn(() => Promise.resolve()),
+    requestPermission: jest.fn(() => Promise.resolve(AuthorizationStatus.AUTHORIZED)),
+    getInitialNotification: jest.fn(() => Promise.resolve(null)),
+    onMessage: jest.fn(() => jest.fn()),
+    onNotificationOpenedApp: jest.fn(() => jest.fn()),
+    setBackgroundMessageHandler: jest.fn(),
+    isDeviceRegisteredForRemoteMessages: true,
+    registerDeviceForRemoteMessages: jest.fn(() => Promise.resolve()),
+  };
+
+  return {
+    __esModule: true,
+    default: jest.fn(() => instance),
+    getMessaging: jest.fn(() => ({})),
+    getToken: instance.getToken,
+    deleteToken: instance.deleteToken,
+    hasPermission: jest.fn(() => Promise.resolve(AuthorizationStatus.AUTHORIZED)),
+    requestPermission: instance.requestPermission,
+    onMessage: instance.onMessage,
+    registerDeviceForRemoteMessages: instance.registerDeviceForRemoteMessages,
+    setBackgroundMessageHandler: instance.setBackgroundMessageHandler,
+    AuthorizationStatus,
+  };
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@jestMockSetup.js` around lines 61 - 72, The mock currently only provides a
default callable but tests import named exports; update the mock to export named
symbols alongside the default factory: export getMessaging (or a getMessaging
factory returning the same object), and named functions/values getToken,
deleteToken, hasPermission, requestPermission, getInitialNotification,
onMessage, onNotificationOpenedApp, setBackgroundMessageHandler,
isDeviceRegisteredForRemoteMessages, registerDeviceForRemoteMessages, and
AuthorizationStatus so imports in src/sagas/pushNotification.js and
src/workers/backgroundListeners.js resolve; keep existing jest.fn()
implementations and Promise.resolve behaviors and ensure the default export
still returns the same object.

Comment thread package.json Outdated
tuliomir and others added 3 commits April 28, 2026 12:44
Pin behavior, state-shape, and action-type contracts for the in-scope
reducer surface so the upcoming RTK-slices refactor surfaces drift in
explicit, reviewable test diffs rather than silent regressions.

- reducer.wallet.test.ts: add START_WALLET_NOT_STARTED case, initial
  state shape contract, action-type contract.
- reducer.reown.test.ts (new): same three-layer pattern for the
  REOWN_SET_PENDING_REQUESTS handler added on this branch.
- testing-guide.md: rewrite docs/testing-ci-plan.md as a contributor
  guide covering the four-layer pyramid, PR checklist, design
  principles, and the slice safety-net pattern. Old CI content kept
  as an appendix.

Reducer tests: 14 → 38. Full suite: 116 passed, 0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- package.json: pin jest-circus to 29.7.0 (was ^30.3.0). Aligns with
  jest 29.7.0 to avoid major-version drift, and matches the repo-wide
  rule of using exact versions to reduce supply-chain attack surface.
- package.json: drop the now-stale lavamoat allowScripts entry for
  jest-circus>jest-runtime>jest-resolve>unrs-resolver. That transitive
  dep only existed under jest-circus 30.x and is gone after the pin.
- package-lock.json: regenerated by npm install; jest-circus subtree
  resolves to 29.7.0 with no caret/tilde ranges.
- __tests__/helpers/mockNavigation.ts: remove the broken
  setupNavigationMocks helper (zero callers, and jest.mock inside a
  function body is not hoisted by Babel so it silently no-ops anyway).
  Keep createMockNavigation/createMockRoute factories which are
  correct and reusable.
- __tests__/screens/BackupWords.test.tsx: replace silent `return`
  guard with an explicit assertion that wrongWord !== correctWord, so
  a fixture change can never silently mask the test. Rename the
  'failure modal dismiss navigates back' test to 'shows failure modal
  for wrong selection' since it only verifies modal appearance; the
  dismiss → goBack assertion is left as a follow-up.

Other review findings checked but unchanged:
- historyNanoContract.test.js Error compare: false positive;
  toStrictEqual structurally compares Error since Jest 24.
- wallet.test.ts mockWallet.stop assertion: defensible — beforeEach
  jest.clearAllMocks() makes it a real isolation check, and the
  reviewer's expectSaga.not.call.fn alternative has the same target
  weakness.
- jestMockSetup.js Firebase named exports: latent but unexercised by
  any current test path; no fix needed today.

Full suite: 116 passed, 0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add inline comments at three sites flagged in review so future
reviewers (and other AI agents) see the rationale up front and don't
re-raise the same complaint:

- historyNanoContract.test.js: toStrictEqual structurally compares
  Error since Jest 24, so two `new Error(...)` instances match.
- wallet.test.ts: jest.clearAllMocks() in beforeEach makes the
  mockWallet.stop assertion a real cross-test isolation check; the
  expectSaga.not.call.fn alternative has the same target weakness.
- jestMockSetup.js: the Firebase messaging mock returns a default
  callable; production code imports named exports that resolve to
  undefined, but no current test path invokes them. Comment lists the
  named exports that must be added if/when a test exercises
  pushNotification.js or backgroundListeners.js.

No behavior changes; only comments. Suite still 116 passed, 0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
package.json (1)

1-1: ⚠️ Potential issue | 🔴 Critical

Pipeline failure: package-lock.json is out of sync.

The CI pipeline reports:

npm ci can only install when package.json and package-lock.json are in sync; lock file does not include typescript@6.0.3

This indicates the lockfile references a dependency version not declared in package.json. The package.json shows typescript@4.8.4, but the error mentions 6.0.3.

Please regenerate the lockfile:

rm package-lock.json
npm install

Then commit the updated package-lock.json.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 1, CI fails because package-lock.json is out of sync
with package.json (lock references typescript@6.0.3 while package.json lists
typescript@4.8.4); fix by regenerating the lockfile: remove the current
package-lock.json, run npm install (or npm ci after ensuring package.json is
correct) to produce a lockfile matching package.json, then commit the updated
package-lock.json so npm ci succeeds; ensure you run this locally and push the
regenerated package-lock.json.
jestMockSetup.js (1)

95-121: ⚠️ Potential issue | 🟠 Major

Fix Unleash mock exports to match actual imports.

The mock exports HathorUnleashClient as a named export, but the code imports a default export:

import UnleashClient, { FetchTogglesStatus } from '@hathor/unleash-client';

Additionally, FetchTogglesStatus is used in src/sagas/featureToggle.js:74 but is not exported by the mock. Without these exports, the saga will fail at runtime.

Fix
 jest.mock('@hathor/unleash-client', () => ({
+  default: jest.fn(() => ({
-  HathorUnleashClient: jest.fn(() => ({
     getAllToggles: () => ({}),
     isEnabled: () => false,
     getVariant: () => ({}),
     updateContext: () => {},
     getContext: () => ({
       appName: 'hathor-wallet-mobile',
       environment: 'test',
       userId: 'test-user',
       sessionId: 'test-session',
       remoteAddress: undefined,
       properties: {},
     }),
     setContextField: () => {},
     start: () => {},
     stop: () => {},
     on: () => {},
   })),
   EVENTS: {
     INIT: 'INIT',
     ERROR: 'ERROR',
     READY: 'READY',
     UPDATE: 'UPDATE',
     IMPRESSION: 'IMPRESSION',
   },
+  FetchTogglesStatus: {
+    Updated: 'updated',
+    Fetched: 'fetched',
+    NotFetched: 'not-fetched',
+  },
 }));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@jestMockSetup.js` around lines 95 - 121, The mock currently provides
HathorUnleashClient as a named export but your code imports a default
UnleashClient and also expects the FetchTogglesStatus export; update the
jest.mock for '@hathor/unleash-client' so it exports a default (UnleashClient)
that maps to the existing HathorUnleashClient mock and add a named export
FetchTogglesStatus with the same enum/values used in src/sagas/featureToggle.js
(and keep EVENTS and other methods intact) so imports like "import
UnleashClient, { FetchTogglesStatus } from '@hathor/unleash-client';" resolve at
test runtime.
🧹 Nitpick comments (4)
docs/testing-guide.md (1)

207-207: Optional: Consider more concise wording.

The phrase "lags behind" is clear, but could be simplified to "stays in sync with" or "remains current with" for slightly more direct language.

✍️ Suggested alternative wording
-This keeps the contract honest — it never lags behind the code.
+This keeps the contract honest — it stays in sync with the code.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/testing-guide.md` at line 207, Replace the phrase "lags behind the code"
in the sentence "keeps the contract honest — it never lags behind the code."
with a more concise alternative such as "stays in sync with the code" (resulting
in "keeps the contract honest — it never stays in sync with the code."), or
better yet "remains current with the code" to yield "keeps the contract honest —
it remains current with the code."; update the line in docs/testing-guide.md
accordingly to use the chosen concise wording.
__tests__/sagas/wallet.test.ts (1)

13-26: Multiple unused imports.

Several imported actions appear unused in the test file: setWallet, setServerInfo, startWalletSuccess, setUseWalletService, setAvailablePushNotification, setUniqueDeviceId, firstAddressRequest, walletRefreshSharedAddress, setFullNodeNetworkName.

These may have been intended for additional tests or are remnants from development. Consider removing them to reduce noise.

♻️ Proposed cleanup
 import {
   startWalletRequested,
-  setWallet,
-  setServerInfo,
-  startWalletSuccess,
   startWalletFailed,
   resetWalletSuccess,
-  setUseWalletService,
-  setAvailablePushNotification,
-  setUniqueDeviceId,
-  firstAddressRequest,
-  walletRefreshSharedAddress,
-  setFullNodeNetworkName,
 } from '../../src/actions';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/sagas/wallet.test.ts` around lines 13 - 26, The import list in the
test imports many unused action creators (setWallet, setServerInfo,
startWalletSuccess, setUseWalletService, setAvailablePushNotification,
setUniqueDeviceId, firstAddressRequest, walletRefreshSharedAddress,
setFullNodeNetworkName); remove these unused symbols from the import statement
at the top of __tests__/sagas/wallet.test.ts so only actually used actions
(e.g., startWalletRequested, resetWalletSuccess) remain, or reintroduce any you
intend to use in new assertions—update the import to reference only the needed
action names to eliminate lint/test noise.
__tests__/screens/BackupWords.test.tsx (2)

162-181: Duplicate test coverage for failure modal.

it('shows failure modal when wrong word is selected') (lines 162-181) and it('shows failure modal for wrong selection') (lines 202-223) test the same behavior with identical assertions. The second test's comment explains it was renamed, but both tests verify the same thing: pressing a wrong word shows 'Wrong word.' text.

Consider removing the duplicate test to reduce maintenance overhead:

♻️ Suggested consolidation
-  it('shows failure modal for wrong selection', async () => {
-    // Renamed from 'failure modal dismiss navigates back' — this test only
-    // verifies the modal appears. Simulating the BackdropModal dismiss to
-    // assert goBack() requires reaching into modal internals; left as a
-    // follow-up. The dismiss → goBack wiring is currently verified by
-    // reading the screen source.
-    const { getByText, queryByText } = render(
-      <BackupWords navigation={mockNavigation} route={mockRoute} />
-    );
-
-    const correctWord = TEST_WORDS_ARR[FIXED_INDEXES[0] - 1];
-    const wrongWord = TEST_WORDS_ARR.find((w) => w !== correctWord);
-    expect(wrongWord).toBeDefined();
-
-    await act(async () => {
-      fireEvent.press(getByText(wrongWord!));
-    });
-
-    await waitFor(() => {
-      expect(queryByText('Wrong word.')).toBeTruthy();
-    });
-  });

Also applies to: 202-223

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/screens/BackupWords.test.tsx` around lines 162 - 181, Remove the
duplicate failure-modal test: either delete the redundant it('shows failure
modal when wrong word is selected') or it('shows failure modal for wrong
selection') in __tests__/screens/BackupWords.test.tsx, keeping only one test
that asserts pressing a wrong word displays 'Wrong word.'; ensure the retained
test uses the existing selection logic (BackupWords render with
mockNavigation/mockRoute, deriving correctWord from TEST_WORDS_ARR and
FIXED_INDEXES, finding wrongWord, firing press, and waiting for
queryByText('Wrong word.')), and update the remaining test's description to be
clear and unique.

163-163: Unused destructured variable queryByText.

Line 163 destructures queryByText but only uses getByText. This is harmless but could be cleaned up.

♻️ Proposed fix
-    const { getByText, queryByText } = render(
+    const { getByText } = render(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/screens/BackupWords.test.tsx` at line 163, The test destructures
queryByText from the render call but never uses it; update the destructuring in
the BackupWords.test.tsx test (the line with const { getByText, queryByText } =
render(...)) to remove queryByText (i.e., only destructure getByText) or else
use queryByText where appropriate so the unused variable is eliminated; ensure
any references to render/getByText remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@__tests__/reducers/reducer.reown.test.ts`:
- Around line 13-15: The test file is missing Jest global imports, causing
describe/it/expect to be flagged by no-undef; add explicit imports from
'@jest/globals' at the top of the test file (e.g., import { describe, it, expect
} from '@jest/globals') so the existing imports (reducer,
setReownPendingRequests, types) remain unchanged and ESLint no-undef is
satisfied.

---

Outside diff comments:
In `@jestMockSetup.js`:
- Around line 95-121: The mock currently provides HathorUnleashClient as a named
export but your code imports a default UnleashClient and also expects the
FetchTogglesStatus export; update the jest.mock for '@hathor/unleash-client' so
it exports a default (UnleashClient) that maps to the existing
HathorUnleashClient mock and add a named export FetchTogglesStatus with the same
enum/values used in src/sagas/featureToggle.js (and keep EVENTS and other
methods intact) so imports like "import UnleashClient, { FetchTogglesStatus }
from '@hathor/unleash-client';" resolve at test runtime.

In `@package.json`:
- Line 1: CI fails because package-lock.json is out of sync with package.json
(lock references typescript@6.0.3 while package.json lists typescript@4.8.4);
fix by regenerating the lockfile: remove the current package-lock.json, run npm
install (or npm ci after ensuring package.json is correct) to produce a lockfile
matching package.json, then commit the updated package-lock.json so npm ci
succeeds; ensure you run this locally and push the regenerated
package-lock.json.

---

Nitpick comments:
In `@__tests__/sagas/wallet.test.ts`:
- Around line 13-26: The import list in the test imports many unused action
creators (setWallet, setServerInfo, startWalletSuccess, setUseWalletService,
setAvailablePushNotification, setUniqueDeviceId, firstAddressRequest,
walletRefreshSharedAddress, setFullNodeNetworkName); remove these unused symbols
from the import statement at the top of __tests__/sagas/wallet.test.ts so only
actually used actions (e.g., startWalletRequested, resetWalletSuccess) remain,
or reintroduce any you intend to use in new assertions—update the import to
reference only the needed action names to eliminate lint/test noise.

In `@__tests__/screens/BackupWords.test.tsx`:
- Around line 162-181: Remove the duplicate failure-modal test: either delete
the redundant it('shows failure modal when wrong word is selected') or it('shows
failure modal for wrong selection') in __tests__/screens/BackupWords.test.tsx,
keeping only one test that asserts pressing a wrong word displays 'Wrong word.';
ensure the retained test uses the existing selection logic (BackupWords render
with mockNavigation/mockRoute, deriving correctWord from TEST_WORDS_ARR and
FIXED_INDEXES, finding wrongWord, firing press, and waiting for
queryByText('Wrong word.')), and update the remaining test's description to be
clear and unique.
- Line 163: The test destructures queryByText from the render call but never
uses it; update the destructuring in the BackupWords.test.tsx test (the line
with const { getByText, queryByText } = render(...)) to remove queryByText
(i.e., only destructure getByText) or else use queryByText where appropriate so
the unused variable is eliminated; ensure any references to render/getByText
remain unchanged.

In `@docs/testing-guide.md`:
- Line 207: Replace the phrase "lags behind the code" in the sentence "keeps the
contract honest — it never lags behind the code." with a more concise
alternative such as "stays in sync with the code" (resulting in "keeps the
contract honest — it never stays in sync with the code."), or better yet
"remains current with the code" to yield "keeps the contract honest — it remains
current with the code."; update the line in docs/testing-guide.md accordingly to
use the chosen concise wording.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e4d3c63c-4bd7-4ec5-be8e-4273986cff41

📥 Commits

Reviewing files that changed from the base of the PR and between 4f4e9d3 and ec3b546.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • __tests__/helpers/mockNavigation.ts
  • __tests__/reducers/reducer.reown.test.ts
  • __tests__/reducers/reducer.wallet.test.ts
  • __tests__/sagas/nanoContracts/historyNanoContract.test.js
  • __tests__/sagas/wallet.test.ts
  • __tests__/screens/BackupWords.test.tsx
  • docs/testing-guide.md
  • jestMockSetup.js
  • package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/helpers/mockNavigation.ts

Comment thread __tests__/reducers/reducer.reown.test.ts Outdated
tuliomir and others added 3 commits April 28, 2026 13:21
The previous commit's lock was generated with Node 24 / npm 11, which
resolves transitive deps differently from CI's Node 22 / npm 10 and
dropped entries CI's `npm ci` expected (notably the nested
@react-native-community/cli-config>typescript@6.0.3, surfaced as
'Missing: typescript@6.0.3 from lock file').

Regenerated the lock from the bdbf958 baseline using Node 22.22.2 +
npm 10.9.7 (matching the .github/workflows/main.yml matrix), so
`npm ci` succeeds. The intended jest-circus@29.7.0 pin and the dropped
unrs-resolver subtree are preserved.

Suite: 116 passed, 0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The repo's .eslintrc has no `env: { jest: true }` and no test-file
override, so describe/it/expect tripped no-undef in both reducer
test files (111 errors total). Following the reviewer's suggestion,
import the jest globals explicitly at the top of each file from
'@jest/globals' (the package is already a transitive dep, used by
__tests__/helpers/mockNavigation.ts).

Also fold in two trivial auto-fixes for pre-existing dot-notation
errors at reducer.wallet.test.ts:87,99 ('tokens["token123"]' →
'tokens.token123') so `npm run lint` passes on this branch.

Suite: 38 passed (reducers); no behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- __tests__/sagas/wallet.test.ts: drop unused imports flagged by
  reviewer + ESLint (setWallet, setServerInfo, startWalletSuccess,
  setUseWalletService, setAvailablePushNotification, setUniqueDeviceId,
  firstAddressRequest, walletRefreshSharedAddress,
  setFullNodeNetworkName, throwError, select, call,
  IGNORE_WS_TOGGLE_FLAG, mockServerInfo). Imports actually referenced
  via function-identity comparisons inside .provide()
  (isWalletServiceEnabled, isPushNotificationEnabled,
  getNetworkSettings, PRE_SETTINGS_MAINNET) are kept.

- __tests__/screens/BackupWords.test.tsx: remove the duplicate
  failure-modal test (the renamed 'shows failure modal for wrong
  selection' was a near-clone of the canonical 'shows failure modal
  when wrong word is selected'). Also remove the unused queryByText
  destructure at the 'advances to next step' test. The deferred
  dismiss → goBack assertion is documented as a follow-up next to
  the canonical failure-modal test.

- jestMockSetup.js: annotate the @hathor/unleash-client mock with the
  same pattern used for the Firebase messaging mock — the production
  imports (default UnleashClient + named FetchTogglesStatus) don't
  match this mock's named-only export shape, but no current test
  exercises featureToggle.js so it never matters at runtime. Comment
  documents what to add if a future test runs through that saga.

Findings deliberately not changed:
- testing-guide.md "lags behind the code": reviewer's suggested
  replacements ("never stays in sync" / "never remains current")
  invert the intended meaning. Original phrasing is correct.

Suite: 115 passed (1 fewer than before — the deleted duplicate test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tuliomir tuliomir moved this from Todo to In Progress (WIP) in Hathor Network Apr 28, 2026
@tuliomir tuliomir changed the title feat: automated test suite — Layers 1-3 (unit, integration, component) feat: Automated test suite — unit, integration, component Apr 28, 2026
tuliomir and others added 2 commits April 28, 2026 16:00
- __tests__/helpers/getInitialState.js (new): shared initial-state
  helper. Both reducer test files now import from here instead of
  redefining the same one-liner. (.js, not .ts: repo ESLint has no
  TS-aware import resolver, so .ts helpers fail import/no-unresolved.)

- reducer.wallet.test.ts:
  * shape contract now uses sorted-keys equality (toEqual on a sorted
    list) instead of toHaveProperty, so a refactor that *moves* a key
    under a sub-tree fails the test. Adds a sample-uid pin on
    state.tokens to catch a hypothetical state.tokens.byId reshape.
  * action-type contract now uses minimal valid payloads (real start
    payload, real token, real wallet object) instead of {} / null,
    so a future RTK `prepare` callback validating inputs can't mask
    the type-string assertion behind a creator-level error.

- reducer.reown.test.ts: import the shared getInitialState helper.

- wallet.test.ts: tighten the wallet.stop comment to accurately
  describe what it covers (null-guard short-circuit, paired with
  clearAllMocks for cross-test isolation).

- BackupWords.test.tsx: pick the wrong word at FIXED_INDEXES[0] (i.e.
  immediately after the correct one) instead of TEST_WORDS_ARR.find
  scanning from index 0 — guarantees the wrong word is inside the
  on-screen options window regardless of future FIXED_INDEXES values.

- testing-guide.md: fix two doc-accuracy issues — replace
  mockNavigation() (no such export) with createMockNavigation /
  createMockRoute, and replace the onSetWallet ❌ example (no such
  symbol exported from reducer.js) with a real one (onResetWalletSuccess).

Suite: 115 passed, 0 failed (Node 22).
Lint: 0 errors on modified reducer files and the new helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per the PR's "low-friction for future agents" acceptance criterion,
ship the test conventions in-repo so colleagues' AI agents (Claude
Code, Cursor, Codex, etc.) can pick them up without prior context.

- AGENTS.md (root): cross-tool entry point. Lists the repo
  conventions agents must follow — pinned versions, Node-22 lock
  regen, the test rules (with pointer to docs/testing-guide.md),
  commit/PR conventions.

- CLAUDE.md (root): one-line pointer to AGENTS.md plus a note that
  the test skill auto-loads on test work.

- .claude/skills/writing-tests/SKILL.md: detailed Claude Code skill
  that auto-loads when an agent edits files under __tests__/ or
  matching *.test.{ts,tsx,js}. Encodes the conventions surfaced by
  repeated reviews — jest globals import, three-layer reducer safety
  net, helper reuse, mock-annotation pattern, anti-patterns list, and
  the verify-before-done checklist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
jestMockSetup.js (1)

95-129: ⚠️ Potential issue | 🟠 Major

Unleash mock exports incompatible symbols with featureToggle.js imports.

Production code imports default UnleashClient and FetchTogglesStatus from @hathor/unleash-client, but this mock exports named HathorUnleashClient and EVENTS instead. When the saga is tested, these imports will resolve to undefined, causing failures at the comparison FetchTogglesStatus.Updated (line 74 of featureToggle.js).

Replace this mock to export default UnleashClient (or HathorUnleashClient as default) and add FetchTogglesStatus enum matching production values.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@jestMockSetup.js` around lines 95 - 129, The mock currently exports named
HathorUnleashClient and EVENTS which don't match production imports; update the
jest.mock to export a default UnleashClient (aliasing HathorUnleashClient as the
default export) and add a named FetchTogglesStatus enum with the same members
used in production (including Updated) so imports like UnleashClient (default)
and FetchTogglesStatus.Updated resolve correctly; keep the existing methods
(getAllToggles, isEnabled, getVariant, updateContext, getContext,
setContextField, start, stop, on) and EVENTS to preserve behavior.
♻️ Duplicate comments (2)
__tests__/sagas/wallet.test.ts (1)

97-105: ⚠️ Potential issue | 🟡 Minor

Remove the vacuous mockWallet.stop assertion in the null-wallet branch.

This assertion does not validate saga behavior for Line 87 (wallet: null) because the outer mockWallet is unrelated to that branch’s state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/sagas/wallet.test.ts` around lines 97 - 105, Remove the vacuous
assertion that checks mockWallet.stop in the null-wallet branch: delete the
expect(mockWallet.stop).not.toHaveBeenCalled() assertion and keep only the
meaningful assertion verifying result.storeState.walletStartState equals
WALLET_STATUS.NOT_STARTED so the test reflects the null `wallet` branch
behavior.
jestMockSetup.js (1)

61-84: ⚠️ Potential issue | 🟠 Major

Mock shape for Firebase Messaging still mismatches production imports.

The mock returns a default callable only, while production paths import named symbols. This stays brittle and will break as soon as those code paths are exercised in tests.

#!/bin/bash
set -euo pipefail

echo "== Named imports of `@react-native-firebase/messaging` =="
rg -nP "import\\s*\\{[^}]*\\}\\s*from\\s*'@react-native-firebase/messaging'" src __tests__

echo
echo "== Current jest mock block =="
sed -n '61,90p' jestMockSetup.js
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@jestMockSetup.js` around lines 61 - 84, The current mock returns only a
default callable which breaks production code that imports named symbols; update
the jest.mock implementation so it exports the named symbols used in production
(e.g. getMessaging, getToken, deleteToken, hasPermission, requestPermission,
getInitialNotification, onMessage, onNotificationOpenedApp,
setBackgroundMessageHandler, isDeviceRegisteredForRemoteMessages,
registerDeviceForRemoteMessages, AuthorizationStatus) as properties on the
mocked module (each as jest.fn() or the correct value) and also provide a
default export if the codebase expects the factory style; ensure each named
export matches the shapes/return types used in tests (e.g. getToken returns a
Promise.resolve('mock-token'), AuthorizationStatus is the same enum object).
🧹 Nitpick comments (2)
__tests__/sagas/wallet.test.ts (1)

54-56: Prefer getInitialState helper over inline reducer init boilerplate.

The repeated reducer(undefined, { type: '@@INIT' }) setup can drift from the shared test pattern. Please reuse the centralized helper.

Refactor sketch
+import getInitialState from '../helpers/getInitialState.js';

       .withState({
-        ...reducer(undefined, { type: '@@INIT' }),
+        ...getInitialState(),
         wallet: mockWallet,
         walletStartState: WALLET_STATUS.READY,
       })

       .withState({
-        ...reducer(undefined, { type: '@@INIT' }),
+        ...getInitialState(),
         wallet: null,
         walletStartState: WALLET_STATUS.NOT_STARTED,
       })

As per coding guidelines: **/__tests__/**/*.{test,spec}.ts?(x): Reuse helpers from __tests__/helpers/ and do not redefine setup boilerplate; note that helpers use .js, not .ts.

Also applies to: 86-87

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/sagas/wallet.test.ts` around lines 54 - 56, Replace the inline
reducer initialization (reducer(undefined, { type: '@@INIT' })) with the shared
test helper getInitialState(); import or require the helper used by tests and
change occurrences like .withState({ ...reducer(undefined, { type: '@@INIT' }),
wallet: mockWallet }) to .withState({ ...getInitialState(), wallet: mockWallet
}) and do the same for the other occurrence noted (the block around the second
instance).
__tests__/screens/BackupWords.test.tsx (1)

10-11: Use shared test helpers instead of local setup boilerplate.

This test redefines navigation/render setup that should come from __tests__/helpers/ for consistency and lower maintenance.

As per coding guidelines: **/__tests__/**/*.{test,spec}.ts?(x): Reuse helpers from __tests__/helpers/ and do not redefine setup boilerplate; note that helpers use .js, not .ts, due to lack of TS-aware import resolver.

Also applies to: 106-114, 120-122

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/screens/BackupWords.test.tsx` around lines 10 - 11, Replace the
local test setup/boilerplate in BackupWords.test.tsx (the imports and any custom
navigation/render mocks) with the shared test helpers used across tests: import
the provided helper(s) (e.g., the project’s
renderWithProviders/renderWithNavigation helper) instead of directly importing
render/fireEvent/act/waitFor and avoid redefining navigation mocks; remove local
navigation/render setup code blocks and call the shared helper (e.g.,
renderWithNavigation or renderWithProviders) to render the BackupWords component
and to obtain fireEvent/waitFor utilities, noting the shared helpers are
JavaScript modules (use the .js helper import name) so import them accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@__tests__/sagas/wallet.test.ts`:
- Around line 7-47: Add explicit Jest globals import at the top of the test file
to satisfy the linter: import the needed symbols (describe, it/test, beforeEach,
expect, jest) from '@jest/globals' so usages in this file (e.g., describe,
beforeEach, jest, expect, it/test) are defined; update the top of the file to
include a single import line that brings these globals in before any test
definitions.

In `@__tests__/screens/BackupWords.test.tsx`:
- Around line 9-20: Tests rely on implicit Jest globals causing `no-undef`; add
an explicit import from '@jest/globals' at the top of the test file (e.g.,
import { describe, it, expect, jest } from '@jest/globals') so all usages like
expect(...), jest.mock(...), and any describe/it blocks are defined; place this
import above the existing imports near TEST_WORDS / FIXED_INDEXES declarations
to satisfy linting and ensure jest is available in the file.
- Around line 135-143: The test name says "shows 5 word option buttons" but the
assertion only checks that a single expected word exists; update the test body
to actually assert five option buttons exist (or rename the test to reflect the
single-word check). Concretely, in the "shows 5 word option buttons" test for
BackupWords, replace the single-word assertion with a check that queries the
option controls (e.g., getAllByRole('button') or the component's option test id)
and asserts .length === 5, or alternatively rename the it(...) string to match
the current expectation if you prefer not to change the assertion; reference
TEST_WORDS_ARR, FIXED_INDEXES and the BackupWords test block when making the
change.
- Around line 193-197: The loop uses ++ and awaits inside the loop which
triggers lint rules `no-plusplus` and `no-await-in-loop`; instead build the list
of target words from FIXED_INDEXES and dispatch all presses concurrently using
Promise.all. Specifically, compute words = FIXED_INDEXES.slice(0, 5).map(idx =>
TEST_WORDS_ARR[idx - 1]), then create an array of promises by mapping each word
to act(async () => fireEvent.press(getByText(word))) and finally await
Promise.all(promises); this replaces the for loop and references TEST_WORDS_ARR,
FIXED_INDEXES, act, fireEvent.press and getByText.

---

Outside diff comments:
In `@jestMockSetup.js`:
- Around line 95-129: The mock currently exports named HathorUnleashClient and
EVENTS which don't match production imports; update the jest.mock to export a
default UnleashClient (aliasing HathorUnleashClient as the default export) and
add a named FetchTogglesStatus enum with the same members used in production
(including Updated) so imports like UnleashClient (default) and
FetchTogglesStatus.Updated resolve correctly; keep the existing methods
(getAllToggles, isEnabled, getVariant, updateContext, getContext,
setContextField, start, stop, on) and EVENTS to preserve behavior.

---

Duplicate comments:
In `@__tests__/sagas/wallet.test.ts`:
- Around line 97-105: Remove the vacuous assertion that checks mockWallet.stop
in the null-wallet branch: delete the
expect(mockWallet.stop).not.toHaveBeenCalled() assertion and keep only the
meaningful assertion verifying result.storeState.walletStartState equals
WALLET_STATUS.NOT_STARTED so the test reflects the null `wallet` branch
behavior.

In `@jestMockSetup.js`:
- Around line 61-84: The current mock returns only a default callable which
breaks production code that imports named symbols; update the jest.mock
implementation so it exports the named symbols used in production (e.g.
getMessaging, getToken, deleteToken, hasPermission, requestPermission,
getInitialNotification, onMessage, onNotificationOpenedApp,
setBackgroundMessageHandler, isDeviceRegisteredForRemoteMessages,
registerDeviceForRemoteMessages, AuthorizationStatus) as properties on the
mocked module (each as jest.fn() or the correct value) and also provide a
default export if the codebase expects the factory style; ensure each named
export matches the shapes/return types used in tests (e.g. getToken returns a
Promise.resolve('mock-token'), AuthorizationStatus is the same enum object).

---

Nitpick comments:
In `@__tests__/sagas/wallet.test.ts`:
- Around line 54-56: Replace the inline reducer initialization
(reducer(undefined, { type: '@@INIT' })) with the shared test helper
getInitialState(); import or require the helper used by tests and change
occurrences like .withState({ ...reducer(undefined, { type: '@@INIT' }), wallet:
mockWallet }) to .withState({ ...getInitialState(), wallet: mockWallet }) and do
the same for the other occurrence noted (the block around the second instance).

In `@__tests__/screens/BackupWords.test.tsx`:
- Around line 10-11: Replace the local test setup/boilerplate in
BackupWords.test.tsx (the imports and any custom navigation/render mocks) with
the shared test helpers used across tests: import the provided helper(s) (e.g.,
the project’s renderWithProviders/renderWithNavigation helper) instead of
directly importing render/fireEvent/act/waitFor and avoid redefining navigation
mocks; remove local navigation/render setup code blocks and call the shared
helper (e.g., renderWithNavigation or renderWithProviders) to render the
BackupWords component and to obtain fireEvent/waitFor utilities, noting the
shared helpers are JavaScript modules (use the .js helper import name) so import
them accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e662efe8-9668-4e50-ae18-ccab472a3c0f

📥 Commits

Reviewing files that changed from the base of the PR and between ec3b546 and ee6848a.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • .claude/skills/writing-tests/SKILL.md
  • AGENTS.md
  • CLAUDE.md
  • __tests__/helpers/getInitialState.js
  • __tests__/reducers/reducer.reown.test.ts
  • __tests__/reducers/reducer.wallet.test.ts
  • __tests__/sagas/wallet.test.ts
  • __tests__/screens/BackupWords.test.tsx
  • docs/testing-guide.md
  • jestMockSetup.js
✅ Files skipped from review due to trivial changes (2)
  • tests/helpers/getInitialState.js
  • CLAUDE.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/reducers/reducer.reown.test.ts
  • tests/reducers/reducer.wallet.test.ts

Comment thread __tests__/sagas/wallet.test.ts
Comment thread __tests__/screens/BackupWords.test.tsx
Comment thread __tests__/screens/BackupWords.test.tsx Outdated
tuliomir and others added 2 commits April 28, 2026 19:33
Audit by a Test/QA specialist subagent against the strategic premise
"docs should cover what a frontier agent can't infer from the code,
not restate what's plain in the existing tests." Net: cut ~30%
filler, add seven concrete repo-specific gotchas, dedupe between the
two files.

SKILL.md (auto-loads on test work):
- Frontmatter description tightened to a trigger condition + scope.
- New "Repo-specific gotchas" section at the top with the seven
  concrete invariants surfaced by the audit:
  * jest-circus pinned at 29.7.0 (don't bump)
  * @hathor/wallet-lib sub-paths already mocked in jestMockSetup.js
  * legacy_createStore in mockStore.ts is intentional (no RTK yet)
  * networkSettings.test.ts is tech debt — don't pattern-match from
  * testPathIgnorePatterns carve-outs (helpers/ + nanoContracts fixture)
  * silenceTimeout idiom for sagas using delay()
  * 'bound start' matcher fragility in wallet.test.ts:147-164
- Section 4 (helpers) collapsed to the two non-obvious points.
- Section 6 (component) cut to the snapshot-blob rule only.
- Section 8 (anti-patterns) cut generic items lint already catches.
- Section 10 leads with the mutation drill.
- Escalation header: SKILL wins over testing-guide.md on conflict.

testing-guide.md (long-form / non-Claude-Code agents):
- Cut the four-layer pyramid prose; kept the policy table.
- Cut "Test the public contract", "Reuse helpers", "One test one fact"
  (all duplicated SKILL.md word-for-word).
- Cut "Running tests locally", "Coverage thresholds", "Known
  historical issues", and the CI-follow-ups appendix.
- Kept the load-bearing principles: "Don't mock what you can run",
  "Don't leak persistence", "Use real action creators" with the
  no-creator exception.
- Pointer at top: SKILL.md is canonical; this file lags it.

No code changes. Suite: 115 passed, 0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CodeRabbit's stricter ESLint pass surfaced four issues that the
repo's looser local lint config was tolerating. Local lint will be
hardened in a follow-up PR; this commit just fixes the issues.

- __tests__/sagas/wallet.test.ts: import describe/it/expect/jest/
  beforeEach from '@jest/globals' to clear no-undef per repo
  convention (also documented in .claude/skills/writing-tests/SKILL.md).

- __tests__/screens/BackupWords.test.tsx:
  * Add the same jest globals import.
  * Strengthen the 'shows 5 word option buttons' test — it previously
    only asserted the correct word was present (mismatching its name).
    Now asserts each of the 5 expected words in the visible window
    around FIXED_INDEXES[0] = 3 (TEST_WORDS_ARR[0..4]). Fixes the
    co-located max-len comment as a side effect.
  * Refactor the 5-step success-modal loop from
    `for (let i = 0; i < 5; i++) { await ... }` to a serial
    FIXED_INDEXES.reduce chain, clearing both no-plusplus and
    no-await-in-loop without parallelizing (steps must run serially).

Suite: 115 passed, 0 failed (Node 22).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
__tests__/screens/BackupWords.test.tsx (1)

174-186: Make wrong-option selection truly boundary-safe.

correctIdx + 1 is not robust for future index choices; pick a rendered option that is not the correct word.

Proposed refactor
     const correctIdx = FIXED_INDEXES[0] - 1;
     const correctWord = TEST_WORDS_ARR[correctIdx];
-    const wrongWord = TEST_WORDS_ARR[correctIdx + 1];
-    expect(wrongWord).not.toEqual(correctWord);
+    const wrongWord = TEST_WORDS_ARR.find(
+      (word) => word !== correctWord && queryByText(word),
+    );
+    expect(wrongWord).toBeTruthy();
 
     await act(async () => {
-      fireEvent.press(getByText(wrongWord));
+      fireEvent.press(getByText(wrongWord as string));
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/screens/BackupWords.test.tsx` around lines 174 - 186, The test
assumes wrongWord = TEST_WORDS_ARR[correctIdx + 1] which can break if
FIXED_INDEXES changes; instead compute the set of rendered options around
FIXED_INDEXES[0] (the same slice/indices BackupWords uses) and pick the first
entry from that renderedOptions array that is not equal to correctWord. Update
the test to derive wrongWord by scanning renderedOptions for w => w !==
correctWord (use the existing FIXED_INDEXES, TEST_WORDS_ARR, correctIdx,
correctWord identifiers) and then call fireEvent.press(getByText(wrongWord)) so
the chosen wrong option is guaranteed to be on-screen and different from the
correct word.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@__tests__/screens/BackupWords.test.tsx`:
- Around line 45-55: The mock classes MemoryStore and Storage violate the
lines-between-class-members lint rule; edit the mock so each method declaration
in class MemoryStore (getItem, setItem, cleanStorage, clearItems) and each
method/constructor in class Storage are separated by a blank line (add an empty
line between members) to satisfy lines-between-class-members; update the mock
block around the MemoryStore and Storage classes referenced here so methods are
spaced accordingly.
- Around line 9-14: Remove the inline require and useless fragment in the Portal
mock and move the BackupWords import to the top of the test file: replace the
Portal mock implementation so it no longer does require('react') and simply
returns its children directly (no <>...</> fragment), and relocate the import of
BackupWords to the top-level imports section so all imports are first; ensure
the mock still exports Portal (or the same identifier used in tests) and that
BackupWords is imported before any mocks or test code runs.

In @.claude/skills/writing-tests/SKILL.md:
- Around line 134-138: Update the doc to clarify that the ".js helpers" rule is
prospective: state that new helper files should be authored as .js to avoid
import/no-unresolved failures for consumers that lack a TS-aware resolver, and
explicitly call out existing exceptions (mockNavigation.ts, mockStore.ts,
renderWithProviders.tsx) which remain TS until eslint-import-resolver-typescript
is added; alternatively note that getInitialState.js is the current JS example
and describe which consumers (ESLint import resolver) require .js and that a
follow-up task will convert or allow TS once the resolver is configured.

---

Nitpick comments:
In `@__tests__/screens/BackupWords.test.tsx`:
- Around line 174-186: The test assumes wrongWord = TEST_WORDS_ARR[correctIdx +
1] which can break if FIXED_INDEXES changes; instead compute the set of rendered
options around FIXED_INDEXES[0] (the same slice/indices BackupWords uses) and
pick the first entry from that renderedOptions array that is not equal to
correctWord. Update the test to derive wrongWord by scanning renderedOptions for
w => w !== correctWord (use the existing FIXED_INDEXES, TEST_WORDS_ARR,
correctIdx, correctWord identifiers) and then call
fireEvent.press(getByText(wrongWord)) so the chosen wrong option is guaranteed
to be on-screen and different from the correct word.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d1ccd625-ee73-4b50-90fe-85facb3e55ef

📥 Commits

Reviewing files that changed from the base of the PR and between ee6848a and d5ff9f0.

📒 Files selected for processing (4)
  • .claude/skills/writing-tests/SKILL.md
  • __tests__/sagas/wallet.test.ts
  • __tests__/screens/BackupWords.test.tsx
  • docs/testing-guide.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/sagas/wallet.test.ts

Comment thread __tests__/screens/BackupWords.test.tsx
Comment thread __tests__/screens/BackupWords.test.tsx
Comment thread .claude/skills/writing-tests/SKILL.md Outdated
tuliomir and others added 3 commits April 28, 2026 20:24
Reviewer audit flagged that 8f9eeb2 silently shrunk nanoContract test
coverage during a refactor-driven test rewrite. Restore the lost
contracts and add equivalents for code paths whose original tests
targeted removed production code:

- restore describe('sagas/nanoContract/fetchHistory') as direct-function
  tests, adapted to the new fetchHistory(req) signature; assert the
  snake_case→camelCase tx-shape transform and authority mapping
- restore expect(fetchHistoryCall.payload.fn).toBe(fetchHistory) ref-pin
- restore termination + property-existence asserts on the success paths
  of both files
- add equivalent test for the lock short-circuit (production lines
  405-412), since the original 'history loading' test exercised a
  redux-state-based mechanism that no longer exists
- add two tests for the getBlueprintInformation throw branches (404 vs
  generic error), since the original 'getNanoContractState error' test
  exercised an API call that no longer exists

Mutation drill: flipped txId field, weakened lock check, swapped
404 failure message — each broke exactly one new assertion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CodeRabbit flagged that __tests__/utils.test.ts and
__tests__/screens/InitWallet.test.tsx use describe/it/expect/jest
without importing them from '@jest/globals'. They lint clean under
CI's `eslint .` because that defaults to .js only and silently
skips .ts/.tsx — but the import is mandatory by the writing-tests
skill rule §1, and any future shift to a TS-aware lint pass will
trip 100+ no-undef errors otherwise.

Both files still pass jest (33/33 on the affected suites).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BackupWords + InitWallet:
- add blank lines between class members in MemoryStore/Storage
  mocks and InitWallet's InvalidWords (lines-between-class-members)
- BackupWords: hoist BackupWords import to module top (import/first);
  jest.mock babel-hoists regardless of where the import sits
- BackupWords: drop dead `const React = require('react')` from the
  Portal mock (global-require) and the redundant fragment wrapper —
  ReactNode is itself a valid render value
- BackupWords: remove unused `shuffleCallCount` accumulator
- InitWallet: hoist `Switch` import out of the function body
  (global-require); narrowly suppress camelCase rule for RTL's
  upstream `UNSAFE_getByType` API; drop double space before comment

SKILL.md §4.2: clarify the "new helpers must be .js" rule is
prospective. Name getInitialState.js as the JS reference example
and explicitly list the three TS helper exceptions (mockNavigation.ts,
mockStore.ts, renderWithProviders.tsx) as scheduled-to-flip-or-keep
pending the typescript resolver.

Remaining lint error in both files is the pre-existing
`react/jsx-filename-extension`-vs-`.tsx` repo config tension
(JSX disallowed in .tsx). Out of scope; CodeRabbit didn't flag it.

Tests: 15/15 pass on the two screen suites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tuliomir tuliomir requested a review from r4mmer April 29, 2026 00:14

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@__tests__/screens/BackupWords.test.tsx`:
- Around line 138-150: The test only checks that each expected label from
expectedOptions appears, but doesn't assert the number of selectable options;
update the 'shows 5 word option buttons' test in the BackupWords spec to first
query the rendered option controls (e.g., using getAllByRole('button') or a
dedicated testID query for the option elements produced by BackupWords) and
assert the returned array length is exactly 5, then keep the existing loop that
uses getByText(word) against expectedOptions to ensure the correct labels are
present; reference the BackupWords render call, expectedOptions, and the current
getByText assertions when making the change.

In `@__tests__/screens/InitWallet.test.tsx`:
- Around line 110-119: The test currently infers the Start control is disabled
by asserting mockNavigation.navigate wasn't called; instead locate the Start
label (getByText('Start')) and assert the actual pressable wrapper
(TouchableOpacity or Pressable used in WelcomeScreen) exposes the disabled
contract—e.g., its disabled prop is true or its accessibilityState.disabled is
true—so change the assertion in InitWallet.test.tsx to check the parent
pressable component around the 'Start' text for disabled/accessibilityState
rather than relying on navigate not being called.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4812dc1e-e466-4dd8-ab18-877320fcf763

📥 Commits

Reviewing files that changed from the base of the PR and between d5ff9f0 and 1c6dc07.

📒 Files selected for processing (7)
  • .claude/skills/writing-tests/SKILL.md
  • __tests__/sagas/nanoContracts/fixtures.js
  • __tests__/sagas/nanoContracts/historyNanoContract.test.js
  • __tests__/sagas/nanoContracts/registerNanoContract.test.js
  • __tests__/screens/BackupWords.test.tsx
  • __tests__/screens/InitWallet.test.tsx
  • __tests__/utils.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/utils.test.ts
  • tests/sagas/nanoContracts/registerNanoContract.test.js
  • tests/sagas/nanoContracts/historyNanoContract.test.js

Comment thread __tests__/screens/BackupWords.test.tsx
Comment thread __tests__/screens/InitWallet.test.tsx
BackupWords: 'shows 5 word option buttons' previously verified the
expected 5 words appear but didn't pin the count — a regression that
expanded the visible window to 6+ would silently pass. Add a negative
assertion that the 6th source word ('absent', TEST_WORDS_ARR[5])
does NOT render. Equivalent to the role-based count check CodeRabbit
suggested, but works with NewHathorButton (which lacks an explicit
accessibilityRole that would let getAllByRole('button') find it).

InitWallet: 'renders the Start button as disabled initially' previously
inferred disabled-ness from "navigate wasn't called" — would also pass
for unrelated reasons (mock wiring drift, etc.). Switch to a direct
contract assertion: UNSAFE_getByType(NewHathorButton) and verify
props.disabled === true. Keep the press+navigate check as
belt-and-suspenders. (toBeDisabled() matcher would be cleaner but
@testing-library/jest-native is in deps without a setupFilesAfterEach
wiring it up — out of scope.)

Mutation drill: extending the BackupWords slice end +1 pushed 'absent'
into render → queryByText catches it. Forcing disabled={false} on the
Start button → props.disabled assertion fails. Both new assertions are
non-degenerate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tuliomir added a commit to HathorNetwork/rfcs that referenced this pull request Apr 29, 2026
Promotes 0004-automated-test-suite.md into a folder following the
0003-token-swap pattern. The deep iOS Fabric/XCUITest implementation
detail and the wallet-creation E2E walkthrough move into a companion
phase-4-e2e-deep-dive.md annex; the main RFC keeps a Phase 4 summary
with anchors into the annex.

Syncs the RFC against the proposed Layers 1-3 implementation in
HathorNetwork/hathor-wallet-mobile#863:

- name the Layer 3 helper set (renderWithProviders, createTestStore,
  createMockNavigation, getInitialState)
- add a Test layout table (layer to directory) and name jestMockSetup.js
  as the central mock registry
- name the RTK-slices safety net rationale (behavior, state-shape,
  action-type contract triple) for reducer tests
- list the additional dev deps shipped by the PR (jest-circus,
  @jest/globals, @types/jest)
- acknowledge clearLoadingLocksForTesting as the single Layer 1-3
  production-code line
- correct the path of the canonical companion docs in Future
  Possibilities #7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tuliomir added a commit to HathorNetwork/rfcs that referenced this pull request Apr 29, 2026
Promotes 0004-automated-test-suite.md into a folder following the
0003-token-swap pattern. The deep iOS Fabric/XCUITest implementation
detail and the wallet-creation E2E walkthrough move into a companion
phase-4-e2e-deep-dive.md annex; the main RFC keeps a Phase 4 summary
with anchors into the annex.

Syncs the RFC against the proposed Layers 1-3 implementation in
HathorNetwork/hathor-wallet-mobile#863:

- name the Layer 3 helper set (renderWithProviders, createTestStore,
  createMockNavigation, getInitialState)
- add a Test layout table (layer to directory) and name jestMockSetup.js
  as the central mock registry
- name the RTK-slices safety net rationale (behavior, state-shape,
  action-type contract triple) for reducer tests
- list the additional dev deps shipped by the PR (jest-circus,
  @jest/globals, @types/jest)
- acknowledge clearLoadingLocksForTesting as the single Layer 1-3
  production-code line
- correct the path of the canonical companion docs in Future
  Possibilities #7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tuliomir tuliomir requested a review from Copilot April 29, 2026 13:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds the first three layers of automated testing (unit, saga-integration, and component/screen) for hathor-wallet-mobile, plus Jest infra/mocks and contributor/agent-facing testing documentation, to make npm test green and keep future refactors (notably the reducer→RTK slices migration) safer to review.

Changes:

  • Introduces new test suites for utils, wallet lifecycle + Reown pending-requests reducers, wallet sagas, and InitWallet/BackupWords screens.
  • Updates Jest tooling/config (new dev deps, updated mocks, jest ignore/transform patterns) to support RN/WalletConnect/wallet-lib test execution.
  • Adds test-writing guidance docs and agent conventions, and a small production export (clearLoadingLocksForTesting) for test isolation.

Reviewed changes

Copilot reviewed 22 out of 23 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/sagas/nanoContract.js Exports a test-only helper to clear module-level history loading locks.
package.json Adds testing devDependencies and updates Jest config ignore/transform patterns.
package-lock.json Lockfile updates for newly added testing dependencies and transitive resolutions.
jestMockSetup.js Expands Jest mocks for Firebase, Unleash client, WalletConnect compat, camera-kit, and wallet-lib subpaths.
docs/testing-guide.md Adds a long-form testing guide describing the 4-layer pyramid and PR expectations.
__tests__/utils.test.ts Unit tests for src/utils.js helpers (amount parsing, short hash/content, token label).
__tests__/screens/InitWallet.test.tsx Component tests for InitWallet screens (terms toggle, navigation, word generation).
__tests__/screens/BackupWords.test.tsx Component tests for BackupWords 5-step validation flow with deterministic shuffling.
__tests__/sagas/wallet.test.ts Saga integration tests for wallet start/reset using redux-saga-test-plan.
__tests__/sagas/reown.test.js Fixes generator-driving expectations for Reown reject flows (incl. resync).
__tests__/sagas/nanoContracts/registerNanoContract.test.js Updates nano contract registration saga tests for blueprint-id/info behavior and error branches.
__tests__/sagas/nanoContracts/historyNanoContract.test.js Updates nano contract history saga tests and adds lock short-circuit coverage.
__tests__/sagas/nanoContracts/fixtures.js Extends nano contract fixtures to match new history payload shape.
__tests__/reducers/reducer.wallet.test.ts Reducer contract tests (behavior + state shape + action type strings) for wallet lifecycle/tokens/reset.
__tests__/reducers/reducer.reown.test.ts Reducer contract tests for state.reown.pendingRequests + action type literal.
__tests__/helpers/renderWithProviders.tsx Adds RTL helper to render with Redux + optional navigation container.
__tests__/helpers/mockStore.ts Adds helper to create a real-reducer Redux store with optional preloaded state.
__tests__/helpers/mockNavigation.ts Adds navigation/route mock factories for React Navigation 7.
__tests__/helpers/getInitialState.js Adds shared helper to retrieve the root reducer initial state.
__tests__/App.test.tsx Fixes redux mocking to support legacy_createStore usage.
CLAUDE.md Adds a pointer doc for Claude Code usage in this repo.
AGENTS.md Adds repo conventions for agents, including dependency/versioning and testing rules.
.claude/skills/writing-tests/SKILL.md Adds an auto-loaded “writing tests” skill documenting test conventions and pitfalls.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +222 to +233
firstGen.next();

// Start a second saga for the SAME ncId + same (initial) request type.
// The synchronous lock check at the top of requestHistoryNanoContract
// must observe the held lock and `return` immediately — no put(loading),
// no select, no anything observable.
const secondGen = requestHistoryNanoContract(nanoContractHistoryRequest({ ncId }));
const firstYield = secondGen.next();

// `done: true` with `value: undefined` is a generator that returned
// without yielding. That is exactly what the lock short-circuit does.
expect(firstYield).toEqual({ value: undefined, done: true });

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

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

The last test intentionally leaves firstGen paused after acquiring the module-level loading lock, and the file only clears locks in beforeEach. If this test is the last one in the file (or if this file runs before other nanoContract history tests in the same Jest worker), the held lock can leak across test files and create order-dependent failures. Prefer releasing the lock at the end of the test (e.g., drive the generator to completion / call firstGen.return() so the saga’s finally runs, or clear the locks in an afterEach).

Suggested change
firstGen.next();
// Start a second saga for the SAME ncId + same (initial) request type.
// The synchronous lock check at the top of requestHistoryNanoContract
// must observe the held lock and `return` immediately — no put(loading),
// no select, no anything observable.
const secondGen = requestHistoryNanoContract(nanoContractHistoryRequest({ ncId }));
const firstYield = secondGen.next();
// `done: true` with `value: undefined` is a generator that returned
// without yielding. That is exactly what the lock short-circuit does.
expect(firstYield).toEqual({ value: undefined, done: true });
try {
firstGen.next();
// Start a second saga for the SAME ncId + same (initial) request type.
// The synchronous lock check at the top of requestHistoryNanoContract
// must observe the held lock and `return` immediately — no put(loading),
// no select, no anything observable.
const secondGen = requestHistoryNanoContract(nanoContractHistoryRequest({ ncId }));
const firstYield = secondGen.next();
// `done: true` with `value: undefined` is a generator that returned
// without yielding. That is exactly what the lock short-circuit does.
expect(firstYield).toEqual({ value: undefined, done: true });
} finally {
// Close the first generator so its finally block runs and releases the
// module-level loading lock before other tests execute.
firstGen.return();
}

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed on 1e114e7

Comment thread package.json Outdated
Comment on lines +108 to +124
@@ -116,8 +118,10 @@
"eslint-plugin-jsx-a11y": "6.7.1",
"eslint-plugin-react": "7.32.2",
"jest": "29.7.0",
"jest-circus": "29.7.0",
"patch-package": "8.0.0",
"react-test-renderer": "18.3.1",
"redux-saga-test-plan": "^4.0.6",

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

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

New devDependencies are using caret ranges (e.g. @testing-library/*, redux-saga-test-plan). This repo’s package.json otherwise pins exact versions; leaving ranges here defeats reproducible installs / supply-chain pinning and can cause lockfile churn. Please replace these with exact versions and regenerate package-lock.json with the project’s Node/npm versions.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed on 1e114e7

Comment thread package.json Outdated
"@react-native/eslint-config": "0.72.2",
"@react-native/metro-config": "0.77.2",
"@react-native/typescript-config": "0.77.2",
"@testing-library/jest-native": "^5.4.3",

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

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

@testing-library/jest-native is deprecated (it’s marked DEPRECATED in the lockfile). Since @testing-library/react-native v12.4+ ships built-in Jest matchers, consider removing this dependency and enabling the matchers via the recommended @testing-library/react-native setup instead, to avoid relying on an unmaintained package.

Suggested change
"@testing-library/jest-native": "^5.4.3",

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed on 1e114e7

tuliomir and others added 2 commits April 29, 2026 11:28
Three Copilot findings, all real:

1. historyNanoContract.test.js — the new lock-short-circuit test
   acquires the module-level loading lock via firstGen.next() and
   never releases it. beforeEach clears locks BEFORE the next test,
   so anything between leaves stale state in the worker. Wrap the
   inner block in try/finally and call firstGen.return() to drive
   the generator's finally branch (which calls cleanupLoadingLock).

2. package.json — three test devDeps were caret-ranged
   (@testing-library/jest-native, @testing-library/react-native,
   redux-saga-test-plan), violating the repo's exact-pin rule.
   Pin all to exact versions and regenerate the lockfile on
   Node 22 / npm 10 (per CI matrix).

3. @testing-library/jest-native is deprecated and was never wired
   up in this branch (no setupFilesAfterEach loading
   extend-expect). @testing-library/react-native v12.4+ ships
   built-in Jest matchers, so the dep is dead weight. Remove it
   entirely. Lockfile loses 4 packages.

Promoted the exact-pinning rule from AGENTS.md into CLAUDE.md so
Claude Code agents see it on cold start without needing to follow
the pointer chain.

Tests: 120 passed (no delta). Lint: clean. npm ci --dry-run: clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous commit duplicated the pin-exact-versions rule into
CLAUDE.md while AGENTS.md kept its own copy and pointed *to*
CLAUDE.md — circular and a maintenance hazard. Single-source it
in AGENTS.md and reduce CLAUDE.md to a thin pointer.

AGENTS.md is also tightened to roughly the same density as the
old CLAUDE.md draft (about 25% shorter than before, same content).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tuliomir tuliomir changed the title feat: Automated test suite — unit, integration, component feat: Automated test suite — unit, component Apr 29, 2026
@tuliomir tuliomir moved this from In Progress (WIP) to In Progress (Done) in Hathor Network Apr 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: In Progress (Done)

Development

Successfully merging this pull request may close these issues.

2 participants