hope someone can point me the right direction with this. Basically I've created a react app which makes use of hooks, specifically useContext, useEffect and useReducer. My problem is that I can't seem to get tests to detect click or dispatch events of the related component.
Stripped down version of my app can be found at : https://github.com/atlantisstorm/hooks-testing Tests relate to layout.test.js script.
I've tried various approaches, different ways of mocking dispatch, useContext, etc but no joy with it. Most recent version.
layout.test.js
import React from 'react';
import { render, fireEvent } from "@testing-library/react";
import Layout from './layout';
import App from './app';
import { Provider, initialState } from './context';
const dispatch = jest.fn();
const spy = jest
.spyOn(React, 'useContext')
.mockImplementation(() => ({
state: initialState,
dispatch: dispatch
}));
describe('Layout component', () => {
it('starts with a count of 0', () => {
const { getByTestId } = render(
<App>
<Provider>
<Layout />
</Provider>
</App>
);
expect(dispatch).toHaveBeenCalledTimes(1);
const refreshButton = getByTestId('fetch-button');
fireEvent.click(refreshButton);
expect(dispatch).toHaveBeenCalledTimes(3);
});
});
layout.jsx
import React, { useContext, useEffect } from 'react';
import { Context } from "./context";
const Layout = () => {
const { state, dispatch } = useContext(Context);
const { displayThings, things } = state;
const onClickDisplay = (event) => {
// eslint-disable-next-line
event.preventDefault;
dispatch({ type: "DISPLAY_THINGS" });
};
useEffect(() => {
dispatch({ type: "FETCH_THINGS" });
}, [displayThings]);
const btnText = displayThings ? "hide things" : "display things";
return (
<div>
<button data-testid="fetch-button" onClick={onClickDisplay}>{btnText}</button>
{ displayThings ?
<p>We got some things!</p>
:
<p>No things to show!</p>
}
{ displayThings && things.map((thing) =>
<p>{ thing }</p>
)}
</div>
)
}
export default Layout;
app.jsx
import React from 'react';
import Provider from "./context";
import Layout from './layout';
const App = () => {
return (
<Provider>
<Layout />
</Provider>
)
}
export default App;
context.jsx
import React, { createContext, useReducer } from "react";
import { reducer } from "./reducer";
export const Context = createContext();
export const initialState = {
displayThings: false,
things: []
};
export const Provider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<Context.Provider value={{ state, dispatch }}>
{children}
</Context.Provider>
);
};
export default Provider;
reducer.jsx
export const reducer = (state, action) => {
switch (action.type) {
case "DISPLAY_THINGS": {
const displayThings = state.displayThings ? false : true;
return { ...state, displayThings };
}
case "FETCH_THINGS": {
const things = state.displayThings ? [
"thing one",
"thing two"
] : [];
return { ...state, things };
}
default: {
return state;
}
}
};
I'm sure the answer will be easy when I see it, but just trying to figure out I can detect the click event plus detect the 'dispatch' events? (I've already got separate test in the main app to properly test dispatch response/actions)
Thank you in advance.
EDIT Ok, I think I've got a reasonable, though not perfect, solution. First I just added optional testDispatch and testState props to the context.jsx module.
new context.jsx
import React, { createContext, useReducer } from "react";
import { reducer } from "./reducer";
export const Context = createContext();
export const initialState = {
displayThings: false,
things: []
};
export const Provider = ({ children, testDispatch, testState }) => {
const [iState, iDispatch] = useReducer(reducer, initialState);
const dispatch = testDispatch ? testDispatch : iDispatch;
const state = testState ? testState : iState;
return (
<Context.Provider value={{ state, dispatch }}>
{children}
</Context.Provider>
);
};
export default Provider;
Then in layout.test.jsx I just simply pass in mocked jest dispatch function plus state as necessary. Also removed the outer App wrapping as that seemed to prevent the props from being passed through.
new layout.test.jsx
import React from 'react';
import { render, fireEvent } from "@testing-library/react";
import Layout from './layout';
import { Provider } from './context';
describe('Layout component', () => {
it('starts with a count of 0', () => {
const dispatch = jest.fn();
const state = {
displayThings: false,
things: []
};
const { getByTestId } = render(
<Provider testDispatch={dispatch} testState={state}>
<Layout />
</Provider>
);
expect(dispatch).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenNthCalledWith(1, { type: "FETCH_THINGS" });
const refreshButton = getByTestId('fetch-things-button');
fireEvent.click(refreshButton);
expect(dispatch).toHaveBeenCalledTimes(2);
// Important: The order of the calls should be this, but dispatch is reporting them
// the opposite way around in the this test, i.e. FETCH_THINGS, then DISPLAY_THINGS...
//expect(dispatch).toHaveBeenNthCalledWith(1, { type: "DISPLAY_THINGS" });
//expect(dispatch).toHaveBeenNthCalledWith(2, { type: "FETCH_THINGS" });
// ... so as dispatch behaves correctly outside of testing for the moment I'm just settling for
// knowing that dispatch was at least called twice with the correct parameters.
expect(dispatch).toHaveBeenCalledWith({ type: "DISPLAY_THINGS" });
expect(dispatch).toHaveBeenCalledWith({ type: "FETCH_THINGS" });
});
});
One little caveat though, as noted above, when the 'fetch-things-button' was fired, it reported the dispatch in the wrong order. :/ So I just settled for knowing the correct calls where triggered, but if anyone knows why the call order isn't as expected I would be pleased to know.
https://github.com/atlantisstorm/hooks-testing update to reflect the above if anyone is interested.