Skip to content

Commit e3fc7f3

Browse files
authored
feat: warn when the test environment fake timers change unexpectedly (#832)
Closes: #830
1 parent 9494fdc commit e3fc7f3

File tree

4 files changed

+98
-12
lines changed

4 files changed

+98
-12
lines changed

src/__tests__/fake-timers.js

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
import {waitFor, waitForElementToBeRemoved} from '..'
22
import {render} from './helpers/test-utils'
33

4-
beforeAll(() => {
5-
jest.useFakeTimers()
6-
})
7-
8-
afterAll(() => {
9-
jest.useRealTimers()
10-
})
11-
124
async function runWaitFor({time = 300} = {}, options) {
135
const response = 'data'
146
const doAsyncThing = () =>
@@ -48,6 +40,7 @@ test('fake timer timeout', async () => {
4840
})
4941

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

6861
test('recursive timers do not cause issues', async () => {
62+
jest.useFakeTimers()
6963
let recurse = true
7064
function startTimer() {
7165
setTimeout(() => {

src/__tests__/wait-for.js

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,10 @@ test('when a promise is returned, it does not call the callback again until that
180180
test('when a promise is returned, if that is not resolved within the timeout, then waitFor is rejected', async () => {
181181
const sleep = t => new Promise(r => setTimeout(r, t))
182182
const {promise} = deferred()
183-
const waitForPromise = waitFor(() => promise, {timeout: 1}).catch(e => e)
183+
const waitForError = waitFor(() => promise, {timeout: 1}).catch(e => e)
184184
await sleep(5)
185185

186-
expect((await waitForPromise).message).toMatchInlineSnapshot(`
186+
expect((await waitForError).message).toMatchInlineSnapshot(`
187187
"Timed out in waitFor.
188188
189189
<html>
@@ -192,3 +192,64 @@ test('when a promise is returned, if that is not resolved within the timeout, th
192192
</html>"
193193
`)
194194
})
195+
196+
test('if you switch from fake timers to real timers during the wait period you get an error', async () => {
197+
jest.useFakeTimers()
198+
const waitForError = waitFor(() => {
199+
throw new Error('this error message does not matter...')
200+
}).catch(e => e)
201+
202+
// this is the problem...
203+
jest.useRealTimers()
204+
205+
const error = await waitForError
206+
207+
expect(error.message).toMatchInlineSnapshot(
208+
`"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"`,
209+
)
210+
// stack trace has this file in it
211+
expect(error.stack).toMatch(__dirname)
212+
})
213+
214+
test('if you switch from real timers to fake timers during the wait period you get an error', async () => {
215+
const waitForError = waitFor(() => {
216+
throw new Error('this error message does not matter...')
217+
}).catch(e => e)
218+
219+
// this is the problem...
220+
jest.useFakeTimers()
221+
const error = await waitForError
222+
223+
expect(error.message).toMatchInlineSnapshot(
224+
`"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"`,
225+
)
226+
// stack trace has this file in it
227+
expect(error.stack).toMatch(__dirname)
228+
})
229+
230+
test('the fake timers => real timers error shows the original stack trace when configured to do so', async () => {
231+
jest.useFakeTimers()
232+
const waitForError = waitFor(
233+
() => {
234+
throw new Error('this error message does not matter...')
235+
},
236+
{showOriginalStackTrace: true},
237+
).catch(e => e)
238+
239+
jest.useRealTimers()
240+
241+
expect((await waitForError).stack).not.toMatch(__dirname)
242+
})
243+
244+
test('the real timers => fake timers error shows the original stack trace when configured to do so', async () => {
245+
const waitForError = waitFor(
246+
() => {
247+
throw new Error('this error message does not matter...')
248+
},
249+
{showOriginalStackTrace: true},
250+
).catch(e => e)
251+
252+
jest.useFakeTimers()
253+
254+
expect((await waitForError).stack).not.toMatch(__dirname)
255+
})

src/wait-for.js

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ function waitFor(
6060
// waiting or when we've timed out.
6161
// eslint-disable-next-line no-unmodified-loop-condition
6262
while (!finished) {
63+
if (!jestFakeTimersAreEnabled()) {
64+
const error = new Error(
65+
`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`,
66+
)
67+
if (!showOriginalStackTrace) copyStackTrace(error, stackTraceError)
68+
reject(error)
69+
return
70+
}
6371
// we *could* (maybe should?) use `advanceTimersToNextTimer` but it's
6472
// possible that could make this loop go on forever if someone is using
6573
// third party code that's setting up recursive timers so rapidly that
@@ -81,9 +89,9 @@ function waitFor(
8189
await new Promise(r => setImmediate(r))
8290
}
8391
} else {
84-
intervalId = setInterval(checkCallback, interval)
92+
intervalId = setInterval(checkRealTimersCallback, interval)
8593
const {MutationObserver} = getWindowFromNode(container)
86-
observer = new MutationObserver(checkCallback)
94+
observer = new MutationObserver(checkRealTimersCallback)
8795
observer.observe(container, mutationObserverOptions)
8896
checkCallback()
8997
}
@@ -104,6 +112,18 @@ function waitFor(
104112
}
105113
}
106114

115+
function checkRealTimersCallback() {
116+
if (jestFakeTimersAreEnabled()) {
117+
const error = new Error(
118+
`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`,
119+
)
120+
if (!showOriginalStackTrace) copyStackTrace(error, stackTraceError)
121+
return reject(error)
122+
} else {
123+
return checkCallback()
124+
}
125+
}
126+
107127
function checkCallback() {
108128
if (promiseStatus === 'pending') return
109129
try {
@@ -177,3 +197,8 @@ function wait(...args) {
177197
}
178198

179199
export {waitForWrapper as waitFor, wait}
200+
201+
/*
202+
eslint
203+
max-lines-per-function: ["error", {"max": 200}],
204+
*/

tests/setup-env.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ beforeAll(() => {
3636
})
3737
})
3838

39+
afterEach(() => {
40+
if (jest.isMockFunction(global.setTimeout)) {
41+
jest.useRealTimers()
42+
}
43+
})
44+
3945
afterAll(() => {
4046
jest.restoreAllMocks()
4147
})

0 commit comments

Comments
 (0)