Skip to content

'event was not wrapped in act' warning with React 18, react-datepicker and user.type #1231

Closed

Description

Relevant code or config:

describe('ReactDatepicker', () => {
  beforeEach(() => (user = userEvent.setup()));

  it('should not emit act warnings', async () => {
    const now = new Date();
    const endOfNextYear = new Date(now.getFullYear() + 1, 11, 31);
    const dateFormat = 'M/d/yyyy';
    const userFormattedDate = format(endOfNextYear, dateFormat);
    const defaultFormattedDate = format(endOfNextYear, defaultDateFormat);

    render(<ReactDatepicker />);
    const input = screen.getByRole('textbox');
    await user.type(input, `${userFormattedDate}{Enter}`);
    expect(await screen.findByDisplayValue(defaultFormattedDate)).toBeInTheDocument()
  });

  it('should pass with fireEvent', async () => {
    const now = new Date();
    const endOfNextYear = new Date(now.getFullYear() + 1, 11, 31);
    const dateFormat = 'M/d/yyyy';
    const userFormattedDate = format(endOfNextYear, dateFormat);
    const defaultFormattedDate = format(endOfNextYear, defaultDateFormat);

    render(<ReactDatepicker />);
    const input = screen.getByRole('textbox');
    fireEvent.change(input, { target: { value: userFormattedDate } });
    fireEvent.keyDown(input, { key: 'Enter' });
    expect(await screen.findByDisplayValue(defaultFormattedDate)).toBeInTheDocument()
  });
});

What you did:

use userEvent.type when targeting react-datepicker to enter a date. The weird thing is the "not wrapped in act" error only appears when a valid date is typed and the {Enter} key is pressed. This is apparency causing some special condition to happen within the implementation of react-datepicker which userEvent isn't act wrapping appropriately.

What happened:

  console.error
    Warning: An update to r 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 r (C:\git\pzaczkiewicz\forge-contrib-react-datepicker-user-event-test-failure\.yarn\__virtual__\react-datepicker-virtual-69b8f1e761\0\cache\react-datepicker-npm-4.16.0-c94778dc5d-59da0305ca.zip\node_modules\react-datepicker\dist\index.js:1:78951)
        at ReactDatepicker (C:\git\pzaczkiewicz\forge-contrib-react-datepicker-user-event-test-failure\packages\react-datepicker-user-event-test-failure\src\ReactDatepicker\ReactDatepicker.tsx:7:45)

Reproduction:

https://github.com/pzaczkiewicz-athenahealth/react-datepicker-user-event-test-failure

Problem description:

Not wrapped in act warnings are usually good indications that what you are testing is not what the user sees. Ignoring them may hide bigger issues in tests.

Suggested solution:

Unknown. I dug into testing-library source code and couldn't completely wrap my head around why asyncWrapper and eventWrapper flip-flopped IS_REACT_ACT_ENVIRONMENT.

