vitest icon indicating copy to clipboard operation
vitest copied to clipboard

v0.2.0+ useFakeTimers breaks VTU with nested component

Open Akryum opened this issue 3 years ago • 6 comments

Describe the bug

Test fails:

 FAIL  src/components/Tested.spec.ts > Tested > should work
AssertionError: expected '' to include 'cat'

The setValue doesn't work on the nested <input> inside the child component.

Test passes if you comment vi.useFakeTimers() or if you downgrade to vitest 0.1.27.

Reproduction

https://github.com/Akryum/repro-vite-fake-timers-issue-649/blob/main/src/components/Tested.spec.ts

System Info

System:
    OS: Linux 5.13 elementary OS 6.1 Jólnir
    CPU: (24) x64 AMD Ryzen 9 3900XT 12-Core Processor
    Memory: 23.64 GB / 62.72 GB
    Container: Yes
    Shell: 5.0.17 - /bin/bash
  Binaries:
    Node: 16.13.1 - ~/.nvm/versions/node/v16.13.1/bin/node
    Yarn: 1.22.15 - ~/.yarn/bin/yarn
    npm: 8.1.2 - ~/.nvm/versions/node/v16.13.1/bin/npm
  Browsers:
    Chrome: 97.0.4692.99
  npmPackages:
    @vitejs/plugin-vue: ^2.0.1 => 2.1.0 
    vite: ^2.7.13 => 2.7.13 
    vitest: ^0.2.4 => 0.2.4

Used Package Manager

pnpm

Validations

Akryum avatar Jan 27 '22 22:01 Akryum

Maybe caused by https://github.com/vitest-dev/vitest/commit/cf30e5a46f5da52164fea56f7d78d9112089acd2 ?

Akryum avatar Jan 27 '22 22:01 Akryum

We had our own fake timers implementation, but it was largly incompatible with modern jest implementation. The current fake timers are identical to jest.

Does your code work with jest?

sheremet-va avatar Jan 28 '22 04:01 sheremet-va

I have encountered the same problem in tests using useFakeTimers and VTU. In my case, the test worked correctly with Jest.

"vite": "^2.6.4",
"vitest": "^0.6.0",
"@vue/test-utils": "^2.0.0-rc.18",

om-thn avatar Mar 09 '22 14:03 om-thn

I have encountered the same problem in tests using useFakeTimers and VTU. In my case, the test worked correctly with Jest.

"vite": "^2.6.4",
"vitest": "^0.6.0",
"@vue/test-utils": "^2.0.0-rc.18",

+1 But if i change for environment: 'happy-dom', current test working(but other files breaked with happy-dom)

And after move vi.useFakeTimers() to end of beforeAll(after first component mount), all working

For me, events not triggered their handlers

Kolobok12309 avatar Jun 16 '22 06:06 Kolobok12309

Triggering events also doesn't work:

<div @click="$emit('event')">
  Title
</div>
await userEvent.click(getByText('title'))
console.log(mounted.emitted())

https://stackblitz.com/edit/vitest-dev-vitest-feebp8?file=vite.config.ts,package.json,test%2Fsuite.test.ts&initialPath=vitest

sheremet-va avatar Jun 17 '22 11:06 sheremet-va

So, the problem is with this code:

https://github.com/vuejs/core/blob/3bdc41dff305422cb5334a64353c314bce1202a4/packages/runtime-dom/src/modules/events.ts#L120

When Event is triggered its timestamp is equal to mocked time, but Vue always uses real time, because it caches _getNow. And the check is not getting triggered. There are two solutions:

  1. Call vi.useFakeTimers() with appropriate config:
vi.useFakeTimers({
  toFake: ['setTimeout', 'clearTimeout']
})
  1. Mock navigator, so this check will return true:

https://github.com/vuejs/core/blob/3bdc41dff305422cb5334a64353c314bce1202a4/packages/runtime-dom/src/modules/events.ts#L33

sheremet-va avatar Jun 17 '22 18:06 sheremet-va

Wanted to follow up on this since the vue codebase appears to have gotten an update since @sheremet-va posted the workarounds above.

We are using VTU and ran into this issue recently:

  • using jest when we tried to update vue from 3.2.40 to 3.2.41
  • switching our project from jest to vitest on vue 3.2.40

In both of these scenarios, tests that used jest.useFakeTimers or vi.useFakeTimers caused vue component event bindings to stop working.

In some cases, the workaround above of only mocking certain timers did help:

vi.useFakeTimers({
  toFake: ['setTimeout', 'clearTimeout']
})

Faking the navigator did not work, as this check appears to have been removed from vue.

In one occasion, we had a component using lodash/debounce to delay API calls, and we wanted fine grained control over advancing the timers. lodash/debounce uses now() internally, so we had to update our toFake config to include Date:

vi.useFakeTimers({
  toFake: ['setTimeout', 'clearTimeout', 'Date']
})

This caused vue event bindings to stop working again, because vue is also using Date.now() internally.

https://github.com/vuejs/core/blob/5ee40532a63e0b792e0c1eccf3cf68546a4e23e9/packages/runtime-dom/src/modules/events.ts#L100-L104

vue uses timestamps internally to ensure that events are never processed by handlers that were attached after the event was triggered. This is to ensure an event handler that causes the DOM to update doesn't attach new listeners that then handle the same event. To achieve this vue adds a timestamp to each event, and a timestamp to each handler, and if the event timestamp is equal or older than the handler timestamp the handler is not invoked.

The solution we found for this is to advance the faked timers by 1ms right before triggering the event:

vi.useFakeTimers();
const wrapper = mount(SomeComponent); // handlers get a timestamp of Date.now() = 0
vi.advanceTimersByTime(1);
wrapper.find('selector').trigger('click'); // event created with timestamp of Date.now() = 1
// event triggered because event._vts > handler.attached

aethr avatar Nov 09 '22 04:11 aethr

None of these solution did work for me. I have a search component, which triggers a new search with the help of lodasg.debounce after 200ms from the last stroke. As soon as I use vi.useFakeTimers my test will run in the timeout.

kwiat1990 avatar Nov 16 '22 14:11 kwiat1990

This should be fixed with https://github.com/vuejs/test-utils/issues/1854

sheremet-va avatar Jan 21 '23 22:01 sheremet-va