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

Created on 23 Feb 2021  路  12Comments  路  Source: testing-library/user-event

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?

Most helpful comment

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. :)

All 12 comments

Could you reproduce the bug in a sandbox ?

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.

i have the same issue

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 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!

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 just got my test worked via this issue! thx a lot for detailed comment :)

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.

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.

Thanks for the follow-up @ph-fritsche -- paste worked for me as we're not directly testing the action of typing, more the result of content of the field (validation results.)

I would like to solve this properly in the future but paste presents a different approach if what you're testing is the effects of content in a field rather than the effects of typing in a field.

import { runWithRealTimers } from '@testing-library/dom/dist/helpers' is not a very stable approach for us and doesn't provide types for TS. I'd love to know why useRealTimers isn't working, but I've spent all day on these tests and my energy to deal with it further is depleted 馃槃

@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 });

@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.

Was this page helpful?
0 / 5 - 0 ratings