NickColley / jest-axe

Custom Jest matcher for aXe for testing accessibility ♿️🃏
MIT License
1.06k stars 54 forks source link

jest.useFakeTimers causes test timeout #156

Closed dbasilio closed 2 years ago

dbasilio commented 3 years ago

Here is a simple repro, the test below passes. If you uncomment the jest.useFakeTimers() in the beforeEach the test will timeout no matter what else you do.

import React from 'react'
import { mount } from 'enzyme'

import { toHaveNoViolations, axe } from 'jest-axe'

expect.extend(toHaveNoViolations)

const MyComponent = () => {
    return <div></div>
}

describe('myComponent', () => {
    beforeEach(() => {
        //jest.useFakeTimers()
    })

    it('does not hit a timeout', async () => {
        const wrapper = mount(<MyComponent />)

        const result = await axe(wrapper.getDOMNode())

        expect(result).toHaveNoViolations()
    })
})

Version Details: jest-axe: 4.1.0 jest: 26.6.3 axe-core: 4.1.1

dbasilio commented 3 years ago

For those looking for a workaround to the problem:

it('does not hit a timeout', (done) => {
    const wrapper = mount(<MyComponent />)

    axe(wrapper.getDOMNode()).then(axeResult => {
        expect(axeResult).toHaveNoViolations()
        done()
    })

    jest.advanceTimersByTime(1000)
})

The jest.advanceTimersByTime will log a console error if fakeTimers is not being used.

dsamburskyi commented 3 years ago

@dbasilio I had similar issue. Simply putting jest.useRealTimers(); in the axe test helped. Interestingly afterEach(() => jest.useRealTimers()); doesn't work.

dbasilio commented 3 years ago

@dsamburskyi If your jest.useFakeTimers() call appears in a beforeAll instead of a beforeEach then calling jest.useRealTimers() could make tests non-deterministic depending on the order run. afterEach runs after the test has completed, so the timeout would have already occurred.

peterlawless commented 3 years ago

My solution is to just.useRealTimers() run my axe tests, then call jest.useFakeTimers within the same test block. Even with jest.useFakeTimers() in a beforeEach block, I ran into issues where jest.advanceTimersByTime() would execute twice and throw off my tests if I did not close out the axe tests with jest.useFakeTimers()!

NickColley commented 2 years ago

This seems to be an issue that is caused by timers in your own code being tested so will close this out unless you know of something we should change in this package itself :)

Thank you for posting the useful workarounds for people Googling to find, appreciated.

dbasilio commented 2 years ago

Well ideally using fake timers doesn't cause the test to time out. This is an annoying issue to run into and annoying to solve. It points to something being done with timers in this package that is causing that behaviour.

Without my solution posted above (which is a fairly un-intuitive piece of code IMO), you need to isolate a11y tests to a separate test file, or a separate describe block. Both of those are not ideal solutions. The package is jest-axe but if you use a fairly common jest feature in your other tests this package breaks.

merrywhether commented 2 years ago

We've added a wrapper function that makes usage seamless in our repo (we have timers: 'modern' in our config).

export async function axe(...args: Parameters<JestAxe>): ReturnType<JestAxe> {
  const [results] = await Promise.all([
    jestAxe(...args),
    new Promise<void>((resolve) => {
      jest.runAllTimers();
      resolve();
    }),
  ]);

  return results;
}

This works with both real and fake timers, but Jest warns for real timers because runAllTimers is a noop then. Unfortunately, Jest doesn't appear to expose a way to programmatically detect what type of timers currently are in use. This "worked" as a way to detect fake timers silence the error:

if (jest.getRealSystemTime() !== Date.now()) {
  jest.runAllTimers();
}
resolve();

but it's pretty brittle for a variety of reasons. Maybe just exposing a second export that covers the Fake Timers use-case would be the best path forward? I'd be happy to PR that if there's interest. The hardest part of that would be the naming (axeFT, axeFake, etc) 🤔.

syntactic-salt commented 2 years ago

It looks like this is actually an issue that's introduced by axe-core. axe-core uses setTimeout under the hood.

https://github.com/dequelabs/axe-core/issues/3055