How to cover onIdle callback function with test react-idle-timer - javascript

I use react-idle-timer 4.6.4
I have a component with useIdleTimer hook.
import {useDispatch} from "react-redux";
import {logoutRequest} from "auth/actions";
const App = () => {
const dispatch = useDispatch();
const logout = () => dispatch(logoutRequest);
useIdleTimer({
timeout: 1000 * 60 * 5,
onIdle: logout,
debounce: 500
});
return <Component />
}
Also, I have a test that looks like this
test("render App", () => {
render(<App />);
const someElement = screen.getByText("Some element");
expect(someElement).toBeInTheDocument();
})
This test covers all lines of code except for the logout function.
How to cover the logout function with tests?

Related

How to test the updated state in React after API call

I have a component like the one below for example, Since the state changes after the API is called, I am not able to test the HTML after API call finishes, it always goes to the default state
import React, { useEffect, useState } from 'react';
const App = props => {
const [user, setUser] = useState([]);
const [userLoaded, setUserLoaded] = useState(false);
const fetchUser = async () => {
try {
let response = await fetch('https://randomuser.me/api');
let json = await response.json();
return { success: true, data: json };
} catch (error) {
console.log(error);
return { success: false };
}
}
useEffect(() => {
(async () => {
setUserLoaded(false);
let res = await fetchUser();
if (res.success) {
setUser(res.data.results[0]);
setUserLoaded(true);
}
})();
}, []);
return (
<div>
{userLoaded ? (
<div>
<ul>
<li><strong>First name:</strong> {user.name.first}</li>
<li><strong>Last name:</strong> {user.name.last}</li>
<li><strong>Email:</strong> {user.email}</li>
</ul>
</div>
) : (
<p> Loading Please wait </p>
)}
</div>
);
}
export default UserID;
So for me, it always goes to the else block and all the tests fail, I want to test the userLoaded part, assuming I am going to mock the API call, how does testing update the state and get to the uerLoaded part?
import { mount } from 'enzyme';
import * as React from 'react';
import { App } from './App';
describe('App', () => {
it ('Should display loading until data arrives', async () => {
const wrapper = mount(<App />);
expect(wrapper.html()).toBe('<div>First name:</strong> {user.name.first}</div>');
});
});
I tried writing the below unit tests, but they are not working
import { mount } from 'enzyme';
import * as React from 'react';
import { App } from './App';
describe('App', () => {
it ('Should display loading until data arrives', async () => {
const wrapper = mount(<App />);
expect(wrapper.html()).toBe('First name:</strong> {user.name.first}');
});
});

React search using debounce

I am trying to implement a search that makes a new query on each character change. After n milliseconds, I need to make a change to the object that stores some properties.
//user typing
const onInputChange = (e) => {
let searchInput = e.target.value;
useDebounce(
handleSearchPropsChange({
filter: {
searchInput,
dateRange: {
start,
end
}
}
}), 1000
);
}
The function I am using for the delayed call
import {debounce} from 'lodash';
import {useRef} from 'react';
export function useDebounce(callback = () => {}, time = 500) {
return useRef(debounce(callback, time)).current;
}
But I am getting the error:
Invalid hook call. Hooks can only be called inside of the body of a function component. This
could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
A example without lodash, just Hooks.
UseDebounce.js
import { useEffect, useCallback } from 'react';
export default function useDebounce(effect, dependencies, delay) {
const callback = useCallback(effect, dependencies);
useEffect(() => {
const timeout = setTimeout(callback, delay);
return () => clearTimeout(timeout);
}, [callback, delay]);
}
App.js
import React, { useState } from 'react';
import useDebounce from './useDebounce';
import data from './data';
export default function App() {
const [search, setSearch] = useState('');
const [filteredTitle, setFilteredTitle] = useState([]);
// DeBounce Function
useDebounce(() => {
setFilteredTitle(
data.filter((d) => d.title.toLowerCase().includes(search.toLowerCase()))
);
}, [data, search], 800
);
const handleSearch = (e) => setSearch(e.target.value);
return (
<>
<input
id="search"
type="text"
spellCheck="false"
placeholder="Search a Title"
value={search || ''}
onChange={handleSearch}
/>
<div>
{filteredTitle.map((f) => (
<p key={f.id}>{f.title}</p>
))}
</div>
</>
);
}
Demo : Stackblitz

