Asyncronous test using React Testing Library/WaitFor - javascript

I'm looking for the simplest solution to this problem using the popular React Testing Library. I've looked at a lot of links out there and nothing seems to fit..
Here is the component in it's simplest form below:
import React, { useState, useEffect } from "react";
function App() {
const [loggedIn, setLoggedIn] = useState(false)
useEffect(() => {
setTimeout(() => setLoggedIn(true), 1000)
}, [])
return (
<button data-testid="login-button">{loggedIn ? "Log Out" : "Log In"}</button>
);
}
export default App;
And below is the test for the 'no wait' state of the button but would like to know how to implement 'waitFor' within the same test for the change in text in the button after 1 second..
import { render, screen, waitFor } from "#testing-library/react";
import App from "./App";
test('Check the text "Log out" is eventually there', () => {
render(<App />);
const LoginButton = screen.getByText(/Log/)
expect(LoginButton).toHaveTextContent(/Log In/i);
/*How to use waitFor here*/
});
I know we can mock the return from an API call easily and for this problem we probably have to return a promise before we can test using 'waitFor'. How can we use 'WaitFor' in the example test file above?

You can use jest's Timer Mocks to handle the setTimeouts.
import { act, render, screen } from "#testing-library/react";
import App from "./App";
test("renders learn react link", () => {
// use the fake timer
jest.useFakeTimers();
render(<App />);
// assert the initial text of the button
expect(screen.getByRole("button", { name: "Log In" })).toBeInTheDocument();
// advance the timer
act(() => jest.advanceTimersByTime(1000));
// assert the changed text of the button
expect(screen.getByRole("button", { name: "Log Out" })).toBeInTheDocument();
// switch back to use real timers
jest.useRealTimers();
});
Reference:
Using act() for timers

Thankyou Shyam for helping me discover the solution, which was not
using 'waitFor' from the testing-library in this case:
test('Check the text "Log out" is eventually there', () => {
jest.useFakeTimers();
render();
//Reference the element by locating using Regex
const LoginButton = screen.getByText(/Log/);
//Search in real time for the initial/default text on the button
expect(LoginButton).toHaveTextContent(/Log In/i);
// advance the timer
act(() => jest.advanceTimersByTime(1000));
// assert the changed text of the button
expect(LoginButton).toHaveTextContent(/Log Out/i);
// switch back to use real timers
jest.useRealTimers();
});

Related

useEffect running two times, but my dependencies array is empty, why is this happening? [duplicate]

