Testing method inside functional component in React using Jest / Enzyme - javascript

Following is my React Functional Component which I am trying to test using jest / enzyme.
React Functional Component Code -
export const UserForm = props => {
const {labels, formFields, errorMessages} = props;
const [showModal, setShowModal] = React.useState(false);
const [newId, setNewId] = React.useState('');
const showModal = () => {
setShowModal(true);
}
const closeModal = () => {
setShowModal(false);
};
const handleSubmit = data => {
Post(url, data)
.then(resp => {
const userData = resp.data;
setNewId(() => userData.id);
showModal();
})
}
return (
<div className="user-form">
<UserForm
fields={formFields}
handleSubmit={handleSubmit}
labels={labels}
errors={errorMessages}
/>
{showModal && <Modal closeModal={closeModal}>
<div className="">
<h3>Your new id is - {newId}</h3>
<Button
type="button"
buttonLabel="Close"
handleClick={closeModal}
classes="btn btn-close"
/>
</div>
</Modal>}
</div>
)
};
Now I am trying to test showModal, closeModal and handleSubmit method, but my tests are failing. Let me know the correct way of testing React Hooks and methods inside functional component.
My test case -
import React from 'react';
import { UserForm } from '../index';
import { shallow } from 'enzyme';
describe('<UserForm />', () => {
let wrapper;
const labels = {
success: 'Success Message'
};
const formFields = [];
const errorMessages = {
labels: {
firstName: 'First Name Missing'
}
};
function renderShallow() {
wrapper = shallow(<UserForm
labels={labels}
formFields={formFields}
errorMessages={errorMessages}
/>);
}
it('should render with props(snapshot)', () => {
renderShallow();
expect(wrapper).toMatchSnapshot();
});
it('should test showModal method', () => {
const mockSetShowModal = jest.fn();
React.useState = jest.fn(() => [false, mockSetShowModal]);
renderShallow();
expect(mockSetShowModal).toHaveBeenCalledWith(true);
});
});
Error I am getting -
Expected mock function to have been called with:
[true]
But it was not called.
Let me know how can i test the showModal, closeModal and handleSubmit methods in a functional component.

Generally, functional components in React aren't meant to be tested in that way. The React team are suggesting that you use the approach of React Testing Library which is more focused on the actual user interface scenarios. Instead of testing React component instances, you're testing DOM nodes.
This is the main reason why people are moving away from Enzyme and starting to use RTL, because you want to avoid testing implementation details as much as you can.

Related

react testing library toHaveBeenCalled 0 mocking a simple click prop

I try to mock a prop called actionClicked but my test failed, I have a simple component as below
const ButtonAction: React.FC = ({ actionClicked }) => {
const handleClick = (action) => () => actionClicked(action)
return (
<div
data-testid="some-action"
onClick={handleClick('something')}
>
<Icon />
</div>
)
}
this is my test, button.spec.tsx
import { render, screen } from '#testing-library/react'
import userEvent from '#testing-library/user-event'
import React from 'react'
import ButtonAction from './ButtonAction'
test('render ButtonAction', () => {
const actionClicked = jest.fn()
render(<ButtonAction actionClicked={actionClicked} />)
userEvent.click(screen.getByTestId('some-action'))
expect(actionClicked).toHaveBeenCalled() //failed here
})
I'm not sure why actionClicked is not been called.
Expected number of calls: >= 1
Received number of calls: 0
Any thoughts?
This behaviour occurs because userEvent.click returns a promise.
First you should turn your test to async and then await userEvent.click:
test("render ButtonAction", async () => {
const actionClicked = jest.fn();
render(<ButtonAction actionClicked={actionClicked} />);
await userEvent.click(screen.getByTestId("some-action"));
expect(actionClicked).toHaveBeenCalledTimes(1);
});

Create the same ref functionality in React Function component as in React Class component