Mock different results of an api called inside react component in jest

I have a react component that calls an API that returns two different results which the default value is
{init:false}
And based on users actions, it will be true:
{init:true}
Now I want to test these two states in my app.test.tsx, It will work when I skip one of them(each working fine without another one):
import { screen } from '#testing-library/react';
import { render } from 'src/mocks/renderViaAllProviders';
import App from './app';
import * as apis from 'src/api/consul';
import { mockedRawConsul } from 'src/mocks/db/consul';
test("Show init page when 'initialized:false' in consul.", async () => {
render(<App />);
const loading = screen.getByRole('heading', { name: /loading/i });
expect(loading).toBeInTheDocument();
const initTitle = await screen.findByRole('heading', {
name: /init page/i
});
expect(initTitle).toBeInTheDocument();
});
test("Show Login page when 'initialized:true' in consul", async () => {
const initializedConsul = {
...mockedRawConsul,
...{ configs: { initialized: true } }
};
/*eslint-disable */
//#ts-ignore
apis.getConsulPublicConfig = jest.fn(() =>
Promise.resolve(initializedConsul)
);
render(<App />);
const loginButton = await screen.findByRole('button', {
name: /regularLogin/i
});
expect(loginButton).toBeInTheDocument();
});
How can I fix this?
Update
Here is the reprex and the error :
● Show Login page when 'initialized:true' in consul
Unable to find role="textbox"
console.error
TypeError: Cannot read property 'status' of undefined
at onResponseRejected (\src\api\
service\interceptors.ts:18:23)
at processTicksAndRejections (internal/process/task_queues.js:95:5)
at getLicense (\src\api\license .
ts:10:20)
I have tried to simulate the example that you are trying, I am able to mock the API which returns different results and test for the same, but since we want different results when a Component is rendered the API will be called only once(assuming the API is called on mounting) and upon some user actions the Component will be mounted again that's why called render function again not sure whether it is a good practice or not
//App.js
export default function App() {
const [value, setValue] = useState('loading');
const [show, setShow] = useState({init: false})
useEffect(() => {
setTimeout(() => {
setValue('init page')
fetchData().then(data => {
setShow(data)
}).catch((error) => {
console.log(`ERROR`)
})
},0)
},[])
const { init = false} = show
return (
<>
<p>IT S APP</p>
<h1>Value is {value}</h1>
{ init ? <button>regular Login</button> : null}
</>
);
}
//api.js
function fetchData() {
return fetch("https://jsonplaceholder.typicode.com/posts").then((response) =>
Promise.resolve({init: true})
);
}
export { fetchData };
//App.test.js
import App from "./App";
import { fetchData }from './api';
jest.mock('./api')
describe("<App />", () => {
it("check if loading, login button is present",async () => {
fetchData.mockImplementationOnce(() => Promise.resolve({init: false}))
fetchData.mockImplementationOnce(() => Promise.resolve({init: true}))
render(<App />);
const loading = screen.getByRole('heading', { name: /loading/i });
expect(loading).toBeInTheDocument();
const initTitle = await screen.findByRole('heading', {
name: /init page/i
});
expect(initTitle).toBeInTheDocument();
render(<App />);
await waitFor(() => {
expect(screen.queryByRole('button', {
name: /regular Login/i
})).toBeInTheDocument();
})
});
});

How to mock/spy useState hook in jest?

