Skip to content

Commit

Permalink
fix: Only use a single clock (testing-library#966)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Remove deprecated `waitFormDOMChange`

BREAKING CHANGE: The `timeout` in `waitFor(callback, { interval, timeout } )` now uses the same clock as `interval`. Previously `timeout` was always using the real clock while `interval` was using the global clock which could've been mocked out. For the old behavior I'd recommend `waitFor(callback, { interval, timeout: Number.PositiveInfinity })` and rely on your test runner to timeout considering real timers.
  • Loading branch information
eps1lon authored Jun 3, 2021
1 parent 86fb094 commit 3ae2702
Show file tree
Hide file tree
Showing 10 changed files with 22 additions and 333 deletions.
30 changes: 0 additions & 30 deletions src/__tests__/deprecation-warnings.js

This file was deleted.

13 changes: 6 additions & 7 deletions src/__tests__/fake-timers.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,11 @@ test('times out after 1000ms by default', async () => {
).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)
// NOTE: this assertion ensures that the timeout runs in the declared (fake) clock
// while in real time the time was only a fraction since the real clock is only bound by the CPU
// So 10ms is really just an approximation on how long the CPU needs to execute our code.
// If people want to timeout in real time they should rely on their test runners.
expect(performance.now() - start).toBeLessThanOrEqual(10)
})

test('recursive timers do not cause issues', async () => {
Expand All @@ -68,7 +67,7 @@ test('recursive timers do not cause issues', async () => {
}

startTimer()
await runWaitFor({time: 800}, {timeout: 100})
await runWaitFor({time: 800}, {timeout: 900})

recurse = false
})
71 changes: 1 addition & 70 deletions src/__tests__/helpers.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import {screen} from '../'
import {
getDocument,
getWindowFromNode,
checkContainerType,
runWithRealTimers,
} from '../helpers'
import {getDocument, getWindowFromNode, checkContainerType} from '../helpers'

test('returns global document if exists', () => {
expect(getDocument()).toBe(document)
Expand Down Expand Up @@ -61,67 +56,3 @@ describe('query container validation throws when validation fails', () => {
)
})
})

describe('run with real timers', () => {
const realSetTimeout = global.setTimeout

afterEach(() => {
// restore timers replaced by jest.useFakeTimers()
jest.useRealTimers()
// restore setTimeout replaced by assignment
global.setTimeout = realSetTimeout
})

test('use real timers when timers are faked with jest.useFakeTimers(legacy)', () => {
// legacy timers use mocks and do not rely on a clock instance
jest.useFakeTimers('legacy')
runWithRealTimers(() => {
expect(global.setTimeout).toBe(realSetTimeout)
})
expect(global.setTimeout._isMockFunction).toBe(true)
expect(global.setTimeout.clock).toBeUndefined()
})

test('use real timers when timers are faked with jest.useFakeTimers(modern)', () => {
// modern timers use a clock instance instead of a mock
jest.useFakeTimers('modern')
runWithRealTimers(() => {
expect(global.setTimeout).toBe(realSetTimeout)
})
expect(global.setTimeout._isMockFunction).toBeUndefined()
expect(global.setTimeout.clock).toBeDefined()
})

test('do not use real timers when timers are not faked with jest.useFakeTimers', () => {
// useFakeTimers is not used, timers are faked in some other way
const fakedSetTimeout = callback => {
callback()
}
fakedSetTimeout.clock = jest.fn()
global.setTimeout = fakedSetTimeout

runWithRealTimers(() => {
expect(global.setTimeout).toBe(fakedSetTimeout)
})
expect(global.setTimeout).toBe(fakedSetTimeout)
})

describe('run with setImmediate and clearImmediate deleted', () => {
const setImmediate = global.setImmediate
const clearImmediate = global.clearImmediate

beforeEach(() => {
delete global.setImmediate
delete global.clearImmediate
})

afterEach(() => {
global.setImmediate = setImmediate
global.clearImmediate = clearImmediate
})

test('safe check for setImmediate and clearImmediate', () => {
expect(() => runWithRealTimers(() => {})).not.toThrow()
})
})
})
54 changes: 0 additions & 54 deletions src/__tests__/wait-for-dom-change.js

This file was deleted.

88 changes: 10 additions & 78 deletions src/helpers.js
Original file line number Diff line number Diff line change
@@ -1,85 +1,21 @@
const globalObj = typeof window === 'undefined' ? global : window
// Constant node.nodeType for text nodes, see:
// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType#Node_type_constants
const TEXT_NODE = 3

// Currently this fn only supports jest timers, but it could support other test runners in the future.
function runWithRealTimers(callback) {
return hasJestTimers()
? runWithJestRealTimers(callback).callbackReturnValue
: // istanbul ignore next
callback()
}

function hasJestTimers() {
return (
typeof jest !== 'undefined' &&
jest !== null &&
typeof jest.useRealTimers === 'function'
)
}

function runWithJestRealTimers(callback) {
const timerAPI = {
clearInterval,
clearTimeout,
setInterval,
setTimeout,
}

// For more on why we have the check here,
// checkout https://github.com/testing-library/dom-testing-library/issues/914
if (typeof setImmediate === 'function') {
timerAPI.setImmediate = setImmediate
}
if (typeof clearImmediate === 'function') {
timerAPI.clearImmediate = clearImmediate
}

jest.useRealTimers()

const callbackReturnValue = callback()

const usedFakeTimers = Object.entries(timerAPI).some(
([name, func]) => func !== globalObj[name],
)

if (usedFakeTimers) {
jest.useFakeTimers(timerAPI.setTimeout?.clock ? 'modern' : 'legacy')
}

return {
callbackReturnValue,
usedFakeTimers,
}
}

function jestFakeTimersAreEnabled() {
return hasJestTimers()
? runWithJestRealTimers(() => {}).usedFakeTimers
: // istanbul ignore next
false
}

// we only run our tests in node, and setImmediate is supported in node.
// istanbul ignore next
function setImmediatePolyfill(fn) {
return globalObj.setTimeout(fn, 0)
}

function getTimeFunctions() {
// istanbul ignore next
return {
clearTimeoutFn: globalObj.clearTimeout,
setImmediateFn: globalObj.setImmediate || setImmediatePolyfill,
setTimeoutFn: globalObj.setTimeout,
/* istanbul ignore else */
if (typeof jest !== 'undefined' && jest !== null) {
return (
// legacy timers
setTimeout._isMockFunction === true ||
// modern timers
Object.prototype.hasOwnProperty.call(setTimeout, 'clock')
)
}
// istanbul ignore next
return false
}

const {clearTimeoutFn, setImmediateFn, setTimeoutFn} = runWithRealTimers(
getTimeFunctions,
)

function getDocument() {
/* istanbul ignore if */
if (typeof window === 'undefined') {
Expand Down Expand Up @@ -144,10 +80,6 @@ function checkContainerType(container) {
export {
getWindowFromNode,
getDocument,
clearTimeoutFn as clearTimeout,
setImmediateFn as setImmediate,
setTimeoutFn as setTimeout,
runWithRealTimers,
checkContainerType,
jestFakeTimersAreEnabled,
TEXT_NODE,
Expand Down
1 change: 0 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export * from './queries'
export * from './wait-for'
export * from './wait-for-element'
export * from './wait-for-element-to-be-removed'
export * from './wait-for-dom-change'
export {getDefaultNormalizer} from './matches'
export * from './get-node-text'
export * from './events'
Expand Down
64 changes: 0 additions & 64 deletions src/wait-for-dom-change.js

This file was deleted.

Loading

0 comments on commit 3ae2702

Please sign in to comment.