Activity

  1. pzaczkiewicz-athenahealth commented on Aug 10, 2023

    @pzaczkiewicz-athenahealth
    Author

    Similar issue as #1216, but this happens in the middle of await user.type instead of upon unmount.

  2. chawes13 commented on Sep 14, 2023

    @chawes13

    I'm experiencing this issue as well after upgrading from @testing-library/react@^12 to @testing-library/react@^14 and react@^17 to react@^18.

    Here's my stacktrace:

          at printWarning (node_modules/react-dom/cjs/react-dom.development.js:86:30)
          at error (node_modules/react-dom/cjs/react-dom.development.js:60:7)
          at warnIfUpdatesNotWrappedWithActDEV (node_modules/react-dom/cjs/react-dom.development.js:27589:9)
          at scheduleUpdateOnFiber (node_modules/react-dom/cjs/react-dom.development.js:25508:5)
          at Object.enqueueSetState (node_modules/react-dom/cjs/react-dom.development.js:14067:7)
          at r.Object.<anonymous>.Component.setState (node_modules/react/cjs/react.development.js:354:16)
          at node_modules/react-datepicker/dist/index.js:1:82654
          at HTMLUnknownElement.callCallback (node_modules/react-dom/cjs/react-dom.development.js:4164:14)
          at HTMLUnknownElement.callTheUserObjectsOperation (node_modules/jsdom/lib/jsdom/living/generated/EventListener.js:26:30)
          at innerInvokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:350:25)
          at invokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:286:3)
          at HTMLUnknownElementImpl._dispatch (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:233:9)
          at HTMLUnknownElementImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:104:17)
          at HTMLUnknownElement.dispatchEvent (node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:241:34)
          at Object.invokeGuardedCallbackDev (node_modules/react-dom/cjs/react-dom.development.js:4213:16)
          at invokeGuardedCallback (node_modules/react-dom/cjs/react-dom.development.js:4277:31)
          at invokeGuardedCallbackAndCatchFirstError (node_modules/react-dom/cjs/react-dom.development.js:4291:25)
          at executeDispatch (node_modules/react-dom/cjs/react-dom.development.js:9041:3)
          at processDispatchQueueItemsInOrder (node_modules/react-dom/cjs/react-dom.development.js:9073:7)
          at processDispatchQueue (node_modules/react-dom/cjs/react-dom.development.js:9086:5)
          at dispatchEventsForPlugins (node_modules/react-dom/cjs/react-dom.development.js:9097:3)
          at node_modules/react-dom/cjs/react-dom.development.js:9288:12
          at batchedUpdates$1 (node_modules/react-dom/cjs/react-dom.development.js:26140:12)
          at batchedUpdates (node_modules/react-dom/cjs/react-dom.development.js:3991:12)
          at dispatchEventForPluginEventSystem (node_modules/react-dom/cjs/react-dom.development.js:9287:3)
          at dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay (node_modules/react-dom/cjs/react-dom.development.js:6465:5)
          at dispatchEvent (node_modules/react-dom/cjs/react-dom.development.js:6457:5)
          at dispatchDiscreteEvent (node_modules/react-dom/cjs/react-dom.development.js:6430:5)
          at HTMLDivElement.callTheUserObjectsOperation (node_modules/jsdom/lib/jsdom/living/generated/EventListener.js:26:30)
          at innerInvokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:350:25)
          at invokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:286:3)
          at HTMLInputElementImpl._dispatch (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:233:9)
          at Object.exports.fireFocusEventWithTargetAdjustment (node_modules/jsdom/lib/jsdom/living/helpers/focusing.js:103:10)
          at HTMLInputElementImpl.blur (node_modules/jsdom/lib/jsdom/living/nodes/HTMLOrSVGElement-impl.js:79:14)
          at HTMLInputElement.blur (node_modules/jsdom/lib/jsdom/living/generated/HTMLElement.js:126:34)
          at r.setBlur (node_modules/react-datepicker/dist/index.js:1:81473)
    

    It looks like the function that calls setState is coming from setOpen on blur of the input.

    Could it be possible that their use of nested second args to setState (e.g., https://github.com/Hacker0x01/react-datepicker/blob/3b6ff0163514facbfd6d38a84e1fe874c4388d77/src/index.jsx#L439) is not getting flushed with act? TBH I'm not exactly sure how I would go about testing that theory.

    Here's another repro (if that's helpful): LaunchPadLab/lp-components#613

  3. chawes13 commented on Sep 14, 2023

    @chawes13

    I also tried this with waitFor (below) as well as invoking cleanup and unmount at the end of this specific test to no avail.

      user.type(input, '02/02/2023{Enter}')
      await waitFor(() => {
        expect(screen.queryByLabelText('Next Month')).not.toBeInTheDocument()
        expect(input).toHaveValue('02/02/2023')
      })
  4. pzaczkiewicz-athenahealth commented on Sep 14, 2023

    @pzaczkiewicz-athenahealth
    Author

    Could it be possible that their use of nested second args to setState (e.g., https://github.com/Hacker0x01/react-datepicker/blob/3b6ff0163514facbfd6d38a84e1fe874c4388d77/src/index.jsx#L439) is not getting flushed with act? TBH I'm not exactly sure how I would go about testing that theory.

    Good find! That's definitely some non-standard behavior. Hopefully one of the testing-library maintainers is able to figure out what to do.

    The error is definitely coming from within user.type, because I've mocked console.error as such:

    const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {
      return;
    });
    
    describe('DateInput', () => {
      beforeEach(() => {
        user = userEvent.setup();
        consoleError.mockClear();
      });
      afterEach(() => {
        expect(consoleError).not.toBeCalled();
      });

    I can then check for its usage in my test:

          await user.type(inputField, `${userFormattedDate}{Enter}`);
          // Ignoring act warnings in this instance because of a testing-library bug:
          // https://github.com/testing-library/react-testing-library/issues/1216
          expect(consoleError).toBeCalled();
          consoleError.mockClear();

    I want my tests to fail if a console error happens because they are usually indicative of behavior I need to correct. This is the only way around the bug while still giving me a clean console.

  5. KutynaMateusz commented on Apr 25, 2024

    @KutynaMateusz

    Any update on this? I'm having a similar issue

  6. josephdangerstewart commented on Jul 18, 2024

    @josephdangerstewart

    Could it be possible that their use of nested second args to setState (e.g., https://github.com/Hacker0x01/react-datepicker/blob/3b6ff0163514facbfd6d38a84e1fe874c4388d77/src/index.jsx#L439) is not getting flushed with act? TBH I'm not exactly sure how I would go about testing that theory.

    I think it has something to do with blurring the input in that setState callback. Blurring the input triggers more set state calls, which I don't think are being flushed. I managed this minimal repro. I haven't figured out why that produces the warning, but I have noticed a few things

    Firstly, changing

    this.setState({ submitted: true }, () => this.blurInput());

    to

    this.setState({ submitted: true }, () => this.setState({ blurred: true }));

    Fixes the issue. So we're flushing setState calls made directly within that callback.

    Secondly, changing that same line to

    this.setState({ submitted: true });
    this.blurInput();

    Also fixes the issue. So we're flushing setState calls made as a result of blurring the input if we blur the input before the state update is applied.

    I encountered this issue in the context of my own project, but it looks like it also applies to react-datepicker. Here we call setBlur in the callback to a setState which then blurs an input which has an event handler which then sets some state.

  7. josephdangerstewart commented on Jul 29, 2024

    @josephdangerstewart

    I managed this hack to force the setState callback to be wrapped in act. It resolves the warning in the minimal repro in my previous comment. Judging by the react-datetime code I skimmed, it may be sufficient for the original issue

    const originalSetState = React.Component.prototype.setState;
    jest.spyOn(React.Component.prototype, 'setState').mockImplementation(function (
    	this: ThisType<React.Component>,
    	...args: Parameters<typeof originalSetState>
    ) {
    	const [update, callback] = args;
    
    	const wrappedCallback = callback && (() => act(() => callback()));
    
    	originalSetState.apply(this, [update, wrappedCallback]);
    });

    however it unfortunately isn't sufficient for my use case. If callback triggers a state update in an effect in a function component, then I still see the warnings. To give a concrete example, my hack doesn't fix this example

    it('minimal fc repro', async () => {
    	render(<MinimalRepro />);
    	const user = userEvent.setup();
    
    	await user.click(screen.getByText('Test'));
    
    	expect(1).toBe(1);
    });
    
    class MinimalReproParent extends React.Component<any> {
    	state = {
    		foo: 'bar',
    	};
    
    	doThing = () => this.setState({ foo: 'baz' }, () => this.props.onClick());
    
    	render() {
    		return <button onClick={this.doThing}>{'Test'}</button>;
    	}
    }
    
    function MinimalRepro() {
    	const [state, setState] = useState(false);
    
    	useEffect(() => {
    		if (state) {
    			setState(true);
    		}
    	}, [state]);
    
    	return <MinimalReproParent onClick={() => setState(true)} />;
    }

    The set state in the MinimalRepro effect appears to not be flushed even with my hack forcing the set state callback to be wrapped in act

  8. github-actions commented on Aug 27, 2026

    @github-actions

    🎉 This issue has been resolved in version 16.3.3 🎉

    The release is available on:

    Your semantic-release bot 📦🚀

  9. pzaczkiewicz-athenahealth commented on Sep 1, 2026

    @pzaczkiewicz-athenahealth
    Author

    Wow, thanks @snowystinger. I wrote this so long ago that I had almost forgotten about it. Also funny running into you again in a completely different repo from react-spectrum.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions