Skip to content

Commit

Permalink
feat(waitFor): add complete support for fake timers (#662)
Browse files Browse the repository at this point in the history
Closes #661
  • Loading branch information
kentcdodds committed Jun 23, 2020
1 parent 260e1e8 commit 5b2640a
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 445 deletions.
37 changes: 0 additions & 37 deletions src/__tests__/__snapshots__/wait-for-dom-change.js.snap

This file was deleted.

30 changes: 30 additions & 0 deletions src/__tests__/deprecation-warnings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {waitForElement, waitForDomChange, wait} from '..'

afterEach(() => {
console.warn.mockClear()
})

test('deprecation warnings only warn once', async () => {
await wait(() => {}, {timeout: 1})
await waitForElement(() => {}, {timeout: 1}).catch(e => e)
await waitForDomChange({timeout: 1}).catch(e => e)
expect(console.warn.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"\`wait\` has been deprecated and replaced by \`waitFor\` instead. In most cases you should be able to find/replace \`wait\` with \`waitFor\`. Learn more: https://testing-library.com/docs/dom-testing-library/api-async#waitfor.",
],
Array [
"\`waitForElement\` has been deprecated. Use a \`find*\` query (preferred: https://testing-library.com/docs/dom-testing-library/api-queries#findby) or use \`waitFor\` instead: https://testing-library.com/docs/dom-testing-library/api-async#waitfor",
],
Array [
"\`waitForDomChange\` has been deprecated. Use \`waitFor\` instead: https://testing-library.com/docs/dom-testing-library/api-async#waitfor.",
],
]
`)

console.warn.mockClear()
await wait(() => {}, {timeout: 1})
await waitForElement(() => {}, {timeout: 1}).catch(e => e)
await waitForDomChange({timeout: 1}).catch(e => e)
expect(console.warn).not.toHaveBeenCalled()
})
92 changes: 0 additions & 92 deletions src/__tests__/example.js

This file was deleted.

167 changes: 47 additions & 120 deletions src/__tests__/fake-timers.js
Original file line number Diff line number Diff line change
@@ -1,139 +1,66 @@
import {waitFor, waitForElementToBeRemoved} from '..'
import {render} from './helpers/test-utils'

// Because we're using fake timers here and I don't want these tests to run
// for the actual length of the test (because it's waiting for a timeout error)
// we'll mock the setTimeout, clearTimeout, and setImmediate to be the ones
// that jest will mock for us.
jest.mock('../helpers', () => {
const actualHelpers = jest.requireActual('../helpers')
return {
...actualHelpers,
setTimeout,
clearTimeout,
setImmediate,
}
beforeAll(() => {
jest.useFakeTimers()
})

jest.useFakeTimers()

// Because of the way jest mocking works here's the order of things (and no, the order of the code above doesn't make a difference):
// 1. Just mocks '../helpers' and setTimeout/clearTimeout/setImmediate are set to their "correct" values
// 2. We tell Jest to use fake timers
// 3. We reset the modules and we mock '../helpers' again so now setTimeout/clearTimeout/setImmediate are set to their mocked values
// We're only doing this because want to mock those values so this test doesn't take 4501ms to run.
jest.resetModules()

const {
wait,
waitForElement,
waitForDomChange,
waitForElementToBeRemoved,
} = require('../')

test('waitForElementToBeRemoved: times out after 4500ms by default', () => {
const {container} = render(`<div></div>`)
// there's a bug with this rule here...
// eslint-disable-next-line jest/valid-expect
const promise = expect(
waitForElementToBeRemoved(() => container),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Timed out in waitForElementToBeRemoved."`,
)
jest.advanceTimersByTime(4501)
return promise
afterAll(() => {
jest.useRealTimers()
})

test('wait: can time out', async () => {
const promise = wait(() => {
// eslint-disable-next-line no-throw-literal
throw undefined
})
jest.advanceTimersByTime(4600)
await expect(promise).rejects.toThrow(/timed out/i)
})

test('waitForElement: can time out', async () => {
const promise = waitForElement(() => {})
jest.advanceTimersByTime(4600)
await expect(promise).rejects.toThrow(/timed out/i)
})
async function runWaitFor() {
const response = 'data'
const doAsyncThing = () =>
new Promise(r => setTimeout(() => r(response), 300))
let result
doAsyncThing().then(r => (result = r))

test('waitForElement: can specify our own timeout time', async () => {
const promise = waitForElement(() => {}, {timeout: 4700})
const handler = jest.fn()
promise.then(handler, handler)
// advance beyond the default
jest.advanceTimersByTime(4600)
// promise was neither rejected nor resolved
expect(handler).toHaveBeenCalledTimes(0)
await waitFor(() => expect(result).toBe(response))
}

// advance beyond our specified timeout
jest.advanceTimersByTime(150)

// timed out
await expect(promise).rejects.toThrow(/timed out/i)
test('real timers', async () => {
// the only difference when not using fake timers is this test will
// have to wait the full length of the timeout
await runWaitFor()
})

test('waitForDomChange: can time out', async () => {
const promise = waitForDomChange()
jest.advanceTimersByTime(4600)
await expect(promise).rejects.toThrow(/timed out/i)
test('legacy', async () => {
jest.useFakeTimers('legacy')
await runWaitFor()
})

test('waitForDomChange: can specify our own timeout time', async () => {
const promise = waitForDomChange({timeout: 4700})
const handler = jest.fn()
promise.then(handler, handler)
// advance beyond the default
jest.advanceTimersByTime(4600)
// promise was neither rejected nor resolved
expect(handler).toHaveBeenCalledTimes(0)

// advance beyond our specified timeout
jest.advanceTimersByTime(150)

// timed out
await expect(promise).rejects.toThrow(/timed out/i)
test('modern', async () => {
jest.useFakeTimers()
await runWaitFor()
})

test('wait: ensures the interval is greater than 0', async () => {
// Arrange
const spy = jest.fn()
spy.mockImplementationOnce(() => {
throw new Error('first time does not work')
})
const promise = wait(spy, {interval: 0})
expect(spy).toHaveBeenCalledTimes(1)
spy.mockClear()

// Act
// this line will throw an error if wait does not make the interval 1 instead of 0
// which is why it does that!
jest.advanceTimersByTime(0)

// Assert
expect(spy).toHaveBeenCalledTimes(0)
spy.mockImplementationOnce(() => 'second time does work')

// Act
jest.advanceTimersByTime(1)
await promise

// Assert
expect(spy).toHaveBeenCalledTimes(1)
test('fake timer timeout', async () => {
jest.useFakeTimers()
await expect(
waitFor(
() => {
throw new Error('always throws')
},
{timeout: 10},
),
).rejects.toMatchInlineSnapshot(`[Error: always throws]`)
})

test('wait: times out if it runs out of attempts', () => {
const spy = jest.fn(() => {
throw new Error('example error')
})
test('times out after 1000ms by default', async () => {
const {container} = render(`<div></div>`)
const start = performance.now()
// there's a bug with this rule here...
// eslint-disable-next-line jest/valid-expect
const promise = expect(
wait(spy, {interval: 1, timeout: 3}),
).rejects.toThrowErrorMatchingInlineSnapshot(`"example error"`)
jest.advanceTimersByTime(1)
jest.advanceTimersByTime(1)
jest.advanceTimersByTime(1)
return promise
await expect(
waitForElementToBeRemoved(() => container),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Timed out in waitForElementToBeRemoved."`,
)
// NOTE: this assertion ensures that even when we have fake timers, the
// timeout still takes the full 1000ms
// unfortunately, timeout clocks aren't super accurate, so we simply verify
// that it's greater than or equal to 900ms. That's enough to be confident
// that we're using real timers.
expect(performance.now() - start).toBeGreaterThanOrEqual(900)
})

0 comments on commit 5b2640a

Please sign in to comment.