Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ecd05ec
feat: add automated test suite PoC — 57 tests across 3 layers
tuliomir Apr 28, 2026
8f9eeb2
fix: make entire test suite pass — fix pre-existing test failures
tuliomir Apr 14, 2026
67c4b44
chore: allow-scripts entry for jest-circus's unrs-resolver
tuliomir Apr 28, 2026
4f4e9d3
fix: align reown.test.js with rejectAll saga's actual yield order
tuliomir Apr 28, 2026
bdbf958
test: add slice-refactor safety net + testing guide
tuliomir Apr 28, 2026
49a4e57
chore: address review nits + pin jest-circus
tuliomir Apr 28, 2026
ec3b546
docs: annotate review findings that were intentionally not fixed
tuliomir Apr 28, 2026
940f684
fix(ci): regenerate package-lock.json with Node 22
tuliomir Apr 28, 2026
0864921
fix(lint): import jest globals in reducer test files
tuliomir Apr 28, 2026
d19358e
chore: address second review pass — lint hygiene + duplicate test
tuliomir Apr 28, 2026
26638c6
test: address third-pass review — strengthen contracts, dedupe helper
tuliomir Apr 28, 2026
ee6848a
docs: add AGENTS.md, CLAUDE.md, and writing-tests skill
tuliomir Apr 28, 2026
7a20c5f
docs: refine test docs per QA audit — gotchas first, less filler
tuliomir Apr 28, 2026
d5ff9f0
fix(test): address CodeRabbit lint findings on PR #863
tuliomir Apr 28, 2026
86e8352
test: restore nanoContract coverage lost in 8f9eeb2
tuliomir Apr 28, 2026
037b6e8
fix(test): import jest globals in utils + InitWallet tests
tuliomir Apr 28, 2026
1c6dc07
fix(test): address CodeRabbit lint findings on screen tests
tuliomir Apr 29, 2026
adeff1a
test: tighten screen test contracts per CodeRabbit nitpicks
tuliomir Apr 29, 2026
1e114e7
fix: address Copilot review on PR #863
tuliomir Apr 29, 2026
a5ae036
docs: break circular AGENTS.md ↔ CLAUDE.md pointer
tuliomir Apr 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion __tests__/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,18 @@ import renderer from 'react-test-renderer';

jest.mock('redux-saga', () => () => ({ run: jest.fn() }));
jest.mock('redux', () => ({
createStore: jest.fn(),
createStore: jest.fn(() => ({
getState: jest.fn(() => ({})),
dispatch: jest.fn(),
subscribe: jest.fn(),
replaceReducer: jest.fn(),
})),
legacy_createStore: jest.fn(() => ({
getState: jest.fn(() => ({})),
dispatch: jest.fn(),
subscribe: jest.fn(),
replaceReducer: jest.fn(),
})),
applyMiddleware: jest.fn(),
}));

Expand Down
42 changes: 42 additions & 0 deletions __tests__/helpers/mockNavigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Test helper: mock navigation objects for React Navigation 7.
*
* Provides plain mock objects that can be passed as props or returned
* from `useNavigation()`/`useRoute()` in a `jest.mock(...)` factory at
* module scope in the consuming test file.
*
* Note: this file does NOT call `jest.mock(...)` itself. `jest.mock` is
* hoisted by Babel only when written at top-level of the test module —
* wrapping it inside an exported helper would defeat the hoist and run
* after the modules under test are already imported.
*/
import { jest } from '@jest/globals';

export function createMockNavigation() {
return {
navigate: jest.fn(),
push: jest.fn(),
replace: jest.fn(),
goBack: jest.fn(),
reset: jest.fn(),
setOptions: jest.fn(),
setParams: jest.fn(),
dispatch: jest.fn(),
addListener: jest.fn(() => jest.fn()), // returns unsubscribe
canGoBack: jest.fn(() => true),
isFocused: jest.fn(() => true),
getParent: jest.fn(),
getState: jest.fn(() => ({
index: 0,
routes: [{ name: 'MockRoute', key: 'mock-key' }],
})),
};
}

export function createMockRoute(params: Record<string, unknown> = {}) {
return {
key: 'mock-route-key',
name: 'MockRoute',
params,
};
}
21 changes: 21 additions & 0 deletions __tests__/helpers/mockStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Test helper: creates a Redux store with optional preloaded state.
*
* Uses the real reducer from the app so that dispatched actions
* produce the same state transitions as production code.
*/
import { legacy_createStore as createStore } from 'redux';
import { reducer } from '../../src/reducers/reducer';

