waitFor unable to find HTML element after setTimeout and advanceTimersByTime - javascript

I have a component that utilises quite a few setTimeouts in order to do three things: a) mount a child to the DOM, b) animate that child in, c) unmount that child after user interaction.
I'm trying to mock this behaviour in a test, and am specifically testing step b - I'm doing a queryByText to grab the text value of the child that I animate in.
However, the text never seems to show up in the HTML that gets printed to the CLI when the test fails. The error is as follows:
expect(received).toBeInTheDocument()
received value must be an HTMLElement or an SVGElement.
Received has value: null
My test looks like this:
it("should show message after setTimeout", async () => {
jest.useFakeTimers();
jest.advanceTimersByTime(5000);
const amendedMockContext = {
...baseMockContext,
introductionData: {
data: {
cta: "This is a cta!",
},
},
};
customRender(<FAB />, { contextProps: amendedMockContext });
const message = screen.queryByText(/this/i);
await waitFor(() => expect(message).toBeInTheDocument());
});
and my component animates in and out like so. Note the $visible flag triggers the animation:
const CTA_REVEAL_TIME = 5000;
export default function FAB() {
const [showCTA, setShowCTA] = useState(false);
const [mountCTA, setMountCTA] = useState(false);
// mount just before animating (step a)
useEffect(() => {
const mountCTATimer = setTimeout(
() => setMountCTA(true),
CTA_REVEAL_TIME - 1000
);
return () => {
clearTimeout(mountCTATimer);
};
}, []);
// animate in message (step b)
useEffect(() => {
const CTATimer = setTimeout(
() => setShowCTA((prevState) => !prevState),
CTA_REVEAL_TIME
);
return () => {
clearTimeout(CTATimer);
};
}, []);
return (
<>
{mountCta && <p $visible={showCTA}>{message}</p>}
</>
);
}
In my test, I've tried using various methods of async/await, waitFor, all the different query/get/find methods, but can't seem to get it to actually await the message revealing, even though I'm artificially advaning time by 5000ms. I also use advanceTimersBytime in other tests and have no such problems.

You should mount the component first, then advance the timers.
import { act, render, screen, waitFor } from '#testing-library/react';
import '#testing-library/jest-dom';
import React from 'react';
import FAB from './FAB';
describe('71867883', () => {
it('should show message after setTimeout', async () => {
jest.useFakeTimers();
render(<FAB />);
act(() => {
jest.advanceTimersByTime(5000);
})
await waitFor(() => expect(screen.queryByText(/this/i)).toBeInTheDocument());
});
});

Related

React: ES2020 dynamic import in useEffect blocks rendering