I am new to reactJS and am writing code so that before the data is loaded from DB, it will show loading message, and then after it is loaded, render components with the loaded data. To do this, I am using both useState hook and useEffect hook. Here is the code:
The problem is, useEffect is triggered twice when I check with console.log. The code is thus querying the same data twice, which should be avoided.
Below is the code that I wrote:
import React from 'react';
import './App.css';
import {useState,useEffect} from 'react';
import Postspreview from '../components/Postspreview'
const indexarray=[]; //The array to which the fetched data will be pushed
function Home() {
const [isLoading,setLoad]=useState(true);
useEffect(()=>{
/*
Query logic to query from DB and push to indexarray
*/
setLoad(false); // To indicate that the loading is complete
})
},[]);
if (isLoading===true){
console.log("Loading");
return <div>This is loading...</div>
}
else {
console.log("Loaded!"); //This is actually logged twice.
return (
<div>
<div className="posts_preview_columns">
{indexarray.map(indexarray=>
<Postspreview
username={indexarray.username}
idThumbnail={indexarray.profile_thumbnail}
nickname={indexarray.nickname}
postThumbnail={indexarray.photolink}
/>
)}
</div>
</div>
);
}
}
export default Home;
Can someone help me out in understanding why it is called twice, and how to fix the code properly?
Thank you very much!
Put the console.log inside the useEffect
Probably you have other side effects that cause the component to rerender but the useEffect itself will only be called once. You can see this for sure with the following code.
useEffect(()=>{
/*
Query logic
*/
console.log('i fire once');
},[]);
If the log "i fire once" is triggered more than once it means your issue is
one of 3 things.
This component appears more than once in your page
This one should be obvious, your component is in the page a couple of times and each one will mount and run the useEffect
Something higher up the tree is unmounting and remounting
The component is being forced to unmount and remount on its initial render. This could be something like a "key" change happening higher up the tree. you need to go up each level with this useEffect until it renders only once. then you should be able to find the cause or the remount.
React.Strict mode is on
StrictMode renders components twice (on dev but not production) in order to detect any problems with your code and warn you about them (which can be quite useful).
This answer was pointed out by #johnhendirx and written by #rangfu, see link and give him some love if this was your problem. If you're having issues because of this it usually means you're not using useEffect for its intended purpose. There's some great information about this in the beta docs you can read that here
Remove <React.StrictMode> from index.js
This code will be
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
this
root.render(
<App />
);
React StrictMode renders components twice on dev server
You are most likely checking the issue on a dev environment with strict mode enabled.
To validate this is the case, search for <React.StrictMode> tag and remove it, or build for production. The double render issue should be gone.
From React official documentation
Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:
Functions passed to useState, useMemo, or useReducer
[...]
Strict Mode - Reactjs docs
Similar question here My React Component is rendering twice because of Strict Mode
Please check your index.js
<React.StrictMode>
<App />
</React.StrictMode>
Remove the <React.StrictMode> wrapper
you should now fire once
root.render(
<App />
);
react root > index.js > remove <React.StrictMode> wrapper
It is the feature of ReactJS while we use React.StrictMode. StrictMode activates additional checks and warnings for its descendants nodes. Because app should not crash in case of any bad practice in code. We can say StrictMode is a safety check to verify the component twice to detect an error.
You will get this <React.StricyMode> at root of the component.
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
if you want to restrict components to render twice, You can remove <React.StrictMode> and check it. But It is necessary to use StrictMode to detect a run time error in case of bad code practice.
if you are using Next js, change reactStrictMode from "true" to false :
add this to your next.config.js
reactStrictMode: false,
I have found a very good explanation behind twice component mounting in React 18. UseEffect called twice in React
Note: In production, it works fine. Under strict mode in the development environment, twice mounting is intentionally added to handle the errors and required cleanups.
I'm using this as my alternative useFocusEffect. I used nested react navigation stacks like tabs and drawers and refactoring using useEffect doesn't work on me as expected.
import React, { useEffect, useState } from 'react'
import { useFocusEffect } from '#react-navigation/native'
const app = () = {
const [isloaded, setLoaded] = useState(false)
useFocusEffect(() => {
if (!isloaded) {
console.log('This should called once')
setLoaded(true)
}
return () => {}
}, [])
}
Also, there's an instance that you navigating twice on the screen.
Not sure why you won't put the result in state, here is an example that calls the effect once so you must have done something in code not posted that makes it render again:
const App = () => {
const [isLoading, setLoad] = React.useState(true)
const [data, setData] = React.useState([])
React.useEffect(() => {
console.log('in effect')
fetch('https://jsonplaceholder.typicode.com/todos')
.then(result => result.json())
.then(data => {
setLoad(false)//causes re render
setData(data)//causes re render
})
},[])
//first log in console, effect happens after render
console.log('rendering:', data.length, isLoading)
return <pre>{JSON.stringify(data, undefined, 2)}</pre>
}
//render app
ReactDOM.render(<App />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
To prevent the extra render you can combine data and loading in one state:
const useIsMounted = () => {
const isMounted = React.useRef(false);
React.useEffect(() => {
isMounted.current = true;
return () => isMounted.current = false;
}, []);
return isMounted;
};
const App = () => {
const [result, setResult] = React.useState({
loading: true,
data: []
})
const isMounted = useIsMounted();
React.useEffect(() => {
console.log('in effect')
fetch('https://jsonplaceholder.typicode.com/todos')
.then(result => result.json())
.then(data => {
//before setting state in async function you should
// alsways check if the component is still mounted or
// react will spit out warnings
isMounted.current && setResult({ loading: false, data })
})
},[isMounted])
console.log(
'rendering:',
result.data.length,
result.loading
)
return (
<pre>{JSON.stringify(result.data, undefined, 2)}</pre>
)
}
//render app
ReactDOM.render(<App />, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
The new React docs (currently in beta) have a section describing precisely this behavior:
How to handle the Effect firing twice in development
From the docs:
Usually, the answer is to implement the cleanup function. The cleanup function should stop or undo whatever the Effect was doing. The rule of thumb is that the user shouldn’t be able to distinguish between the Effect running once (as in production) and a setup → cleanup → setup sequence (as you’d see in development).
So this warning should make you double check your useEffect, usually means you need to implement a cleanup function.
This may not be the ideal solution. But I used a workaround.
var ranonce = false;
useEffect(() => {
if (!ranonce) {
//Run you code
ranonce = true
}
}, [])
Even though useEffect runs twice code that matters only run once.
As others have already pointed out, this happens most likely due to a Strict Mode feature introduced in React 18.0.
I wrote a blog post that explains why this is happening and what you can do to work around it.
But if you just want to see the code, here you go:
let initialized = false
useEffect(() => {
if (!initialized) {
initialized = true
// My actual effect logic...
...
}
}, [])
Or as a re-usable hook:
import type { DependencyList, EffectCallback } from "react"
import { useEffect } from "react"
export function useEffectUnsafe(effect: EffectCallback, deps: DependencyList) {
let initialized = false
useEffect(() => {
if (!initialized) {
initialized = true
effect()
}
}, deps)
}
Please keep in mind that you should only resort to this solution if you absolutely have to!
I've had this issue where something like:
const [onChainNFTs, setOnChainNFTs] = useState([]);
would trigger this useEffect twice:
useEffect(() => {
console.log('do something as initial state of onChainNFTs changed'); // triggered 2 times
}, [onChainNFTs]);
I confirmed that the component MOUNTED ONLY ONCE and setOnChainNFTs was NOT called more than once - so this was not the issue.
I fixed it by converting the initial state of onChainNFTs to null and doing a null check.
e.g.
const [onChainNFTs, setOnChainNFTs] = useState(null);
useEffect(() => {
if (onChainNFTs !== null) {
console.log('do something as initial state of onChainNFTs changed'); // triggered 1 time!
}
}, [onChainNFTs]);
Here is the custom hook for your purpose. It might help in your case.
import {
useRef,
EffectCallback,
DependencyList,
useEffect
} from 'react';
/**
*
* #param effect
* #param dependencies
* #description Hook to prevent running the useEffect on the first render
*
*/
export default function useNoInitialEffect(
effect: EffectCallback,
dependancies?: DependencyList
) {
//Preserving the true by default as initial render cycle
const initialRender = useRef(true);
useEffect(() => {
let effectReturns: void | (() => void) = () => {};
/**
* Updating the ref to false on the first render, causing
* subsequent render to execute the effect
*
*/
if (initialRender.current) {
initialRender.current = false;
} else {
effectReturns = effect();
}
/**
* Preserving and allowing the Destructor returned by the effect
* to execute on component unmount and perform cleanup if
* required.
*
*/
if (effectReturns && typeof effectReturns === 'function') {
return effectReturns;
}
return undefined;
}, dependancies);
}
There is nothing to worry about. When you are running React in development mode. It will sometimes run twice. Test it in prod environment and your useEffect will only run once. Stop Worrying!!
It is strict mode in my case. Remove strict mode component at index.tsx or index.jsx
If someone comes here using NextJS 13, in order to remove the Strict mode you need to add the following on the next.config.js file:
const nextConfig = {
reactStrictMode: false
}
module.exports = nextConfig
When I created the project it used "Strict mode" by default that's why I must set it explicitly.
Ok this is maybe a bit late to comment on this - but I found a rather useful solution which is 100% react.
In my case I have a token which I'm using to make a POST request which logs out my current user.
I'm using a reducer with state like this:
export const INITIAL_STATE = {
token: null
}
export const logoutReducer = (state, action) => {
switch (action.type) {
case ACTION_SET_TOKEN :
state = {
...state,
[action.name] : action.value
};
return state;
default:
throw new Error(`Invalid action: ${action}`);
}
}
export const ACTION_SET_TOKEN = 0x1;
Then in my component I'm checking the state like this:
import {useEffect, useReducer} from 'react';
import {INITIAL_STATE, ACTION_SET_TOKEN, logoutReducer} from "../reducers/logoutReducer";
const Logout = () => {
const router = useRouter();
const [state, dispatch] = useReducer(logoutReducer, INITIAL_STATE);
useEffect(() => {
if (!state.token) {
let token = 'x' // .... get your token here, i'm using some event to get my token
dispatch(
{
type : ACTION_SET_TOKEN,
name : 'token',
value : token
}
);
} else {
// make your POST request here
}
}
The design is actually nice - you have the opportunity to discard your token from storage after the POST request, make sure the POST succeeds before anything. For async stuff you can use the form :
POST().then(async() => {}).catch(async() => {}).finally(async() => {})
all running inside useEffect - works 100% and within I think what the REACT developers had in mind - this pointed out that I actually had more cleanup to do (like removing my tokens from storage etc) before everything was working but now I can navigate to and from my logout page without anything weird happening.
My two cents...
I used CodeSandbox and removing prevented the issue.
CodeSandbox_sample

How to mock the elements of react-hook-form when testing with react-testing-library?

Having a basic component which uses react-hook-form:
const { handleSubmit, reset, control } = useForm({
resolver: yupResolver(schema)
});
...
<MyComponent
title='title'
open={isOpened}
control={control}
/>
This component has 3 props, title - a string, open - a function, control - no idea what is it, all of them mandatory.
So, when writing a test for it I got stuck here:
import { render } from '#testing-library/react';
import '#testing-library/jest-dom';
import MyComponent from './my-component';
describe('MyComponent', () => {
const title = 'title';
it('should render successfully', () => {
const { baseElement, getByText } = render(
<TaskModal
title={title}
open={jest.fn()}
control={} // what should be written here?
/>
);
expect(baseElement).toBeTruthy();
expect(getByText(title)).toBeTruthy();
});
How can control be mocked for this test?
Maybe just passing in the real control from useForm, like they are doing in react-hook-form's testing. https://github.com/react-hook-form/react-hook-form/blob/master/src/__tests__/useController.test.tsx
control came from useForm:
const { control } = useForm();
Control is necessary when you use Controller or useController
The doc: https://react-hook-form.com/api/usecontroller
I suppose the TaskModal component is in a form. I recommend to put the form inside the modal and it would be easier to test otherwise you can wrap your component (TaskModal) with a form for your test.
If anyone is getting any error while using hooks inside tests, try renderHook from testing-library/react:
import { render, renderHook } from '#testing-library/react'
const { result } = renderHook(() => useForm())
render(
<TextField control={result.current.control} />
)

Why only first time React function component getting props

React throw an error when we try to update the state on an unmounted component.So When I test react component for that I am getting errors on the first render only.
I made a component that enable child component based on click. And child component have button which updates state after some settimeout which throw react warning
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
Which is perfectly valid. But to overcome this I am passing enable props from the parent component based on that there is a condition just before setTimeout in the child component. So why does it throw an error the first time only?
To reproduce
Click on the child component button which is false and click on parent component button enable which unmount the child component.
**My question is why react throw an error on the first time only ? And why it is working fine on second time **
Parent component
import { useState } from "react";
import "./styles.css";
import { Test } from "./Test";
export default function App() {
const [state, setstate] = useState(true);
const changeState = () => {
setstate(!state);
};
return (
<div className="App">
<button onClick={changeState}>enable </button>
{state && <Test enable={state} />}
</div>
);
}
Child Component
import React, { useState } from "react";
export const Test = (props) => {
const [state, setstate] = useState(false);
const fetchData = () => {
setstate(!state);
if (props.enable) {
setTimeout(() => {
setstate(false);
}, 1000);
}
};
return (
<>
<button onClick={fetchData}> {`${state}`}</button>
</>
);
};
Codesandbox link to test
Nice track, Just you are missing a minor point, when you write a state thats needed some time to execute and the same time we can visit the flow again and again base on any action, then we need to clear old subscription before go to new one...
For example, in your code here, you update state flow, but the state flow is register a new subscription every time we visit a component with valid props and click on button, so that, prev execution may still work when you trigger new event, so simply, what we need to do unmounted old subscription and we can do that by this for your case:
import React, { useState, useEffect } from "react";
export const Test = (props) => {
const [state, setstate] = useState(false);
useEffect(() => {
if (props.enable) {
const timer = setTimeout(() => {
setstate(false);
}, 1000);
return () => clearTimeout(timer);
}
}, [state, props.enable]);
const fetchData = () => {
setstate((prev) => !prev);
};
return (
<>
<button onClick={fetchData}> {`${state}`}</button>
</>
);
};
Look at code above, simply we add code need to cleanup in effect which its look to my state and prop, now when I click on button, the effect will trigger, if we do that again, the clearTime will work for prev subscription and then add new one and so on...
Notes:
In your case we can remove function and use setState direct on your button.
Prefer to use useCallBack in your function like const fetchData = useCAllback...
You can use setstate((prev) => !prev); its will be work as snapshot, and its usefull when you depends on old value..but may it not needed in some cases too, but just to know about this feature.
Update 1:
What is Subscription:
You can say the subscription represents a disposable resource, such as the execution of an Observable. A Subscription has one important method, unsubscribe, that takes no argument and just disposes the resource held by the subscription, in another word, you can say yes, any async task or any job will be invoke to react life-cycle state and its needed to observe changes, then you talk about subscribe, like API or time out or time interval and so on, any of these action thats need to clear prev subscribe (stop observer - unsubscribe) to prevent any memory leek and clear memory to keep state flow safe and prevent unneeded reredner.

How to test a click event on a function sent through prop drilling

I have a click event in my component that I am trying to test with #testing-library/react. This click event is being issued a function from the parent component, like so:
<DownloadButton>
{handleDownload => (
<ActionButton
onClick={handleDownload}
data-testid={CONFIG.TEST_IDS.BUTTONS.DOWNLOAD}
>
Download
</ActionButton>
)}
</DownloadButton>
I can getByText and fireEvent.click on the button, but unsure how to test if the handleDownload function actually fired.
So if I understand your question correctly you can't be sure if the onClick handler is called when you press your ActionButton?
another case that you want to test is if the DownloadButton provides the handleDownload render prop.
I would split the one test into two tests and separate each component on its own.
import React from "react";
import { DownloadButton, ActionButton } from "./App";
import { render, fireEvent } from "#testing-library/react";
describe("DownloadButton", () => {
it("returns handleDownloadFunction", () => {
const childrenMock = jest.fn();
render(<DownloadButton children={childrenMock} />);
expect(childrenMock).toHaveBeenCalledTimes(1);
expect(childrenMock.mock.calls[0][0].handleDownload).toBeDefined();
});
});
describe("ActionButton", () => {
it("onClick invokes function", () => {
const onClickMock = jest.fn();
const { getByTestId, debug } = render(
<ActionButton onClick={onClickMock} data-testid={"test-button"} />
);
debug();
const button = getByTestId("test-button");
fireEvent.click(button);
expect(onClickMock).toHaveBeenCalledTimes(1);
});
});
for more detail take a look at the codesandbox

Mock out imported Lazy React component

Here's my lazy component:
const LazyBones = React.lazy(() => import('#graveyard/Bones')
.then(module => ({default: module.BonesComponent}))
export default LazyBones
I'm importing it like this:
import Bones from './LazyBones'
export default () => (
<Suspense fallback={<p>Loading bones</p>}>
<Bones />
</Suspense>
)
And in my test I have this kind of thing:
import * as LazyBones from './LazyBones';
describe('<BoneYard />', function() {
let Bones;
let wrapper;
beforeEach(function() {
Bones = sinon.stub(LazyBones, 'default');
Bones.returns(() => (<div />));
wrapper = shallow(<BoneYard />);
});
afterEach(function() {
Bones.restore();
});
it('renders bones', function() {
console.log(wrapper)
expect(wrapper.exists(Bones)).to.equal(true);
})
})
What I expect is for the test to pass, and the console.log to print out:
<Suspense fallback={{...}}>
<Bones />
</Suspense>
But instead of <Bones /> I get <lazy /> and it fails the test.
How can I mock out the imported Lazy React component, so that my simplistic test passes?
I'm not sure this is the answer you're looking for, but it sounds like part of the problem is shallow. According to this thread, shallow won't work with React.lazy.
However, mount also doesn't work when trying to stub a lazy component - if you debug the DOM output (with console.log(wrapper.debug())) you can see that Bones is in the DOM, but it's the real (non-stubbed-out) version.
The good news: if you're only trying to check that Bones exists, you don't have to mock out the component at all! This test passes:
import { Bones } from "./Bones";
import BoneYard from "./app";
describe("<BoneYard />", function() {
it("renders bones", function() {
const wrapper = mount(<BoneYard />);
console.log(wrapper.debug());
expect(wrapper.exists(Bones)).to.equal(true);
wrapper.unmount();
});
});
If you do need to mock the component for a different reason, jest will let you do that, but it sounds like you're trying to avoid jest. This thread discusses some other options in the context of jest (e.g.
mocking Suspense and lazy) which may also work with sinon.
You don't need to resolve lazy() function by using .then(x => x.default) React already does that for you.
React.lazy takes a function that must call a dynamic import(). This must return a Promise which resolves to a module with a default export containing a React component. React code splitting
Syntax should look something like:
const LazyBones = React.lazy(() => import("./LazyBones"))
Example:
// LazyComponent.js
import React from 'react'
export default () => (
<div>
<h1>I'm Lazy</h1>
<p>This component is Lazy</p>
</div>
)
// App.js
import React, { lazy, Suspense } from 'react'
// This will import && resolve LazyComponent.js that located in same path
const LazyComponent = lazy(() => import('./LazyComponent'))
// The lazy component should be rendered inside a Suspense component
function App() {
return (
<div className="App">
<Suspense fallback={<p>Loading...</p>}>
<LazyComponent />
</Suspense>
</div>
)
}
As for Testing, you can follow the React testing example that shipped by default within create-react-app and change it a little bit.
Create a new file called LazyComponent.test.js and add:
// LazyComponent.test.js
import React, { lazy, Suspense } from 'react'
import { render, screen } from '#testing-library/react'
const LazyComponent = lazy(() => import('./LazyComponent'))
test('renders lazy component', async () => {
// Will render the lazy component
render(
<Suspense fallback={<p>Loading...</p>}>
<LazyComponent />
</Suspense>
)
// Match text inside it
const textToMatch = await screen.findByText(/I'm Lazy/i)
expect(textToMatch).toBeInTheDocument()
})
Live Example: Click on the Tests Tab just next to Browser tab. if it doesn't work, just reload the page.
You can find more react-testing-library complex examples at their Docs website.
I needed to test my lazy component using Enzyme. Following approach worked for me to test on component loading completion:
const myComponent = React.lazy(() =>
import('#material-ui/icons')
.then(module => ({
default: module.KeyboardArrowRight
})
)
);
Test Code ->
//mock actual component inside suspense
jest.mock("#material-ui/icons", () => {
return {
KeyboardArrowRight: () => "KeyboardArrowRight",
}
});
const lazyComponent = mount(<Suspense fallback={<div>Loading...</div>}>
{<myComponent>}
</Suspense>);
const componentToTestLoaded = await componentToTest.type._result; // to get actual component in suspense
expect(componentToTestLoaded.text())`.toEqual("KeyboardArrowRight");
This is hacky but working well for Enzyme library.
To mock you lazy component first think is to transform the test to asynchronous and wait till component exist like:
import CustomComponent, { Bones } from './Components';
it('renders bones', async () => {
const wrapper = mount(<Suspense fallback={<p>Loading...</p>}>
<CustomComponent />
</Suspense>
await Bones;
expect(wrapper.exists(Bones)).toBeTruthy();
}

Categories