Skip to content

Commit 847d322

Browse files
authored
fix(browser): only use locator.element on last expect.element attempt (fix #7139) (#7152)
1 parent c31472d commit 847d322

File tree

5 files changed

+108
-16
lines changed

5 files changed

+108
-16
lines changed

packages/browser/src/client/tester/expect-element.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,26 @@ export async function setupExpectDom() {
1818

1919
const isNot = chai.util.flag(this, 'negate') as boolean
2020
const name = chai.util.flag(this, '_name') as string
21+
// element selector uses prettyDOM under the hood, which is an expensive call
22+
// that should not be called on each failed locator attempt to avoid memory leak:
23+
// https://github.com/vitest-dev/vitest/issues/7139
24+
const isLastPollAttempt = chai.util.flag(this, '_isLastPollAttempt')
2125
// special case for `toBeInTheDocument` matcher
2226
if (isNot && name === 'toBeInTheDocument') {
2327
return elementOrLocator.query()
2428
}
25-
return elementOrLocator.element()
29+
30+
if (isLastPollAttempt) {
31+
return elementOrLocator.element()
32+
}
33+
34+
const result = elementOrLocator.query()
35+
36+
if (!result) {
37+
throw new Error(`Cannot find element with locator: ${JSON.stringify(elementOrLocator)}`)
38+
}
39+
40+
return result
2641
}, options)
2742
}
2843
}

packages/vitest/src/integrations/chai/poll.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -66,19 +66,9 @@ export function createExpectPoll(expect: ExpectStatic): ExpectStatic['poll'] {
6666
const STACK_TRACE_ERROR = new Error('STACK_TRACE_ERROR')
6767
const promise = () => new Promise<void>((resolve, reject) => {
6868
let intervalId: any
69+
let timeoutId: any
6970
let lastError: any
7071
const { setTimeout, clearTimeout } = getSafeTimers()
71-
const timeoutId = setTimeout(() => {
72-
clearTimeout(intervalId)
73-
reject(
74-
copyStackTrace(
75-
new Error(`Matcher did not succeed in ${timeout}ms`, {
76-
cause: lastError,
77-
}),
78-
STACK_TRACE_ERROR,
79-
),
80-
)
81-
}, timeout)
8272
const check = async () => {
8373
try {
8474
chai.util.flag(assertion, '_name', key)
@@ -90,9 +80,28 @@ export function createExpectPoll(expect: ExpectStatic): ExpectStatic['poll'] {
9080
}
9181
catch (err) {
9282
lastError = err
93-
intervalId = setTimeout(check, interval)
83+
if (!chai.util.flag(assertion, '_isLastPollAttempt')) {
84+
intervalId = setTimeout(check, interval)
85+
}
9486
}
9587
}
88+
timeoutId = setTimeout(() => {
89+
clearTimeout(intervalId)
90+
chai.util.flag(assertion, '_isLastPollAttempt', true)
91+
const rejectWithCause = (cause: any) => {
92+
reject(
93+
copyStackTrace(
94+
new Error(`Matcher did not succeed in ${timeout}ms`, {
95+
cause,
96+
}),
97+
STACK_TRACE_ERROR,
98+
),
99+
)
100+
}
101+
check()
102+
.then(() => rejectWithCause(lastError))
103+
.catch(e => rejectWithCause(e))
104+
}, timeout)
96105
check()
97106
})
98107
let awaited = false

test/browser/specs/runner.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@ describe('running browser tests', async () => {
2828
console.error(stderr)
2929
})
3030

31-
expect(browserResultJson.testResults).toHaveLength(19 * instances.length)
32-
expect(passedTests).toHaveLength(17 * instances.length)
31+
// This should match the number of actual tests from browser.json
32+
// if you added new tests, these assertion will fail and you should
33+
// update the numbers
34+
expect(browserResultJson.testResults).toHaveLength(20 * instances.length)
35+
expect(passedTests).toHaveLength(18 * instances.length)
3336
expect(failedTests).toHaveLength(2 * instances.length)
3437

3538
expect(stderr).not.toContain('optimized dependencies changed')
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { page } from '@vitest/browser/context'
2+
import { expect, test, vi } from 'vitest'
3+
4+
// element selector uses prettyDOM under the hood, which is an expensive call
5+
// that should not be called on each failed locator attempt to avoid memory leak:
6+
// https://github.com/vitest-dev/vitest/issues/7139
7+
test('should only use element selector on last expect.element attempt', async () => {
8+
const div = document.createElement('div')
9+
const spanString = '<span>test</span>'
10+
div.innerHTML = spanString
11+
document.body.append(div)
12+
13+
const locator = page.getByText('non-existent')
14+
const locatorElementMock = vi.spyOn(locator, 'element')
15+
const locatorQueryMock = vi.spyOn(locator, 'query')
16+
17+
try {
18+
await expect.element(locator, { timeout: 500, interval: 100 }).toBeInTheDocument()
19+
}
20+
catch {}
21+
22+
expect(locatorElementMock).toBeCalledTimes(1)
23+
expect(locatorElementMock).toHaveBeenCalledAfter(locatorQueryMock)
24+
})

test/core/test/expect-poll.test.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, test, vi } from 'vitest'
1+
import { chai, expect, test, vi } from 'vitest'
22

33
test('simple usage', async () => {
44
await expect.poll(() => false).toBe(false)
@@ -106,3 +106,44 @@ test('toBeDefined', async () => {
106106
}),
107107
}))
108108
})
109+
110+
test('should set _isLastPollAttempt flag on last call', async () => {
111+
const fn = vi.fn(function (this: object) {
112+
return chai.util.flag(this, '_isLastPollAttempt')
113+
})
114+
await expect(async () => {
115+
await expect.poll(fn, { interval: 100, timeout: 500 }).toBe(false)
116+
}).rejects.toThrowError()
117+
fn.mock.results.forEach((result, index) => {
118+
const isLastCall = index === fn.mock.results.length - 1
119+
expect(result.value).toBe(isLastCall ? true : undefined)
120+
})
121+
})
122+
123+
test('should handle success on last attempt', async () => {
124+
const fn = vi.fn(function (this: object) {
125+
if (chai.util.flag(this, '_isLastPollAttempt')) {
126+
return 1
127+
}
128+
return undefined
129+
})
130+
await expect.poll(fn, { interval: 100, timeout: 500 }).toBe(1)
131+
})
132+
133+
test('should handle failure on last attempt', async () => {
134+
const fn = vi.fn(function (this: object) {
135+
if (chai.util.flag(this, '_isLastPollAttempt')) {
136+
return 3
137+
}
138+
return 2
139+
})
140+
await expect(async () => {
141+
await expect.poll(fn, { interval: 10, timeout: 100 }).toBe(1)
142+
}).rejects.toThrowError(expect.objectContaining({
143+
message: 'Matcher did not succeed in 100ms',
144+
cause: expect.objectContaining({
145+
// makes sure cause message reflects the last attempt value
146+
message: 'expected 3 to be 1 // Object.is equality',
147+
}),
148+
}))
149+
})

0 commit comments

Comments
 (0)