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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy