I have a React function component.
I pass a function to the component as a prop, that returns a promise.
I use that function on an onClick event and once the promise is resolved, I change the state of the component.
Something like:
import React, { useState } from 'react';
function myComponent({ aPromiseReturningFunction }) {
const [myState, setState] = useState('12');
const clickHandler = () => {
aPromiseReturningFunction().then(() => { setState('123') })
};
return <div onClick={ clickHandler }>{myState}</div>
}
Inside my test:
const myFunc = jest.fn(() => Promise.resolve(true));
const componentWrapper = shallow(<myComponent aPromiseReturningFunction={ myFunc }/>);
componentWrapper.simulate('click');
expect(componentWrapper.text()).toEqual('123');
Obviously the above fails, but I have not found anything that would explain how to properly test the above. Of course If I change the state outside the promise, the test passes.
Any suggestions?
Since click is updating the state after a promise aka asynchronously, I would use act
import { act } from 'react-dom/test-utils'; // other testing libraries have similar methods that test async events
const myFunc = jest.fn(() => Promise.resolve(true));
it('updates text after onclick', () => {
const componentWrapper = shallow(<myComponent aPromiseReturningFunction={ myFunc }/>);
act(() => {
componentWrapper.simulate('click');
});
expect(componentWrapper.text()).toEqual('123');
});
Thanks to alextrastero, I managed to come to a solution eventually.
What is missing from alextrastero's answer is that we should enclose the act() inside async/await like:
import { act } from 'react-dom/test-utils'; // other testing libraries have similar methods that test async events
const myFunc = jest.fn(() => Promise.resolve(true));
it('updates text after onclick', async () => {
const componentWrapper = shallow(<myComponent aPromiseReturningFunction={ myFunc }/>);
await act(() => {
componentWrapper.simulate('click');
});
expect(componentWrapper.text()).toEqual('123');
});
And in order for that to work, I also needed to use the regenerator-runtime/runtime package.
Related
I want to test a React component that, internally, uses a custom hook (Jest used). I successfully mock this hook but I can't find a way to test the calls on the functions that this hook returns.
Mocked hook
const useAutocomplete = () => {
return {
setQuery: () => {}
}
}
React component
import useAutocomplete from "#/hooks/useAutocomplete";
const MyComponent = () => {
const { setQuery } = useAutocomplete();
useEffect(() => {
setQuery({});
}, [])
...
}
Test
jest.mock("#/hooks/useAutocomplete");
it("sets the query with an empty object", () => {
render(<MyComponent />);
// I want to check the calls to setQuery here
// e.g. mockedSetQuery.mock.calls
});
CURRENT SOLUTION
I currently made the useAutocomplete hook an external dependency:
import useAutocomplete from "#/hooks/useAutocomplete";
const MyComponent = ({ autocompleteHook }) => {
const { setQuery } = autocompleteHook();
useEffect(() => {
setQuery({});
}, [])
...
}
MyConsole.defaultProps = {
autocompleteHook: useAutocomplete
}
And then I test like this:
const mockedSetQuery = jest.fn(() => {});
const useAutocomplete = () => ({
setQuery: mockedSetQuery,
});
it("Has access to mockedSetQuery", () => {
render(<MyComponent autocompleteHook={useAutocomplete} />);
// Do something
expect(mockedSetQuery.mock.calls.length).toBe(1);
})
You can mock the useAutocomplete's setQuery method to validate if it's invoked.
jest.mock("#/hooks/useAutocomplete");
it("sets the query with an empty object", () => {
const useAutocompleteMock = jest.requireMock("#/hooks/useAutocomplete");
const setQueryMock = jest.fn();
useAutocompleteMock.setQuery = setQueryMock;
render(<MyComponent />);
// The mock function is called twice
expect(setQueryMock.mock.calls.length).toBe(1);
});
I am trying unsuccessfully to mock a Promise:
utils.js
export const fetching = () => {
const data = [1,2,3];
return new Promise(resolve=>resolve(data));
}
MyComponent
export function MyComponent() {
...
useEffect(()=>{
async function callback() {
const fetchedData = await fetching();
}
callback();
}, []);
return (
...
);
}
MyTest
import * as myStuff from ...;
jest.spyOn(myStuff, 'fetching').mockImplementation(()=>Promise.resolve([5]))
test('should ...', async () => {
// given
render(<MyComponent/>);
});
What am I getting as fetchedData by running the above test is undefined instead of [5]. Any idea why?
You're using the call in useEffect which means it's triggering after the component is actually mounted. Your test is async, but you're not advancing timers. Try:
jest.useFakeTimers()
const fetchSpy = jest.spyOn(myStuff, 'fetching').mockImplementation(()=>Promise.resolve([5]))
test('should ...', async () => {
// given
render(<MyComponent/>);
// here, we essentially push time forward, after useEffect has been triggered by the mounted component
act(() => {
jest.advanceTimersByTime(1000)
})
expect(fetchSpy).toBeCalled()
});
I'm trying to test a simple hook i've made for intercepting offline/online events:
import { useEffect } from 'react';
const useOfflineDetection = (
setOffline: (isOffline: boolean) => void
): void => {
useEffect(() => {
window.addEventListener('offline', () => setOffline(true));
window.addEventListener('online', () => setOffline(false));
return () => {
window.removeEventListener('offline', () => setOffline(true));
window.removeEventListener('online', () => setOffline(false));
};
}, []);
};
export default useOfflineDetection;
------------------------------------
//...somewhere else in the code
useOfflineDetection((isOffline: boolean) => Do something with 'isOffline');
But I'm not sure I'm using the correct way to return value and moreover I'm not sure to get how to test it with jest, #testing-library & #testing-library/react-hooks.
I missunderstand how to mount my hook and then catch the return provide by callback.
Is someone can help me ? I'm stuck with it :'(
Thanks in advance!
EDIT:
Like Estus Flask said, I can use useEffect instead callback like I design it first.
import { useEffect, useState } from 'react';
const useOfflineDetection = (): boolean => {
const [isOffline, setIsOffline] = useState<boolean>(false);
useEffect(() => {
window.addEventListener('offline', () => setIsOffline(true));
window.addEventListener('online', () => setIsOffline(false));
return () => {
window.removeEventListener('offline', () => setIsOffline(true));
window.removeEventListener('online', () => setIsOffline(false));
};
}, []);
return isOffline;
};
export default useOfflineDetection;
------------------------------------
//...somewhere else in the code
const isOffline = useOfflineDetection();
Do something with 'isOffline'
But if I want to use this hook in order to store "isOffline" with something like redux or other, the only pattern I see it's using useEffect:
const isOffline = useOfflineDetection();
useEffect(() => {
dispatch(setIsOffline(isOffline));
}, [isOffline])
instead of just:
useOfflineDetection(isOffline => dispatch(setIsOffline(isOffline)));
But is it that bad ?
The problem with the hook is that clean up will fail because addEventListener and removeEventListener callbacks are different. They should be provided with the same functions:
const setOfflineTrue = useCallback(() => setOffline(true), []);
const setOfflineFalse = useCallback(() => setOffline(false), []);
useEffect(() => {
window.addEventListener('offline', setOfflineTrue);
...
Then React Hooks Testing Library can be used to test a hook.
Since DOM event targets have determined behaviour that is supported by Jest DOM to some extent, respective events can be dispatched to test a callback:
const mockSetOffline = jest.fn();
const wrapper = renderHook(() => useOfflineDetection(mockSetOffline));
expect(mockSetOffline).not.toBeCalled();
// called only on events
window.dispatchEvent(new Event('offline'));
expect(mockSetOffline).toBeCalledTimes(1);
expect(mockSetOffline).lastCalledWith(false);
window.dispatchEvent(new Event('online'));
expect(mockSetOffline).toBeCalledTimes(2);
expect(mockSetOffline).lastCalledWith(true);
// listener is registered once
wrapper.rerender();
expect(mockSetOffline).toBeCalledTimes(2);
window.dispatchEvent(new Event('offline'));
expect(mockSetOffline).toBeCalledTimes(3);
expect(mockSetOffline).lastCalledWith(false);
window.dispatchEvent(new Event('online'));
expect(mockSetOffline).toBeCalledTimes(4);
expect(mockSetOffline).lastCalledWith(true);
// cleanup is done correctly
window.dispatchEvent(new Event('offline'));
window.dispatchEvent(new Event('online'));
expect(mockSetOffline).toBeCalledTimes(4);
I'm having trouble understanding how to write a test for a hook without the following warning when using renderHook from "#testing-library/react-hooks".
"Warning: An update to TestHook inside a test was not wrapped in act(...)."
Basically the hook sets initial value in state using useState and then within a useEffect hook I do something asynchronously which ends up updating the state value.
import React from "react";
// fake request
const fetchData = () => Promise.resolve("data");
export const useGetData = () => {
const initialData = { state: "loading" };
const [data, setData] = React.useState(initialData);
React.useEffect(() => {
fetchData()
.then(() => setData({ state: "loaded" }));
}, []);
return data;
};
The hook simply returns the state value at all times.. so I've written a test to assert that it returns the initial value at first and eventually returns the new state value.
import { renderHook } from "#testing-library/react-hooks";
import { useGetData } from "./useGetData";
describe("useGetData", async () => {
it('Should initially return an object with state as "loading"', () => {
const { result } = renderHook(() => useGetData());
expect(result.current).toEqual({ state: "loading" });
});
it('Should eventually return an object with state as "loaded"', async () => {
const { result, waitForNextUpdate } = renderHook(() => useGetData());
await waitForNextUpdate();
expect(result.current).toEqual({ state: "loaded" });
});
});
I've created a sandbox that replicates this:
https://codesandbox.io/s/dazzling-faraday-ht4cd?file=/src/useGetData.test.ts
I've looked into what this warning means and what act is.. but for this particular scenario I'm not sure whats missing.
You can fix it by doing this:
await act(async () => {
await waitForNextUpdate();
});
You need to wrap any function that's going to update the state by the act function
I might be missing something in here, I'm trying to mock an API function inside of my useEffect hook.
The API call should return a version number (i.e 1.0.0)
I tried to mock and sent it to the component, but for some reason, it's not working.
My useEffect hook:
useEffect(() => {
async function loader() {
const ver = await loadVersion();
if (ver === 401) {
history.push("/login");
return;
}
setVersion(ver);
};
loader();
}, []);
My test:
describe("Footer", () => {
const loader = jest.fn();
const loadVersion = jest.fn(() => '2.0.0');
let wrapper;
beforeEach(() => {
wrapper = shallow(<Footer loader={loader} loadVersion={loadVersion}/>)
})
it("Renders without crashing", () => {
expect.assertions(1)
expect(wrapper).not.toBeNull();
wrapper.update();
console.log(wrapper.debug()) // I'm trying to get the version from mocked loadVersion.
})
})
I tried to debug it to check if it is getting to mocked function but it's not, and in my coverage report it is not passing the first line of the async function inside the useEffect hook.