user-event: userEvent.type's delay hangs forever

Up front… This issue probably belongs in angular-testing-library, jest-preset-angular, or maybe jest-dom, but I can’t say where at the moment, and the problem is manifest in user-event . Also, there is may be a better title depending on where this belongs. Please feel free to modify any of this or redirect me as appropriate.

  • @testing-library/user-event version: 12.7.3

  • Testing Framework and version: jest: 26.6.3 angular: 11.2.2 node: 12.18.4 jest-dom: 5.11.9 angular-testing-library: 10.3.2 jest-preset-angular: 8.3.2

Relevant code or config

My jest test simulates the user clicking an input, and clearing it’s existing text:

await userEvent.type(inputElem, '{selectall}{backspace}', {delay: 10, skipClick: false});

What happened: If delay is set to zero the test passes as expected. If delay is greater than zero, the test hangs until jest times out (longer jest timeout does not help).

What you did: user-event/src/type.js currently contains the following code:

async function runCallbacks(callbacks) {
	...
for (const callback of callbacks) {
	if (delay > 0) await wait(delay)
	if (!currentElement().disabled) {
		...

A breakpoint on if (delay > 0) is always hit. A breakpoint on if (!currentElement().disabled) is never hit (assuming you call with delay > 0).

What I tried: Disabling the zone.js patch of setTimeout allows the test to pass (although obviously not a real solution).

declare var window;
(window as any).__Zone_disable_timers = true;

Problem description: This issue seems like an interaction problem between packages in the testing-library ecosystem. I’ve followed each packages installation and setup guides, and believe my import ordering and configurations are correct, but obviously something was missed somewhere.

Any suggestions?

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 1
  • Comments: 17 (5 by maintainers)

Commits related to this issue

Most upvoted comments

I solve the issue by setting real timers of jest before the type. Hope it helps 😄

jest.useRealTimers()
await userEvent.type(element, text, { delay: 350 })

I’m glad your problem is solved. To add some more information about relationship of timers and userEvent.type with delay…

https://github.com/testing-library/user-event/blob/7a5c51e7f89c4ab0592174eff591b9e9275ff81c/src/type.js#L124

Each block of events for a character is delayed per setTimeout. You don’t need to use real timers. You just can’t await the typing in your test since you have to wind your fake timers while the type implementation is running.

If you want to use real timers for some part of your code, there is also this solution by @testing-library/dom. It’s just an internal, but it won’t go away in foreseeable future:

import { runWithRealTimers } from '@testing-library/dom/dist/helpers'

runWithRealTimers(() => {
  // do something with real timers
})
// do something with the timers you had in place before - real or fake

If you don’t like to use an internal but also don’t want to repeat the code, open an issue there. Maybe adding this to the exports of the main module is worth a discussion. 😃

I solve the issue by setting real timers of jest before the type. Hope it helps 😄

jest.useRealTimers()
await userEvent.type(element, text, { delay: 350 })

If I could get back all the time I’ve wasted chasing defects caused by jest’s replacement of standard Javascript functions…

That solved my issue as well. Thanks!

We push resolve() onto the event loop per setTimeout and wait for it to be called. If you’re using fake timers this won’t happen after delay microseconds, but when you wind your fake timer. advanceTimers allows you to provide a callback to do this.

https://github.com/testing-library/user-event/blob/ee062e762f9ac185d982dbf990387e97e05b3c9d/src/utils/misc/wait.ts#L3-L12

E.g. with Jest’s fake timers:

jest.useFakeTimers()
const user = userEvent.setup({advanceTimers: jest.advanceTimersByTime})

await user.type(element, 'foo')

@ph-fritsche - Would it be possible to add an advance option to type? The typeImpl function could then call that with the delay before await ing on the promise.

As an example of the approach, I’m using this “wrapper” around type (it only works for plaintext):

async function typeWithDelay(input, text, delayInMilliseconds) {
  let previous = Promise.resolve();
  for (const codepoint of text) {
    await previous;
    userEvent.type(input, codepoint);

    previous = new Promise((resolve) => setTimeout(() => resolve(), delayInMilliseconds));

    act(() => {
      jest.advanceTimersByTime(delayInMilliseconds);
    });
  }
}

So instead of above, I could just call userEvent.type(input, 'some text', { delay: 20, advance: jest.advanceTimersByTime });

advanceTimers option was introduced in #907 . It is included in v14.1.0

@ph-fritsche - Would it be possible to add an advance option to type? The typeImpl function could then call that with the delay before await ing on the promise.

As an example of the approach, I’m using this “wrapper” around type (it only works for plaintext):

async function typeWithDelay(input, text, delayInMilliseconds) {
  let previous = Promise.resolve();
  for (const codepoint of text) {
    await previous;
    userEvent.type(input, codepoint);

    previous = new Promise((resolve) => setTimeout(() => resolve(), delayInMilliseconds));

    act(() => {
      jest.advanceTimersByTime(delayInMilliseconds);
    });
  }
}

So instead of above, I could just call userEvent.type(input, 'some text', { delay: 20, advance: jest.advanceTimersByTime });

Actually this would be really cool, to advance timers. It could remove instabilities that could appear for waiting the type function.

Ran into this issue while writing a test with a large character count in an input field that had to be async; useRealTimers didn’t help, but a neat workaround was to just use userEvent.paste instead. Might help if anyone’s looking for a different approach.

Please note that userEvent.paste provides a completely different abstraction than userEvent.type. If you expect the user to type some input, using userEvent.paste is no better than to call fireEvent.input directly.

If you have mocked the timer functions by other means than jest.useFakeTimers, jest.useRealTimers will have no effect.

Could you reproduce the bug in a sandbox ?

Yeah, that’s what I should have done in the first place. In a small sample I might even find my configuration mistake 😃

Will reply with an update once I know one way or the other.