feat(suite-common): Add renderHookWithStoreProvider utility

This commit is contained in:
Jirka Bažant
2026-02-18 15:48:10 +01:00
committed by Jiří Bažant
parent 575b875be1
commit 52be006507
10 changed files with 259 additions and 7 deletions

View File

@@ -57,6 +57,7 @@
- [Playwright contribution guide](./symlink/tests/e2e-playwright-contribution-guide.md)
- [GitHub Test Reporter](./symlink/tests/e2e-github-reporter.md)
- [regtest](./tests/regtest.md)
- [@suite-common/test-utils](./tests/suite-common-test-utils.md)
- [@suite-native/test-utils](./tests/suite-native-test-utils.md)
- [Miscellaneous](./misc/index.md)
- [build](./misc/build.md)

View File

@@ -8,5 +8,6 @@ This chapter contains information about tests.
- [GitHub Test Reporter](../symlink/tests/e2e-github-reporter.md)
- [regtest](./regtest.md)
- [@suite-native/test-utils](./suite-native-test-utils.md)
- [@suite-common/test-utils](./suite-common-test-utils.md)
See also general [code-style-guide/tests](../symlink/code-style-guide/tests.md) for more information about writing tests in Trezor Suite.

View File

@@ -0,0 +1,173 @@
# @suite-common/test-utils
This package provides common utilities for testing React hooks that depend on Redux store.
It also re-exports everything from `@testing-library/react`, so you should use it as a drop-in replacement.
## configureMockStore
`configureMockStore` is a utility for creating a Redux store configured for testing. It wraps `@reduxjs/toolkit`'s `configureStore` with additional testing features like action logging.
### Usage example
```ts
import { configureMockStore } from '@suite-common/test-utils';
const store = configureMockStore({
reducer: {
counter: (state = { value: 0 }, action) => {
if (action.type === 'counter/increment') {
return { value: state.value + 1 };
}
return state;
},
},
preloadedState: {
counter: { value: 5 },
},
});
// Dispatch actions
store.dispatch({ type: 'counter/increment' });
// Get current state
expect(store.getState().counter.value).toBe(6);
// Get all dispatched actions
expect(store.getActions()).toEqual([{ type: 'counter/increment' }]);
// Clear action log
store.clearActions();
```
### Configuration options
- `reducer` - Redux reducer or reducer map
- `preloadedState` - Initial state for the store
- `middleware` - Additional middleware to include
- `extra` - Extra dependencies for thunk middleware
- `serializableCheck` - Configuration for serializable check middleware
## initPreloadedState
`initPreloadedState` is a utility for merging partial state with the initial state from a reducer. This is useful when you want to override only specific parts of the state while keeping the rest at their default values.
### Usage example
```ts
import { configureMockStore, initPreloadedState } from '@suite-common/test-utils';
const rootReducer = (state = { counter: { value: 0 }, user: { name: 'John' } }, action) => {
// reducer logic
return state;
};
const preloadedState = initPreloadedState({
rootReducer,
partialState: {
counter: { value: 10 },
// user.name will remain 'John' from default state
},
});
const store = configureMockStore({
reducer: rootReducer,
preloadedState,
});
expect(store.getState()).toEqual({
counter: { value: 10 },
user: { name: 'John' },
});
```
## Testing hooks
```mermaid
flowchart TD
A{{Do you need redux store?}} -- Yes --> B[Use 'renderHookWithStoreProvider']
A -- No --> C[Use 'renderHook']
```
### Using renderHook
`renderHook` function is just re-exported from `@testing-library/react`. For more information, please refer to
the [official documentation](https://testing-library.com/docs/react-testing-library/intro/).
You should use it when your hook does not depend on any context providers.
### Using renderHookWithStoreProvider
`renderHookWithStoreProvider` is a custom utility that wraps your hook with Redux `Provider`.
Use this when your hook depends on Redux store.
**Note:** Unlike `@suite-native/test-utils`, this utility requires you to provide your own store instance.
There is no `preloadedState` option - you must create and configure the store yourself.
#### Usage example
```ts
import {
type TestStore,
act,
renderHookWithStoreProvider,
configureMockStore,
} from '@suite-common/test-utils';
describe('useCounter', () => {
const createStore = (initialValue: number) =>
configureMockStore({
reducer: {
counter: (state = { value: initialValue }, action) => {
if (action.type === 'counter/increment') {
return { value: state.value + 1 };
}
return state;
},
},
});
const renderUseCounter = (store: TestStore) =>
renderHookWithStoreProvider(() => useCounter(), { store });
it('should initialize with count from store', () => {
const store = createStore(5);
const { result } = renderUseCounter(store);
expect(result.current.count).toBe(5);
});
it('should increment count and update store', () => {
const store = createStore(0);
const { result } = renderUseCounter(store);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
expect(store.getState().counter.value).toBe(1);
});
});
```
#### Injecting custom providers
To inject a custom provider, you can use the `wrapper` option. The custom wrapper will be rendered inside
the Redux `Provider`.
```tsx
import { type TestStore, renderHookWithStoreProvider } from '@suite-common/test-utils';
const renderUseCounterA = (store: TestStore) =>
renderHookWithStoreProvider(() => useCounter(), { store, wrapper: MyCustomProvider });
const renderUseCounterB = (store: TestStore) =>
renderHookWithStoreProvider(() => useCounter(), {
store,
wrapper: ({ children }) => (
<MyCustomProvider someProp={someValue}>{children}</MyCustomProvider>
),
});
```

38
skills/tests-common.md Normal file
View File

@@ -0,0 +1,38 @@
# Writing Common Tests (TDD)
This skill enforces Test-Driven Development (TDD) practices for suite-common packages and ensures proper usage of the
`@suite-common/test-utils` package.
## Core Principles
### 1. Test-First Development
- **ALWAYS** write tests before implementing features in suite-common packages.
- Follow the Red-Green-Refactor cycle:
1. **Red**: Write a failing test that describes the desired behavior. Run test to confirm it fails for the right
reason (e.g., "Expected value to be X but got Y").
2. **Green**: Write the minimum code to make the test pass. Run test to confirm it passes.
3. **Refactor**: Clean up the code while keeping tests green. Run test to confirm it still passes after refactoring.
### 2. Writing tests
- Focus on testing behavior and user interactions, not implementation details.
- Follow arrange-act-assert pattern for test structure.
### 3. Use @suite-common/test-utils Package
- **REQUIRED**: Use `@suite-common/test-utils` for all suite-common testing that involves React hooks.
- **NEVER** import directly from `@testing-library/react` when testing hooks.
- The test-utils package provides all necessary utilities including `renderHookWithStoreProvider`.
## Checklist
Before submitting code, ensure:
- [ ] Tests were written BEFORE implementation
- [ ] Using `@suite-common/test-utils` for hook tests (not direct `@testing-library/react`)
- [ ] Correct render function used (`renderHookWithStoreProvider` for hooks needing store, `renderHook` otherwise)
- [ ] Fixtures are properly typed
- [ ] Tests focus on behavior, not implementation
- [ ] Tests follow naming conventions (.hook.test.ts, .test.ts)
- [ ] All tests pass locally

5
suite-common/AGENTS.md Normal file
View File

@@ -0,0 +1,5 @@
## Skills
Apart from [top-level AGENTS.md](../AGENTS.md), the following skills are mandatory for this package:
- [Tests Common](../skills/tests-common.md) Guidelines for writing tests for suite-common hooks and functions

View File

@@ -18,6 +18,8 @@
"@suite-common/suite-types": "workspace:*",
"@suite-common/wallet-config": "workspace:*",
"@suite-common/wallet-types": "workspace:*",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.2",
"@trezor/analytics-uploader": "workspace:*",
"@trezor/connect": "workspace:*",
"@trezor/device-utils": "workspace:*",
@@ -25,6 +27,8 @@
"@trezor/utils": "workspace:*",
"core-js": "^3.48.0",
"fake-indexeddb": "^6.2.5",
"react": "19.1.0",
"react-redux": "9.2.0",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0"
}

