Skip to content

Commit

Permalink
feat: warn when the test environment fake timers change unexpectedly
Browse files Browse the repository at this point in the history
Closes: #830
  • Loading branch information
kentcdodds committed Nov 18, 2020
1 parent 9494fdc commit 9442fb0
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 12 deletions.
10 changes: 2 additions & 8 deletions src/__tests__/fake-timers.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import {waitFor, waitForElementToBeRemoved} from '..'
import {render} from './helpers/test-utils'

beforeAll(() => {
jest.useFakeTimers()
})

afterAll(() => {
jest.useRealTimers()
})

async function runWaitFor({time = 300} = {}, options) {
const response = 'data'
const doAsyncThing = () =>
Expand Down Expand Up @@ -48,6 +40,7 @@ test('fake timer timeout', async () => {
})

test('times out after 1000ms by default', async () => {
jest.useFakeTimers()
const {container} = render(`<div></div>`)
const start = performance.now()
// there's a bug with this rule here...
Expand All @@ -66,6 +59,7 @@ test('times out after 1000ms by default', async () => {
})

test('recursive timers do not cause issues', async () => {
jest.useFakeTimers()
let recurse = true
function startTimer() {
setTimeout(() => {
Expand Down
65 changes: 63 additions & 2 deletions src/__tests__/wait-for.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,10 @@ test('when a promise is returned, it does not call the callback again until that
test('when a promise is returned, if that is not resolved within the timeout, then waitFor is rejected', async () => {
const sleep = t => new Promise(r => setTimeout(r, t))
const {promise} = deferred()
const waitForPromise = waitFor(() => promise, {timeout: 1}).catch(e => e)
const waitForError = waitFor(() => promise, {timeout: 1}).catch(e => e)
await sleep(5)

expect((await waitForPromise).message).toMatchInlineSnapshot(`
expect((await waitForError).message).toMatchInlineSnapshot(`
"Timed out in waitFor.
<html>
Expand All @@ -192,3 +192,64 @@ test('when a promise is returned, if that is not resolved within the timeout, th
</html>"
`)
})

test('if you switch from fake timers to real timers during the wait period you get an error', async () => {
jest.useFakeTimers()
const waitForError = waitFor(() => {
throw new Error('this error message does not matter...')
}).catch(e => e)

// this is the problem...
jest.useRealTimers()

const error = await waitForError

expect(error.message).toMatchInlineSnapshot(
`"Changed from using fake timers to real timers while using waitFor. This is not allowed and will result in very strange behavior. Please ensure you're awaiting all async things your test is doing before changing to real timers. For more info, please go to https://github.com/testing-library/dom-testing-library/issues/830"`,
)
// stack trace has this file in it
expect(error.stack).toMatch(__dirname)
})

test('if you switch from real timers to fake timers during the wait period you get an error', async () => {
const waitForError = waitFor(() => {
throw new Error('this error message does not matter...')
}).catch(e => e)

// this is the problem...
jest.useFakeTimers()
const error = await waitForError

expect(error.message).toMatchInlineSnapshot(
`"Changed from using real timers to fake timers while using waitFor. This is not allowed and will result in very strange behavior. Please ensure you're awaiting all async things your test is doing before changing to fake timers. For more info, please go to https://github.com/testing-library/dom-testing-library/issues/830"`,
)
// stack trace has this file in it
expect(error.stack).toMatch(__dirname)
})

test('the fake timers => real timers error shows the original stack trace when configured to do so', async () => {
jest.useFakeTimers()
const waitForError = waitFor(
() => {
throw new Error('this error message does not matter...')
},
{showOriginalStackTrace: true},
).catch(e => e)

jest.useRealTimers()

expect((await waitForError).stack).not.toMatch(__dirname)
})

test('the real timers => fake timers error shows the original stack trace when configured to do so', async () => {
const waitForError = waitFor(
() => {
throw new Error('this error message does not matter...')
},
{showOriginalStackTrace: true},
).catch(e => e)

jest.useFakeTimers()

expect((await waitForError).stack).not.toMatch(__dirname)
})
29 changes: 27 additions & 2 deletions src/wait-for.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ function waitFor(
// waiting or when we've timed out.
// eslint-disable-next-line no-unmodified-loop-condition
while (!finished) {
if (!jestFakeTimersAreEnabled()) {
const error = new Error(
`Changed from using fake timers to real timers while using waitFor. This is not allowed and will result in very strange behavior. Please ensure you're awaiting all async things your test is doing before changing to real timers. For more info, please go to https://github.com/testing-library/dom-testing-library/issues/830`,
)
if (!showOriginalStackTrace) copyStackTrace(error, stackTraceError)
reject(error)
return
}
// we *could* (maybe should?) use `advanceTimersToNextTimer` but it's
// possible that could make this loop go on forever if someone is using
// third party code that's setting up recursive timers so rapidly that
Expand All @@ -81,9 +89,9 @@ function waitFor(
await new Promise(r => setImmediate(r))
}
} else {
intervalId = setInterval(checkCallback, interval)
intervalId = setInterval(checkRealTimersCallback, interval)
const {MutationObserver} = getWindowFromNode(container)
observer = new MutationObserver(checkCallback)
observer = new MutationObserver(checkRealTimersCallback)
observer.observe(container, mutationObserverOptions)
checkCallback()
}
Expand All @@ -104,6 +112,18 @@ function waitFor(
}
}

function checkRealTimersCallback() {
if (jestFakeTimersAreEnabled()) {
const error = new Error(
`Changed from using real timers to fake timers while using waitFor. This is not allowed and will result in very strange behavior. Please ensure you're awaiting all async things your test is doing before changing to fake timers. For more info, please go to https://github.com/testing-library/dom-testing-library/issues/830`,
)
if (!showOriginalStackTrace) copyStackTrace(error, stackTraceError)
return reject(error)
} else {
return checkCallback()
}
}

function checkCallback() {
if (promiseStatus === 'pending') return
try {
Expand Down Expand Up @@ -177,3 +197,8 @@ function wait(...args) {
}

export {waitForWrapper as waitFor, wait}

/*
eslint
max-lines-per-function: ["error", {"max": 200}],
*/
6 changes: 6 additions & 0 deletions tests/setup-env.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ beforeAll(() => {
})
})

afterEach(() => {
if (jest.isMockFunction(global.setTimeout)) {
jest.useRealTimers()
}
})

afterAll(() => {
jest.restoreAllMocks()
})

0 comments on commit 9442fb0

Please sign in to comment.