Skip to content

Commit debe7b2

Browse files
brapifraKent C. Dodds
authored and
Kent C. Dodds
committed
fix: make async utilities work with fake timers (#342)
1 parent 8729c0a commit debe7b2

7 files changed

+208
-10
lines changed

src/__tests__/wait-for-dom-change.js

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1-
import {waitForDomChange} from '../wait-for-dom-change'
21
import {renderIntoDocument} from './helpers/test-utils'
32

3+
function importModule() {
4+
return require('../').waitForDomChange
5+
}
6+
7+
let waitForDomChange
8+
9+
beforeEach(() => {
10+
jest.useRealTimers()
11+
jest.resetModules()
12+
waitForDomChange = importModule()
13+
})
14+
415
test('waits for the dom to change in the document', async () => {
516
const {container} = renderIntoDocument('<div />')
617
const promise = waitForDomChange()
@@ -48,3 +59,52 @@ Array [
4859
]
4960
`)
5061
})
62+
63+
describe('timers', () => {
64+
const expectElementToChange = async () => {
65+
const importedWaitForDomChange = importModule()
66+
const {container} = renderIntoDocument('<div />')
67+
68+
setTimeout(() => container.firstChild.setAttribute('id', 'foo'), 100)
69+
70+
const promise = importedWaitForDomChange({container, timeout: 200})
71+
72+
if (setTimeout._isMockFunction) {
73+
jest.advanceTimersByTime(110)
74+
}
75+
76+
await expect(promise).resolves.toMatchInlineSnapshot(`
77+
Array [
78+
Object {
79+
"addedNodes": Array [],
80+
"attributeName": "id",
81+
"attributeNamespace": null,
82+
"nextSibling": null,
83+
"oldValue": null,
84+
"previousSibling": null,
85+
"removedNodes": Array [],
86+
"target": <div
87+
id="foo"
88+
/>,
89+
"type": "attributes",
90+
},
91+
]
92+
`)
93+
}
94+
95+
it('works with real timers', async () => {
96+
jest.useRealTimers()
97+
await expectElementToChange()
98+
})
99+
it('works with fake timers', async () => {
100+
jest.useFakeTimers()
101+
await expectElementToChange()
102+
})
103+
})
104+
105+
test("doesn't change jest's timers value when importing the module", () => {
106+
jest.useFakeTimers()
107+
importModule()
108+
109+
expect(window.setTimeout._isMockFunction).toEqual(true)
110+
})

src/__tests__/wait-for-element-to-be-removed.js

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1-
import {waitForElementToBeRemoved} from '../'
21
import {renderIntoDocument} from './helpers/test-utils'
32

3+
function importModule() {
4+
return require('../').waitForElementToBeRemoved
5+
}
6+
7+
let waitForElementToBeRemoved
8+
9+
beforeEach(() => {
10+
jest.useRealTimers()
11+
jest.resetModules()
12+
waitForElementToBeRemoved = importModule()
13+
})
14+
415
test('resolves on mutation only when the element is removed', async () => {
516
const {queryAllByTestId} = renderIntoDocument(`
617
<div data-testid="div"></div>
@@ -57,3 +68,52 @@ test('requires an unempty array of elements to exist first', () => {
5768
`"The callback function which was passed did not return an element or non-empty array of elements. waitForElementToBeRemoved requires that the element(s) exist before waiting for removal."`,
5869
)
5970
})
71+
72+
describe('timers', () => {
73+
const expectElementToBeRemoved = async () => {
74+
const importedWaitForElementToBeRemoved = importModule()
75+
76+
const {queryAllByTestId} = renderIntoDocument(`
77+
<div data-testid="div"></div>
78+
<div data-testid="div"></div>
79+
`)
80+
const divs = queryAllByTestId('div')
81+
// first mutation
82+
setTimeout(() => {
83+
divs.forEach(d => d.setAttribute('id', 'mutated'))
84+
})
85+
// removal
86+
setTimeout(() => {
87+
divs.forEach(div => div.parentElement.removeChild(div))
88+
}, 100)
89+
90+
const promise = importedWaitForElementToBeRemoved(
91+
() => queryAllByTestId('div'),
92+
{
93+
timeout: 200,
94+
},
95+
)
96+
97+
if (setTimeout._isMockFunction) {
98+
jest.advanceTimersByTime(110)
99+
}
100+
101+
await promise
102+
}
103+
104+
it('works with real timers', async () => {
105+
jest.useRealTimers()
106+
await expectElementToBeRemoved()
107+
})
108+
it('works with fake timers', async () => {
109+
jest.useFakeTimers()
110+
await expectElementToBeRemoved()
111+
})
112+
})
113+
114+
test("doesn't change jest's timers value when importing the module", () => {
115+
jest.useFakeTimers()
116+
importModule()
117+
118+
expect(window.setTimeout._isMockFunction).toEqual(true)
119+
})

src/__tests__/wait-for-element.js

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1-
import {waitForElement} from '../wait-for-element'
21
import {render, renderIntoDocument} from './helpers/test-utils'
32

3+
function importModule() {
4+
return require('../').waitForElement
5+
}
6+
7+
let waitForElement
8+
9+
beforeEach(() => {
10+
jest.useRealTimers()
11+
jest.resetModules()
12+
waitForElement = importModule()
13+
})
14+
415
test('waits for element to appear in the document', async () => {
516
const {rerender, getByTestId} = renderIntoDocument('<div />')
617
const promise = waitForElement(() => getByTestId('div'))
@@ -48,3 +59,34 @@ test('waits until callback does not return null', async () => {
4859
test('throws error if no callback is provided', async () => {
4960
await expect(waitForElement()).rejects.toThrow(/callback/i)
5061
})
62+
63+
describe('timers', () => {
64+
const expectElementToExist = async () => {
65+
const importedWaitForElement = importModule()
66+
67+
const {rerender, getByTestId} = renderIntoDocument('<div />')
68+
69+
setTimeout(() => rerender('<div data-testid="div" />'), 100)
70+
71+
const promise = importedWaitForElement(() => getByTestId('div'), {
72+
timeout: 200,
73+
})
74+
75+
if (setTimeout._isMockFunction) {
76+
jest.advanceTimersByTime(110)
77+
}
78+
79+
const element = await promise
80+
81+
await expect(element).toBeInTheDocument()
82+
}
83+
84+
it('works with real timers', async () => {
85+
jest.useRealTimers()
86+
await expectElementToExist()
87+
})
88+
it('works with fake timers', async () => {
89+
jest.useFakeTimers()
90+
await expectElementToExist()
91+
})
92+
})

src/helpers.js

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,42 @@ import MutationObserver from '@sheerun/mutationobserver-shim'
22

33
const globalObj = typeof window === 'undefined' ? global : window
44

5+
// Currently this fn only supports jest timers, but it could support other test runners in the future.
6+
function runWithRealTimers(callback) {
7+
const usingJestFakeTimers =
8+
globalObj.setTimeout._isMockFunction && typeof jest !== 'undefined'
9+
10+
if (usingJestFakeTimers) {
11+
jest.useRealTimers()
12+
}
13+
14+
const callbackReturnValue = callback()
15+
16+
if (usingJestFakeTimers) {
17+
jest.useFakeTimers()
18+
}
19+
20+
return callbackReturnValue
21+
}
22+
523
// we only run our tests in node, and setImmediate is supported in node.
624
// istanbul ignore next
725
function setImmediatePolyfill(fn) {
826
return globalObj.setTimeout(fn, 0)
927
}
1028

11-
const clearTimeoutFn = globalObj.clearTimeout
12-
// istanbul ignore next
13-
const setImmediateFn = globalObj.setImmediate || setImmediatePolyfill
14-
const setTimeoutFn = globalObj.setTimeout
29+
function getTimeFunctions() {
30+
// istanbul ignore next
31+
return {
32+
clearTimeoutFn: globalObj.clearTimeout,
33+
setImmediateFn: globalObj.setImmediate || setImmediatePolyfill,
34+
setTimeoutFn: globalObj.setTimeout,
35+
}
36+
}
37+
38+
const {clearTimeoutFn, setImmediateFn, setTimeoutFn} = runWithRealTimers(
39+
getTimeFunctions,
40+
)
1541

1642
function newMutationObserver(onMutation) {
1743
const MutationObserverConstructor =
@@ -37,4 +63,5 @@ export {
3763
clearTimeoutFn as clearTimeout,
3864
setImmediateFn as setImmediate,
3965
setTimeoutFn as setTimeout,
66+
runWithRealTimers,
4067
}

src/wait-for-dom-change.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
setImmediate,
55
setTimeout,
66
clearTimeout,
7+
runWithRealTimers,
78
} from './helpers'
89
import {getConfig} from './config'
910

@@ -20,7 +21,9 @@ function waitForDomChange({
2021
return new Promise((resolve, reject) => {
2122
const timer = setTimeout(onTimeout, timeout)
2223
const observer = newMutationObserver(onMutation)
23-
observer.observe(container, mutationObserverOptions)
24+
runWithRealTimers(() =>
25+
observer.observe(container, mutationObserverOptions),
26+
)
2427

2528
function onDone(error, result) {
2629
clearTimeout(timer)

src/wait-for-element-to-be-removed.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
setImmediate,
55
setTimeout,
66
clearTimeout,
7+
runWithRealTimers,
78
} from './helpers'
89
import {getConfig} from './config'
910

@@ -43,7 +44,9 @@ function waitForElementToBeRemoved(
4344
)
4445
} else {
4546
// Only observe for mutations only if there is element while checking synchronously
46-
observer.observe(container, mutationObserverOptions)
47+
runWithRealTimers(() =>
48+
observer.observe(container, mutationObserverOptions),
49+
)
4750
}
4851
} catch (error) {
4952
onDone(error)

src/wait-for-element.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
setImmediate,
55
setTimeout,
66
clearTimeout,
7+
runWithRealTimers,
78
} from './helpers'
89
import {getConfig} from './config'
910

@@ -31,7 +32,9 @@ function waitForElement(
3132
const timer = setTimeout(onTimeout, timeout)
3233

3334
const observer = newMutationObserver(onMutation)
34-
observer.observe(container, mutationObserverOptions)
35+
runWithRealTimers(() =>
36+
observer.observe(container, mutationObserverOptions),
37+
)
3538
function onDone(error, result) {
3639
clearTimeout(timer)
3740
setImmediate(() => observer.disconnect())

0 commit comments

Comments
 (0)