testing-library / user-event

🐕 Simulate user events
https://testing-library.com/user-event
MIT License
2.18k stars 246 forks source link

`userEvent.click()` fails when used with `vi.useFakeTimers()`, all available solutions are not working #1115

Open xsjcTony opened 1 year ago

xsjcTony commented 1 year ago

Reproduction example

https://stackblitz.com/edit/vitejs-vite-askvcq?file=src/__tests__/App.test.tsx

Prerequisites

Describe the bug

I have a React component which is a timer (minimal reproduction), that starts automatically after mounted, and there's a button to RESET the timer.

In the test, I'm using vi.useFakeTimers() and await vi.advanceTimersByTimeAsync(500) to test the timer segmentally.

However, I'm not able to use await user.click() to click the button.

This is a known issue with fake timers, however, none of the existing solutions works.

None of these works

const user = userEvent.setup({
  advanceTimers: vi.advanceTimersByTime,
  // advanceTimers: vi.advanceTimersByTimeAsync
  // advanceTimers: vi.advanceTimersByTime.bind(vi)
  // advanceTimers: vi.advanceTimersByTimeAsync.bind(vi)
  // delay: null,
})

And I cannot use vi.useRealTimers() before clicking button, since it will break the fake timer to further test the component's reset functionality.


Those solutions above are all based on Jest since almost all resources on the internet are for Jest. But since I've never used Jest, so I'm not sure if it's working in Jest, and is this an issue with @testing-library/user-event or Vitest

Expected behavior

The button is successfully clicked

Actual behavior

It makes the test timed out

User-event version

14.0.0

Environment

System:
  OS: Windows 10 10.0.22000
  CPU: (8) x64 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz
  Memory: 9.23GB / 31.69GB
Binaries:
  Node: 16.18.0 - C:\Program Files\nodejs\node.EXE
  npm: 8.19.2 - C:\Program Files\nodejs\npm.CMD
Browsers:
  Edge: Spartan (44.22000.120.0), Chromium (112.0.1722.39)
  Internet Explorer: 11.0.22000.120
npmPackages:
  @vitejs/plugin-react: ^3.1.0 => 3.1.0
  vite: ^4.2.1 => 4.2.1
  vitest: ^0.30.1 => 0.30.1
  @testing-library/jest-dom: ^5.16.5 => 5.16.5
  @testing-library/react: ^14.0.0 => 14.0.0
  @testing-library/user-event: ^14.4.3 => 14.4.3
  jsdom: ^21.1.1 => 21.1.1
  react: ^18.2.0 => 18.2.0
  react-dom: ^18.2.0 => 18.2.0

Additional context

Please also refer to https://github.com/vitest-dev/vitest/issues/3184

ph-fritsche commented 1 year ago

This is an issue with @testing-library/react. See https://github.com/testing-library/react-testing-library/issues/1197

Trick this piece of code into recognizing your environment as Jest.

globalThis.jest = 'neitherUndefinedNorNull'
xsjcTony commented 1 year ago

@ph-fritsche Thanks a lot, I got what the problem is.

But regarding your solutions, I think for a temporary workaround, this makes more sense to me https://github.com/wojtekmaj/react-async-button/commit/2d26f217a375b7020ddf42f76891254586fc3ce4

Is there any ETA to fix this issue? since I personally don't like temp workaround to be there forever in my code🤣

Regarding the fix, just like letting users pass advanceTimer option in userEvent.setup(), instead of using jest.advanceTimersByTime, this should be testing-framework agnostic, since there are also some ppl like me who had never used Jest

xsjcTony commented 1 year ago

In your test suites using fake timers

import { beforeAll, vi, describe } from 'vitest';

describe('this suite uses fake timers', () => {
  // Temporarily workaround for bug in @testing-library/react when use user-event with `vi.useFakeTimers()`
  beforeAll(() => {
    const _jest = globalThis.jest;

    globalThis.jest = {
      ...globalThis.jest,
      advanceTimersByTime: vi.advanceTimersByTime.bind(vi)
    };

    return () => void (globalThis.jest = _jest);
  });
})
iulspop commented 1 year ago

