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

userEvent.type's delay hangs forever #565

Closed
pcafstockf opened this issue Feb 23, 2021 · 17 comments
Closed

userEvent.type's delay hangs forever #565

pcafstockf opened this issue Feb 23, 2021 · 17 comments

Comments

@pcafstockf
Copy link

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?

@ph-fritsche
Copy link
Member

Could you reproduce the bug in a sandbox ?

@pcafstockf
Copy link
Author

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.

@renardete
Copy link

i have the same issue

@renardete
Copy link

renardete commented Feb 26, 2021

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

@pcafstockf
Copy link
Author

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!

@ph-fritsche
Copy link
Member

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

return new Promise(resolve => setTimeout(() => resolve(), time))

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

@kimploo
Copy link

kimploo commented Mar 7, 2021

I just got my test worked via this issue! thx a lot for detailed comment :)

@fenetic
Copy link

fenetic commented Mar 8, 2021

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.

@ph-fritsche
Copy link
Member

ph-fritsche commented Mar 8, 2021

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.

@fenetic
Copy link

fenetic commented Mar 8, 2021

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 😄

@icedtoast
Copy link

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

@renardete
Copy link

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

@ciampo
Copy link

ciampo commented Mar 10, 2022

Just incurred in something similar while writing tests with version 14.0.0-beta.13

Basically, the await userEvent.type() function hangs indefinitely, causing the tests to time out. After a good hour+ spent debugging the issue, I found out that setting delay: null in the setup options allows the tests to pass — which means that somehow, the setTimeout call in the wait function seems to the culprit:

return new Promise(resolve => setTimeout(() => resolve(), time))

(cc @ph-fritsche )

@alastor-lee
Copy link

I'm seeing the similar instability in 14.0.0-beta.8

Sometimes multiple tests timeout, sometimes none do.
jest.useRealTimers() or real timers in general is not a surefire bandaid.

Setting the delay to null didn't obviously change behavior, and either way fake timers need winding and it's a guess how long it should be since the userEvent.type() returns a promise in an arbitrary amount of time.

@ph-fritsche
Copy link
Member

ph-fritsche commented Apr 11, 2022

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

stanleynguyen added a commit to opengovsg/postmangovsg that referenced this issue Apr 18, 2022
* ci(github-actions): run all tests on PR to develop

* test(frontend): fix outdated frontend test

* test(frontend): fix issue with userEvent timing out test cases

More info testing-library/user-event#565 (comment)

* test(frontend): add back tests for protected email template
@ciampo
Copy link

ciampo commented May 2, 2022

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

Thank you for the update, @ph-fritsche — although I still don't understand how the advanceTimers option may fix the problem I described above. Could you be more specific?

Thank you!

@ph-fritsche
Copy link
Member

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.

export function wait(config: Config) {
const delay = config.delay
if (typeof delay !== 'number') {
return
}
return Promise.all([
new Promise<void>(resolve => globalThis.setTimeout(() => resolve(), delay)),
config.advanceTimers(delay),
])
}

E.g. with Jest's fake timers:

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

await user.type(element, 'foo')

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

8 participants