How test custom hooks with setTimeOut - reactjs

We have a functionality, when the content is not visible on the screen then we scroll content till the end of content. here is the hooks i have written. quite new to testing Lib it would be great if someone have already written unit test for something like this.
import { useEffect, useRef } from 'react';
export default function useOnScreen() {
const ref = useRef<HTMLElement | null>();
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
const scrollToView = (timer: number) => {
const scrollRef = ref?.current;
if (ref) {
timeoutRef.current && clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
scrollRef?.current.scrollIntoView({
behavior: 'smooth',
block: 'end',
inline: 'end',
});
}, timer);
}
};
return [ref, scrollToView];
}
Need help here to write test case
import useOnScreen from '../useOnScreen';
import {renderHook, act} from '#testing-library/react-hooks';
describe('useOnScreen', () => {
it('useOnScreen', () => {
const [ref, scrollToView] = renderHook(() => useOnScreen());
});
});

Related

How can I render Google Translate Widget only once using useEffect Hook in Next JS Project

import { useEffect } from "react";
const GoogleTranslate = () => {
useEffect(() => {
let addScript = document.createElement("script");
addScript.setAttribute(
"src",
"//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"
);
document.body.appendChild(addScript);
window.googleTranslateElementInit = googleTranslateElementInit;
}, []);
const googleTranslateElementInit = () => {
return new window.google.translate.TranslateElement(
{
pageLanguage: "en",
layout: google.translate.TranslateElement.InlineLayout.BUTTON,
},
"google_translate_element"
);
};
return <div id="google_translate_element"></div>;
};
export default GoogleTranslate;
Here is my Component. I have used in a single page. But instead of once I am getting two separate instance of google translate button.
So, I want to know how to render only once this button.
Here is the UI image
Double Google translate button bug
useEffect may run twice despite an empty dependency array in multiple circumstances (for example, if a parent component is re-rendering, or if you have React StrictMode enabled).
Hence, you are probably creating 2 elements inside useEffect.
You could analyze your code and check for potential unwanted re-renders or simply replace useEffect with this useEffectOnce hook:
import { useEffect, useRef, useState } from 'react';
export const useEffectOnce = (effect: () => void | (() => void)) => {
const effectFn = useRef<() => void | (() => void)>(effect);
const destroyFn = useRef<void | (() => void)>();
const effectCalled = useRef(false);
const rendered = useRef(false);
const [, setVal] = useState<number>(0);
if (effectCalled.current) {
rendered.current = true;
}
useEffect(() => {
if (!effectCalled.current) {
destroyFn.current = effectFn.current();
effectCalled.current = true;
}
setVal(val => val + 1);
return () => {
if (!rendered.current) {
return;
}
if (destroyFn.current) {
destroyFn.current();
}
};
}, []);
};
Just create a file with this code, and import and use it instead of useEffect.

React + Jest infinite loop only in test

