11import type { Assertion , ExpectStatic } from '@vitest/expect'
22import type { Test } from '@vitest/runner'
33import { chai } from '@vitest/expect'
4- import { getSafeTimers } from '@vitest/utils/timers'
4+ import { delay , getSafeTimers } from '@vitest/utils/timers'
55import { getWorkerState } from '../../runtime/utils'
66
77// these matchers are not supported because they don't make sense with poll
@@ -26,6 +26,25 @@ const unsupported = [
2626 // resolves
2727]
2828
29+ /**
30+ * Attaches a `cause` property to the error if missing, copies the stack trace from the source, and throws.
31+ *
32+ * @param error - The error to throw
33+ * @param source - Error to copy the stack trace from
34+ *
35+ * @throws Always throws the provided error with an amended stack trace
36+ */
37+ function throwWithCause ( error : any , source : Error ) {
38+ if ( error . cause == null ) {
39+ error . cause = new Error ( 'Matcher did not succeed in time.' )
40+ }
41+
42+ throw copyStackTrace (
43+ error ,
44+ source ,
45+ )
46+ }
47+
2948export function createExpectPoll ( expect : ExpectStatic ) : ExpectStatic [ 'poll' ] {
3049 return function poll ( fn , options = { } ) {
3150 const state = getWorkerState ( )
@@ -64,47 +83,49 @@ export function createExpectPoll(expect: ExpectStatic): ExpectStatic['poll'] {
6483
6584 return function ( this : any , ...args : any [ ] ) {
6685 const STACK_TRACE_ERROR = new Error ( 'STACK_TRACE_ERROR' )
67- const promise = ( ) => new Promise < void > ( ( resolve , reject ) => {
68- let intervalId : any
69- let timeoutId : any
70- let lastError : any
86+ const promise = async ( ) => {
7187 const { setTimeout, clearTimeout } = getSafeTimers ( )
72- const check = async ( ) => {
73- try {
74- chai . util . flag ( assertion , '_name' , key )
75- const obj = await fn ( )
76- chai . util . flag ( assertion , 'object' , obj )
77- resolve ( await assertionFunction . call ( assertion , ...args ) )
78- clearTimeout ( intervalId )
79- clearTimeout ( timeoutId )
80- }
81- catch ( err ) {
82- lastError = err
83- if ( ! chai . util . flag ( assertion , '_isLastPollAttempt' ) ) {
84- intervalId = setTimeout ( check , interval )
88+
89+ let executionPhase : 'fn' | 'assertion' = 'fn'
90+ let hasTimedOut = false
91+
92+ const timerId = setTimeout ( ( ) => {
93+ hasTimedOut = true
94+ } , timeout )
95+
96+ chai . util . flag ( assertion , '_name' , key )
97+
98+ try {
99+ while ( true ) {
100+ const isLastAttempt = hasTimedOut
101+
102+ if ( isLastAttempt ) {
103+ chai . util . flag ( assertion , '_isLastPollAttempt' , true )
85104 }
86- }
87- }
88- timeoutId = setTimeout ( ( ) => {
89- clearTimeout ( intervalId )
90- chai . util . flag ( assertion , '_isLastPollAttempt' , true )
91- const rejectWithCause = ( error : any ) => {
92- if ( error . cause == null ) {
93- error . cause = new Error ( 'Matcher did not succeed in time.' )
105+
106+ try {
107+ executionPhase = 'fn'
108+ const obj = await fn ( )
109+ chai . util . flag ( assertion , 'object' , obj )
110+
111+ executionPhase = 'assertion'
112+ const output = await assertionFunction . call ( assertion , ...args )
113+
114+ return output
115+ }
116+ catch ( err ) {
117+ if ( isLastAttempt || ( executionPhase === 'assertion' && chai . util . flag ( assertion , '_poll.assert_once' ) ) ) {
118+ throwWithCause ( err , STACK_TRACE_ERROR )
119+ }
120+
121+ await delay ( interval , setTimeout )
94122 }
95- reject (
96- copyStackTrace (
97- error ,
98- STACK_TRACE_ERROR ,
99- ) ,
100- )
101123 }
102- check ( )
103- . then ( ( ) => rejectWithCause ( lastError ) )
104- . catch ( e => rejectWithCause ( e ) )
105- } , timeout )
106- check ( )
107- } )
124+ }
125+ finally {
126+ clearTimeout ( timerId )
127+ }
128+ }
108129 let awaited = false
109130 test . onFinished ??= [ ]
110131 test . onFinished . push ( ( ) => {
0 commit comments