React jest - Verify the state has been changed - javascript

I'm using jest in order to test my components.
I want to test a specific method that I know that the method change the state.
it('should calculate width of publisher menu', () => {
window.innerWidth = 3000
const publisherContainer = new PubContainer(props)
console.log(publisherContainer.state.maxVisibleTabs) // maxVisibleTabs is 1
publisherContainer.resize() // here i'm using setState({maxVisibleTabs : 10})
console.log(publisherContainer.state.maxVisibleTabs) // maxVisibleTabs is 1
})
I would like to check that resize actually did the setState to 10, but the the value 1 is still there.
I know that setState is async and that the state does not mutate immediately but do I have any elegant\workaround to solve this?

There are usually three ways to deal with asynchronous code in tests:
Make the asynchronous code synchronous somehow. Probably not an option here.
Poll the system under test until you get the right result or timeout.
Create a mechanism in your production code that will allow the test to know that something happened - for example, add an event mechanism, so that code from outside the component can register to state changes. Then the test code can register for them and wait for the event to come.
Unfortunately, Jest doesn't come with a polling mechanism built-in, but you can probably write something simple yourself using a setTimeout loop.

You can use enzyme with jest, it makes testing react components a lot easier. It comes with an update function that solves this problem.

Related

Potential bug in "official" useInterval example