I'm having the following problem with my react + nextJS project...
The component is something like this:
import React, { FC, useCallback, useEffect, useState } from 'react';
import InputMask, { Props } from 'react-input-mask';
import {
getPayersDetails,
PayerCompany,
PayerContact,
PayerDocuments,
} from 'services';
import { Formik } from 'formik';
import { Field, Loading, Page, Tooltip } from 'components';
import { Button, IconButton, Typography } from '#mui/material';
import { TextField, TextFieldProps } from '#material-ui/core';
import { SvgSelfCheckout } from 'images';
import {
FaEdit,
FaFileInvoiceDollar,
FaUserCheck,
FaUserAltSlash,
} from 'react-icons/fa';
import theme from 'styles/theme';
import * as S from './styles';
import { useRouter } from 'next/router';
import { PAYER_HOME } from 'src/routes';
const PayersDetails: FC = () => {
const [payerCompany, setPayerCompany] = useState<PayerCompany[]>([]);
const [payerContact, setPayerContact] = useState<PayerContact[]>([]);
const [payerDocument, setPayerDocument] = useState<PayerDocuments[]>([]);
const [loading, setLoading] = useState(true);
const [isActivePayer, setIsActivePayer] = useState(false);
const router = useRouter();
const getPayerDetails = useCallback(async (payerId: number) => {
setLoading(true);
const payerDetails = await getPayersDetails(payerId);
setPayerCompany(payerDetails.payerCompany);
setPayerDocument(payerDetails.payerDocument);
setPayerContact(payerDetails.payerContact);
setLoading(false);
}, []);
useEffect(() => {
if (!router.isReady) {
return;
}
const payerId = router.query.payerId as string;
try {
const safePayerId = parseInt(payerId);
getPayerDetails(safePayerId);
} catch (e) {
router.push(PAYER_HOME);
return;
}
}, [getPayerDetails, router]);
const contact = payerContact.length > 0 ? payerContact[0] : null;
const mobilePhone = payerContact
.filter(contact => contact.contactType.contactTypeName === 'mobile')
.map(contact => contact.value)[0];
return (
<Page
title="Detalhes do pagador"
pageTitle="Detalhes do pagador"
pageSubtitle="Dados pessoais"
pageSubitleColor={`${theme.palette.primary.light}`}
>
{loading ? (
<Loading show={loading} />
) : (
<Formik
enableReinitialize={true}
initialValues={{
name: contact?.contactName,
email: contact?.value,
}}
onSubmit={() => console.log('onSumit')}
>
....
)}
</Formik>
)}
</Page>
);
};
export default PayersDetails;
And I'm trying to test it with the following code:
import { render, screen, fireEvent } from '#testing-library/react';
import { getCompany } from 'services/companies';
import { getPayersDetails } from 'services/payers';
import PayersImport from '.';
import { useRouter } from 'next/router';
import userEvent from '#testing-library/user-event';
jest.mock('services/payers', () => ({
__esModule: true, // this property makes it work
default: 'mockedDefaultExport',
getPayersDetails: jest.fn(),
}));
jest.mock('next/router', () => ({
useRouter: jest.fn().mockImplementation(() => ({
route: '/',
pathname: '',
query: '',
asPath: '',
})),
}));
describe('payers details layout', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
describe('when rendering', () => {
let getPayersDetailsMock;
beforeEach(() => {
getPayersDetailsMock = {
...
};
getPayersDetails.mockResolvedValue(getPayersDetailsMock);
useRouter.mockImplementation(() => ({
route: '/',
pathname: '',
isReady: true,
query: { payerId: 1 },
asPath: '',
}));
render(<PayersImport />);
});
it('Calls details api with the correct id', () => {
expect(getPayersDetails).toHaveBeenCalledWith(1);
});
});
});
The issue is:
The component load ok when we go to a browser, but when I run it on jest I get the following error:
console.error
Warning: An update to PayersDetails 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 PayersDetails (/home/thiago/finnet/repos/welcome/apps/lunapay-front/src/layouts/Payers/PayersDetails/index.tsx:29:43)
43 | setPayerContact(payerDetails.payerContact);
44 |
> 45 | setLoading(false);
| ^
46 | }, []);
47 |
48 | useEffect(() => {
I get it is only a warning and it wouldn't be a problem for me, but the issue is that it goes into a infinite loop trying to render again.
What am I doing wrong??
I solved the issue, but it was not the best solution.
Basically the render was being triggered by the <Loading /> component being rendered again.
What I did was just adding a new property telling me if it was already fetched and canceling the loop.
const [payerDetailsResponse, setPayerDetailsResponse] =
useState<PayerDetailsResponse | null>(null);
const [payerCompany, setPayerCompany] = useState<PayerCompany[]>([]);
const [payerContact, setPayerContact] = useState<PayerContact[]>([]);
const [payerDocument, setPayerDocument] = useState<PayerDocuments[]>([]);
const [loading, setLoading] = useState(true);
const [isActivePayer, setIsActivePayer] = useState(false);
const router = useRouter();
const getPayerDetails = useCallback(async (payerId: number) => {
setLoading(true);
const payerDetails = await getPayersDetails(payerId);
setPayerDetailsResponse(payerDetails);
setPayerCompany(payerDetails.payerCompany);
setPayerDocument(payerDetails.payerDocument);
setPayerContact(payerDetails.payerContact);
setLoading(false);
}, []);
useEffect(() => {
if (payerDetailsResponse) {
return;
}
if (!router.isReady) {
return;
}
const payerId = router.query.payerId as string;
try {
const safePayerId = parseInt(payerId);
getPayerDetails(safePayerId);
} catch (e) {
router.push(PAYER_HOME);
return;
}
}, [router, getPayerDetails]);
Sorry not to be able to help more, but that was a solution for my problem :)
Sometimes jest fall into an infinite loop with useEffect, if that is your case, you can make a mock of it
import React from 'react'
const mockUseEffect = jest.fn()
jest.spyOn(React, 'useEffect').mockImplementation(mockUseEffect)
and try with that and/or adapt to your needs

Testing a custom React hook that shows an exit prompt

I apologize if this question has been asked before. I did some searching but couldn't find an answer. I'm fairly new to React, so I may be missing something obvious.
I have inherited some TypeScript React code that includes a custom React hook to display a prompt when a user reloads or exits a page when there are edits on the page.
import { useState, useEffect } from 'react';
const initBeforeUnload = (showExitPrompt: boolean) => {
window.onbeforeunload = (event: BeforeUnloadEvent) => {
if (showExitPrompt) {
const e = event || window.event;
e.preventDefault();
if (e) {
e.returnValue = '';
}
return '';
}
};
};
// Hook
const useExitPrompt = (
bool: boolean
): [boolean, (showPrompt: boolean) => void] => {
const [showExitPrompt, setShowExitPrompt] = useState<boolean>(bool);
window.onload = () => {
initBeforeUnload(showExitPrompt);
};
useEffect(() => {
initBeforeUnload(showExitPrompt);
}, [showExitPrompt]);
useEffect(() => () => {
initBeforeUnload(false);
}, []);
return [showExitPrompt, setShowExitPrompt];
};
export default useExitPrompt;
No unit tests were included with this code when I inherited it, so I'm writing tests now. So far I've come up with the following:
/**
* #jest-environment jsdom
*/
import { renderHook, act } from '#testing-library/react-hooks';
import useExitPrompt from './useExitPrompt';
describe('useExitPrompt', () => {
it('should update the value of showExitPrompt', () => {
const { result } = renderHook(({ usePrompt }) => useExitPrompt(usePrompt),
{
initialProps: {
usePrompt: true
}
});
const [initialShowExitPrompt, setShowExitPrompt] = result.current;
expect(initialShowExitPrompt).toBe(true);
act(() => {
setShowExitPrompt(false);
});
const [updatedShowExitPrompt] = result.current;
expect(updatedShowExitPrompt).toBe(false);
});
});
This test passes, but it doesn't really get to the meat of what the hook is about: when showExitPrompt is true and the page is unloaded, is the exit prompt displayed? (In fact, it really only tests that the useState hook works, which is not what I want at all.) Unfortunately, I've run into a brick wall in trying to figure out how to test this; any suggestions?

how to test a hook with async state update in useEffect?

i have a simple hook that fetches the value and sets it to option as follows:
import Fuse from 'fuse.js'
import React from 'react'
// prefetches options and uses fuzzy search to search on that option
// instead of fetching on each keystroke
export function usePrefetchedOptions<T extends {}>(fetcher: () => Promise<T[]>) {
const [options, setOptions] = React.useState<T[]>([])
React.useEffect(() => {
// fetch options initially
const optionsFetcher = async () => {
try {
const data = await fetcher()
setOptions(data)
} catch (err) {
errorSnack(err)
}
}
optionsFetcher()
}, [])
// const fuseOptions = {
// isCaseSensitive: false,
// keys: ['name'],
// }
// const fuse = new Fuse(options, fuseOptions)
// const dataServiceProxy = (options) => (pattern: string) => {
// // console.error('options inside proxy call', { options })
// const optionsFromSearch = fuse.search(pattern).map((fuzzyResult) => fuzzyResult.item)
// return new Promise((resolve) => resolve(pattern === '' ? options : optionsFromSearch))
// }
return options
}
i am trying to test it with following code:
import { act, renderHook, waitFor } from '#testing-library/react-hooks'
import { Wrappers } from './test-utils'
import { usePrefetchedOptions } from './usePrefetchedOptions'
import React from 'react'
const setup = ({ fetcher }) => {
const {
result: { current },
waitForNextUpdate,
...rest
} = renderHook(() => usePrefetchedOptions(fetcher), { wrapper: Wrappers })
return { current, waitForNextUpdate, ...rest }
}
describe('usePrefetchedOptions', () => {
const mockOptions = [
{
value: 'value1',
text: 'Value one',
},
{
value: 'value2',
text: 'Value two',
},
{
value: 'value3',
text: 'Value three',
},
]
test('searches for appropriate option', async () => {
const fetcher = jest.fn(() => new Promise((resolve) => resolve(mockOptions)))
const { rerender, current: options, waitForNextUpdate } = setup({ fetcher })
await waitFor(() => {
expect(fetcher).toHaveBeenCalled()
})
// async waitForNextUpdate()
expect(options).toHaveLength(3) // returns initial value of empty options = []
})
})
the problem is when i am trying to assert the options at the end of the test, it still has the initial value of []. However if I log the value inside the hook, it returns the mockOptions. How do I update the hook after it is update by useEffect but in async manner.
I have also tried using using waitForNextUpdate where it is commented in the code. it times out with following error:
Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error:
Couple things, currently you're waiting for fetcher to be called in your tests, but the state update actually happens not after fetcher is called but after the promise that fetcher returns is resolved. So you'd need to wait on the resolution of that promise in your test
Also, you're destructuring the value of result.current when you first render your hook. That value is just a copy of result.current after that first render and it will not update after that. To query the current value of options, you should query result.current in your assertion instead.
const fetcherPromise = Promise.resolve(mockOptions);
const fetch = jest.fn(() => fetcherPromise);
const { result } = renderHook(() => usePrefetchedOptions(fetcher), { wrappers: Wrappers })
await act(() => fetcherPromise);
expect(result.current).toHaveLength(3)
Here's what worked for me whenI needed to test the second effect of my context below:
import React, {createContext, useContext, useEffect, useState} from "react";
import {IGlobalContext} from "../models";
import {fetchGravatar} from "../services";
import {fetchTokens, Token} from "#mylib/utils";
const GlobalContext = createContext<IGlobalContext>({} as IGlobalContext);
function useGlobalProvider(): IGlobalContext {
const [token, setToken] = useState<Token>(Token.deserialize(undefined));
const [gravatar, setGravatar] = useState<string>('');
useEffect(() => {
setToken(fetchTokens());
}, []);
useEffect(() => {
if (token?.getIdToken()?.getUsername()) {
fetchGravatar(token.getIdToken().getUsername())
.then(setGravatar)
}
}, [token]);
const getToken = (): Token => token;
const getGravatar = (): string => gravatar;
return {
getToken,
getGravatar
}
}
const GlobalProvider: React.FC = ({children}) => {
const globalContextData: IGlobalContext = useGlobalProvider();
return (
<GlobalContext.Provider value={globalContextData}>{children}</GlobalContext.Provider>
);
};
function useGlobalContext() {
if (!useContext(GlobalContext)) {
throw new Error('GlobalContext must be used within a Provider');
}
return useContext<IGlobalContext>(GlobalContext);
}
export {GlobalProvider, useGlobalContext};
corresponding tests:
import React from "react";
import {GlobalProvider, useGlobalContext} from './Global';
import {act, renderHook} from "#testing-library/react-hooks";
import utils, {IdToken, Token} from "#mylib/utils";
import {getRandomGravatar, getRandomToken} from 'mock/Token';
import * as myService from './services/myService';
import {Builder} from "builder-pattern";
import faker from "faker";
jest.mock('#mylib/utils', () => ({
...jest.requireActual('#mylib/utils')
}));
describe("GlobalContext", () => {
it("should set Token when context loads", () => {
const expectedToken = getRandomToken('mytoken');
const spyFetchToken = spyOn(utils, 'fetchTokens').and.returnValue(expectedToken);
const wrapper = ({children}: { children?: React.ReactNode }) => <GlobalProvider>{children} </GlobalProvider>;
const {result} = renderHook(() => useGlobalContext(), {wrapper});
expect(spyFetchToken).toHaveBeenCalled();
expect(result.current.getToken()).toEqual(expectedToken);
})
it("should fetch Gravatar When Token username changes", async () => {
const expectedToken = getRandomToken('mytoken');
const expectedGravatar = getRandomGravatar();
const returnedGravatarPromise = Promise.resolve(expectedGravatar);
const spyFetchToken = spyOn(utils, 'fetchTokens').and.returnValue(expectedToken);
const spyFetchGravatar = spyOn(myService, 'fetchGravatar').and.returnValue(returnedGravatarPromise);
const wrapper = ({children}: { children?: React.ReactNode }) =>
<GlobalProvider>{children} </GlobalProvider>;
const {result, waitForValueToChange} = renderHook(() => useGlobalContext(), {wrapper});
// see here
// we need to wait for the promise to be resolved, even though the gravatar spy returned it
let resolvedGravatarPromise;
act(() => {
resolvedGravatarPromise = returnedGravatarPromise;
})
await waitForValueToChange(() => result.current.getGravatar());
expect(spyFetchToken).toHaveBeenCalled();
expect(result.current.getToken()).toEqual(expectedToken);
expect(spyFetchGravatar).toHaveBeenCalledWith(expectedToken.getIdToken().getUsername());
expect(resolvedGravatarPromise).toBeInstanceOf(Promise);
expect(result.current.getGravatar()).toEqual(expectedGravatar);
})
})

React Hooks + Mobx => Invalid hook call. Hooks can only be called inside of the body of a function component

I have a React Native App,
Here i use mobx ("mobx-react": "^6.1.8") and react hooks.
i get the error:
Invalid hook call. Hooks can only be called inside of the body of a function component
Stores index.js
import { useContext } from "react";
import UserStore from "./UserStore";
import SettingsStore from "./SettingsStore";
const useStore = () => {
return {
UserStore: useContext(UserStore),
SettingsStore: useContext(SettingsStore),
};
};
export default useStore;
helper.js OLD
import React from "react";
import useStores from "../stores";
export const useLoadAsyncProfileDependencies = userID => {
const { ExamsStore, UserStore, CTAStore, AnswersStore } = useStores();
const [user, setUser] = useState({});
const [ctas, setCtas] = useState([]);
const [answers, setAnswers] = useState([]);
useEffect(() => {
if (userID) {
(async () => {
const user = await UserStore.initUser();
UserStore.user = user;
setUser(user);
})();
(async () => {
const ctas = await CTAStore.getAllCTAS(userID);
CTAStore.ctas = ctas;
setCtas(ctas);
})();
(async () => {
const answers = await AnswersStore.getAllAnswers(userID);
UserStore.user.answers = answers.items;
AnswersStore.answers = answers.items;
ExamsStore.initExams(answers.items);
setAnswers(answers.items);
})();
}
}, [userID]);
};
Screen
import React, { useEffect, useState, useRef } from "react";
import {
View,
Dimensions,
SafeAreaView,
ScrollView,
StyleSheet
} from "react-native";
import {
widthPercentageToDP as wp,
heightPercentageToDP as hp
} from "react-native-responsive-screen";
import { observer } from "mobx-react";
import useStores from "../../stores";
import { useLoadAsyncProfileDependencies } from "../../helper/app";
const windowWidth = Dimensions.get("window").width;
export default observer(({ navigation }) => {
const {
UserStore,
ExamsStore,
CTAStore,
InternetConnectionStore
} = useStores();
const scrollViewRef = useRef();
const [currentSlide, setCurrentSlide] = useState(0);
useEffect(() => {
if (InternetConnectionStore.isOffline) {
return;
}
Tracking.trackEvent("opensScreen", { name: "Challenges" });
useLoadAsyncProfileDependencies(UserStore.userID);
}, []);
React.useEffect(() => {
const unsubscribe = navigation.addListener("focus", () => {
CTAStore.popBadget(BadgetNames.ChallengesTab);
});
return unsubscribe;
}, [navigation]);
async function refresh() {
const user = await UserStore.initUser(); //wird das gebarucht?
useLoadAsyncProfileDependencies(UserStore.userID);
if (user) {
InternetConnectionStore.isOffline = false;
}
}
const name = UserStore.name;
return (
<SafeAreaView style={styles.container} forceInset={{ top: "always" }}>
</SafeAreaView>
);
});
so now, when i call the useLoadAsyncProfileDependencies function, i get this error.
The Problem is that i call useStores in helper.js
so when i pass the Stores from the Screen to the helper it is working.
export const loadAsyncProfileDependencies = async ({
ExamsStore,
UserStore,
CTAStore,
AnswersStore
}) => {
const userID = UserStore.userID;
if (userID) {
UserStore.initUser().then(user => {
UserStore.user = user;
});
CTAStore.getAllCTAS(userID).then(ctas => {
console.log("test", ctas);
CTAStore.ctas = ctas;
});
AnswersStore.getAllAnswers(userID).then(answers => {
AnswersStore.answers = answers.items;
ExamsStore.initExams(answers.items);
});
}
};
Is there a better way? instead passing the Stores.
So that i can use this function in functions?
As the error says, you can only use hooks inside the root of a functional component, and your useLoadAsyncProfileDependencies is technically a custom hook so you cant use it inside a class component.
https://reactjs.org/warnings/invalid-hook-call-warning.html
EDIT: Well after showing the code for app.js, as mentioned, hook calls can only be done top level from a function component or the root of a custom hook. You need to rewire your code to use custom hooks.
SEE THIS: https://reactjs.org/docs/hooks-rules.html
You should return the value for _handleAppStateChange so your useEffect's the value as a depdendency in your root component would work properly as intended which is should run only if value has changed. You also need to rewrite that as a custom hook so you can call hooks inside.
doTasksEveryTimeWhenAppWillOpenFromBackgorund and doTasksEveryTimeWhenAppGoesToBackgorund should also be written as a custom hook so you can call useLoadAsyncProfileDependencies inside.
write those hooks in a functional way so you are isolating specific tasks and chain hooks as you wish without violiating the rules of hooks. Something like this:
const useGetMyData = (params) => {
const [data, setData] = useState()
useEffect(() => {
(async () => {
const apiData = await myApiCall(params)
setData(apiData)
})()
}, [params])
return data
}
Then you can call that custom hook as you wish without violation like:
const useShouldGetData = (should, params) => {
if (should) {
return useGetMyData()
}
return null
}
const myApp = () => {
const myData = useShouldGetData(true, {id: 1})
return (
<div>
{JSON.stringify(myData)}
</div>
)
}

Resources