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

feat(waitFor): add complete and transparent support for fake timers #662

Merged
merged 1 commit into from
Jun 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
})
Loading