mirror of
https://github.com/trezor/trezor-suite.git
synced 2026-02-19 16:22:25 +01:00
feat(suite-common): Add renderHookWithStoreProvider utility
This commit is contained in:
committed by
Jiří Bažant
parent
575b875be1
commit
52be006507
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
173
docs/tests/suite-common-test-utils.md
Normal file
173
docs/tests/suite-common-test-utils.md
Normal 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
38
skills/tests-common.md
Normal 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
5
suite-common/AGENTS.md
Normal 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
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
21
suite-common/test-utils/src/renderWithStore.tsx
Normal file
21
suite-common/test-utils/src/renderWithStore.tsx
Normal 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,
|
||||
});
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user