Skip to content

Commit c787491

Browse files
committed
fix: remove side-effect from runWithRealTimers #884 #886
1 parent 24ebf28 commit c787491

File tree

2 files changed

+46
-123
lines changed

2 files changed

+46
-123
lines changed

src/__tests__/helpers.js

Lines changed: 21 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@ import {
55
runWithRealTimers,
66
} from '../helpers'
77

8-
const globalObj = typeof window === 'undefined' ? global : window
9-
10-
afterEach(() => jest.useRealTimers())
11-
128
test('returns global document if exists', () => {
139
expect(getDocument()).toBe(document)
1410
})
@@ -53,107 +49,44 @@ describe('query container validation throws when validation fails', () => {
5349
})
5450
})
5551

56-
describe('analyze test leak', () => {
52+
describe('run with real timers', () => {
5753
const realSetTimeout = global.setTimeout
5854

5955
afterEach(() => {
56+
jest.useRealTimers()
6057
global.setTimeout = realSetTimeout
6158
})
6259

63-
test('jest.getRealSystemTime does not throw after modern timers have been used', () => {
64-
expect(global.setTimeout).toBe(realSetTimeout)
65-
66-
expect(jest.getRealSystemTime).toThrow()
67-
68-
jest.useRealTimers()
69-
expect(global.setTimeout).toBe(realSetTimeout)
70-
71-
jest.useFakeTimers('modern')
72-
expect(jest.getRealSystemTime).not.toThrow()
73-
74-
jest.useRealTimers()
75-
expect(global.setTimeout).toBe(realSetTimeout)
76-
77-
// current implementation assumes this throws
78-
// see https://github.com/testing-library/dom-testing-library/blob/5bc93643f312d9ca4210b97681686c9aa8a902d7/src/helpers.js#L43-L45
79-
expect(jest.getRealSystemTime).not.toThrow()
80-
81-
// jest.useRealTimers() is still no problem
82-
jest.useRealTimers()
83-
expect(global.setTimeout).toBe(realSetTimeout)
60+
test('use real timers when timers are faked with jest.useFakeTimers(legacy)', () => {
61+
// legacy timers use mocks and do not rely on a clock instance
62+
jest.useFakeTimers('legacy')
63+
runWithRealTimers(() => {
64+
expect(global.setTimeout).toEqual(realSetTimeout)
65+
})
66+
expect(global.setTimeout._isMockFunction).toBe(true)
67+
expect(global.setTimeout.clock).toBeUndefined()
8468
})
8569

86-
test('BUG: runWithRealTimers fakes timers after modern fake timers have been used', () => {
70+
test('use real timers when timers are faked with jest.useFakeTimers(modern)', () => {
71+
// modern timers use a clock instance instead of a mock
8772
jest.useFakeTimers('modern')
88-
jest.useRealTimers()
89-
90-
// modern fake timers have been used, but the current timeout is the real one
91-
expect(global.setTimeout).toBe(realSetTimeout)
73+
runWithRealTimers(() => {
74+
expect(global.setTimeout).toEqual(realSetTimeout)
75+
})
76+
expect(global.setTimeout._isMockFunction).toBeUndefined()
77+
expect(global.setTimeout.clock).toBeDefined()
78+
})
9279

93-
// repeat the test for the fake implementation
80+
test('do not use real timers when timers are not faked with jest.useFakeTimers', () => {
81+
// useFakeTimers is not used, timers are faked in some other way
9482
const fakedSetTimeout = callback => {
9583
callback()
9684
}
9785
fakedSetTimeout.clock = jest.fn()
9886
global.setTimeout = fakedSetTimeout
9987

100-
// runWithRealTimers assumes jest.getRealSystemTime() would throw, but it does not
101-
// it calls jest.useRealTimers() before the callback - which does nothing because no fake is active
102-
// it calls jest.useFakeTimers('modern') after the callback
10388
runWithRealTimers(() => {
104-
expect(fakedSetTimeout).toEqual(globalObj.setTimeout)
89+
expect(global.setTimeout).toEqual(fakedSetTimeout)
10590
})
106-
107-
// it should be fakedSetTimeout
108-
expect(global.setTimeout).not.toBe(fakedSetTimeout)
109-
110-
// this line makes the bug hard to track down
111-
// without it jest.useRealTimers() will restore fakedSetTimeout
112-
// with it jest.useRealTimers() 'restores' setTimeout to undefined
113-
global.setTimeout = realSetTimeout
114-
115-
// now useRealTimers is broken
116-
jest.useRealTimers()
117-
expect(global.setTimeout).not.toBe(realSetTimeout)
11891
})
11992
})
120-
121-
test('should always use realTimers before using callback when timers are faked with useFakeTimers', () => {
122-
const originalSetTimeout = globalObj.setTimeout
123-
124-
// legacy timers use mocks and do not rely on a clock instance
125-
jest.useFakeTimers('legacy')
126-
runWithRealTimers(() => {
127-
expect(originalSetTimeout).toEqual(globalObj.setTimeout)
128-
})
129-
expect(globalObj.setTimeout._isMockFunction).toBe(true)
130-
expect(globalObj.setTimeout.clock).toBeUndefined()
131-
132-
jest.useRealTimers()
133-
134-
// modern timers use a clock instance instead of a mock
135-
jest.useFakeTimers('modern')
136-
runWithRealTimers(() => {
137-
expect(originalSetTimeout).toEqual(globalObj.setTimeout)
138-
})
139-
expect(globalObj.setTimeout._isMockFunction).toBeUndefined()
140-
expect(globalObj.setTimeout.clock).toBeDefined()
141-
})
142-
143-
test('should not use realTimers when timers are not faked with useFakeTimers', () => {
144-
const originalSetTimeout = globalObj.setTimeout
145-
146-
// useFakeTimers is not used, timers are faked in some other way
147-
const fakedSetTimeout = callback => {
148-
callback()
149-
}
150-
fakedSetTimeout.clock = jest.fn()
151-
152-
globalObj.setTimeout = fakedSetTimeout
153-
154-
runWithRealTimers(() => {
155-
expect(fakedSetTimeout).toEqual(globalObj.setTimeout)
156-
})
157-
158-
globalObj.setTimeout = originalSetTimeout
159-
})

src/helpers.js

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,52 +5,42 @@ const TEXT_NODE = 3
55

66
// Currently this fn only supports jest timers, but it could support other test runners in the future.
77
function runWithRealTimers(callback) {
8-
const fakeTimersType = getJestFakeTimersType()
9-
if (fakeTimersType) {
8+
return _runWithRealTimers(callback).callbackReturnValue
9+
}
10+
11+
function _runWithRealTimers(callback) {
12+
const timerAPI = {
13+
clearImmediate,
14+
clearInterval,
15+
clearTimeout,
16+
setImmediate,
17+
setInterval,
18+
setTimeout,
19+
}
20+
21+
// istanbul ignore else
22+
if (jest) {
1023
jest.useRealTimers()
1124
}
1225

1326
const callbackReturnValue = callback()
1427

15-
if (fakeTimersType) {
16-
jest.useFakeTimers(fakeTimersType)
17-
}
28+
const usedJestFakeTimers = Object.keys(timerAPI).filter(
29+
k => timerAPI[k] !== globalObj[k],
30+
).length
1831

19-
return callbackReturnValue
20-
}
21-
22-
function getJestFakeTimersType() {
23-
// istanbul ignore if
24-
if (
25-
typeof jest === 'undefined' ||
26-
typeof globalObj.setTimeout === 'undefined'
27-
) {
28-
return null
32+
if (usedJestFakeTimers) {
33+
jest.useFakeTimers(timerAPI.setTimeout?.clock ? 'modern' : 'legacy')
2934
}
3035

31-
if (
32-
typeof globalObj.setTimeout._isMockFunction !== 'undefined' &&
33-
globalObj.setTimeout._isMockFunction
34-
) {
35-
return 'legacy'
36-
}
37-
38-
if (
39-
typeof globalObj.setTimeout.clock !== 'undefined' &&
40-
typeof jest.getRealSystemTime !== 'undefined'
41-
) {
42-
try {
43-
// jest.getRealSystemTime is only supported for Jest's `modern` fake timers and otherwise throws
44-
jest.getRealSystemTime()
45-
return 'modern'
46-
} catch {
47-
// not using Jest's modern fake timers
48-
}
36+
return {
37+
callbackReturnValue,
38+
usedJestFakeTimers,
4939
}
50-
return null
5140
}
5241

53-
const jestFakeTimersAreEnabled = () => Boolean(getJestFakeTimersType())
42+
const jestFakeTimersAreEnabled = () =>
43+
Boolean(_runWithRealTimers(() => {}).usedJestFakeTimers)
5444

5545
// we only run our tests in node, and setImmediate is supported in node.
5646
// istanbul ignore next

0 commit comments

Comments
 (0)