/**
* Create a Redux store for testing with optional state overrides.
*
* @param preloadedState - Partial state merged on top of the reducer's
* built-in initialState. Pass only the keys you care about.
*/
export function createTestStore(preloadedState: Record<string, unknown> = {}) {
// Dispatching an unknown action returns initialState from the reducer,
// which we then merge with the caller's overrides.
const baseState = reducer(undefined, { type: '@@INIT' });
return createStore(reducer, { ...baseState, ...preloadedState } as any);
}
46 changes: 46 additions & 0 deletions __tests__/helpers/renderWithProviders.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Test helper: renders a React component wrapped in all the providers
* that the Hathor Wallet app expects (Redux, Navigation).
*
* Usage:
* const { getByText } = renderWithProviders(<MyScreen />, {
* preloadedState: { isOnline: true },
* });
*/
import React from 'react';
import { render, RenderOptions } from '@testing-library/react-native';
import { Provider } from 'react-redux';
import { NavigationContainer } from '@react-navigation/native';
import { createTestStore } from './mockStore';

interface ExtendedRenderOptions extends Omit<RenderOptions, 'wrapper'> {
/** Partial Redux state merged on top of initialState. */
preloadedState?: Record<string, unknown>;
/** Supply your own store (e.g. if you need to dispatch in the test). */
store?: ReturnType<typeof createTestStore>;
/** If false, skip wrapping in NavigationContainer (default: true). */
withNavigation?: boolean;
}

export function renderWithProviders(
ui: React.ReactElement,
{
preloadedState = {},
store = createTestStore(preloadedState),
withNavigation = true,
...renderOptions
}: ExtendedRenderOptions = {},
) {
function Wrapper({ children }: { children: React.ReactNode }) {
const content = <Provider store={store}>{children}</Provider>;
if (withNavigation) {
return <NavigationContainer>{content}</NavigationContainer>;
}
return content;
}

return {
store,
...render(ui, { wrapper: Wrapper, ...renderOptions }),
};
}
88 changes: 88 additions & 0 deletions __tests__/reducers/reducer.reown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* Unit tests for the Reown pending-requests reducer slice.
*
* Covers the slice introduced on this branch:
* - state.reown.pendingRequests
* - REOWN_SET_PENDING_REQUESTS action
*
* Acts as a safety net for the future RTK-slices refactor by pinning:
* - Behavior (action -> state)
* - Initial state shape of state.reown
* - Action-type literal string
*/
import { reducer } from '../../src/reducers/reducer';
import { setReownPendingRequests, types } from '../../src/actions';

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const getInitialState = () => reducer(undefined, { type: '@@INIT' });

// ─── Behavior ──────────────────────────────────────────────────────────────
describe('REOWN_SET_PENDING_REQUESTS', () => {
it('initial state.reown.pendingRequests is an empty array', () => {
expect(getInitialState().reown.pendingRequests).toEqual([]);
});

it('replaces pendingRequests with the payload (does not append)', () => {
let state = reducer(getInitialState(), setReownPendingRequests([{ id: 'a' }]));
expect(state.reown.pendingRequests).toEqual([{ id: 'a' }]);

state = reducer(state, setReownPendingRequests([{ id: 'b' }, { id: 'c' }]));
expect(state.reown.pendingRequests).toEqual([{ id: 'b' }, { id: 'c' }]);
});

it('accepts an empty array (clears pending)', () => {
let state = reducer(getInitialState(), setReownPendingRequests([{ id: 'a' }]));
state = reducer(state, setReownPendingRequests([]));
expect(state.reown.pendingRequests).toEqual([]);
});

it('does not mutate other reown sub-keys', () => {
const initial = getInitialState();
const next = reducer(initial, setReownPendingRequests([{ id: 'a' }]));
expect(next.reown.sessions).toBe(initial.reown.sessions);
expect(next.reown.connectionFailed).toBe(initial.reown.connectionFailed);
expect(next.reown.modal).toBe(initial.reown.modal);
expect(next.reown.client).toBe(initial.reown.client);
expect(next.reown.error).toBe(initial.reown.error);
});

it('returns a new state reference (no in-place mutation)', () => {
const initial = getInitialState();
const next = reducer(initial, setReownPendingRequests([{ id: 'a' }]));
expect(next).not.toBe(initial);
expect(next.reown).not.toBe(initial.reown);
});
});

// ─── Initial-State Shape Contract ──────────────────────────────────────────
// Pins the keys of state.reown. A future RTK-slices refactor must preserve
// this shape OR consciously update this snapshot.
describe('initial state.reown shape contract', () => {
it('exposes the expected reown keys', () => {
const reownKeys = Object.keys(getInitialState().reown).sort();
expect(reownKeys).toEqual([
'client',
'connectionFailed',
'createNanoContractCreateTokenTx',
'createToken',
'error',
'forceNavigateToDashboard',
'modal',
'newNanoContractTransaction',
'pendingRequests',
'sendTransaction',
'sessions',
]);
});
});

// ─── Action-Type Contract ──────────────────────────────────────────────────
// Pins the literal `.type` strings; catches RTK auto-renaming.
describe('action-type contract', () => {
it('setReownPendingRequests.type', () => {
expect(setReownPendingRequests([]).type).toBe('REOWN_SET_PENDING_REQUESTS');
});

it('types.REOWN_SET_PENDING_REQUESTS literal string', () => {
expect(types.REOWN_SET_PENDING_REQUESTS).toBe('REOWN_SET_PENDING_REQUESTS');
});
});
Loading
Loading