Skip to content

Commit e7c5e1f

Browse files
authored
fix(runner): fix fixture cleanup when test times out (#4679)
1 parent 640881a commit e7c5e1f

File tree

4 files changed

+54
-41
lines changed

4 files changed

+54
-41
lines changed

packages/runner/src/fixture.ts

+27-38
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createDefer } from '@vitest/utils'
12
import { getFixture } from './map'
23
import type { TestContext } from './types'
34

@@ -78,49 +79,37 @@ export function withFixtures(fn: Function, testContext?: TestContext) {
7879
if (!pendingFixtures.length)
7980
return fn(context)
8081

81-
let cursor = 0
82-
83-
return new Promise((resolve, reject) => {
84-
async function use(fixtureValue: any) {
85-
const fixture = pendingFixtures[cursor++]
86-
context![fixture.prop] = fixtureValue
87-
88-
if (!fixtureValueMap.has(fixture)) {
89-
fixtureValueMap.set(fixture, fixtureValue)
90-
cleanupFnArray.unshift(() => {
91-
fixtureValueMap.delete(fixture)
92-
})
93-
}
94-
95-
if (cursor < pendingFixtures.length) {
96-
await next()
82+
async function resolveFixtures() {
83+
for (const fixture of pendingFixtures) {
84+
// fixture could be already initialized during "before" hook
85+
if (fixtureValueMap.has(fixture))
86+
continue
87+
88+
let resolvedValue: unknown
89+
if (fixture.isFn) {
90+
// wait for `use` call to extract fixture value
91+
const useFnArgPromise = createDefer()
92+
fixture.value(context, async (useFnArg: unknown) => {
93+
useFnArgPromise.resolve(useFnArg)
94+
// suspend fixture teardown until cleanup
95+
const teardownPromise = createDefer<void>()
96+
cleanupFnArray.push(teardownPromise.resolve)
97+
await teardownPromise
98+
}).catch(useFnArgPromise.reject) // treat fixture function error (until `use` call) as test failure
99+
resolvedValue = await useFnArgPromise
97100
}
98101
else {
99-
// When all fixtures setup, call the test function
100-
try {
101-
resolve(await fn(context))
102-
}
103-
catch (err) {
104-
reject(err)
105-
}
106-
return new Promise<void>((resolve) => {
107-
cleanupFnArray.push(resolve)
108-
})
102+
resolvedValue = fixture.value
109103
}
104+
context![fixture.prop] = resolvedValue
105+
fixtureValueMap.set(fixture, resolvedValue)
106+
cleanupFnArray.unshift(() => {
107+
fixtureValueMap.delete(fixture)
108+
})
110109
}
110+
}
111111

112-
async function next() {
113-
const fixture = pendingFixtures[cursor]
114-
const { isFn, value } = fixture
115-
if (fixtureValueMap.has(fixture))
116-
return use(fixtureValueMap.get(fixture))
117-
else
118-
return isFn ? value(context, use) : use(value)
119-
}
120-
121-
const setupFixturePromise = next().catch(reject)
122-
cleanupFnArray.unshift(() => setupFixturePromise)
123-
})
112+
return resolveFixtures().then(() => fn(context))
124113
}
125114
}
126115

test/core/test/test-extend.test.ts

+24
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,28 @@ describe('test.extend()', () => {
177177
})
178178
})
179179

180+
describe('fixture only in beforeEach', () => {
181+
beforeEach<Fixtures>(({ todoList }) => {
182+
expect(todoList).toEqual([1, 2, 3])
183+
expect(todoFn).toBeCalledTimes(1)
184+
})
185+
186+
myTest('no fixture in test', () => {
187+
expect(todoFn).toBeCalledTimes(1)
188+
})
189+
})
190+
191+
describe('fixture only in afterEach', () => {
192+
afterEach<Fixtures>(({ todoList }) => {
193+
expect(todoList).toEqual([1, 2, 3])
194+
expect(todoFn).toBeCalledTimes(1)
195+
})
196+
197+
myTest('no fixture in test', () => {
198+
expect(todoFn).toBeCalledTimes(0)
199+
})
200+
})
201+
180202
describe('fixture call times', () => {
181203
const apiFn = vi.fn(() => true)
182204
const serviceFn = vi.fn(() => true)
@@ -203,6 +225,8 @@ describe('test.extend()', () => {
203225
beforeEach<APIFixture>(({ api, service }) => {
204226
expect(api).toBe(true)
205227
expect(service).toBe(true)
228+
expect(apiFn).toBeCalledTimes(1)
229+
expect(serviceFn).toBeCalledTimes(1)
206230
})
207231

208232
testAPI('Should init 1 time', ({ api }) => {

test/fails/fixtures/test-extend/fixture-error.test.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ describe('error thrown in test fixtures', () => {
3939
myTest('fixture errors', ({ a }) => {})
4040
})
4141

42-
// TODO: enable when #4669 is fixed
43-
describe.skip('correctly fails when test times out', () => {
42+
describe('correctly fails when test times out', () => {
4443
const myTest = test.extend<{ a: number }>({
4544
a: async ({}, use) => {
4645
await use(2)

test/fails/test/__snapshots__/runner.test.ts.snap

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ TypeError: failure"
4343
exports[`should fail test-extend/circular-dependency.test.ts > test-extend/circular-dependency.test.ts 1`] = `"Error: Circular fixture dependency detected: a <- b <- a"`;
4444
4545
exports[`should fail test-extend/fixture-error.test.ts > test-extend/fixture-error.test.ts 1`] = `
46-
"Error: Error thrown in test fixture
46+
"Error: Test timed out in 20ms.
47+
Error: Error thrown in test fixture
4748
Error: Error thrown in afterEach fixture
4849
Error: Error thrown in beforeEach fixture"
4950
`;

0 commit comments

Comments
 (0)