I ran into same problem today. My work around:

  beforeEach(() => {
    vi.useFakeTimers()

    globalThis.jest = {
      advanceTimersByTime: vi.advanceTimersByTime.bind(vi),
    }
  })

  beforeAll(() => {
    vi.useRealTimers()
  })

 test('given a new recurring question submitted: form data contains only question text', async () => {
    ...
    const user = userEvent.setup({
      advanceTimers: vi.advanceTimersByTime.bind(vi),
    })
  })

This worked but causes a Warning: An update to RouterProvider inside a test was not wrapped in act(...). warning.

Would appreciate if y'all fixed this. Testing library should work out of the box with Vitest imo.

Full test file ```ts import { unstable_createRemixStub as createRemixStub } from '@remix-run/testing' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest' import { CreateQuestionFormComponent } from './create-question-form-component' describe('CreateQuestionForm component', () => { beforeEach(() => { vi.useFakeTimers() globalThis.jest = { advanceTimersByTime: vi.advanceTimersByTime.bind(vi), } }) beforeAll(() => { vi.useRealTimers() }) test('given a new recurring question submitted: form data contains only question text', async () => { const date = new Date('2022-10-20T01:00:00.000Z') vi.setSystemTime(date) let formData: FormData | undefined const RemixStub = createRemixStub([ { path: '/', element: , action: async ({ request }) => { formData = await request.formData() return null }, }, ]) const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime.bind(vi), }) render() const questionText = 'Did you go to bed between 8 and 9PM?' await user.type(screen.getByLabelText('What is the recurring question?'), questionText) await user.click(screen.getByRole('button', { name: /submit/i })) const formEntries = Object.fromEntries(formData.entries()) expect(formEntries).toEqual({ text: questionText, timestamp: date.toISOString(), utcOffsetInMinutes: String(date.getTimezoneOffset()), }) }) test('given form render: has cancel link to back to /questions', async () => { const RemixStub = createRemixStub([ { path: '/', element: , }, ]) render() expect(screen.getByRole('link', { name: /cancel/i })).toHaveAttribute('href', '/questions') }) test('given click submit before input text: does not submit form', async () => { let formData: FormData | undefined const RemixStub = createRemixStub([ { path: '/', element: , action: async ({ request }) => { formData = await request.formData() return null }, }, ]) const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime.bind(vi), }) render() await user.click(screen.getByRole('button', { name: /submit/i })) expect(formData).toEqual(undefined) }) }) ```
mrwwalmsley commented 8 months ago

Should this be added to documentation somewhere? I spent a few hours trying to get my tests with fake timers migrated from Jest to Vite. This solved most of my issues. It would be great to have this in the docs somewhere??

majidmade commented 3 days ago

Thank you to the comments above; this saved me a lot of time! I encountered the same thing while moving a test from jest to vitest.

Here's my version of the workaround, with business logic removed, in case it helps some future human (or LLM?).

There aren't any act warnings, and my tests still fail when expected. (I recognize this is very similar to something that OP explicitly said was not working for them, so ymmv.)

describe('myTestSubject', () => {
  beforeEach(() => {
    vi.useFakeTimers();
  });

  // seems like there's a bug in the interaction between userEvent / vi.useFakeTimers
  // see: https://github.com/testing-library/user-event/issues/1115
  const getUserEventInstance = () =>
    userEvent.setup({
      advanceTimers: vi.advanceTimersByTime.bind(vi),
    });

  it(`has some timer-based user interaction`, async () => {
    const { getBySomething } = render(<TestComponent />);

    const user = getUserEventInstance();
    await act(() => user.click(getBySomething(something)));
    await act(() => user.keyboard('a'));

    // (assert)
    vi.advanceTimersByTime(t);
    // (assert)
  });
});
felixbores commented 2 days ago

Hey there. I ran into this issue when migrating Jest to Vitest. After trying out different solutions, the easiest one was adding this line to the setup files:

vi.stubGlobal('jest', { advanceTimersByTime: vi.advanceTimersByTime.bind(vi) });

combined with:

const user = userEvent.setup({ delay: null });

although the only actual change was the first one, since the other one was needed to make Jest work.

Hope this help.

Happy coding.