Skip to content

Commit 0374ee8

Browse files
authored
Merge pull request #6067 from ValentinnDimitroff/feat/test-customReducers
Add customReducers to TestContext
2 parents d8d4ced + e702b3e commit 0374ee8

File tree

4 files changed

+151
-6
lines changed

4 files changed

+151
-6
lines changed

packages/ra-test/README.md

+44
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,20 @@ testUtils = render(
6565

6666
This means that reducers will work as they will within the app.
6767

68+
### Passing your custom reducers
69+
70+
If your component relies on customReducers which are passed originally to the `<Admin/>` component, you can plug them in the TestContext using the `customReducers` props:
71+
72+
```jsx
73+
testUtils = render(
74+
<TestContext enableReducers customReducers={myCustomReducers}>
75+
<MyCustomEditView />
76+
</TestContext>
77+
);
78+
```
79+
80+
Note you should also enable the default react-admin reducers in order to supply the custom ones.
81+
6882
### Spying on the store 'dispatch'
6983

7084
If you are using `useDispatch` within your components, it is likely you will want to test that actions have been dispatched with the correct arguments. You can return the `store` being used within the tests using a `renderProp`.
@@ -86,6 +100,36 @@ it('should send the user to another url', () => {
86100
});
87101
```
88102

103+
### Using the 'renderWithRedux' wrapper function
104+
105+
Instead of wrapping the component under test with the `TestContext` by yourself you can use all of the above options and test your components almost like using just `@testing-library/react` thanks to the `renderWithRedux` wrapper function.
106+
107+
It will return the same output as the `render` method from `@testing-library/react` but will add the `dispatch` and `reduxStore` helpers.
108+
109+
```jsx
110+
import { defaultStore } from 'ra-test'
111+
...
112+
const { dispatch, reduxStore, ...testUtils } = renderWithRedux(
113+
<MyCustomEditView />,
114+
initialState,
115+
options,
116+
myCustomReducers
117+
);
118+
119+
it('should initilize store', () => {
120+
const storeState = reduxStore.getState();
121+
storeState.router.location.key = ''
122+
expect(storeState).toEqual({...defaultStore, ...initialState});
123+
});
124+
125+
it('send the user to another url', () => {
126+
fireEvent.click(testUtils.getByText('Go to next'));
127+
expect(dispatch).toHaveBeenCalledWith(`/next-url`);
128+
});
129+
```
130+
131+
All of the arguments except the first one - the component under test, are optional and could be omitted by passing an empty object - `{}`
132+
89133
### Testing Permissions
90134

91135
As explained on the [Auth Provider chapter](https://marmelab.com/react-admin/Authentication.html#authorization), it's possible to manage permissions via the `authProvider` in order to filter page and fields the users can see.

packages/ra-test/src/TestContext.spec.tsx

+78-2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,35 @@ const primedStore = {
3232
},
3333
};
3434

35+
const CHANGE_FOO = 'CHANGE_FOO';
36+
37+
const customReducerInitialState = {
38+
foo: 'bar',
39+
foo2: 'bar2',
40+
};
41+
42+
const customAction = payload => ({
43+
type: CHANGE_FOO,
44+
payload,
45+
});
46+
47+
const customReducer = (prevState = customReducerInitialState, action) => {
48+
switch (action.type) {
49+
case CHANGE_FOO:
50+
return {
51+
...prevState,
52+
foo: action.payload,
53+
};
54+
default:
55+
return prevState;
56+
}
57+
};
58+
59+
const eraseRouterKey = state => {
60+
state.router.location.key = ''; // react-router initializes the state with a random key
61+
return state;
62+
};
63+
3564
describe('TestContext.js', () => {
3665
it('should render the given children', () => {
3766
const { queryAllByText } = render(
@@ -69,8 +98,7 @@ describe('TestContext.js', () => {
6998
}}
7099
</TestContext>
71100
);
72-
const initialstate = testStore.getState();
73-
initialstate.router.location.key = ''; // react-router initializes the state with a random key
101+
const initialstate = eraseRouterKey(testStore.getState());
74102
expect(initialstate).toEqual(primedStore);
75103

76104
testStore.dispatch(refreshView());
@@ -103,5 +131,53 @@ describe('TestContext.js', () => {
103131

104132
expect(testStore.getState()).toEqual(defaultStore);
105133
});
134+
135+
it('should initilize the state with customReducers initialState', () => {
136+
let testStore;
137+
render(
138+
<TestContext
139+
enableReducers={true}
140+
customReducers={{ customReducer }}
141+
>
142+
{({ store }) => {
143+
testStore = store;
144+
return <span>foo</span>;
145+
}}
146+
</TestContext>
147+
);
148+
const initialstate = eraseRouterKey(testStore.getState());
149+
150+
expect(initialstate).toEqual({
151+
...primedStore,
152+
customReducer: customReducerInitialState,
153+
});
154+
});
155+
156+
it('should update the state on customReducers action', () => {
157+
const testValue = 'test';
158+
let testStore;
159+
render(
160+
<TestContext
161+
enableReducers={true}
162+
customReducers={{ customReducer }}
163+
>
164+
{({ store }) => {
165+
testStore = store;
166+
return <span>foo</span>;
167+
}}
168+
</TestContext>
169+
);
170+
171+
testStore.dispatch(customAction(testValue));
172+
const alteredState = eraseRouterKey(testStore.getState());
173+
174+
expect(alteredState).toEqual({
175+
...primedStore,
176+
customReducer: {
177+
...customReducerInitialState,
178+
foo: testValue,
179+
},
180+
});
181+
});
106182
});
107183
});

packages/ra-test/src/TestContext.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export interface TestContextProps {
3333
initialState?: object;
3434
enableReducers?: boolean;
3535
history?: History;
36+
customReducers?: object;
3637
children: ReactNode | TextContextChildrenFunction;
3738
}
3839

@@ -69,7 +70,11 @@ export class TestContext extends Component<TestContextProps> {
6970
constructor(props) {
7071
super(props);
7172
this.history = props.history || createMemoryHistory();
72-
const { initialState = {}, enableReducers = false } = props;
73+
const {
74+
initialState = {},
75+
enableReducers = false,
76+
customReducers = {},
77+
} = props;
7378

7479
this.storeWithDefault = enableReducers
7580
? createAdminStore({
@@ -78,6 +83,7 @@ export class TestContext extends Component<TestContextProps> {
7883
Promise.resolve(dataProviderDefaultResponse)
7984
),
8085
history: createMemoryHistory(),
86+
customReducers,
8187
})
8288
: createStore(() => merge({}, defaultStore, initialState));
8389
}

packages/ra-test/src/renderWithRedux.tsx

+22-3
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,19 @@ export interface RenderWithReduxResult extends RenderResult {
1616
* initialState
1717
* );
1818
*
19+
* render with react-testing library adding redux context for unit test and passing customReducers.
20+
* @example
21+
* const { dispatch, reduxStore, ...otherReactTestingLibraryHelper } = renderWithRedux(
22+
* <TestedComponent />,
23+
* initialState,
24+
* {},
25+
* customReducers
26+
* );
27+
*
1928
* @param {ReactNode} component: The component you want to test in jsx
2029
* @param {Object} initialState: Optional initial state of the redux store
2130
* @param {Object} options: Render options, e.g. to use a custom container element
31+
* @param {Object} customReducers: Custom reducers to be added to the default store
2232
* @return {{ dispatch, reduxStore, ...rest }} helper function to test rendered component.
2333
* Same as @testing-library/react render method with added dispatch and reduxStore helper
2434
* dispatch: spy on the redux store dispatch method
@@ -27,12 +37,17 @@ export interface RenderWithReduxResult extends RenderResult {
2737
export const renderWithRedux = (
2838
component,
2939
initialState = {},
30-
options = {}
40+
options = {},
41+
customReducers = {}
3142
): RenderWithReduxResult => {
3243
let dispatch;
3344
let reduxStore;
3445
const renderResult = render(
35-
<TestContext initialState={initialState} enableReducers>
46+
<TestContext
47+
initialState={initialState}
48+
customReducers={customReducers}
49+
enableReducers
50+
>
3651
{({ store }) => {
3752
dispatch = jest.spyOn(store, 'dispatch');
3853
reduxStore = store;
@@ -46,7 +61,11 @@ export const renderWithRedux = (
4661
...renderResult,
4762
rerender: newComponent => {
4863
return renderResult.rerender(
49-
<TestContext initialState={initialState} enableReducers>
64+
<TestContext
65+
initialState={initialState}
66+
customReducers={customReducers}
67+
enableReducers
68+
>
5069
{({ store }) => {
5170
dispatch = jest.spyOn(store, 'dispatch');
5271
reduxStore = store;

0 commit comments

Comments
 (0)