React useEffect Hook fires prematurely in React Testing Library and Enzyme tests - reactjs

I am writing unit tests in React Testing Library and Enzyme for a React component that has a useEffect hook. When I run the tests I get an error from the useEffect hook saying that the accessTheRef function is undefined. If I move the accessTheRef function above the useEffect hook I then get an error that the ref is undefined.
Component:
import React, { useEffect } from 'react';
import { fireEvent, render } from '#testing-library/react';
const RefTest = ({theFunc}) => {
const ref = React.useRef(null);
useEffect(() => {
accessTheRef()
}, [])
const accessTheRef = () => {
const newValue = ref.current?.value
theFunc(newValue)
};
return (
<div>
<input ref={ref} value='defined' readOnly={true}/>
<button type="button" onClick={(e) => accessTheRef()}>
Select text
</button>
</div>
);
};
Test:
describe('<RefTest>', () => {
it('has a null ref in testing environments in the initial call of useEffect', () => {
const theFunc = jest.fn()
const rendered = render(<RefTest theFunc={theFunc} />);
fireEvent(
rendered.getByText('Select text'),
new MouseEvent('click', {
bubbles: true,
cancelable: true,
})
)
expect(theFunc.mock.calls[0][0]).toBe(undefined) // what on earth why
expect(theFunc.mock.calls[1][0]).toBe('defined')
})
});
The code works without any errors in the browser. The error occurs when I run the test with both the React Testing Library tests and the Enzyme tests. It seems like useEffect is not properly deferring and is firing before layout and paint have finished.
Why am I running into these errors in my testing environment? How can I fix this issue in my tests?

Related

Can't test timer in react using vitest (jest)

I have this simple component in react and I want to test it but I cannot find a way to mock the setInterval in order to trigger the timer.
The count value is 0 all the time but when I run the component it's working.
UPDATE: I've added this sample respository on stackblitz for running this test.
This is my test file:
import {
render,
screen
} from "#testing-library/react";
import React, { useEffect, useState } from "react";
import { expect, test, vi } from "vitest";
function Timer() {
const [count, setCount] = useState(0)
useEffect(() => {
let timer = setInterval(() => {
setCount(v => v + 1)
}, 1000)
return () => clearInterval(timer)
}, [])
return <div>{count}</div>
}
test("should render correctly", () => {
vi.useFakeTimers();
render(<Timer />);
vi.advanceTimersByTime(2000);
screen.debug();
expect(screen.getByText("2")).toBeDefined();
});
The problem was with vitest not being able to notice the dom changes made during the test. The dom update code should be wrapped within an act method from react-dom/test-utils.
So vi.advanceTimersByTime(2000); must be in act.
Theis is the link to the guthub issue I opened for this problem

How do you use Enzyme to check for changes to a React Hooks component after onClick?

I am trying to write a simple integration test in my 100% React Hooks (React v16.12) project with Enzyme (v3.10), Jest (v24.0) and TypeScript where if I click a button component in my App container, another component displaying a counter will go up by one. The current value of the counter is stored in the state of the App container (see snippets below).
Basically, I mount the App component to render its children, then try to simulate a click on the button with Enzyme and check the props of the counter display component to see if its value has gone up. But nothing happens. Not only does the onClick handler not get called but I don't seem to be able to retrieve the value prop I pass to the PaperResource component. So basically I can't test the counter display changes when I click on the button in my Enzyme integration test! The test asserts that the value prop goes from 0 to 1, but this assertion fails without an error per seenter code here. Is this because Enzyme support for Hooks is still not there yet or am I doing something daft here? When I run the app on my browser, everything works as expected.
Here's my integration test
import React from 'react';
import App from './App';
import { mount, ReactWrapper } from 'enzyme';
import { act } from 'react-dom/test-utils';
import MakePaperButton from './components/MakePaperButton';
import PaperResource from './components/PaperResource';
describe('App', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
act(() => {
wrapper = mount(<App />);
});
});
describe('when make paper button is clicked', () => {
beforeEach(() => {
act(() => {
wrapper.find('.make-paper__button').simulate('click');
});
});
it('should increase paper resource', () => {
expect(wrapper.find('.resources__paper').prop('value')).toEqual(1);
});
});
});
And here is my React code
import React, { useState } from 'react';
import './App.scss';
import MakePaperButton from './components/MakePaperButton';
import PaperResource from './components/PaperResource';
const App: React.FC = () => {
const [ resources, setResources ] = useState({
paper: 0,
});
const handleMakePaperButtonClick = () => {
setResources({
...resources,
paper: resources.paper + 1,
});
};
return (
<div className="App">
<MakePaperButton onClick={handleMakePaperButtonClick} />
<div className="resources">
<PaperResource value={resources.paper} />
</div>
</div>
);
}
export default App;
My components are very simple
// PaperResource.tsx
import React from 'react';
export default (props: { value: number }) => (
<div className="resources__paper">
<span>Paper: {props.value}</span>
</div>
);
// MakePaperButton.tsx
import React from 'react';
export default (props: { onClick: () => void }) => (
<div className="make-paper__button">
<button onClick={props.onClick}>Make Paper</button>
</div>
);
The only solution I've found so far is wrapping the expect statement in a setTimeout().
it('should increase paper resource', () => {
setTimeout(() => {
expect(wrapper.find('.resources__paper').prop('value')).toEqual(1);
}, 0);
});

