How can I mock this file, can't covered setTimeout callback - reactjs

need test below code, how can i mock the setTimeout callback
import { message } from 'antd';
const showMessage = ({ type = 'info', text }) => {
message.destroy();
setTimeout(() => {
message[type](text); // this line can't covered
}, 10);
};
export function error(text) {
showMessage({ type: 'error', text });
}
export function info(text) {
showMessage({ type: 'info', text });
}

You can use jest.runAllTimers() :
jest.mock('antd');
const { message } = require('antd');
const { error } = require('../message');
describe('src/shared/utils/message', () => {
describe('#error', () => {
it('should call message.error', () => {
error('dummy');
jest.runAllTimers();
expect(message.error).toBeCalledWith('dummy');
}
}
}

Related

How can I test useEffect with async function in Jest?

I have this function inside a helper:
export const useDAMProductImages = (imageId: string) => {
const {
app: { baseImgDomain },
} = getConfig();
const response: MutableRefObject<string[]> = useRef([]);
useEffect(() => {
const getProductImages = async (imageId: string) => {
try {
const url = new URL(FETCH_URL);
const res = await fetchJsonp(url.href, {
jsonpCallbackFunction: 'callback',
});
const jsonData = await res.json();
response.current = jsonData;
} catch (error) {
response.current = ['error'];
}
};
if (imageId) {
getProductImages(imageId);
}
}, [imageId]);
return response.current;
};
In test file:
import .....
jest.mock('fetch-jsonp', () =>
jest.fn().mockImplementation(() =>
Promise.resolve({
status: 200,
json: () => Promise.resolve({ set: { a: 'b' } }),
}),
),
);
describe('useDAMProductImages', () => {
beforeEach(() => {
jest.clearAllMocks();
cleanup();
});
it('should return empty array', async () => {
const { result: hook } = renderHook(() => useDAMProductImages('a'), {});
expect(hook.current).toMatchObject({ set: { a: 'b' } });
});
});
The problem is that hook.current is an empty array. Seems that useEffect is never called. Can someone explain to me what I'm doing wrong and how I should write the test? Thank you in advance

Testing Optimistic update in react query

I am trying to write the test case for an optimistic update in react query. But it's not working. Here is the code that I wrote to test it. Hope someone could help me. Thanks in advance. When I just write the onSuccess and leave an optimistic update, it works fine but here it's not working. And how can we mock the getQueryData and setQueryData here?
import { act, renderHook } from "#testing-library/react-hooks";
import axios from "axios";
import { createWrapper } from "../../test-utils";
import { useAddColorHook, useFetchColorHook } from "./usePaginationReactQuery";
jest.mock("axios");
describe('Testing custom hooks of react query', () => {
it('Should add a new color', async () => {
axios.post.mockReturnValue({data: [{label: 'Grey', id: 23}]})
const { result, waitFor } = renderHook(() => useAddColorHook(1), { wrapper: createWrapper() });
await act(() => {
result.current.mutate({ label: 'Grey' })
})
await waitFor(() => result.current.isSuccess);
})
})
export const createTestQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
cacheTime: Infinity,
},
},
logger: {
log: console.log,
warn: console.warn,
error: () => {},
}
});
export function createWrapper() {
const testQueryClient = createTestQueryClient();
return ({ children }) => (
<QueryClientProvider client={testQueryClient}>
{children}
</QueryClientProvider>
);
}
export const useAddColorHook = (page) => {
const queryClient = useQueryClient()
return useMutation(addColor, {
// onSuccess: () => {
// queryClient.invalidateQueries(['colors', page])
// }
onMutate: async color => {
// newHero refers to the argument being passed to the mutate function
await queryClient.cancelQueries(['colors', page])
const previousHeroData = queryClient.getQueryData(['colors', page])
queryClient.setQueryData(['colors', page], (oldQueryData) => {
return {
...oldQueryData,
data: [...oldQueryData.data, { id: oldQueryData?.data?.length + 1, ...color }]
}
})
return { previousHeroData }
},
onSuccess: (response, variables, context) => {
queryClient.setQueryData(['colors', page], (oldQueryData) => {
console.log(oldQueryData, 'oldQueryData', response, 'response', variables, 'var', context, 'context', 7984)
return {
...oldQueryData,
data: oldQueryData.data.map(data => data.label === variables.label ? response.data : data)
}
})
},
onError: (_err, _newTodo, context) => {
queryClient.setQueryData(['colors', page], context.previousHeroData)
},
onSettled: () => {
queryClient.invalidateQueries(['colors', page])
}
})
}
The error that you are getting actually shows a bug in the way you've implemented the optimistic update:
queryClient.setQueryData(['colors', page], (oldQueryData) => {
return {
...oldQueryData,
data: [...oldQueryData.data, { id: oldQueryData?.data?.length + 1, ...color }]
}
})
what if there is no entry in the query cache that matches this query key? oldQueryData will be undefined, but you're not guarding against that, you are spreading ...oldQueryData.data and this will error out at runtime.
This is what happens in your test because you start with a fresh query cache for every test.
An easy way out would be, since you have previousHeroData already:
const previousHeroData = queryClient.getQueryData(['colors', page])
if (previousHeroData) {
queryClient.setQueryData(['colors', page], {
...previousHeroData,
data: [...previousHeroData.data, { id: previousHeroData.data.length + 1, ...color }]
}
}
If you are using TanStack/query v4, you can also return undefined from the updater function. This doesn't work in v3 though:
queryClient.setQueryData(['colors', page], (oldQueryData) => {
return oldQueryData ? {
...oldQueryData,
data: [...oldQueryData.data, { id: oldQueryData?.data?.length + 1, ...color }]
} : undefined
})
This doesn't perform an optimistic update then though. If you know how to create a valid cache entry from undefined previous data, you can of course also do that.

How to spyOn to react custom hook returned value?

Here is my custom hook:
useCustomModal.ts
export const useCustomModal = (modalType: string) => {
const setModal = () => {
// ... functionality here
};
const handleModalClose = (modalType: string) => {
// ... functionality here
setModal();
// ...
};
return {
handleModalClose,
setModal,
};
};
And here is my test:
useCustomModal.ts
import { act } from '#testing-library/react-hooks';
import { useCustomModal } from './useCustomModal';
describe('some', () => {
it('a test', async () => {
await act(async () => {
const actions = useCustomModal('test');
const spy = jest.spyOn(actions, 'setModal');
actions.handleModalClose('test');
expect(spy).toBeCalledTimes(1);
});
});
});
Test failed :
Expected number of calls: 1
Received number of calls: 0
How to properly spyOn on custom react hooks?
You need to use renderHook in conjunction with act. Something like this:
import { renderHook, act } from '#testing-library/react-hooks';
import { useCustomModal } from './useCustomModal';
describe('some', () => {
it('a test', () => {
const { result } = renderHook(() => useCustomModal('test'));
const spy = jest.spyOn(result.current, 'setModal');
act(() => {
result.current.handleModalClose('test');
});
expect(spy).toBeCalledTimes(1);
});
});

How to test function and inner statement after form submit handler in jest/enzyme react

I am working with React functional component which has a form. I have created a form handler which triggers onSuccess and onFailure, onSuccess function closes the model.
Now I need to write a test for onFailure, onSuccess functions and for the modal closing statement.
Component's code is:
export const TestUpdateModal = function( props ) {
const onSuccess = () => {
MiModal.close( 'AccUpdate' );
}
const onFailure = ( message ) => {
// show error message
}
const { handleSubmit } = updateFormHandler({
onSuccess: onSuccess,
onFailure: onFailure
});
return <form onSubmit={ e => handleSubmit( e ) }/>
}
----------------------------
updateFormHandler is
import { useMutation } from '#apollo/react-hooks';
import UpdateProfile_MUTATION from '../../../graphql/mutations/updateProfile/updateProfile';
export const updateFormHandler = ( { onSuccess, onFailure } ) => {
const onCompleted = response => {
const {
UpdateProfile: {
meta: {
status
},
messages
}
} = response;
const isSuccess = status === 'success';
if( isSuccess ){
onSuccess();
}
else {
onFailure( {
type: 'error',
messages: messages
} );
}
}
const onError = ( response ) => {
const { message } = response;
message &&
onFailure( {
type: 'error',
messages: [{
description: response.message
}]
} );
}
const [updateProfile] = useMutation( UpdateProfile_MUTATION, {
onCompleted: onCompleted,
onError: onError
} );
const handleSubmit = ( e, digitalId, version ) => {
e.preventDefault();
updateProfile( {
variables: {
emailAddress: e.target.emailId.value,
password: e.target.newPasswordId.value,
firstName: e.target.firstName.value,
lastName: e.target.lastName.value,
dateOfBirth: e.target.birthday?.value,
version: version,
digitalId: digitalId
}
} );
}
return {
handleSubmit
};
}
If all you want is to test onSuccess and onFailure, you don't need to use Enzyme or include the form in the test.
Export onSuccess and onFailure functions:
export const TestUpdateModal = function( props ) {
const { handleSubmit } = updateFormHandler({
onSuccess: onSuccess,
onFailure: onFailure
});
return <form onSubmit={ e => handleSubmit( e ) }/>
}
export const onSuccess = () => {
MiModal.close( 'AccUpdate' );
}
export const onFailure = ( message ) => {
// show error message
}
And then test them on their own:
import {onFailure, onSuccess} from './TestUpdateModal';
import {MiModal} from './MiModal';
describe('TestUpdateModal', () => {
it('Success called', () => {
onSuccess();
expect(MiModal.close).toHaveBeenCalledWith('AccUpdate');
});
it('Failure called', () => {
onFailure('Error');
// Here test on failure
// (the example code does nothing)
});
});
Have you attempted to mock useMutation? It seems that by mocking useMutation and then substituting your own implementation that returns whatever you want in your test suite would allow you to assert certain types of scenarios.
I would assume you can pass in mock functions for success/failure then mock useMutation to do various things like trigger those mock functions or return certain data.
This is all pseudo code which I haven't tested, but maybe it will point you in the right direction:
import { useMutation } from "#apollo/react-hooks";
jest.mock("#apollo/react-hooks");
describe("something", () => {
test("should work", () => {
const successMock = jest.fn();
const failureMock = jest.fn();
const updateProfileMock = jest.fn();
// TODO: change the implementation to trigger things like
// firing success or error
useMutation.mockImplementationOnce(updateProfileMock);
const { handleSubmit } = updateFormHandler({
onSuccess: successMock,
onFailure: failureMock,
});
handleSubmit(
// Fake event data which you could use to assert against
{
target: {
emailId: "some-id",
newPasswordId: "some-pw",
firstName: "first-name",
lastName: "last-name",
birthday: "birthday",
},
},
"digital-id",
"version-here"
);
expect(updateProfileMock).toBeCalled();
// TODO: assert that updateProfileMock was called with the right data
});
});
You can also mock the updateFormHandler function in your components in the same way and pass mock handlers and data to assert against.
Hope this helps 👍🏻

debounce async/await and update component state

I have question about debounce async function. Why my response is undefined? validatePrice is ajax call and I receive response from server and return it (it is defined for sure).
I would like to make ajax call after user stops writing and update state after I get reponse. Am I doing it right way?
handleTargetPriceDayChange = ({ target }) => {
const { value } = target;
this.setState(state => ({
selected: {
...state.selected,
Price: {
...state.selected.Price,
Day: parseInt(value)
}
}
}), () => this.doPriceValidation());
}
doPriceValidation = debounce(async () => {
const response = await this.props.validatePrice(this.state.selected);
console.log(response);
//this.setState({ selected: res.TOE });
}, 400);
actions.js
export function validatePrice(product) {
const actionUrl = new Localization().getURL(baseUrl, 'ValidateTargetPrice');
return function (dispatch) {
dispatch({ type: types.VALIDATE_TARGET_PRICE_REQUEST });
dispatch(showLoader());
return axios.post(actionUrl, { argModel: product }, { headers })
.then((res) => {
dispatch({ type: types.VALIDATE_TARGET_PRICE_REQUEST_FULFILLED, payload: res.data });
console.log(res.data); // here response is OK (defined)
return res;
})
.catch((err) => {
dispatch({ type: types.VALIDATE_TARGET_PRICE_REQUEST_REJECTED, payload: err.message });
})
.then((res) => {
dispatch(hideLoader());
return res.data;
});
};
}
Please find below the working code with lodash debounce function.
Also here is the codesandbox link to play with.
Some changes:-
1) I have defined validatePrice in same component instead of taking from prop.
2) Defined the debounce function in componentDidMount.
import React from "react";
import ReactDOM from "react-dom";
import _ from "lodash";
import "./styles.css";
class App extends React.Component {
state = {
selected: { Price: 10 }
};
componentDidMount() {
this.search = _.debounce(async () => {
const response = await this.validatePrice(this.state.selected);
console.log(response);
}, 2000);
}
handleTargetPriceDayChange = ({ target }) => {
const { value } = target;
console.log(value);
this.setState(
state => ({
selected: {
...state.selected,
Price: {
...state.selected.Price,
Day: parseInt(value)
}
}
}),
() => this.doPriceValidation()
);
};
doPriceValidation = () => {
this.search();
};
validatePrice = selected => {
return new Promise(resolve => resolve(`response sent ${selected}`));
};
render() {
return (
<div className="App">
<input type="text" onChange={this.handleTargetPriceDayChange} />
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Hope that helps!!!
You can use the throttle-debounce library to achieve your goal.
Import code in top
import { debounce } from 'throttle-debounce';
Define below code in constructor
// Here I have consider 'doPriceValidationFunc' is the async function
this.doPriceValidation = debounce(400, this.doPriceValidationFunc);
That's it.

Resources