From what I understand, async code inside useEffect runs without blocking the rendering process. So if I have a component like this:
const App = () => {
useEffect(() => {
const log = () => console.log("window loaded");
window.addEventListener("load", log);
return () => {
window.removeEventListener("load", log);
};
}, []);
useEffect(() => {
const getData = async () => {
console.log("begin");
const response = await fetch(
"https://jsonplaceholder.typicode.com/todos/1"
);
const data = await response.json();
console.log("end");
};
getData();
}, []);
return null;
};
The console output is (in order):
begin
window loaded
end
However if I use ES2020 dynamic import inside the useEffect:
const App = () => {
useEffect(() => {
const log = () => console.log("window loaded");
window.addEventListener("load", log);
return () => {
window.removeEventListener("load", log);
};
}, []);
useEffect(() => {
const getData = async () => {
console.log("begin");
const data = await import("./data.json");
console.log("end");
};
getData();
}, []);
return null;
};
The output is (in order):
begin
end
window loaded
This is a problem because when data.json is very large, the browser hangs while importing the large file before React renders anything.
All the necessary and useful information is in Terry's comments. here is the implementation of what you want according to the comments:
First goal:
I would like to import the data after window has loaded for SEO reasons.
Second goal:
In my case the file I'm trying to dynamically import is actually a function that requires a large dataset. And I want to run this function whenever some state has changed so that's why I put it in a useEffect hook.
Let's do it
You can create a function to get the data as you created it as getData with useCallback hook to use it anywhere without issue.
import React, {useEffect, useState, useCallback} from 'react';
function App() {
const [data, setData] = useState({});
const [counter, setCounter] = useState(0);
const getData = useCallback(async () => {
try {
const result = await import('./data.json');
setData(result);
} catch (error) {
// do a proper action for failure case
console.log(error);
}
}, []);
useEffect(() => {
window.addEventListener('load', () => {
getData().then(() => console.log('data loaded successfully'));
});
return () => {
window.removeEventListener('load', () => {
console.log('page unmounted');
});
};
}, [getData]);
useEffect(() => {
if (counter) {
getData().then(() => console.log('re load data after the counter state get change'));
}
}, [getData, counter]);
return (
<div>
<button onClick={() => setCounter((prevState) => prevState + 1)}>Increase Counter</button>
</div>
);
}
export default App;
Explanation:
With component did mount, the event listener will load the data.json on 'load' event. So far, the first goal has been met.
I use a sample and simple counter to demonstrate change in the state and reload data.json scenario. Now, with every change on counter value, the getData function will call because the second useEffect has the counter in its array of dependencies. Now the second goal has been met.
Note: The if block in the second useEffect prevent the getData calling after the component did mount and first invoking of useEffect
Note: Don't forget to use the catch block for failure cases.
Note: I set the result of getData to the data state, you might do a different approach for this result, but the other logics are the same.

React Unit Testing State Updates

