···2929 return elementOrLocator.elements() as unknown as HTMLElement
3030 }
31313232+ if (name === 'toMatchScreenshot' && !chai.util.flag(this, '_poll.assert_once')) {
3333+ // `toMatchScreenshot` should only run once after the element resolves
3434+ chai.util.flag(this, '_poll.assert_once', true)
3535+ }
3636+3237 // element selector uses prettyDOM under the hood, which is an expensive call
3338 // that should not be called on each failed locator attempt to avoid memory leak:
3439 // https://github.com/vitest-dev/vitest/issues/7139
+59-38
packages/vitest/src/integrations/chai/poll.ts
···11import type { Assertion, ExpectStatic } from '@vitest/expect'
22import type { Test } from '@vitest/runner'
33import { chai } from '@vitest/expect'
44-import { getSafeTimers } from '@vitest/utils/timers'
44+import { delay, getSafeTimers } from '@vitest/utils/timers'
55import { getWorkerState } from '../../runtime/utils'
6677// these matchers are not supported because they don't make sense with poll
···2525 // rejects,
2626 // resolves
2727]
2828+2929+/**
3030+ * Attaches a `cause` property to the error if missing, copies the stack trace from the source, and throws.
3131+ *
3232+ * @param error - The error to throw
3333+ * @param source - Error to copy the stack trace from
3434+ *
3535+ * @throws Always throws the provided error with an amended stack trace
3636+ */
3737+function throwWithCause(error: any, source: Error) {
3838+ if (error.cause == null) {
3939+ error.cause = new Error('Matcher did not succeed in time.')
4040+ }
4141+4242+ throw copyStackTrace(
4343+ error,
4444+ source,
4545+ )
4646+}
28472948export function createExpectPoll(expect: ExpectStatic): ExpectStatic['poll'] {
3049 return function poll(fn, options = {}) {
···64836584 return function (this: any, ...args: any[]) {
6685 const STACK_TRACE_ERROR = new Error('STACK_TRACE_ERROR')
6767- const promise = () => new Promise<void>((resolve, reject) => {
6868- let intervalId: any
6969- let timeoutId: any
7070- let lastError: any
8686+ const promise = async () => {
7187 const { setTimeout, clearTimeout } = getSafeTimers()
7272- const check = async () => {
7373- try {
7474- chai.util.flag(assertion, '_name', key)
7575- const obj = await fn()
7676- chai.util.flag(assertion, 'object', obj)
7777- resolve(await assertionFunction.call(assertion, ...args))
7878- clearTimeout(intervalId)
7979- clearTimeout(timeoutId)
8080- }
8181- catch (err) {
8282- lastError = err
8383- if (!chai.util.flag(assertion, '_isLastPollAttempt')) {
8484- intervalId = setTimeout(check, interval)
8888+8989+ let executionPhase: 'fn' | 'assertion' = 'fn'
9090+ let hasTimedOut = false
9191+9292+ const timerId = setTimeout(() => {
9393+ hasTimedOut = true
9494+ }, timeout)
9595+9696+ chai.util.flag(assertion, '_name', key)
9797+9898+ try {
9999+ while (true) {
100100+ const isLastAttempt = hasTimedOut
101101+102102+ if (isLastAttempt) {
103103+ chai.util.flag(assertion, '_isLastPollAttempt', true)
104104+ }
105105+106106+ try {
107107+ executionPhase = 'fn'
108108+ const obj = await fn()
109109+ chai.util.flag(assertion, 'object', obj)
110110+111111+ executionPhase = 'assertion'
112112+ const output = await assertionFunction.call(assertion, ...args)
113113+114114+ return output
115115+ }
116116+ catch (err) {
117117+ if (isLastAttempt || (executionPhase === 'assertion' && chai.util.flag(assertion, '_poll.assert_once'))) {
118118+ throwWithCause(err, STACK_TRACE_ERROR)
119119+ }
120120+121121+ await delay(interval, setTimeout)
85122 }
86123 }
87124 }
8888- timeoutId = setTimeout(() => {
8989- clearTimeout(intervalId)
9090- chai.util.flag(assertion, '_isLastPollAttempt', true)
9191- const rejectWithCause = (error: any) => {
9292- if (error.cause == null) {
9393- error.cause = new Error('Matcher did not succeed in time.')
9494- }
9595- reject(
9696- copyStackTrace(
9797- error,
9898- STACK_TRACE_ERROR,
9999- ),
100100- )
101101- }
102102- check()
103103- .then(() => rejectWithCause(lastError))
104104- .catch(e => rejectWithCause(e))
105105- }, timeout)
106106- check()
107107- })
125125+ finally {
126126+ clearTimeout(timerId)
127127+ }
128128+ }
108129 let awaited = false
109130 test.onFinished ??= []
110131 test.onFinished.push(() => {