Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add customReducers to TestContext #6067

Merged
merged 8 commits into from
Apr 13, 2021
44 changes: 44 additions & 0 deletions packages/ra-test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,20 @@ testUtils = render(

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

### Passing your custom reducers

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:

```jsx
testUtils = render(
<TestContext enableReducers customReducers={myCustomReducers}>
<MyCustomEditView />
</TestContext>
);
```

Note you should also enable the default react-admin reducers in order to supply the custom ones.

### Spying on the store 'dispatch'

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`.
Expand All @@ -86,6 +100,36 @@ it('should send the user to another url', () => {
});
```

### Using the 'renderWithRedux' wrapper function

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.

It will return the same output as the `render` method from `@testing-library/react` but will add the `dispatch` and `reduxStore` helpers.

```jsx
import { defaultStore } from 'ra-test'
...
const { dispatch, reduxStore, ...testUtils } = renderWithRedux(
<MyCustomEditView />,
initialState,
options,
myCustomReducers
);

it('should initilize store', () => {
const storeState = reduxStore.getState();
storeState.router.location.key = ''
expect(storeState).toEqual({...defaultStore, ...initialState});
});

it('send the user to another url', () => {
fireEvent.click(testUtils.getByText('Go to next'));
expect(dispatch).toHaveBeenCalledWith(`/next-url`);
});
```

All of the arguments except the first one - the component under test, are optional and could be omitted by passing an empty object - `{}`

### Testing Permissions

As explained on the [Auth Provider chapter](./Authentication.md#authorization), it's possible to manage permissions via the `authProvider` in order to filter page and fields the users can see.
Expand Down
80 changes: 78 additions & 2 deletions packages/ra-test/src/TestContext.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,35 @@ const primedStore = {
},
};

const CHANGE_FOO = 'CHANGE_FOO';

const customReducerInitialState = {
foo: 'bar',
foo2: 'bar2',
};

const customAction = payload => ({
type: CHANGE_FOO,
payload,
});

const customReducer = (prevState = customReducerInitialState, action) => {
switch (action.type) {
case CHANGE_FOO:
return {
...prevState,
foo: action.payload,
};
default:
return prevState;
}
};

const eraseRouterKey = state => {
state.router.location.key = ''; // react-router initializes the state with a random key
return state;
};

describe('TestContext.js', () => {
it('should render the given children', () => {
const { queryAllByText } = render(
Expand Down Expand Up @@ -69,8 +98,7 @@ describe('TestContext.js', () => {
}}
</TestContext>
);
const initialstate = testStore.getState();
initialstate.router.location.key = ''; // react-router initializes the state with a random key
const initialstate = eraseRouterKey(testStore.getState());
expect(initialstate).toEqual(primedStore);

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

expect(testStore.getState()).toEqual(defaultStore);
});

it('should initilize the state with customReducers initialState', () => {
let testStore;
render(
<TestContext
enableReducers={true}
customReducers={{ customReducer }}
>
{({ store }) => {
testStore = store;
return <span>foo</span>;
}}
</TestContext>
);
const initialstate = eraseRouterKey(testStore.getState());

expect(initialstate).toEqual({
...primedStore,
customReducer: customReducerInitialState,
});
});

it('should update the state on customReducers action', () => {
const testValue = 'test';
let testStore;
render(
<TestContext
enableReducers={true}
customReducers={{ customReducer }}
>
{({ store }) => {
testStore = store;
return <span>foo</span>;
}}
</TestContext>
);

testStore.dispatch(customAction(testValue));
const alteredState = eraseRouterKey(testStore.getState());

expect(alteredState).toEqual({
...primedStore,
customReducer: {
...customReducerInitialState,
foo: testValue,
},
});
});
});
});
8 changes: 7 additions & 1 deletion packages/ra-test/src/TestContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface TestContextProps {
initialState?: object;
enableReducers?: boolean;
history?: History;
customReducers?: object;
children: ReactNode | TextContextChildrenFunction;
}

Expand Down Expand Up @@ -69,7 +70,11 @@ export class TestContext extends Component<TestContextProps> {
constructor(props) {
super(props);
this.history = props.history || createMemoryHistory();
const { initialState = {}, enableReducers = false } = props;
const {
initialState = {},
enableReducers = false,
customReducers = {},
} = props;

this.storeWithDefault = enableReducers
? createAdminStore({
Expand All @@ -78,6 +83,7 @@ export class TestContext extends Component<TestContextProps> {
Promise.resolve(dataProviderDefaultResponse)
),
history: createMemoryHistory(),
customReducers,
})
: createStore(() => merge({}, defaultStore, initialState));
}
Expand Down
25 changes: 22 additions & 3 deletions packages/ra-test/src/renderWithRedux.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,19 @@ export interface RenderWithReduxResult extends RenderResult {
* initialState
* );
*
* render with react-testing library adding redux context for unit test and passing customReducers.
* @example
* const { dispatch, reduxStore, ...otherReactTestingLibraryHelper } = renderWithRedux(
* <TestedComponent />,
* initialState,
* {},
* customReducers
* );
*
* @param {ReactNode} component: The component you want to test in jsx
* @param {Object} initialState: Optional initial state of the redux store
* @param {Object} options: Render options, e.g. to use a custom container element
* @param {Object} customReducers: Custom reducers to be added to the default store
* @return {{ dispatch, reduxStore, ...rest }} helper function to test rendered component.
* Same as @testing-library/react render method with added dispatch and reduxStore helper
* dispatch: spy on the redux store dispatch method
Expand All @@ -27,12 +37,17 @@ export interface RenderWithReduxResult extends RenderResult {
export const renderWithRedux = (
component,
initialState = {},
options = {}
options = {},
customReducers = {}
): RenderWithReduxResult => {
let dispatch;
let reduxStore;
const renderResult = render(
<TestContext initialState={initialState} enableReducers>
<TestContext
initialState={initialState}
customReducers={customReducers}
enableReducers
>
{({ store }) => {
dispatch = jest.spyOn(store, 'dispatch');
reduxStore = store;
Expand All @@ -46,7 +61,11 @@ export const renderWithRedux = (
...renderResult,
rerender: newComponent => {
return renderResult.rerender(
<TestContext initialState={initialState} enableReducers>
<TestContext
initialState={initialState}
customReducers={customReducers}
enableReducers
>
{({ store }) => {
dispatch = jest.spyOn(store, 'dispatch');
reduxStore = store;
Expand Down