fix!: fail expect.poll when function didn't resolve in time (#10233) · vitest-dev/vitest@4df048c (original) (raw)
`@@ -133,40 +133,57 @@ export function createExpectPoll(expect: ExpectStatic): ExpectStatic['poll'] {
`
133
133
`}
`
134
134
``
135
135
`const { setTimeout, clearTimeout } = getSafeTimers()
`
136
``
-
137
``
`-
let executionPhase: 'fn' | 'assertion' = 'fn'
`
138
``
`-
let hasTimedOut = false
`
139
``
-
140
``
`-
const timerId = setTimeout(() => {
`
141
``
`-
hasTimedOut = true
`
142
``
`-
}, timeout)
`
``
136
`+
let timerId: ReturnType | undefined
`
``
137
`+
const timeoutController = new AbortController()
`
``
138
`+
const timeoutPromise = new Promise((resolve) => {
`
``
139
`+
timerId = setTimeout(() => {
`
``
140
`+
timeoutController.abort()
`
``
141
`+
resolve()
`
``
142
`+
}, timeout)
`
``
143
`+
})
`
``
144
`+
let lastError: unknown
`
143
145
``
144
146
`try {
`
145
147
`while (true) {
`
146
``
`-
const isLastAttempt = hasTimedOut
`
147
``
-
148
``
`-
if (isLastAttempt) {
`
149
``
`-
chai.util.flag(assertion, '_isLastPollAttempt', true)
`
150
``
`-
}
`
151
``
-
152
148
`try {
`
153
``
`-
executionPhase = 'fn'
`
154
``
`-
const obj = await fn()
`
``
149
`+
const fnResult = await raceWith(
`
``
150
`+
Promise.resolve().then(() => fn({ signal: timeoutController.signal })),
`
``
151
`+
timeoutPromise,
`
``
152
`+
)
`
``
153
`+
if (!fnResult.ok) {
`
``
154
`` +
lastError ??= new Error(expect.poll() function didn't resolve in time.)
``
``
155
`+
break
`
``
156
`+
}
`
``
157
`+
const obj = fnResult.value
`
155
158
`chai.util.flag(assertion, 'object', obj)
`
156
159
``
157
``
`-
executionPhase = 'assertion'
`
158
``
`-
const output = await assertionFunction.call(assertion, ...args)
`
``
160
`+
const assertionResult = await raceWith(
`
``
161
`+
Promise.resolve().then(() => assertionFunction.apply(assertion, args)),
`
``
162
`+
timeoutPromise,
`
``
163
`+
)
`
``
164
`+
if (!assertionResult.ok) {
`
``
165
`` +
lastError ??= new Error(expect.poll() assertion didn't resolve in time.)
``
``
166
`+
break
`
``
167
`+
}
`
``
168
`+
const output = assertionResult.value
`
159
169
`await onSettled?.({ assertion, status: 'pass' })
`
160
170
``
161
171
`return output
`
162
172
`}
`
163
173
`catch (err) {
`
164
``
`-
if (isLastAttempt || (executionPhase === 'assertion' && chai.util.flag(assertion, '_poll.assert_once'))) {
`
165
``
`-
await onSettled?.({ assertion, status: 'fail' })
`
166
``
`-
throwWithCause(err, STACK_TRACE_ERROR)
`
``
174
`+
lastError = err
`
``
175
`+
// no retry for toMatchScreenshot since
`
``
176
`+
// it owns retry/stability after the first element resolution
`
``
177
`+
if (key === 'toMatchScreenshot') {
`
``
178
`+
break
`
``
179
`+
}
`
``
180
`+
const result = await raceWith(
`
``
181
`+
delay(interval, setTimeout),
`
``
182
`+
timeoutPromise,
`
``
183
`+
)
`
``
184
`+
if (!result.ok) {
`
``
185
`+
break
`
167
186
`}
`
168
``
-
169
``
`-
await delay(interval, setTimeout)
`
170
187
`if (vi.isFakeTimers()) {
`
171
188
`vi.advanceTimersByTime(interval)
`
172
189
`}
`
`@@ -176,6 +193,10 @@ export function createExpectPoll(expect: ExpectStatic): ExpectStatic['poll'] {
`
176
193
`finally {
`
177
194
`clearTimeout(timerId)
`
178
195
`}
`
``
196
`+
if (lastError) {
`
``
197
`+
await onSettled?.({ assertion, status: 'fail' })
`
``
198
`+
throwWithCause(lastError, STACK_TRACE_ERROR)
`
``
199
`+
}
`
179
200
`}
`
180
201
`let awaited = false
`
181
202
`test.onFinished ??= []
`
`@@ -221,3 +242,17 @@ function copyStackTrace(target: Error, source: Error) {
`
221
242
`}
`
222
243
`return target
`
223
244
`}
`
``
245
+
``
246
`+
function raceWith<A, B>(
`
``
247
`+
promise: Promise,
`
``
248
`+
other?: Promise,
`
``
249
`+
): Promise<{ ok: true; value: A } | { ok: false; value: B }> {
`
``
250
`+
const left = promise.then(value => ({ ok: true as const, value }))
`
``
251
`+
if (!other) {
`
``
252
`+
return left
`
``
253
`+
}
`
``
254
`+
return Promise.race([
`
``
255
`+
left,
`
``
256
`+
other.then(value => ({ ok: false as const, value })),
`
``
257
`+
])
`
``
258
`+
}
`