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

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

Related

Reproducable asynchronous bug found in #testing-library/react

Unless I'm mistaken, I believe I've found a bug in how rerenders are triggered (or in this case, aren't) by the #testing-library/react package. I've got a codesandbox which you can download and reproduce in seconds:
https://codesandbox.io/s/asynchronous-react-redux-toolkit-bug-8sleu4?file=/README.md
As a summary for here, I've just got a redux store and I toggle a boolean value from false to true after some async activity in an on-mount useEffect in a component:
import React, { useEffect } from "react";
import { useAppDispatch } from "../hooks/useAppDispatch";
import { setMyCoolBoolean } from "../redux/slices/exampleSlice";
import AnotherComponent from "./AnotherComponent";
export default function InnerComponent() {
const dispatch = useAppDispatch();
const fetchSomeData = async () => {
await fetch("https://swapi.dev/api/people");
dispatch(setMyCoolBoolean(true));
};
// on mount, set some values
useEffect(() => {
fetchSomeData();
}, []);
return <AnotherComponent />;
}
Then, in a different component, I hook into that store value with useAppSelector hook and then useEffect to do something local there (dumb example, but it illustrates my point.):
import { useEffect, useState } from "react";
import { useAppSelector } from "../hooks/useAppSelector";
export default function AnotherComponent() {
const { myCoolBoolean } = useAppSelector((state) => state.example);
const [localBoolean, setLocalBoolean] = useState(false);
// when myCoolBoolean changes, set the local boolean state value in this component
// somewhat a dumb example but it illustrates
// the failure of react-testing-library
useEffect(() => {
// only do something in this component
// if myCoolBoolean changes to true
if (myCoolBoolean) {
console.log("SET TO TRUE!");
setLocalBoolean(myCoolBoolean);
}
}, [myCoolBoolean]);
if (localBoolean) {
return <span data-testid="NEW">I'm new</span>;
}
return <span data-testid="ORIGINAL">I'm original</span>;
}
In result, my test would like to see if the 'new' value is ever shown. Despite issuing rerender, you will see the test fails:
import React from "react";
import { render } from "#testing-library/react";
import App from "../../src/App";
import { act } from "react-test-renderer";
import 'whatwg-fetch'
test("On mount, boolean value changes, causing our new span to show up", async () => {
const { getByTestId, rerender } = render(<App />);
await act(async () => {
// expect(getByTestId("ORIGINAL")).toBeTruthy();
// No matter how many times you call rerender here,
// you'll NEVER see the "NEW" test id (and thus corresponding <span> element) appear in the document
// despite this being the case in any standard browser
await rerender(<App />);
// If you comment this line below out, the test passes fine.
// test ID "ORIGINAL" is found, but "NEW" is never found!!!!
expect(getByTestId("NEW")).toBeTruthy();
});
});
Behaviour is totally as expected in a browser, but fails in my jest test. Can anybody guide me on how to get my test to pass? As far as I know, the code and implementations of my React components and Redux are the cleanest and best practices that are currently out there, so I'm more expecting this is a gross misunderstanding on my part of how #testing-library works, though I thought rerender would do the trick.
I've apparently misunderstood how react-testing-library works under the hood. You don't even need to use rerender or act at all! Simply using a waitFor with await / async is enough to trigger the on mount logic and subsequent rendering:
import React from "react";
import { findByTestId, render, waitFor } from "#testing-library/react";
import App from "../../src/App";
import { act } from "#testing-library/react-hooks/dom";
import "whatwg-fetch";
test("On mount, boolean value changes, causing our new span to show up", async () => {
const { getByTestId, rerender, findByTestId } = render(<App />);
// Works fine, as we would expect
expect(getByTestId("ORIGINAL")).toBeTruthy();
// simply by using 'await' here, react-testing-library must rerender somehow
// note that 'act' isn't even used or needed either!
await waitFor(() => getByTestId("NEW"));
});
Another case of "overthinking it" gone bad...

When testing, code that causes React state updates should be wrapped into act(...) - with simple react-native nested screen/components with jest axios

I am new to unit testing/jest, but I know some about react native.
I want to write a test for my HomeScreen, which contains a component that makes a simple request. The code runs without any issue but fails when I run it with Jest.
HomeScreen.js
import { View } from 'react-native'
import APIExample from '#components/Examples/APIExample'
const HomeScreen = () => {
return (<View> <APIExample /> </View>)
}
export default HomeScreen
HomeScreen.test.js
import { render } from '#testing-library/react-native'
import HomeScreen from '#screens/HomeScreen'
it('should run', async () => {
const { getByText } = await render(<HomeScreen />)
})
APIExample.js
import { useState, useEffect } from 'react'
import { Text, View } from 'react-native'
import API from '../../API'
const APIExample = () => {
const [apiResponse, setApiResponse] = useState(null)
const Submit = async () => {
const response = await API.Test()
setApiResponse(response)
}
useEffect(() => {
Submit()
}, [])
return (
<View>
<Text>
{JSON.stringify(apiResponse)}
</Text>
</View>
)
}
export default APIExample
I try to figure out why does it keep saying that I should wrap it in act and what exactly do I need to wrap?
I already tried to wrap the render whole line but had no success.
The API.Test is a simple axios.get
The error I've kept getting is:
Warning: An update to APIExample inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
It happened to me with fireEvent a couple of days ago. Try this:
await waitFor(()=> render(<HomeScreen />))
The reason you're facing the issue is because of state change that's happening. While on first render the apiResponse data is set as null. And later with the api response the apiResponse has a value so there is re-render that has occured, so jest complains about it.
To resolve you could use await waitFor(() => expect(api).toHaveBeenCalledTimes(1)). This will wait for a specific period of time.
Suggestion: Mock your api's in tests, instead of hitting it directly.

Testing with jest on component with custom hook

i'm trying to write test code for my component that uses a custom hook to seperate logic from view
The problem is that i cannot for whatever reason seem to actually mock this custom hook in a test.
Following is a code example of what im trying to do:
// click-a-button.tsx
import {useClickAButton} from "./hooks/index";
export const ClickAButton = () => {
const { handleClick, total } = useClickAButton();
return <button onClick={handleClick}>{total}</button>;
}
// hooks/use-click-a-button.tsx
import React, {useCallback, useState} from 'react';
export const useClickAButton = () => {
const [total, setTotal] = useState<number>(0);
const handleClick = useCallback(() => {
setTotal(total => total + 1);
}, []);
return {
handleClick,
total,
};
}
// click-a-button.test.tsx
import * as React from 'react';
import {act} from "react-dom/test-utils";
import {render} from "#testing-library/react";
import {useClickAButton} from './hooks/index'
import {ClickAButton} from "./index";
const hooks = { useClickAButton }
test('it runs with a mocked customHook',() => {
const STATE_SPY = jest.spyOn(hooks, 'useClickAButton');
const CLICK_HANDLER = jest.fn();
STATE_SPY.mockReturnValue({
handleClick: CLICK_HANDLER,
total: 5,
});
const component = render(<ClickAButton />);
expect(component.container).toHaveTextContent('5');
act(() => {
component.container.click();
});
expect(CLICK_HANDLER).toHaveBeenCalled();
})
When running the test, neither of the expects is fulfilled.
Context gets to be 0 instead of the mocked 5 and the CLICK_HANDLER is never called.
All in all it seems that the jest.spyon has no effect.
Please help
it seems i found the answer myself.
// right after imports in test file
jest.mock('./hooks')
is all that it took!

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

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?

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.

Resources