Skip to content

Commit f2dcbdc

Browse files
committed
Full test suite
1 parent f5fb9f7 commit f2dcbdc

File tree

2 files changed

+190
-10
lines changed

2 files changed

+190
-10
lines changed

src/__tests__/waitForNode.js

+190-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@
44

55
import {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+
719
function 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+
4767
test('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+
})

src/waitFor.ts

-7
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,6 @@ function waitForImpl<T>(
143143
let error: Error
144144
if (lastError) {
145145
error = lastError as Error
146-
// TODO: Why special casing this name `TestingLibraryElementError`?
147-
if (
148-
!showOriginalStackTrace &&
149-
error.name === 'TestingLibraryElementError'
150-
) {
151-
copyStackTrace(error, stackTraceError)
152-
}
153146
} else {
154147
error = new Error('Timed out in waitFor.')
155148
if (!showOriginalStackTrace) {

0 commit comments

Comments
 (0)