44
55import { waitFor as waitForWeb } from '../'
66
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+
719function jestFakeTimersAreEnabled ( ) {
820 /* istanbul ignore else */
921 // eslint-disable-next-line
@@ -22,7 +34,7 @@ function jestFakeTimersAreEnabled() {
2234/**
2335 * Reference implementation of `waitFor` that supports Jest fake timers
2436 */
25- function waitFor ( callback , { interval = 50 , timeout = 1000 } = { } ) {
37+ function waitFor ( callback , options ) {
2638 /** @type {import('../').FakeClock } */
2739 const jestFakeClock = {
2840 advanceTimersByTime : timeoutMS => {
@@ -39,11 +51,186 @@ function waitFor(callback, {interval = 50, timeout = 1000} = {}) {
3951
4052 return waitForWeb ( callback , {
4153 clock,
42- interval,
43- timeout,
54+ ...options ,
4455 } )
4556}
4657
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+
4767test ( 'runs' , async ( ) => {
4868 await expect ( waitFor ( ( ) => { } ) ) . resolves . toBeUndefined ( )
4969} )
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