I'm in the middle of trying to get as close to 100% unit test coverage with my React application as possible, however I'm in a bit of a pickle with trying to get test coverage on the useAsync hook. I took the following code from use react hooks:
import { useState, useEffect, useCallback } from 'react';
const useAsync = (asyncFunction, immediate = true) => {
const [status, setStatus] = useState('idle');
const [value, setValue] = useState(null);
const [error, setError] = useState(null);
// The execute function wraps asyncFunction and
// handles setting state for pending, value, and error.
// useCallback ensures the below useEffect is not called
// on every render, but only if asyncFunction changes.
const execute = useCallback(() => {
setStatus('pending');
setValue(null);
setError(null);
return asyncFunction()
.then(response => {
setValue(response);
setStatus('success');
})
.catch(error => {
setError(error);
setStatus('error');
});
}, [asyncFunction]);
// Call execute if we want to fire it right away.
// Otherwise execute can be called later, such as
// in an onClick handler.
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return { execute, status, value, error };
};
And it works fine. I've not done anything funky or included any additional testing libraries/dependencies on top of what you'd get when you run npx create-react-app ....
Because I couldn't find a suitable solution for my team to test react hooks without relying on additional dependencies, i.e. react-hooks-testing-library, I created a mock component to wrap the hook in, then I just test the different possible states like so:
import React from "react";
import { render, cleanup } from "#testing-library/react";
import { useAsync } from "../../hooks";
afterEach(cleanup);
const MockComponent = (props) => {
const { status } = useAsync(props.callback, props.immediate);
return <p id="test">{status}</p>
};
const defaultCallback = () => {
new Promise((resolve) => {
setTimeout(() => {
resolve("Success!");
}, 1000)
});
};
// Example of a test that runs fine, no problem at all!
test("useAsync gets to a pending state", () => {
const { container } = render(<MockComponent callback={defaultCallback} immediate={true} />
const pTag = container.querySelector("p#test");
expect(pTag.textContent).toBe("idle");
});
// This is where I get my issue...
test("useAsync gets to a success state", (done) => {
const { container } = render(<MockComponent callback={defaultCallback} immediate={true} />
const pTag = container.querySelector("p#test");
setTimeout(() => {
expect(pTag.textContent).toBe("success");
done();
}, 2000);
});
// Some other tests...
When I run these two tests that I've included above, they actually run fine and if I look at the code coverage report(s) that are generated, it's 100% coverage. In the console, though, when running these tests, when I run the second one, I get the following error and I'm not sure how to resolve it. I have tried to wrap the entire body of the test in act, but that made no difference, so I'm not entirely sure what the best approach may be here.
Here's the console output:
console.error
Warning: An update to MockComponent inside a tests 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 MockComponent....
As I've said, I've tried to wrap my entire test(s) with an act, but I've had no luck there. What I have found interesting is that in the console output, when it apparently errors, it also highlights this snippet of the hook:
.then(response => {
setValue(response);
// ^
setStatus('success');
// ^
})
.catch(error => {
setError(error);
Should I perhaps ignore this issue? After all when all of my tests are run, no tests/test suite fails or anything.
Okay, so after much tinkering & googling, I found an answer to my problem, thankfully it's relatively straightforward! 😃 - The short answer being the use of waitFor.
I made a couple of other modifications, i.e. assigned the timeout delay to a variable called timeout, but nothing major.
import React from "react";
import { render, cleanup, waitFor } from "#testing-library/react";
import { useAsync } from "../../hooks";
afterEach(cleanup);
let timeout = 1000;
beforeEach(() => {
timeout = 1000
});
const MockComponent = (props) => {
const { status } = useAsync(props.callback, props.immediate);
return <p id="test">{status}</p>
};
const defaultCallback = () => {
new Promise((resolve) => {
setTimeout(() => {
resolve("Success!");
}, timeout)
});
};
// Example of a test that runs fine, no problem at all!
test("useAsync gets to a pending state", () => {
const { container } = render(<MockComponent callback={defaultCallback} immediate={true} />
const pTag = container.querySelector("p#test");
expect(pTag.textContent).toBe("idle");
});
// This now works! :D
test("useAsync gets to a success state", async () => {
timeout = 0;
const { container } = render(<MockComponent callback={defaultCallback} immediate={true} />
const pTag = container.querySelector("p#test");
expect(pTag).toBeInTheDocument();
// Yes!
await waitFor(() => {
expect(pTag.textContent).toBe("success");
});
});
// Some other tests...

React testing library how to use waitFor

I'm following a tutorial on React testing. The tutorial has a simple component like this, to show how to test asynchronous actions:
import React from 'react'
const TestAsync = () => {
const [counter, setCounter] = React.useState(0)
const delayCount = () => (
setTimeout(() => {
setCounter(counter + 1)
}, 500)
)
return (
<>
<h1 data-testid="counter">{ counter }</h1>
<button data-testid="button-up" onClick={delayCount}> Up</button>
<button data-testid="button-down" onClick={() => setCounter(counter - 1)}>Down</button>
</>
)
}
export default TestAsync
And the test file is like this:
import React from 'react';
import { render, cleanup, fireEvent, waitForElement } from '#testing-library/react';
import TestAsync from './TestAsync'
afterEach(cleanup);
it('increments counter after 0.5s', async () => {
const { getByTestId, getByText } = render(<TestAsync />);
fireEvent.click(getByTestId('button-up'))
const counter = await waitForElement(() => getByText('1'))
expect(counter).toHaveTextContent('1')
});
The terminal says waitForElement has been deprecated and to use waitFor instead.
How can I use waitFor in this test file?
If you're waiting for appearance, you can use it like this:
it('increments counter after 0.5s', async() => {
const { getByTestId, getByText } = render(<TestAsync />);
fireEvent.click(getByTestId('button-up'));
await waitFor(() => {
expect(getByText('1')).toBeInTheDocument();
});
});
Checking .toHaveTextContent('1') is a bit "weird" when you use getByText('1') to grab that element, so I replaced it with .toBeInTheDocument().
Would it be also possible to wrap the assertion using the act
function? Based on the docs I don't understand in which case to use
act and in which case to use waitFor.
The answer is yes. You could write this instead using act():
import { act } from "react-dom/test-utils";
it('increments counter after 0.5s', async() => {
const { getByTestId, getByText } = render(<TestAsync />);
// you wanna use act() when there is a render to happen in
// the DOM and some change will take place:
act(() => {
fireEvent.click(getByTestId('button-up'));
});
expect(getByText('1')).toBeInTheDocument();
});
Hope this helps.
Current best practice would be to use findByText in that case. This function is a wrapper around act, and will query for the specified element until some timeout is reached.
In your case, you can use it like this:
it('increments counter after 0.5s', async () => {
const { findByTestId, findByText } = render(<TestAsync />);
fireEvent.click(await findByTestId('button-up'))
const counter = await findByText('1')
});
You don't need to call expect on its value, if the element doesn't exist it will throw an exception
You can find more differences about the types of queries here

Testing asynchronous useEffect

My functional component uses the useEffect hook to fetch data from an API on mount. I want to be able to test that the fetched data is displayed correctly.
While this works fine in the browser, the tests are failing because the hook is asynchronous the component doesn't update in time.
Live code:
https://codesandbox.io/s/peaceful-knuth-q24ih?fontsize=14
App.js
import React from "react";
function getData() {
return new Promise(resolve => {
setTimeout(() => resolve(4), 400);
});
}
function App() {
const [members, setMembers] = React.useState(0);
React.useEffect(() => {
async function fetch() {
const response = await getData();
setMembers(response);
}
fetch();
}, []);
return <div>{members} members</div>;
}
export default App;
App.test.js
import App from "./App";
import React from "react";
import { mount } from "enzyme";
describe("app", () => {
it("should render", () => {
const wrapper = mount(<App />);
console.log(wrapper.debug());
});
});
Besides that, Jest throws a warning saying:
Warning: An update to App inside a test was not wrapped in act(...).
I guess this is related? How could this be fixed?
Ok, so I think I've figured something out. I'm using the latest dependencies right now (enzyme 3.10.0, enzyme-adapter-react-16 1.15.1), and I've found something kind of amazing. Enzyme's mount() function appears to return a promise. I haven't seen anything about it in the documentation, but waiting for that promise to resolve appears to solve this problem. Using act from react-dom/test-utils is also essential, as it has all the new React magic to make the behavior work.
it('handles async useEffect', async () => {
const component = mount(<MyComponent />);
await act(async () => {
await Promise.resolve(component);
await new Promise(resolve => setImmediate(resolve));
component.update();
});
console.log(component.debug());
});
Following on from #user2223059's answer it also looks like you can do:
// eslint-disable-next-line require-await
component = await mount(<MyComponent />);
component.update();
Unfortunately you need the eslint-disable-next-line because otherwise it warns about an unnecessary await... yet removing the await results in incorrect behaviour.
I was having this problem and came across this thread. I'm unit testing a hook but the principles should be the same if your async useEffect code is in a component. Because I'm testing a hook, I'm calling renderHook from react hooks testing library. If you're testing a regular component, you'd call render from react-dom, as per the docs.
The problem
Say you have a react hook or component that does some async work on mount and you want to test it. It might look a bit like this:
const useMyExampleHook = id => {
const [someState, setSomeState] = useState({});
useEffect(() => {
const asyncOperation = async () => {
const result = await axios({
url: `https://myapi.com/${id}`,
method: "GET"
});
setSomeState(() => result.data);
}
asyncOperation();
}, [id])
return { someState }
}
Until now, I've been unit testing these hooks like this:
it("should call an api", async () => {
const data = {wibble: "wobble"};
axios.mockImplementationOnce(() => Promise.resolve({ data}));
const { result } = renderHook(() => useMyExampleHook());
await new Promise(setImmediate);
expect(result.current.someState).toMatchObject(data);
});
and using await new Promise(setImmediate); to "flush" the promises. This works OK for simple tests like my one above but seems to cause some sort of race condition in the test renderer when we start doing multiple updates to the hook/component in one test.
The answer
The answer is to use act() properly. The docs say
When writing [unit tests]... react-dom/test-utils provides a helper called act() that makes sure all updates related to these “units” have been processed and applied to the DOM before you make any assertions.
So our simple test code actually wants to look like this:
it("should call an api on render and store the result", async () => {
const data = { wibble: "wobble" };
axios.mockImplementationOnce(() => Promise.resolve({ data }));
let renderResult = {};
await act(async () => {
renderResult = renderHook(() => useMyExampleHook());
})
expect(renderResult.result.current.someState).toMatchObject(data);
});
The crucial difference is that async act around the initial render of the hook. That makes sure that the useEffect hook has done its business before we start trying to inspect the state. If we need to update the hook, that action gets wrapped in its own act block too.
A more complex test case might look like this:
it('should do a new call when id changes', async () => {
const data1 = { wibble: "wobble" };
const data2 = { wibble: "warble" };
axios.mockImplementationOnce(() => Promise.resolve({ data: data1 }))
.mockImplementationOnce(() => Promise.resolve({ data: data2 }));
let renderResult = {};
await act(async () => {
renderResult = renderHook((id) => useMyExampleHook(id), {
initialProps: { id: "id1" }
});
})
expect(renderResult.result.current.someState).toMatchObject(data1);
await act(async () => {
renderResult.rerender({ id: "id2" })
})
expect(renderResult.result.current.someState).toMatchObject(data2);
})
I was also facing similar issue. To solve this I have used waitFor function of React testing library in enzyme test.
import { waitFor } from '#testing-library/react';
it('render component', async () => {
const wrapper = mount(<Component {...props} />);
await waitFor(() => {
wrapper.update();
expect(wrapper.find('.some-class')).toHaveLength(1);
}
});
This solution will wait for our expect condition to fulfill. Inside expect condition you can assert on any HTML element which get rendered after the api call success.
this is a life saver
export const waitForComponentToPaint = async (wrapper: any) => {
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 0));
await wait(0);
wrapper.update();
});
};
in test
await waitForComponentToPaint(wrapper);
After lots of experimentation I have come up with a solution that finally works for me, and hopefully will fit your purpose.
See below
import React from "react";
import { mount } from "enzyme";
import { act } from 'react-dom/test-utils';
import App from "./App";
describe("app", () => {
it("should render", async () => {
const wrapper = mount(<App />);
await new Promise((resolve) => setImmediate(resolve));
await act(
() =>
new Promise((resolve) => {
resolve();
})
);
wrapper.update();
// data loaded
});
});

React Hooks multiple alerts with individual countdowns

I've been trying to build an React app with multiple alerts that disappear after a set amount of time. Sample: https://codesandbox.io/s/multiple-alert-countdown-294lc
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function TimeoutAlert({ id, message, deleteAlert }) {
const onClick = () => deleteAlert(id);
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
});
return (
<p>
<button onClick={onClick}>
{message} {id}
</button>
</p>
);
}
let _ID = 0;
function App() {
const [alerts, setAlerts] = useState([]);
const addAlert = message => setAlerts([...alerts, { id: _ID++, message }]);
const deleteAlert = id => setAlerts(alerts.filter(m => m.id !== id));
console.log({ alerts });
return (
<div className="App">
<button onClick={() => addAlert("test ")}>Add Alertz</button>
<br />
{alerts.map(m => (
<TimeoutAlert key={m.id} {...m} deleteAlert={deleteAlert} />
))}
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
The problem is if I create multiple alerts, it disappears in the incorrect order. For example, test 0, test 1, test 2 should disappear starting with test 0, test 1, etc but instead test 1 disappears first and test 0 disappears last.
I keep seeing references to useRefs but my implementations don't resolve this bug.
With #ehab's input, I believe I was able to head down the right direction. I received further warnings in my code about adding dependencies but the additional dependencies would cause my code to act buggy. Eventually I figured out how to use refs. I converted it into a custom hook.
function useTimeout(callback, ms) {
const savedCallBack = useRef();
// Remember the latest callback
useEffect(() => {
savedCallBack.current = callback;
}, [callback]);
// Set up timeout
useEffect(() => {
if (ms !== 0) {
const timer = setTimeout(savedCallBack.current, ms);
return () => clearTimeout(timer);
}
}, [ms]);
}
You have two things wrong with your code,
1) the way you use effect means that this function will get called each time the component is rendered, however obviously depending on your use case, you want this function to be called once, so change it to
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
}, []);
adding the empty array as a second parameter, means that your effect does not depend on any parameter, and so it should only be called once.
Your delete alert depends on the value that was captured when the function was created, this is problematic since at that time, you don't have all the alerts in the array, change it to
const deleteAlert = id => setAlerts(alerts => alerts.filter(m => m.id !== id));
here is your sample working after i forked it
https://codesandbox.io/s/multiple-alert-countdown-02c2h
well your problem is you remount on every re-render, so basically u reset your timers for all components at time of rendering.
just to make it clear try adding {Date.now()} inside your Alert components
<button onClick={onClick}>
{message} {id} {Date.now()}
</button>
you will notice the reset everytime
so to achieve this in functional components you need to use React.memo
example to make your code work i would do:
const TimeoutAlert = React.memo( ({ id, message, deleteAlert }) => {
const onClick = () => deleteAlert(id);
useEffect(() => {
const timer = setTimeout(onClick, 2000);
return () => clearTimeout(timer);
});
return (
<p>
<button onClick={onClick}>
{message} {id}
</button>
</p>
);
},(oldProps, newProps)=>oldProps.id === newProps.id) // memoization condition
2nd fix your useEffect to not run cleanup function on every render
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
finally something that is about taste, but really do you need to destruct the {...m} object ? i would pass it as a proper prop to avoid creating new object every time !
Both answers kind of miss a few points with the question, so after a little while of frustration figuring this out, this is the approach I came to:
Have a hook that manages an array of "alerts"
Each "Alert" component manages its own destruction
However, because the functions change with every render, timers will get reset each prop change, which is undesirable to say the least.
It also adds another lay of complexity if you're trying to respect eslint exhaustive deps rule, which you should because otherwise you'll have issues with state responsiveness. Other piece of advice, if you are going down the route of using "useCallback", you are looking in the wrong place.
In my case I'm using "Overlays" that time out, but you can imagine them as alerts etc.
Typescript:
// useOverlayManager.tsx
export default () => {
const [overlays, setOverlays] = useState<IOverlay[]>([]);
const addOverlay = (overlay: IOverlay) => setOverlays([...overlays, overlay]);
const deleteOverlay = (id: number) =>
setOverlays(overlays.filter((m) => m.id !== id));
return { overlays, addOverlay, deleteOverlay };
};
// OverlayIItem.tsx
interface IOverlayItem {
overlay: IOverlay;
deleteOverlay(id: number): void;
}
export default (props: IOverlayItem) => {
const { deleteOverlay, overlay } = props;
const { id } = overlay;
const [alive, setAlive] = useState(true);
useEffect(() => {
const timer = setTimeout(() => setAlive(false), 2000);
return () => {
clearTimeout(timer);
};
}, []);
useEffect(() => {
if (!alive) {
deleteOverlay(id);
}
}, [alive, deleteOverlay, id]);
return <Text>{id}</Text>;
};
Then where the components are rendered:
const { addOverlay, deleteOverlay, overlays } = useOverlayManger();
const [overlayInd, setOverlayInd] = useState(0);
const addOverlayTest = () => {
addOverlay({ id: overlayInd});
setOverlayInd(overlayInd + 1);
};
return {overlays.map((overlay) => (
<OverlayItem
deleteOverlay={deleteOverlay}
overlay={overlay}
key={overlay.id}
/>
))};
Basically: Each "overlay" has a unique ID. Each "overlay" component manages its own destruction, the overlay communicates back to the overlayManger via prop function, and then eslint exhaustive-deps is kept happy by setting an "alive" state property in the overlay component that, when changed to false, will call for its own destruction.

Categories

Resources