I would like to create the exact same behaviour in function component ref as there is in class component ref.
Heres an example:
const AnotherComponent = () => {
const DoSomething () => console.log(something);
return <div> There is a content </div>;
}
const MyComponent () => {
let newRef = useRef();
useEffect(() => {
newRef.current.DoSomething();
}, []);
return <AnotherComponent ref={newRef} />;
}
I know about forwardRef, but from what I understand it allows you to bind the ref to a specific input to get its events, but not to the WHOLE component with all its functions.
Is it even possible to achieve this with function components ?
Yes, it's completely possible. React refs are for more than just getting access to DOMNodes. Use a combination of React.forwardRef and the useImperativeHandle React hook to forward a React ref to a function component and expose out functions/values.
const AnotherComponent = React.forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
doSomething
}));
const doSomething () => console.log(something);
return <div> There is a content </div>;
});
...
const MyComponent () => {
const newRef = useRef();
useEffect(() => {
newRef.current.doSomething();
}, []);
return <AnotherComponent ref={newRef} />;
}
Forwarding refs
forwardRef
useImperativeHandle

Jest snapshot testing TypeError: Invalid attempt to destructure non-iterable instance

I have a component in my React codebase and I am trying to create some snapshot tests for it, but unfortunately there is some weird error going on. Jest implies that I'm doing some destructuring on non-iterable instance and logs shows something that however much I tried to think about it, it would work perfectly, but I don't know what is wrong with this snippet of code shown in image below:
Also please note that I did a mock for formik in my component like below:
jest.mock('formik', () => ({
useField: jest.fn(),
useFormikContext: jest.fn(),
}));
and every other question related to the topic either is not for Jest or answer is not working and not even accepted!
Here is my test file:
import React from 'react';
import renderer from 'react-test-renderer';
import MyComponent from '../MyComponent';
jest.mock('../MyComponentManager');
jest.mock('formik', () => ({
useField: jest.fn(),
useFormikContext: jest.fn().mockReturnValue({ isValidating: false, setFieldValue: () => {} }),
}));
describe('MyComponent', () => {
test('should render component properly', () => {
const component = renderer.create(<MyComponent />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});
And here is the minor version of the component that is being testing:
const MyComponent = () => {
<SomeWrapperComponent>
...some jsx
<NumberField {...props} />
</SomeWrapperComponent>
}
and the error happens in side the <NumberField /> component.
I believe your problem is setting useFormikContext: jest.fn(). jest.fn() by default returns a function that returns undefined as mentioned in jest docs. and by that you try to deconstruct undefined.
you need to mock the return value of useFormikContext function:
jest.mock('formik', () => ({
useField: jest.fn(),
useFormikContext: jest.fn().mockReturnValue({ isValidating: false, setFieldValue: () => {} }),
}));
If that doesnt help, please add full and simple runnable example that's not working, so I can take a deeper look. (i.e. test file and code file)
Edit
I see you only mocked useField useFormikContext functions of formik module, although you probably use more from formik module, like Formik tag and such, but they're now overriden.
jest.mock overrides the whole module, and not suitable for your use case, because you only want to mock only useField & useFormikContext.
I like to use jest.spyOn which can be used to mock only specific functions of a module. see jest docs on jest.spyOn.
Here's full working small example mocking formik in jest react testing.
example.js - react component that's being tested.
example.spec.js - simple jest test
example.js:
import React from 'react';
import { useFormikContext, Formik, Form, Field } from 'formik';
export const OtherComponent = () => {
// Grab values and submitForm from context
const { isValidating, setFieldValue } = useFormikContext();
return (
<div>
<div>{isValidating ? 'validating...' : 'validated!'}</div>
<button type="submit" disabled={isValidating}>
submit
</button>
<button
type="button"
disabled={isValidating}
onClick={() => {
setFieldValue('token', '123456');
}}
>
return to default
</button>
</div>
);
};
export const ExmpleComponent = () => {
return (
<div>
<h1>Example</h1>
<Formik
initialValues={{ token: '' }}
validate={(values) => {
return new Promise((resolve, _reject) => setTimeout(resolve, 3000)).then(() => {
// wait 3 seconds
const errors = {}; // no errors
return errors;
});
}}
onSubmit={(_values, _actions) => {
console.log('submitted!');
}}
>
<Form>
<Field name="token" type="tel" />
<OtherComponent />
</Form>
</Formik>
</div>
);
};
example.spec.js:
import React from 'react';
import renderer from 'react-test-renderer';
import * as formik from 'formik';
import { ExmpleComponent } from '../src/example.js';
// jest.mock('formik', () => ({
// useField: jest.fn(),
// useFormikContext: jest.fn().mockReturnValue({ isValidating: false, setFieldValue: () => {} }),
// }));
describe('MyComponent', () => {
test('should render component properly', () => {
const useFieldSpy = jest.spyOn(formik, 'useField');
const useFormikContextSpy = jest.spyOn(formik, 'useFormikContext');
useFieldSpy.mockReturnValue(undefined);
useFormikContextSpy.mockReturnValue({ isValidating: true, setFieldValue: () => {} });
const component = renderer.create(<ExmpleComponent />);
const tree = component.toJSON();
expect(tree.children[1].children[1].children[0].children[0]).toBe('validating...');
useFieldSpy.mockRestore();
useFormikContextSpy.mockRestore();
});
});
If that still doesnt work, please add full & simple example that I can run to test by myself to see what's wrong in your code. You added full example of the test file, but not the code you're testing.
For contribute, I solved similar issue using this code:
jest.mock('formik', () => ({
useField: jest.fn(),
useFormikContext: jest.fn().mockReturnValue({
setFieldValue: () => {},
handleChange: () => {},
initialValues: {} as AddDoctorInfoFormValues,
errors: {} as FormikErrors<AddDoctorInfoFormValues>,
values: {} as AddDoctorInfoFormValues,
}),
}));

Dynamic import of react hooks

Can we dynamically import hooks based on the value passed to the component?
For eg.
App.js
<BaseComponent isActive />
BaseComponent.js
if(props.isActive) {
// import useActive (custom Hook)
}
I donot want these(custom hook) files to be imported and increase the size of BaseComponent even when the props contain falsey values.
You can dynamically import hooks as it is just a function (using require), but you shouldn't because you can't use hooks inside conditions.
See Rules of Hooks
Only call Hooks at the top level. Don’t call Hooks inside loops, conditions, or nested functions.
If you want conditionally use a hook, use the condition in its implementation (look for example at skip option of useQuery hook from Apollo GraphQL Client).
const useActive = (isUsed) => {
if (isUsed) {
// logic
}
}
You should extract the logic inside the useActive hook and dynamically import it instead of dynamically importing the hook since you should not call Hooks inside loops, conditions, or nested functions., checkout Rules of Hooks:
Let's say your useActive hook was trying to update the document title (in real world, it has to be a big chunk of code that you would consider using dynamic import)
It might be implemented as below:
// useActive.js
import { useEffect } from "react";
export function useActive() {
useEffect(() => {
document.title = "(Active) Hello World!";
}, []);
}
And you tried to use it in the BaseComponent:
// BaseComponent.js
function BaseComponent({ isActive }) {
if (isActive) { // <-- call Hooks inside conditions ❌
import("./useActive").then(({ useActive }) => {
useActive();
});
}
return <p>Base</p>;
}
Here you violated the rule "don't call Hooks inside conditions" and will get an Invalid hook call. error.
So instead of dynamically import the hook, you can extract the logic inside the hook and dynamically import it:
// updateTitle.js
export function updateTitle() {
document.title = "(Active) Hello World!"
}
And you do the isActive check inside the hook:
// BaseComponent.js
function BaseComponent({ isActive }) {
useEffect(() => {
if (!isActive) return;
import("./updateTitle").then(({ updateTitle }) => {
updateTitle();
});
}, [isActive]);
return <p>Base</p>;
}
It works fine without violating any rules of hooks.
I have attached a CodeSandbox for you to play around:
You could create a Higher Order Component that fetches the hook and then passes it down as a prop to a wrapped component. By doing so the wrapped component can use the hook without breaking the rules of hooks, eg from the wrapped component's point of view, the reference to the hook never changes and the hook gets called everytime the wrapped component renders. Here is what the code would look like:
export function withDynamicHook(hookName, importFunc, Component) {
return (props) => {
const [hook, setHook] = useState();
useEffect(() => {
importFunc().then((mod) => setHook(() => mod[hookName]));
}, []);
if (!hook) {
return null;
}
const newProps = { ...props, [hookName]: hook };
return <Component {...newProps} />;
};
}
// example of a Component using it:
const MyComponent = ({useMyHook}) => {
let something = useMyHook();
console.log(something)
return <div>myHook returned something, see the console to inspect it </div>
}
const MyComponentWithHook = withDynamicHook('useMyHook', () => import('module-containing-usemyhook'), MyComponent)
To whoever encountered it as well: You can't use Hooks inside dynamically imported components(however, apparently if you does not use hooks even the first example works):
instead of:
const useDynamicDemoImport = (name) => {
const [comp, setComp] = useState(null);
useEffect(() => {
let resolvedComp = false;
import(`#site/src/demos/${name}`)
.then((m) => {
if (!resolvedComp) {
resolvedComp = true;
setComp(m.default);
}
})
.catch(console.error);
return () => {
resolvedComp = true;
};
}, []);
return comp;
};
const DemoPreviewer: FC<DemoPreviewerProps> = (props) => {
comp = useDynamicDemoImport(props.name);
return (
<Paper sx={{ position: "relative" }}>
{comp}
</Paper>
);
};
export default DemoPreviewer
use React Lazy instead and render the component later
const useDynamicDemoImport = (name) => {
const Comp = React.lazy(() => import(`#site/src/demos/${name}`));
return comp;
};
const RootDemoPreviewer: FC<DemoPreviewerProps> = (props) => {
console.log("RootDemoPreviewer");
return (
<React.Suspense fallback={<div>Loading...</div>}>
<DemoPreviewer {...props} />
</React.Suspense>
);
};
const DemoPreviewer: FC<DemoPreviewerProps> = (props) => {
const Comp = useDynamicDemoImport(props.name);
return (
<Paper sx={{ position: "relative" }}>
<Comp />
</Paper>
);
};
export default RootDemoPreviewer

How do I test a react hook method?

I am using jest/istanbul to track code coverage. I have the following component:
// Menu.jsx
const myComponent = ({ initialState }) = {
const [state, setState] = useState(initialState);
const [menuContent, setMenuContent] = useState(undefined);
return (
<Menu>
{state.accordions.map(item, index) => (
<MenuPanel id={item.id}>
{item.details && typeof item.details === 'function'
? <item.details setContent={myContent => setMenuContent(myContent)} />
: undefined}
</MenuPanel>
)}
</Menu>
)
}
In my test for Menu.jsx the jest coverage report is complaining that setMenuContent is not covered in my tests. How am I supposed to test a hook like that? I thought it wasn't possible. I tried testing that the setContent prop exists on the subcomponent but that didn't help. Any ideas on how to get this to pass the coverage report? I am using shallow rendering in my test.
You can mock useState for this specific component, try this:
const stateMock = jest.fn();
jest.mock('react', () => {
const ActualReact = require.requireActual('react');
return {
...ActualReact,
useState: () => ['value', stateMock], // what you want to return when useContext get fired goes here
};
});
Everytime your component calls useState your stateMock will be fired and your case will be covered
This is just a minimal example of what you can do, but you can enhance the mock to recognize each state call
If you want to keep the default behaviour of React state you can declare your callback function outside of the component body, in your case the concerned line of code is here:
<item.details setContent={myContent => setMenuContent(myContent)} />
So instead of this anonymous function which can lead to memory leak anyways, you can take it out of your component and do something like so:
const setContent = (setMenuContent) => (myContent) => setMenuContent(myContent)
const myComponent = ({ initialState }) = {
const [state, setState] = useState(initialState);
const [menuContent, setMenuContent] = useState(undefined);
return (
<Menu>
{state.accordions.map(item, index) => (
<MenuPanel id={item.id}>
{item.details && typeof item.details === 'function'
? <item.details setContent={setContent(setMenuContent)} />
: undefined}
</MenuPanel>
)}
</Menu>
)
}
Now you can export this function and cover it with a text, which will allow you to mock setMenuContent
export const setContent = (setMenuContent) => (myContent) => setMenuContent(myContent)
Your last option is to use something like enzyme or react-testing-lib, find this item.details component in the dom and trigger a click action

Categories

Resources