Testing a component that uses useEffect using Enzyme shallow and not mount

// MyComponent.jsx
const MyComponent = (props) => {
const { fetchSomeData } = props;
useEffect(()=> {
fetchSomeData();
}, []);
return (
// Some other components here
)
};
// MyComponent.react.test.jsx
...
describe('MyComponent', () => {
test('useEffect', () => {
const props = {
fetchSomeData: jest.fn(),
};
const wrapper = shallow(<MyComponent {...props} />);
// THIS DOES NOT WORK, HOW CAN I FIX IT?
expect(props.fetchSomeData).toHaveBeenCalled();
});
});
When running the tests I get:
expect(jest.fn()).toHaveBeenCalled()
Expected mock function to have been called, but it was not called.
The expect fails because shallow does not call useEffect. I cannot use mount because of other issues, need to find a way to make it work using shallow.
useEffect is not supported by Enzyme's shallow rendering. It is on the roadmap (see column 'v16.8+: Hooks') to be fixed for the next version of Enzyme, as mentioned by ljharb
What you're asking is not possible with the current setup. However, a lot of people are struggling with this.
I've solved / worked around this by:
not using shallow rendering from Enzyme anymore
use the React Testing Library instead of Enzyme
mocking out modules via Jest
Here's a summary on how to mock modules, based on Mock Modules from the React docs.
contact.js
import React from "react";
import Map from "./map";
function Contact(props) {
return (
<div>
<p>
Contact us via foo#bar.com
</p>
<Map center={props.center} />
</div>
);
}
contact.test.js
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import Contact from "./contact";
import MockedMap from "./map";
jest.mock("./map", () => {
return function DummyMap(props) {
return (
<p>A dummy map.</p>
);
};
});
it("should render contact information", () => {
const center = { lat: 0, long: 0 };
act(() => {
render(
<Contact
name="Joni Baez"
email="test#example.com"
site="http://test.com"
center={center}
/>,
container
);
});
});
Useful resources:
React Testing Library docs - mocking
React docs - Mocking Modules
Here's a solution from a colleague of mine at CarbonFive:
https://blog.carbonfive.com/2019/08/05/shallow-testing-hooks-with-enzyme/
TL;DR: jest.spyOn(React, 'useEffect').mockImplementation(f => f())
shallow doesn't run effect hooks in React by default (it works in mount though) but you could use jest-react-hooks-shallow to enable the useEffect and useLayoutEffect hooks while shallow mounting in enzyme.
Then testing is pretty straightforward and even your test specs will pass.
Here is a link to a article where testing the use-effect hook has been clearly tackled with shallow mounting in enzyme
https://medium.com/geekculture/testing-useeffect-and-redux-hooks-using-enzyme-4539ae3cb545
So basically with jest-react-hooks-shallow for a component like
const ComponentWithHooks = () => {
const [text, setText] = useState<>();
const [buttonClicked, setButtonClicked] = useState<boolean>(false);
useEffect(() => setText(
`Button clicked: ${buttonClicked.toString()}`),
[buttonClicked]
);
return (
<div>
<div>{text}</div>
<button onClick={() => setButtonClicked(true)}>Click me</button>
</div>
);
};
you'd write tests like
test('Renders default message and updates it on clicking a button', () => {
const component = shallow(<App />);
expect(component.text()).toContain('Button clicked: false');
component.find('button').simulate('click');
expect(component.text()).toContain('Button clicked: true');
});
I'm following this advice and using mount() instead of shallow(). Obviously, that comes with a performance penalty, so mocking of children is advised.

