I have this code:
const handleButton = React.useCallback(async () => {
try {
await client.clearStore();
navigation.push('screen');
} catch (e) {
logger.error('error', e);
}
}, []);
I'm testing the nagivation to screen, but with the await I can't execute or reach the navigation.push(). If I remove the await on client.clearStore it's all ok, but the behaviour could be not as expected (I need first to clearStore then to navigate).
Any idea?
Edit:
My test is like this one:
import { useApolloClient } from '#apollo/react-hooks';
// After imports
jest.mock('#apollo/react-hooks');
// In describe:
describe('text', () => {
beforeEach(async () => {
// #ts-ignore
moment.mockClear();
useApolloClient.mockClear();
});
// My test:
it.only('should redirects to MyComponent when button is pressed', async () => {
const apolloClient = {
clearStore: jest.fn()
};
useApolloClient.mockImplementation(() => apolloClient);
apolloClient.clearStore
.mockReturnValueOnce(
Promise.resolve('foo')
);
moment.mockImplementation(
(args: moment.MomentInput) =>
jest.requireActual('moment')(args || '2020-07-30T20:00:00.000Z')
);
const navigation = {
push: jest.fn()
};
const rendered = render(
<MyComponent
paymentGatewayCode='khipu'
allowToPay={true}
expiresDate={'2020-07-30T20:00:00.000Z'}
navigation={navigation}
/>
);
const button = rendered.getByTestId('button');
fireEvent.press(button);
console.log('navigation.push.mock.calls', navigation.push.mock.calls);
expect(navigation.push.mock.calls.length).toBe(1);
expect(navigation.push.mock.calls[0]).toEqual(['MyComponent']);
});
And I get this while running the test:
Related
I want to render overlay on the long running operations.
Consider I have the following code
let spinnerState = useRecoilValue(overlayState);
return <BrowserRouter>
<Spin indicator={<LoadingOutlined />} spinning={spinnerState.shown} tip={spinnerState.content}>.........</BrowserRouter>
What I do in different components
const [, setOverlayState] = useRecoilState(overlayState);
const onButtonWithLongRunningOpClick = async () => {
Modal.destroyAll();
setOverlayState({
shown: true,
content: text
});
try {
await myApi.post({something});
} finally {
setOverlayState(overlayStateDefault);
}
}
How can I refactor this to use such construction that I have in this onbuttonclick callback? I tried to move it to the separate function, but you cannot use hooks outside of react component. It's frustrating for me to write these try ... finally every time. What I basically want is something like
await withOverlay(async () => await myApi.post({something}), 'Text to show during overlay');
Solution
Write a custom hook that includes both UI and API. This pattern is widely used in a large app but I couldn't find the name yet.
// lib/buttonUi.js
const useOverlay = () => {
const [loading, setLoading] = useState(false);
return {loading, setLoading, spinnerShow: loading };
}
export const useButton = () => {
const overlay = useOverlay();
const someOp = async () => {
overlay.setLoading(true);
await doSomeOp();
/* ... */
overlay.setLoading(false);
}
return {someOp, ...overlay}
}
// components/ButtonComponent.jsx
import { useButton } from 'lib/buttonUi';
const ButtonComponent = () => {
const {spinnerShow, someOp} = useButton();
return <button onClick={someOp}>
<Spinner show={spinnerShow} />
</button>
}
export default ButtonComponent;
create a custom hook that handles the logic for showing and hiding the overlay.
import { useRecoilState } from 'recoil';
const useOverlay = () => {
const [, setOverlayState] = useRecoilState(overlayState);
const withOverlay = async (fn: () => Promise<void>, content: string) => {
setOverlayState({ shown: true, content });
try {
await fn();
} finally {
setOverlayState(overlayStateDefault);
}
};
return withOverlay;
};
You can then use the useOverlay hook in your components
import { useOverlay } from './useOverlay';
const Component = () => {
const withOverlay = useOverlay();
const onButtonWithLongRunningOpClick = async () => {
await withOverlay(async () => await myApi.post({ something }), 'Text to show during overlay');
};
return <button onClick={onButtonWithLongRunningOpClick}>Click me</button>;
};
I'm getting warnings while adding tests for my react components so how can I get rid of these warnings.
warnings
Warning: An update to FormItem 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
at FormItem (/home/yousharizvi/Workspace/zain/app/app-> test/node_modules/antd/lib/form/FormItem.js:103:20)
at form
at Form (/home/yousharizvi/Workspace/zain/app/app-test/node_modules/rc-> field-form/lib/Form.js:33:19)
at SizeContextProvider (/home/yousharizvi/Workspace/zain/app/app- test/node_modules/antd/lib/config-provider/SizeContext.js:19:23)
at InternalForm (/home/yousharizvi/Workspace/zain/app/app-test/node_modules/antd/lib/form/Form.js:66:27)
at Search (/home/yousharizvi/Workspace/zain/app/app-test/src/components/Search.tsx:10:36)
at printWarning (node_modules/react-dom/cjs/react-dom.development.js:86:30)
at error (node_modules/react-dom/cjs/react-dom.development.js:60:7)
at warnIfUpdatesNotWrappedWithActDEV (node_modules/react-dom/cjs/react-dom.development.js:27381:9)
at scheduleUpdateOnFiber (node_modules/react-dom/cjs/react-dom.development.js:25292:5)
at dispatchSetState (node_modules/react-dom/cjs/react-dom.development.js:17342:16)
at safeSetState (node_modules/rc-util/lib/hooks/useState.js:41:5)
at onMetaChange (node_modules/antd/lib/form/FormItem.js:165:5)
at Field._this.triggerMetaEvent (node_modules/rc-field-form/lib/Field.js:146:67)
test file
import { render, fireEvent } from '#testing-library/react';
import { Search } from '.';
describe('Search Component', () => {
test('test fields are rendered', () => {
const props = {
onSubmit: jest.fn(),
loading: false
}
const { container, getByText } = render(<Search {...props} />)
const searchField = container.querySelector('#searchText');
const button = getByText("Submit");
expect(searchField).toBeTruthy();
expect(button).toBeTruthy();
});
test('test search field by entering text', () => {
const props = {
onSubmit: jest.fn(),
loading: false
}
const { container } = render(<Search {...props} />)
const inputs = container.querySelectorAll('#searchText')
const searchField = inputs[0];
fireEvent.change(searchField, { target: { value: "foo" } });
expect(searchField).toHaveValue('foo');
});
test('test search field should throw error', async () => {
const props = {
onSubmit: jest.fn(),
loading: false
}
const { container, getByText } = render(<Search {...props} />)
const button = getByText("Submit");
fireEvent.click(button);
await new Promise((r) => setTimeout(r, 100)); // waiting for error
expect(container.getElementsByClassName('ant-input-status-error').length).toBe(1);
});
test('test search field should not throw error', async () => {
const props = {
onSubmit: jest.fn(),
loading: false
}
const { container, getByText } = render(<Search {...props} />)
const inputs = container.querySelectorAll('#searchText')
const searchField = inputs[0];
const button = getByText("Submit");
fireEvent.change(searchField, { target: { value: "foo" } });
expect(searchField).toHaveValue('foo');
fireEvent.click(button);
expect(container.getElementsByClassName('ant-input-status-error').length).toBe(0);
});
})
setupTests.ts
import '#testing-library/jest-dom';
global.matchMedia = global.matchMedia || function () {
return {
addListener: jest.fn(),
removeListener: jest.fn(),
};
};
I have a custom hook as below
export const useUserSearch = () => {
const [options, setOptions] = useState([]);
const [searchString, setSearchString] = useState("");
const [userSearch] = useUserSearchMutation();
useEffect(() => {
if (searchString.trim().length > 3) {
const searchParams = {
orgId: "1",
userId: "1",
searchQuery: searchString.trim(),
};
userSearch(searchParams)
.then((data) => {
setOptions(data);
})
.catch((err) => {
setOptions([]);
console.log("error", err);
});
}
}, [searchString, userSearch]);
return {
options,
setSearchString,
};
};
and I want to test this hook but am not able to mock userSearch function which is being called inside useEffect.
can anybody help?
this is my test
it('should set state and test function', async () => {
const wrapper = ({ children }) => (
<Provider store={store}>{children}</Provider>
)
const { result } = renderHook(
() => useUserSearch(),
{ wrapper }
)
await act(async () => {
result.current.setSearchString('abc5')
})
expect(result.current.options).toEqual(expected)
})
useUserSearchMutation
import {createApi, fetchBaseQuery} from '#reduxjs/toolkit/query/react';
export const userSearchAPI = createApi({
reducerPath: 'userSearchResult',
baseQuery: fetchBaseQuery({baseUrl: process.env.REACT_APP_BASE_URL}),
tagTypes: ['Users'],
endpoints: build => ({
userSearch: build.mutation({
query: body => ({url: '/org/patient/search', method: 'POST', body}),
invalidatesTags: ['Users'],
}),
}),
});
export const {useUserSearchMutation} = userSearchAPI;
Because it's a named export you should return an object in the mock
it("should set state and test function", async () => {
jest.mock("./useUserSearchMutation", () => ({
useUserSearchMutation: () => [jest.fn().mockResolvedValue(expected)],
}));
const wrapper = ({ children }) => (
...
});
I have created a smaller example based on your code, where I am mocking a hook inside another hook.
hooks/useUserSearch.js
import { useEffect, useState } from "react";
import useUserSearchMutation from "./useUserSearchMutation.js";
const useUserSearch = () => {
const [text, setText] = useState();
const userSearch = useUserSearchMutation();
useEffect(() => {
const newText = userSearch();
setText(newText);
}, [userSearch]);
return text;
};
export default useUserSearch;
hooks/useUSerSearchMutation.js
I had to move this to its own file to be able to mock it when it was called
inside of the other hook.
const useUserSearchMutation = () => {
return () => "Im not mocked";
};
export default useUserSearchMutation;
App.test.js
import { render } from "react-dom";
import useUserSearch from "./hooks/useUserSearch";
import * as useUserSearchMutation from "./hooks/useUserSearchMutation";
import { act } from "react-dom/test-utils";
let container;
beforeEach(() => {
// set up a DOM element as a render target
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
// cleanup on exiting
document.body.removeChild(container);
container = null;
});
function TestComponent() {
const text = useUserSearch();
return <div>{text}</div>;
}
test("should mock userSearch", async () => {
const mockValue = "Im being mocked";
jest
.spyOn(useUserSearchMutation, "default")
.mockImplementation(() => () => mockValue);
act(() => {
render(<TestComponent />, container);
});
expect(container.textContent).toBe(mockValue);
});
I have a component that waits until some data from a resource. I'm using React suspense to show the loading screen as until it gets the response. Everything work as expected.
However when testing, even though onGet is registered, in axiosMock, it never gets the request from <Cmp /> component. Test fails due to connection error.
import { render, waitFor } from '#testing-library/react';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import React, { Suspense } from 'react';
const axiosMock = new MockAdapter(axios, { onNoMatch: 'throwException' });
const wrapPromise = (promise) => {
let status = 'pending';
let result: any;
promise.then(
(r) => {
status = 'success';
result = r;
},
(e) => {
status = 'error';
result = e;
}
);
return {
read() {
return {
status,
result,
};
},
};
};
const fetchData = () => {
return wrapPromise(axios.get('/test').then((r) => r.data));
};
const resource = fetchData();
export const Cmp = () => {
const str = resource.read();
return <h1>{str}</h1>;
};
describe('Main Tests', () => {
beforeAll(() => {
axiosMock.reset();
});
/*
THIS WORKS
*/
it('Sample Request Test', async () => {
axiosMock.onGet('/test').reply(200, 'hello');
expect(await axios.get('/test').then((r) => r.data)).toBe('hello');
});
/*
THIS DOES NOT WORK
*/
it('Component Request Test', async () => {
axiosMock.onGet('/test').reply(200, 'hello');
const { getByText } = render(
<Suspense fallback={<p>Loading...</p>}>
<Cmp />
</Suspense>
);
await waitFor(() => {
return getByText('hello');
});
});
});
I have a React Component MyComponent where I want to test behavior that should be triggered when a user rotates their phone.
Inside the Component:
export class MyComponent extends React.PureComponent<props> {
componentDidMount() {
window.addEventListener('orientationchange', this.onRotation)
}
componentWillUnmount() {
window.removeEventListener('orientationchange', this.onRotation)
}
onRotation = () => {
// do things
}
render() {
// ...
}
}
I found an article on medium that describes how to write tests for this here. However, that doesn't work for me.
describe('<MyComponent />', () => {
it('does things on rotation', () => {
const map : any = {}
window.addEventListener = jest.fn((event, cb) => {
map[event] = cb;
})
const wrapper : any = mount(<MyComponent />)
map.orientationchange()
expect(wrapper.onRotation).toHaveBeenCalled()
})
})
In the article this works, however I get an error:
"Matcher error: received value must be a mock or spy function
Received has value: undefined"
Using a spy also doesn't work:
it('does things on rotation', () => {
const map : any = {}
window.addEventListener = jest.fn((event, cb) => {
map[event] = cb;
})
const wrapper : any = mount(<MyComponent />)
const spy = jest.spyOn(wrapper.instance(), 'onRotation')
map.orientationchange()
expect(spy).toHaveBeenCalled()
})
It says:
"Expected mock function to have been called, but it was not called."
Spy on function inside onRotation.
import React from 'react';
class OrientationChange extends React.Component {
componentDidMount() {
window.addEventListener('orientationchange', this.onRotation)
}
componentWillUnmount() {
window.removeEventListener('orientationchange', this.onRotation)
}
handleRotation = () => {
console.log('inside handle rotation');
}
onRotation = (event) => {
this.handleRotation()
}
render() {
return (
<div>Testing</div>
)
}
}
export default OrientationChange;
describe('<OrientationChange /> orientation change test', () => {
it('does things on rotation', () => {
const map = {}
window.addEventListener = jest.fn((event, cb) => {
map[event] = cb;
})
const wrapper = mount(<OrientationChange />)
const spy = jest.spyOn(wrapper.instance(), 'handleRotation')
map.orientationchange();
expect(spy).toHaveBeenCalled()
})
})