Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
pzaczkiewicz-athenahealth opened this issue Aug 10, 2023 · 7 comments

Comments

@pzaczkiewicz-athenahealth

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.

@pzaczkiewicz-athenahealth
Copy link
Author

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

@chawes13
Copy link

chawes13 commented Sep 14, 2023

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

@chawes13
Copy link

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')
  })

@pzaczkiewicz-athenahealth
Copy link
Author

pzaczkiewicz-athenahealth commented Sep 14, 2023

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.

@KutynaMateusz
Copy link

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

@josephdangerstewart
Copy link

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.

@josephdangerstewart
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants