4
4
5
5
import { waitFor as waitForWeb } from '../'
6
6
7
+ function sleep ( timeoutMS , signal ) {
8
+ return new Promise ( ( resolve , reject ) => {
9
+ const timeoutID = setTimeout ( ( ) => {
10
+ resolve ( )
11
+ } , timeoutMS )
12
+ signal ?. addEventListener ( 'abort' , reason => {
13
+ clearTimeout ( timeoutID )
14
+ reject ( reason )
15
+ } )
16
+ } )
17
+ }
18
+
7
19
function jestFakeTimersAreEnabled ( ) {
8
20
/* istanbul ignore else */
9
21
// eslint-disable-next-line
@@ -22,7 +34,7 @@ function jestFakeTimersAreEnabled() {
22
34
/**
23
35
* Reference implementation of `waitFor` that supports Jest fake timers
24
36
*/
25
- function waitFor ( callback , { interval = 50 , timeout = 1000 } = { } ) {
37
+ function waitFor ( callback , options ) {
26
38
/** @type {import('../').FakeClock } */
27
39
const jestFakeClock = {
28
40
advanceTimersByTime : timeoutMS => {
@@ -39,11 +51,186 @@ function waitFor(callback, {interval = 50, timeout = 1000} = {}) {
39
51
40
52
return waitForWeb ( callback , {
41
53
clock,
42
- interval,
43
- timeout,
54
+ ...options ,
44
55
} )
45
56
}
46
57
58
+ // TODO: Use jest.replaceProperty(global, 'Error', ErrorWithoutStack) and `jest.restoreAllMocks`
59
+ let originalError
60
+ beforeEach ( ( ) => {
61
+ originalError = global . Error
62
+ } )
63
+ afterEach ( ( ) => {
64
+ global . Error = originalError
65
+ } )
66
+
47
67
test ( 'runs' , async ( ) => {
48
68
await expect ( waitFor ( ( ) => { } ) ) . resolves . toBeUndefined ( )
49
69
} )
70
+
71
+ test ( 'ensures the given callback is a function' , ( ) => {
72
+ expect ( ( ) => waitFor ( null ) ) . toThrowErrorMatchingInlineSnapshot (
73
+ `Received \`callback\` arg must be a function` ,
74
+ )
75
+ } )
76
+
77
+ describe ( 'using fake modern timers' , ( ) => {
78
+ beforeEach ( ( ) => {
79
+ jest . useFakeTimers ( 'modern' )
80
+ } )
81
+ afterEach ( ( ) => {
82
+ jest . useRealTimers ( )
83
+ } )
84
+
85
+ test ( 'times out after 1s by default' , async ( ) => {
86
+ let resolved = false
87
+ setTimeout ( ( ) => {
88
+ resolved = true
89
+ } , 1000 )
90
+
91
+ await expect (
92
+ waitFor ( ( ) => {
93
+ if ( ! resolved ) {
94
+ throw new Error ( 'Not resolved' )
95
+ }
96
+ } ) ,
97
+ ) . rejects . toThrowErrorMatchingInlineSnapshot ( `Not resolved` )
98
+ } )
99
+
100
+ test ( 'times out even if the callback never settled' , async ( ) => {
101
+ await expect (
102
+ waitFor ( ( ) => {
103
+ return new Promise ( ( ) => { } )
104
+ } ) ,
105
+ ) . rejects . toThrowErrorMatchingInlineSnapshot ( `Timed out in waitFor.` )
106
+ } )
107
+
108
+ test ( 'callback can return a promise and is not called again until the promise resolved' , async ( ) => {
109
+ const callback = jest . fn ( ( ) => {
110
+ return sleep ( 20 )
111
+ } )
112
+
113
+ await expect ( waitFor ( callback , { interval : 1 } ) ) . resolves . toBeUndefined ( )
114
+ // We configured the waitFor call to ping every 1ms.
115
+ // But the callback only resolved after 20ms.
116
+ // If we would ping as instructed, we'd have 20+1 calls (1 initial, 20 for pings).
117
+ // But the implementation waits for callback to resolve first before checking again.
118
+ expect ( callback ) . toHaveBeenCalledTimes ( 1 )
119
+ } )
120
+
121
+ test ( 'callback is not called again until the promise rejects' , async ( ) => {
122
+ const callback = jest . fn ( async ( ) => {
123
+ await sleep ( 20 )
124
+ throw new Error ( 'Not done' )
125
+ } )
126
+
127
+ await expect (
128
+ waitFor ( callback , { interval : 1 , timeout : 30 } ) ,
129
+ ) . rejects . toThrowErrorMatchingInlineSnapshot ( `Not done` )
130
+ // We configured the waitFor call to ping every 1ms.
131
+ // But the callback only rejected after 20ms.
132
+ // If we would ping as instructed, we'd have 30+1 calls (1 initial, 30 for pings until timeout was reached).
133
+ // But the implementation waits for callback to resolve first before checking again.
134
+ // So we have 1 for the initial check (that takes 20ms) and one for an interval check after the initial check resolved.
135
+ // Next ping would happen at 40ms but we already timed out at this point
136
+ expect ( callback ) . toHaveBeenCalledTimes ( 2 )
137
+ } )
138
+
139
+ test ( 'massages the stack trace to point to the waitFor call not the callback call' , async ( ) => {
140
+ let waitForError
141
+ try {
142
+ await waitFor (
143
+ ( ) => {
144
+ return sleep ( 100 )
145
+ } ,
146
+ { showOriginalStackTrace : false , interval : 100 , timeout : 1 } ,
147
+ )
148
+ } catch ( caughtError ) {
149
+ waitForError = caughtError
150
+ }
151
+
152
+ const stackTrace = waitForError . stack . split ( '\n' ) . slice ( 1 )
153
+ // The earlier a stackframe points to the actual callsite the better
154
+ const testStackFrame = stackTrace [ 1 ]
155
+ const fileLocationRegexp = / \( ( .* ) : \d + : \d + \) $ /
156
+ expect ( testStackFrame ) . toMatch ( fileLocationRegexp )
157
+ const [ , fileLocation ] = testStackFrame . match ( fileLocationRegexp )
158
+ expect ( fileLocation ) . toBe ( __filename )
159
+
160
+ expect ( waitForError . stack ) . toMatchInlineSnapshot ( `
161
+ Error: Timed out in waitFor.
162
+ at waitFor (<PROJECT_ROOT>/src/waitFor.ts:163:27)
163
+ at waitFor (<PROJECT_ROOT>/src/__tests__/waitForNode.js:52:20)
164
+ at Object.<anonymous> (<PROJECT_ROOT>/src/__tests__/waitForNode.js:142:13)
165
+ at Promise.then.completed (<PROJECT_ROOT>/node_modules/jest-circus/build/utils.js:391:28)
166
+ at new Promise (<anonymous>)
167
+ at callAsyncCircusFn (<PROJECT_ROOT>/node_modules/jest-circus/build/utils.js:316:10)
168
+ at _callCircusTest (<PROJECT_ROOT>/node_modules/jest-circus/build/run.js:218:40)
169
+ at processTicksAndRejections (node:internal/process/task_queues:96:5)
170
+ at _runTest (<PROJECT_ROOT>/node_modules/jest-circus/build/run.js:155:3)
171
+ at _runTestsForDescribeBlock (<PROJECT_ROOT>/node_modules/jest-circus/build/run.js:66:9)
172
+ ` )
173
+ } )
174
+
175
+ test ( 'does not crash in runtimes without Error.prototype.stack' , async ( ) => {
176
+ class ErrorWithoutStack extends Error {
177
+ // Not the same as "not having" but close enough
178
+ // stack a non-standard property so we have to guard against stack not existing
179
+ stack = undefined
180
+ }
181
+ const originalGlobalError = global . Error
182
+ global . Error = ErrorWithoutStack
183
+ let waitForError
184
+ try {
185
+ await waitFor (
186
+ ( ) => {
187
+ return sleep ( 100 )
188
+ } ,
189
+ { interval : 100 , timeout : 1 } ,
190
+ )
191
+ } catch ( caughtError ) {
192
+ waitForError = caughtError
193
+ }
194
+ // Restore early so that Jest can use Error.prototype.stack again
195
+ // Still need global restore in case something goes wrong.
196
+ global . Error = originalGlobalError
197
+
198
+ // Feel free to update this snapshot.
199
+ // It's only used to highlight how bad the default stack trace is if we timeout
200
+ // The only frame pointing to this test is the one from the wrapper.
201
+ // An actual test would not have any frames pointing to this test.
202
+ expect ( waitForError . stack ) . toBeUndefined ( )
203
+ } )
204
+
205
+ test ( 'can be configured to throw an error with the original stack trace' , async ( ) => {
206
+ let waitForError
207
+ try {
208
+ await waitFor (
209
+ ( ) => {
210
+ return sleep ( 100 )
211
+ } ,
212
+ { showOriginalStackTrace : true , interval : 100 , timeout : 1 } ,
213
+ )
214
+ } catch ( caughtError ) {
215
+ waitForError = caughtError
216
+ }
217
+
218
+ // Feel free to update this snapshot.
219
+ // It's only used to highlight how bad the default stack trace is if we timeout
220
+ // The only frame pointing to this test is the one from the wrapper.
221
+ // An actual test would not have any frames pointing to this test.
222
+ expect ( waitForError . stack ) . toMatchInlineSnapshot ( `
223
+ Error: Timed out in waitFor.
224
+ at handleTimeout (<PROJECT_ROOT>/src/waitFor.ts:147:17)
225
+ at callTimer (<PROJECT_ROOT>/node_modules/@sinonjs/fake-timers/src/fake-timers-src.js:729:24)
226
+ at doTickInner (<PROJECT_ROOT>/node_modules/@sinonjs/fake-timers/src/fake-timers-src.js:1289:29)
227
+ at doTick (<PROJECT_ROOT>/node_modules/@sinonjs/fake-timers/src/fake-timers-src.js:1370:20)
228
+ at Object.tick (<PROJECT_ROOT>/node_modules/@sinonjs/fake-timers/src/fake-timers-src.js:1378:20)
229
+ at FakeTimers.advanceTimersByTime (<PROJECT_ROOT>/node_modules/@jest/fake-timers/build/modernFakeTimers.js:101:19)
230
+ at Object.advanceTimersByTime (<PROJECT_ROOT>/node_modules/jest-runtime/build/index.js:2228:26)
231
+ at Object.advanceTimersByTime (<PROJECT_ROOT>/src/__tests__/waitForNode.js:41:12)
232
+ at <PROJECT_ROOT>/src/waitFor.ts:75:15
233
+ at new Promise (<anonymous>)
234
+ ` )
235
+ } )
236
+ } )
0 commit comments