I am trying to spy on useState React hook but i always get the test failed
This is my React component:
const Counter= () => {
const[counter, setCounter] = useState(0);
const handleClick=() => {
setCounter(counter + 1);
}
return (
<div>
<h2>{counter}</h2>
<button onClick={handleClick} id="button">increment</button>
</div>
)
}
counter.test.js:
it('increment counter correctlry', () => {
let wrapper = shallow(<Counter/>);
const setState = jest.fn();
const useStateSpy = jest.spyOn(React, 'useState');
useStateSpy.mockImplementation((init) => [init, setState]);
const button = wrapper.find("button")
button.simulate('click');
expect(setState).toHaveBeenCalledWith(1);
})
Unfortunately this doesn't work and i get the test failed with that message:
expected 1
Number of calls: 0
diedu's answer led me the right direction and I came up with this solution:
Mock use state from react to return a jest.fn() as useState:
1.1 Also import useState immediately after - that will now be e jest mock (returned from the jest.fn() call)
jest.mock('react', ()=>({
...jest.requireActual('react'),
useState: jest.fn()
}))
import { useState } from 'react';
Later on in the beforeEach, set it to the original useState, for all the cases where you need it to not be mocked
describe("Test", ()=>{
beforeEach(()=>{
useState.mockImplementation(jest.requireActual('react').useState);
//other preperations
})
//tests
})
In the test itself mock it as needed:
it("Actual test", ()=>{
useState.mockImplementation(()=>["someMockedValue", someMockOrSpySetter])
})
Parting notes: While it might be conceptually somewhat wrong to get your hands dirty inside the "black box" one is unit testing, it is indeed super useful at times to do it.
You need to use React.useState instead of the single import useState.
I think is about how the code gets transpiled, as you can see in the babel repl the useState from the single import ends up being different from the one of the module import
_react.useState // useState
_react.default.useState // React.useState;
So you spy on _react.default.useState but your component uses _react.useState.
It seems impossible to spyOn a single import since you need the function to belong to an object, here is a very extensive guide that explains the ways of mocking/spying modules https://github.com/HugoDF/mock-spy-module-import
And as #Alex Mackay mentioned, you probably want to change your mindset about testing react components, moving to react-testing-library is recommended, but if you really need to stick to enzyme you don't need to go that far as to mock react library itself
Annoyingly Codesandbox is currently having trouble with its testing module so I can't post a working example but I will try to explain why mocking useState is generally a bad thing to do.
The user doesn't care if useState has been called, they care about when I click increment the count should increase by one therefore that is what you should be testing for.
// App
import React, { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount((prev) => prev + 1)}>Increment</button>
</div>
);
}
// Tests
import React from "react";
import App from "./App";
import { screen, render } from "#testing-library/react";
import userEvent from "#testing-library/user-event";
describe("App should", () => {
it('increment count value when "Increment" btn clicked', () => {
// Render the App
render(<App />);
// Get the count in the same way the user would, by looking for 'Count'
let count = screen.getByText(/count:/);
// As long as the h1 element contains a '0' this test will pass
expect(count).toContain(0);
// Once again get the button in the same the user would, by the 'Increment'
const button = screen.getByText(/increment/);
// Simulate the click event
userEvent.click(button);
// Refetch the count
count = screen.getByText(/count:/);
// The 'Count' should no longer contain a '0'
expect(count).not.toContain(0);
// The 'Count' should contain a '1'
expect(count).toContain(1);
});
// And so on...
it('reset count value when "Reset" btn is clicked', () => {});
it('decrement count value when "Decrement" btn is clicked', () => {});
});
Definitely check out #testing-library if you are interested in this style of testing. I switched from enzyme about 2 years ago and haven't touched it since.
just you need to import React in your test file like:
import * as React from 'react';
after that you can use the mock function.
import * as React from 'react';
:
:
it('increment counter correctlry', () => {
let wrapper = shallow(<Counter/>);
const setState = jest.fn();
const useStateSpy = jest.spyOn(React, 'useState');
useStateSpy.mockImplementation((init) => [init, setState]);
const button = wrapper.find("button")
button.simulate('click');
expect(setState).toHaveBeenCalledWith(1);
})
you should use React.useState() instead useState(), But there are other ways...
in React you can set useState without React with this config
// setupTests.js
const { configure } = require('enzyme')
const Adapter = require('#wojtekmaj/enzyme-adapter-react-17')
const { createSerializer } = require('enzyme-to-json')
configure({ adapter: new Adapter() });
expect.addSnapshotSerializer(createSerializer({
ignoreDefaultProps: true,
mode: 'deep',
noKey: true,
}));
import React, { useState } from "react";
const Home = () => {
const [count, setCount] = useState(0);
return (
<section>
<h3>{count}</h3>
<span>
<button id="count-up" type="button" onClick={() => setCount(count + 1)}>Count Up</button>
<button id="count-down" type="button" onClick={() => setCount(count - 1)}>Count Down</button>
<button id="zero-count" type="button" onClick={() => setCount(0)}>Zero</button>
</span>
</section>
);
}
export default Home;
// index.test.js
import { mount } from 'enzyme';
import Home from '../';
import React, { useState as useStateMock } from 'react';
jest.mock('react', () => ({
...jest.requireActual('react'),
useState: jest.fn(),
}));
describe('<Home />', () => {
let wrapper;
const setState = jest.fn();
beforeEach(() => {
useStateMock.mockImplementation(init => [init, setState]);
wrapper = mount(<Home />);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('Count Up', () => {
it('calls setCount with count + 1', () => {
wrapper.find('#count-up').simulate('click');
expect(setState).toHaveBeenCalledWith(1);
});
});
describe('Count Down', () => {
it('calls setCount with count - 1', () => {
wrapper.find('#count-down').props().onClick();
expect(setState).toHaveBeenCalledWith(-1);
});
});
describe('Zero', () => {
it('calls setCount with 0', () => {
wrapper.find('#zero-count').props().onClick();
expect(setState).toHaveBeenCalledWith(0);
});
});
});

Testing with useEffect and jest.useFakeTimer()

I'm trying to create simple refresher component here
Refresher.js
import { useEffect } from 'react';
const Refresher = ({ onRefresh }) => {
useEffect(() => {
const id = setInterval(onRefresh, 60000);
return () => {
clearInterval(id);
};
}, [onRefresh]);
return null;
};
export default Refresher;
However, when i try to test it using jest.useFakeTimers(), somehow it's didn't work. The stub is not called even after jest.runOnlyPendingTimers()
import React from 'react';
import renderer from 'react-test-renderer';
import Refresher from '../Refresher';
describe('Refresher', () => {
test('should refresh the result every 60 seconds', () => {
jest.useFakeTimers();
const onRefreshSpy = jest.fn();
const refresher = renderer.create(<Refresher onRefresh={onRefreshSpy} />);
expect(onRefreshSpy).not.toHaveBeenCalled();
jest.runOnlyPendingTimers();
refresher.update(); // Trying force update here
expect(onRefreshSpy).toHaveBeenCalled();
});
});
If not mistaken, the interval will not run if the component are not update, so i tried to use refresher.update(), but seems like it's not really work.
Anyone know how to fix the test here?
You just need to mock useLayoutEffect instead of useEffect. See issue here
describe('Refresher', () => {
beforeAll(() => jest.spyOn(React, 'useEffect').mockImplementation(React.useLayoutEffect))
test('should refresh the result every 60 seconds', () => {
jest.useFakeTimers();
const onRefreshSpy = jest.fn();
const refresher = renderer.create(<Refresher onRefresh={onRefreshSpy} />);
expect(onRefreshSpy).not.toHaveBeenCalled();
jest.runOnlyPendingTimers();
expect(onRefreshSpy).toHaveBeenCalled();
});
});
You have better control with the modern fake timers.
import React from 'react';
import renderer from 'react-test-renderer';
import Refresher from '../Refresher';
describe('Refresher', () => {
test('should refresh the result every 60 seconds', () => {
jest.useFakeTimers('modern'); // default for Jest 27+
const onRefreshSpy = jest.fn();
const refresher = renderer.create(<Refresher onRefresh={onRefreshSpy} />);
expect(onRefreshSpy).not.toHaveBeenCalled();
jest.advanceTimersByTime(60000);
expect(onRefreshSpy).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(60000);
expect(onRefreshSpy).toHaveBeenCalledTimes(2);
});
});

Categories