useInterval
useInterval from this blog post by Dan Abramov (2019):
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
A Potential Bug
The interval callback may be invoked between the commit phase and the useEffect invocation, causing the old (and hence not up to date) callback to be called. In other words, this may be the execution order:
Render phase - a new value for callback.
Commit phase - state committed to DOM.
useLayoutEffect
Interval callback - using savedCallback.current(), which is different than callback.
useEffect - savedCallback.current = callback;
React's Life Cycle
To further illustrate this, here's a diagram showing React's Life Cycle with hooks:
Dashed lines mean async flow (event loop released) and you can have an interval callback invocation at these points.
Note however, that the dashed line between Render and React updates DOM (commit phase) is most likely a mistake. As this codesandbox demonstrates, you can only have an interval callback invoked after useLayoutEffect or useEffect (but not after the render phase).
So you can set the callback in 3 places:
Render - Incorrect because state changes have not yet been committed to the DOM.
useLayoutEffect - correct because state changes have been committed to the DOM.
useEffect - incorrect because the old interval callback may fire before that (after layout effects) .
Demo
This bug is demonstrated in this codesandebox. To reproduce:
Move the mouse over the grey div - this will lead to a new render with a new callback reference.
Typically you'll see the error thrown in less than 2000 mouse moves.
The interval is set to 50ms, so you need a bit of luck for it to fire right between the render and effect phases.
Use Case
The demo shows that the current callback value may differ from that in useEffect alright, but the real question is which one of them is the "correct" one?
Consider this code:
const [size, setSize] = React.useState();
const onInterval = () => {
console.log(size)
}
useInterval(onInterval, 100);
If onInterval is invoked after the commit phase but before useEffect, it will print the wrong value.
This does not look like a bug to me, although I understand the discussion.
The answer above that suggests updating the ref during render would be a side effect, which should be avoided because it will cause problems.
The demo shows that the current callback value may differ from that in useEffect alright, but the real question is which one of them is the "correct" one?
I believe the "correct" one is the one that has been committed. For one reason, committed effects are the only ones that are guaranteed to have cleanup phase later. (The interval in this question doesn't need a cleanup effect, but other things might.)
Another more compelling reason in this case, perhaps, is that React may pre-render things (either at lower priority, or because they're "offscreen" and not yet visible, or in the future b'c of animation APIs). Pre-rendering work like this should never modify a ref, because the modification would be arbitrary. (Consider a future animation API that pre-renders multiple possible future visual states to make transitions faster in response to user interaction. You wouldn't want the one that happened to render last to just mutate a ref that's used by your currently visible/committed view.)
Edit 1 This discussion mostly seems to be pointing out that when JavaScript isn't synchronous (blocking), when it yields between rendering, there's a chance for other things to happen in between (like a timer/interval that was previously scheduled). That's true, but I don't think it's a bug if this happens during render (before an update is "committed").
If the main concern is that the callback might execute after the UI has been committed and mismatch what's on the screen, then you might want to consider useLayoutEffect instead. This effect type is called during the commit phase, after React has modified the DOM but before React yields back to the browser (aka so no intervals or timers can run in between).
Edit 2 I believe the reason Dan originally suggested using a ref and an effect for this (rather than just an effect) was because updates to the callback wouldn't reset the interval. (If you called clearInterval and setInterval each time the callback changed, the overall timing would be interrupted.)
To attempt to answer your final question strictly:
I can't see any logical harm updating the callback in render() as opposed to useEffect(). useEffect() is never called anything other than after render(), and whatever it is called with will be what the last render was called with, so the only difference logically is that the callback may be more out-of-date by the time the useEffect() is called.
This may be exacerbated by the coming concurrent mode, if there may be multiple calls to render() before a call to useEffect(), but I'm not even sure it works like that.
However: I would say there is a maintenance cost to doing it this way: it implies that it is ok to cause side effects in render(). In general that is not a good idea, and all necessary side effects should really be done in useEffect(), because, as the docs say:
the render method itself shouldn’t cause side effects ... we typically want to perform our effects after React has updated the DOM
So I would recommend putting any side effect inside a useEffect() and having that as a coding standard, even if in certain situations it is OK. And particularly in a blog post by a react core dev that is going to be copied and pasted by "guide" many people it is important to set the right example ;-P
Alternative solution
As for how you can fix your problem, I will just copy and paste my suggested implementation of setInterval() from this answer, which should remove the ambiguity by calling the callback in a separate useEffect(), at which point all state should be consistent and you don't have to worry about which is "correct". Dropping it into your sandbox seemed to solve the problem.
function useTicker(delay) {
const [ticker, setTicker] = useState(0);
useEffect(() => {
const timer = setInterval(() => setTicker(t => t + 1), delay);
return () => clearInterval(timer);
}, [delay]);
return ticker;
}
function useInterval(cbk, delay) {
const ticker = useTicker(delay);
const cbkRef = useRef();
// always want the up to date callback from the caller
useEffect(() => {
cbkRef.current = cbk;
}, [cbk]);
// call the callback whenever the timer pops / the ticker increases.
// This deliberately does not pass `cbk` in the dependencies as
// otherwise the handler would be called on each render as well as
// on the timer pop
useEffect(() => cbkRef.current(), [ticker]);
}
Here is a modification of your example which shows that both/neither approaches are correct: https://codesandbox.io/s/useintervalbug-neither-are-correct-zu2zt?file=/src/App.js
The use of refs is not what you would do in reality but it was necessary to easily detect and report the problem. They do not materially affect the behaviour.
In this example the parent component has created the new "correct" callback, finished rendering and wants that new callback to be used by the child and the timer.
Ultimately there is a race condition between when the "correct" callback ultimately gets passed to useInterval and when the browser decides to call the callback. I do not think it is possible to avoid this.
It makes no difference if you memoize the callback unless of course it has no dependencies and never changes.

How to avoid "act" warnings in a integration tests with React Testing Library

I am rendering the component in an integration test of my single page app and attempting to follow a happy path from landing on a page to the first question of a quiz / memory game.
Warning: An update to GamePlay inside a test was not wrapped in act(...).
The second line is the one that triggers multiple warnings...
const firstItem = getAllByRole("listitem")[0]
userEvent.click(firstItem)
It clicks the first option in a list which causes the gameplay component to render. That component shows a word for 5000ms then hides it using a setTimeout which is set in the component's useEffect hook (below). An answer input field is also disabled and enabled in this routine.
function hideQuestion() {
setInputDisabled(false)
setDisplayWord(false)
}
const [displayWord, setDisplayWord]= useState(true);
useEffect(()=>{
setTimeout(hideQuestion, 5000)
},[displayWord])
const [inputDisabled, setInputDisabled] = useState(true);
From reading around I think it might be this use of setTimeout that's responsible but none of the mitigations I have tried have done anything. I tried wrapping the click line in an "act" function and that did nothing. I tried making the test async and awaiting that line but again that did nothing - I still get this error (and several similar)...
console.error
Warning: An update to GamePlay 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 GamePlay (/home/user/code/spellingapp/src/components/GamePlay.js:3:20)
at MainPage (/home/user/code/spellingapp/src/components/MainPage.js:8:29)
at Route (/home/user/code/spellingapp/node_modules/react-router/cjs/react-router.js:470:29)
at Switch (/home/user/code/spellingapp/node_modules/react-router/cjs/react-router.js:676:29)
at Router (/home/user/code/spellingapp/node_modules/react-router/cjs/react-router.js:99:30)
at BrowserRouter (/home/user/code/spellingapp/node_modules/react-router-dom/cjs/react-router-dom.js:67:35)
at App (/home/user/code/spellingapp/src/App.js:16:29)
5 | function hideQuestion() {
6 | // console.log("I have hidden the word and enabled the text box")
> 7 | setInputDisabled(false)
I tried reading this article: https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning
But I'm struggling to understand it fully and relate it to my own code. I think the gist of it is that you will get these warning when there are "unexpected" state changes but I can't see how I'm supposed to make mine expected. My app is a real time game so the whole thing runs off timers - one timer finishes and another one starts. I don't want to have to run through a full 10 minutes of gameplay so it can finish neatly. I just want to test from landing page to the first interaction as a smoke test of the core integration logic.
Right now all I can think of is to make a test fixture with only one question so I can run the whole game end to end, but this seems like overkill when my current test runs fine (apart from the warnings) and does all I want it to do right now. I'd appreciate any pointers or suggestions, even if it's that I'm just barking up the wrong tree. Thanks!
Here are several things you can do:
The warning tells you that your app continues after your test completed, and thus other things happen during the run that you didn't check in your test and didn't wait for. So if you're happy with what you are testing, be warned and keep it like that.
You can make your app a bit more testable by giving it options. For example you can have an optional prop indicating the amount of time to wait in timers, so that you can put it to 0 or a small number in your tests.
You can mock timers:
beforeEach(jest.useFakeTimers);
afterEach(jest.useRealTimers);
test("your game", async () => {
const { getAllByRole } = render(<Game />);
// [...]
const firstItem = getAllByRole("listitem")[0]
userEvent.click(firstItem)
act(() => jest.advanceTimersByTime(5000));
// [...]
act(() => jest.advanceTimersByTime(5000));
// [...]
});

When is it necessary to use `rerender` with the React Testing Library?

In times past, my colleagues and I would typically write React Testing Library (RTL) tests for the main parent components, which often have many nested child components. That testing made sense and worked well. Btw the child components in question are very much dedicated to that parent component and not of the reusable variety.
But now we're trying to write RTL tests for every single component. Today I was trying to build tests for an Alerts component, which is the parent of an Alert component and about 4 levels down from the top-level component. Here's some sample code in my test file:
function renderDom(component, store) {
return {
...render(<Provider store={store}>{component}</Provider>),
store,
};
}
let store = configureStore(_initialState);
const spy = jest.spyOn(store, 'dispatch');
const { queryByTestId, queryByText, debug } = renderDom(
<Alerts question={store.getState().pageBuilder.userForm.steps[0].tasks[0].questions[1]} />,
store
);
I then started writing the typical RTL code to get the Alerts component to do its thing. One of these was to click on a button which would trigger an ADD_ALERT action. I stepped through all of the code and the Redux reducer was apparently working correctly with a new alert, as I intended, yet back in the Alerts component, question.alerts remained null whereas in the production code it was definitely being updated properly with a new alert.
I spoke with a colleague and he said that for this type of test, I would need to artificially rerender the component like this:
rerender(<Provider store={store}><Alerts question={store.getState().pageBuilder.userForm.steps[0].tasks[0].questions[1]} /></Provider>);
I tried this and it appears to be a solution. I don't fully understand why I have to do this and thought I'd reach out to the community to see if there was a way I could avoid using rerender.
It's hard to be certain without seeing more of your code, but my typical approach with RTL is to take the fireEvent call that simulates clicking the button and wrap it in an act call. This should cause React to finish processing any events from your event, update states, rerender, etc.
Alternatively, if you know that a particular DOM change should occur as a result of firing the event, you can use waitFor. An example from the React Testing Library intro:
render(<Fetch url="/greeting" />)
fireEvent.click(screen.getByText('Load Greeting'))
await waitFor(() => screen.getByRole('alert'))

How to ensure I am reading the most recent version of state?

I may be missing something. I know setState is asynchronous in React, but I still seem to have this question.
Imagine following is a handler when user clicks any of the buttons on my app
1. ButtonHandler()
2. {
3. if(!this.state.flag)
4. {
5. alert("flag is false");
6. }
7. this.setState({flag:true});
8.
9. }
Now imagine user very quickly clicks first one button then second.
Imagine the first time the handler got called this.setState({flag:true}) was executed, but when second time the handler got called, the change to the state from the previous call has not been reflected yet -- and this.state.flag returned false.
Can such situation occur (even theoretically)? What are the ways to ensure I am reading most up to date state?
I know setState(function(prevState, props){..}) gives you access to previous state but what if I want to only read state like on line 3 and not set it?
As you rightly noted, for setting state based on previous state you want to use the function overload.
I know setState(function(prevState, props){..}) gives you access to previous state
So your example would look like this:
handleClick() {
this.setState(prevState => {
return {
flag: !prevState.flag
};
});
}
what if I want to only read state like on line 3 and not set it?
Let's get back to thinking why you want to do this.
If you want to perform a side effect (e.g. log to console or start an AJAX request) then the right place to do it is the componentDidUpdate lifecycle method. And it also gives you access to the previous state:
componentDidUpdate(prevState) {
if (!prevState.flag && this.state.flag) {
alert('flag has changed from false to true!');
}
if (prevState.flag && !this.state.flag) {
alert('flag has changed from true to false!');
}
}
This is the intended way to use React state. You let React manage the state and don't worry about when it gets set. If you want to set state based on previous state, pass a function to setState. If you want to perform side effects based on state changes, compare previous and current state in componentDidUpdate.
Of course, as a last resort, you can keep an instance variable independent of the state.
React's philosophy
The state and props should indicate things the components need for rendering. React's render being called whenever the state and props change.
Side Effects
In your case, you're causing a side effect based on user interaction which requires specific timing. In my opinion, once you step out of rendering - you probably want to reconsider state and props and stick to a regular instance property which is synchronous anyway.
Solving the real issue - Outside of React
Just change this.state.flag to this.flag everywhere, and update it with assignment rather than with setState. That way you
If you still have to use .state
You can get around this, uglily. I wrote code for this, but I'd rather not publish it here so people don't use it :)
First promisify.
Then use a utility for only caring about the last promise resolving in a function call. Here is an example library but the actual code is ~10LoC and simple anyway.
Now, a promisified setState with last called on it gives you the guarantee you're looking for.
Here is how using such code would look like:
explicitlyNotShown({x: 5}).then(() => {
// we are guaranteed that this call and any other setState calls are done here.
});
(Note: with MobX this isn't an issue since state updates are sync).

React testing with asynchronous setState

I'm new to React testing and I'm having a hard time figuring out the following issue:
I'm trying to simulate an input onChange event. It's a text input that filters the results in a table. InteractiveTable has a controlled input field (ControlledInput) and an instance of Facebook's FixedDataTable.
This is the test:
let filter = ReactTestUtils.findRenderedComponentWithType(component, ControlledInput);
let input = ReactTestUtils.findRenderedDOMComponentWithTag(filter, 'input');
input.value = 'a';
ReactTestUtils.Simulate.change(input);
console.log(component.state);
On input change the component updates its state with the value of the input, but since setState is asynchronous, here the console.log will log out the previous state, and I can't query the structure of the component for testing, because it's not updated yet. What am I missing?
Edit: to be clear, if I make the assertion in a setTimeout, it will pass, so it's definitely a problem with the asynchronous nature of setState.
I found one solution, where I overwrite the componentDidUpdate method of the component:
component.componentDidUpdate = () => {
console.log(component.state); // shows the updated state
let cells = ReactTestUtils.scryRenderedComponentsWithType(component, Cell);
expect(cells.length).toBe(30);
done();
};
This wouldn't be possible if the component had its own componentDidUpdate method, so it's not a good solution. This seems to be a very common problem, yet I find no solution to it.
Normally when I run into similar scenarios when testing, I try to break things apart a little. In your current test, (depending on your flavor of test framework), you could mock the component's setState method, and simply ensure that it's called with what you expect when you simulate a change.
If you want further coverage, in a different test you could call the real setState with some mock data, and then use the callback to make assertions about what's rendered, or ensure other internal methods are called.
OR: If your testing framework allows for simulating async stuff, you could try calling that too and test the whole thing in one go.

Categories

Resources