How to get line coverage on React.useEffect hook when using Jest and enzyme?

Using React, enzyme and jest, How can I get code coverage on my closeDrawer() callback prop inside useEffect()
import React, {useEffect} from 'react';
import {Button} from './button';
const DrawerClose = ({closeDrawer}) => {
useEffect(() => {
const handleEsc = (e: any) => {
if (e.key === 'Escape') {
closeDrawer();
}
};
window.addEventListener('keyup', handleEsc);
return () => window.removeEventListener('keyup', handleEsc);
});
return (
<Button>
close
</Button>
);
};
export {DrawerClose};
Test:
import React from 'react';
import {DrawerClose as Block} from './drawer-close';
describe(`${Block.name}`, () => {
it('should have drawer open', () => {
const wrapper = shallow(<Block closeDrawer={() => 'closed'} />);
expect(wrapper).toMatchSnapshot(); // will not hit the useEffect
});
});
shallow() does not call useEffect yet. It's known issue #2086 and happens because of React's shallow renderer.
You can either use mount() and render completely or mock every/some nested component and still use mount() but having results more like shallow() could. Something like:
jest.mock('../../Button.jsx', (props) => <span {...props} />);
As for testing itself you need to ensure that closeDrawer will be called on hitting ESC. So we need to use spy and simulate simulate hitting ESC
const closeDrawerSpy = jest.fn();
const wrapper = mount(<Block closeDrawer={closeDrawerSpy} />);
// simulate ESC pressing
As for simulating ESC pressed on window, I'm not sure if jest-dom allows, you may try(taken from Simulate keydown on document for JEST unit testing)
var event = new KeyboardEvent('keyup', {'keyCode': 27});
window.dispatchEvent(event);
If that does not work(window.dispatchEvent is not a function or something alike) you always can mock addEventListener to have direct access to handlers.

Testing a component that uses React.useEffect using enzyme

Let's say I have this functional React component.
const SomeComponent = ({ onShow }) => {
React.useEffect(onShow, []);
return <div />;
};
Now I want to unit test that SomeComponent calls onShow the first time it is rendered.
Is that possible using enzyme?
I have tried implementing two very similar tests, the difference being that the first is using enzyme, the other one using react-testing-library.
The test that uses react-testing-library passes, but the enzyme test fails, even though they test the same code.
Example:
import * as reactTestingLibrary from "react-testing-library";
import * as React from "react";
import * as enzyme from "enzyme";
const SomeComponent = ({ onShow }) => {
React.useEffect(onShow, []);
return <div />;
};
describe("Testing useEffect with enzyme", () => {
it("calls the side effect", async () => {
const aFn = jest.fn();
enzyme.mount(<SomeComponent onShow={aFn} />);
expect(aFn).toHaveBeenCalledTimes(1); // this test fails
});
});
describe("Testing useEffect with react-testing-library", () => {
it("calls the side effect", async () => {
const aFn = jest.fn();
reactTestingLibrary.render(<SomeComponent onShow={aFn} />);
expect(aFn).toHaveBeenCalledTimes(1); // this test passes
});
});
Is there a way to make enzyme execute the hook and pass the test?
Yes, it is possible to test components that uses hooks, using enzyme.
Upgrading enzyme-adapter-react-16 to latest version (1.12.1) fixed the problem.
i just upgraded my enzyme-adapter-react-16 to v1.12.1, that sorted it out for me.

Resources