View File

@@ -12,6 +12,14 @@ import { mergeDeepObject } from '@trezor/utils';
import { extraDependenciesCommonMock } from './extraDependenciesCommonMock';
export type MockStoreConfig<S = any, A extends AnyAction = AnyAction> = {
middleware?: any[];
extra?: ExtraDependenciesPartial;
reducer?: Reducer<S, A, {}> | ReducersMapObject<S, A, {}>;
preloadedState?: any;
serializableCheck?: { ignoredActions?: string[] };
};
export const initPreloadedState = ({
rootReducer,
partialState,
@@ -29,13 +37,7 @@ export function configureMockStore<S = any, A extends AnyAction = AnyAction>({
reducer = (state: any) => state,
preloadedState,
serializableCheck = {},
}: {
middleware?: any[];
extra?: ExtraDependenciesPartial;
reducer?: Reducer<S, A, {}> | ReducersMapObject<S, A, {}>;
preloadedState?: any;
serializableCheck?: { ignoredActions?: string[] };
} = {}) {
}: MockStoreConfig<S, A> = {}) {
let actions: A[] = [];
const actionLoggerMiddleware = createMiddleware((action, { next }) => {

View File

@@ -2,3 +2,6 @@ export * from './mocks';
export * from './configureMockStore';
export * from './extraDependenciesCommonMock';
export * from './conditionalDescribe';
export { renderHookWithStoreProvider, type TestStore } from './renderWithStore';
export * from '@testing-library/react';

View File

@@ -0,0 +1,21 @@
import { Provider } from 'react-redux';
import { type Store } from '@reduxjs/toolkit';
import { RenderHookOptions, renderHook } from '@testing-library/react';
export type TestStore = Store;
type RenderHookOptionsExtended<Props> = RenderHookOptions<Props> & {
store: TestStore;
};
export const renderHookWithStoreProvider = <Result, Props>(
callback: (props: Props) => Result,
{ wrapper: Wrapper, store, ...options }: RenderHookOptionsExtended<Props>,
) =>
renderHook(callback, {
wrapper: ({ children }) => (
<Provider store={store}>{Wrapper ? <Wrapper>{children}</Wrapper> : children}</Provider>
),
...options,
});

View File

@@ -11543,6 +11543,8 @@ __metadata:
"@suite-common/suite-types": "workspace:*"
"@suite-common/wallet-config": "workspace:*"
"@suite-common/wallet-types": "workspace:*"
"@testing-library/dom": "npm:^10.4.1"
"@testing-library/react": "npm:^16.3.2"
"@trezor/analytics-uploader": "workspace:*"
"@trezor/connect": "workspace:*"
"@trezor/device-utils": "workspace:*"
@@ -11550,6 +11552,8 @@ __metadata:
"@trezor/utils": "workspace:*"
core-js: "npm:^3.48.0"
fake-indexeddb: "npm:^6.2.5"
react: "npm:19.1.0"
react-redux: "npm:9.2.0"
redux: "npm:^5.0.1"
redux-thunk: "npm:^3.1.0"
languageName: unknown