Skip to content

Commit 730b29e

Browse files
Dunqingsheremet-va
authored andcommitted
fix(runner): the fixture of test.extend should be init once time in all test (#4168)
1 parent ff9473a commit 730b29e

File tree

3 files changed

+215
-40
lines changed

3 files changed

+215
-40
lines changed

packages/runner/src/fixture.ts

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,33 @@ export function mergeContextFixtures(fixtures: Record<string, any>, context: { f
4444
return context
4545
}
4646

47+
const fixtureValueMap = new Map<FixtureItem, any>()
48+
const fixtureCleanupFnMap = new Map<string, Array<() => void | Promise<void>>>()
49+
50+
export async function callFixtureCleanup(id: string) {
51+
const cleanupFnArray = fixtureCleanupFnMap.get(id)
52+
if (!cleanupFnArray)
53+
return
54+
55+
for (const cleanup of cleanupFnArray.reverse())
56+
await cleanup()
57+
58+
fixtureCleanupFnMap.delete(id)
59+
}
60+
4761
export function withFixtures(fn: Function, testContext?: TestContext) {
4862
return (hookContext?: TestContext) => {
4963
const context: TestContext & { [key: string]: any } | undefined = hookContext || testContext
5064

5165
if (!context)
5266
return fn({})
5367

68+
let cleanupFnArray = fixtureCleanupFnMap.get(context.task.suite.id)!
69+
if (!cleanupFnArray) {
70+
cleanupFnArray = []
71+
fixtureCleanupFnMap.set(context.task.suite.id, cleanupFnArray)
72+
}
73+
5474
const fixtures = getFixture(context)
5575
if (!fixtures?.length)
5676
return fn(context)
@@ -63,21 +83,47 @@ export function withFixtures(fn: Function, testContext?: TestContext) {
6383
const pendingFixtures = resolveDeps(usedFixtures)
6484
let cursor = 0
6585

66-
async function use(fixtureValue: any) {
67-
const { prop } = pendingFixtures[cursor++]
68-
context![prop] = fixtureValue
69-
70-
if (cursor < pendingFixtures.length)
71-
await next()
72-
else await fn(context)
73-
}
86+
return new Promise((resolve, reject) => {
87+
async function use(fixtureValue: any) {
88+
const fixture = pendingFixtures[cursor++]
89+
context![fixture.prop] = fixtureValue
90+
91+
if (!fixtureValueMap.has(fixture)) {
92+
fixtureValueMap.set(fixture, fixtureValue)
93+
cleanupFnArray.unshift(() => {
94+
fixtureValueMap.delete(fixture)
95+
})
96+
}
97+
98+
if (cursor < pendingFixtures.length) {
99+
await next()
100+
}
101+
else {
102+
// When all fixtures setup, call the test function
103+
try {
104+
resolve(await fn(context))
105+
}
106+
catch (err) {
107+
reject(err)
108+
}
109+
return new Promise<void>((resolve) => {
110+
cleanupFnArray.push(resolve)
111+
})
112+
}
113+
}
74114

75-
async function next() {
76-
const { value } = pendingFixtures[cursor]
77-
typeof value === 'function' ? await value(context, use) : await use(value)
78-
}
115+
async function next() {
116+
const fixture = pendingFixtures[cursor]
117+
const { isFn, value } = fixture
118+
if (fixtureValueMap.has(fixture))
119+
return use(fixtureValueMap.get(fixture))
120+
else
121+
return isFn ? value(context, use) : use(value)
122+
}
79123

80-
return next()
124+
const setupFixturePromise = next()
125+
cleanupFnArray.unshift(() => setupFixturePromise)
126+
})
81127
}
82128
}
83129

packages/runner/src/run.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { collectTests } from './collect'
1010
import { setCurrentTest } from './test-state'
1111
import { hasFailed, hasTests } from './utils/tasks'
1212
import { PendingError } from './errors'
13+
import { callFixtureCleanup } from './fixture'
1314

1415
const now = Date.now
1516

@@ -321,6 +322,7 @@ export async function runSuite(suite: Suite, runner: VitestRunner) {
321322
}
322323

323324
try {
325+
await callFixtureCleanup(suite.id)
324326
await callSuiteHook(suite, suite, 'afterAll', runner, [suite])
325327
await callCleanupHooks(beforeAllCleanups)
326328
}

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

Lines changed: 154 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable prefer-rest-params */
22
/* eslint-disable no-empty-pattern */
3-
import { describe, expect, expectTypeOf, test, vi } from 'vitest'
3+
import { afterAll, afterEach, beforeEach, describe, expect, expectTypeOf, test, vi } from 'vitest'
44

55
interface Fixtures {
66
todoList: number[]
@@ -38,39 +38,34 @@ const myTest = test
3838
})
3939

4040
describe('test.extend()', () => {
41-
myTest('todoList and doneList', ({ todoList, doneList, archiveList }) => {
42-
expect(todoFn).toBeCalledTimes(1)
43-
expect(doneFn).toBeCalledTimes(1)
44-
45-
expectTypeOf(todoList).toEqualTypeOf<number[]>()
46-
expectTypeOf(doneList).toEqualTypeOf<number[]>()
47-
expectTypeOf(doneList).toEqualTypeOf<number[]>()
41+
describe('basic', () => {
42+
myTest('todoList and doneList', ({ todoList, doneList, archiveList }) => {
43+
expect(todoFn).toBeCalledTimes(1)
44+
expect(doneFn).toBeCalledTimes(1)
4845

49-
expect(todoList).toEqual([1, 2, 3])
50-
expect(doneList).toEqual([])
51-
expect(archiveList).toEqual([])
46+
expectTypeOf(todoList).toEqualTypeOf<number[]>()
47+
expectTypeOf(doneList).toEqualTypeOf<number[]>()
48+
expectTypeOf(doneList).toEqualTypeOf<number[]>()
5249

53-
doneList.push(todoList.shift()!)
54-
expect(todoList).toEqual([2, 3])
55-
expect(doneList).toEqual([1])
50+
expect(todoList).toEqual([1, 2, 3])
51+
expect(doneList).toEqual([])
52+
expect(archiveList).toEqual([])
5653

57-
doneList.push(todoList.shift()!)
58-
expect(todoList).toEqual([3])
59-
expect(doneList).toEqual([1, 2])
54+
doneList.push(todoList.shift()!)
55+
expect(todoList).toEqual([2, 3])
56+
expect(doneList).toEqual([1])
6057

61-
archiveList.push(todoList.shift()!)
62-
expect(todoList).toEqual([])
63-
expect(archiveList).toEqual([3])
58+
doneList.push(todoList.shift()!)
59+
expect(todoList).toEqual([3])
60+
expect(doneList).toEqual([1, 2])
6461

65-
archiveList.pop()
66-
})
62+
archiveList.push(todoList.shift()!)
63+
expect(todoList).toEqual([])
64+
expect(archiveList).toEqual([3])
6765

68-
myTest('should called cleanup functions', ({ todoList, doneList, archiveList }) => {
69-
expect(todoList).toEqual([1, 2, 3])
70-
expect(doneList).toEqual([])
71-
expect(archiveList).toEqual([])
66+
archiveList.pop()
67+
})
7268
})
73-
7469
describe('smartly init fixtures', () => {
7570
myTest('should not init any fixtures', function () {
7671
expect(todoFn).not.toBeCalled()
@@ -150,4 +145,136 @@ describe('test.extend()', () => {
150145
expect(archive).toEqual([])
151146
})
152147
})
148+
149+
describe('fixture call times', () => {
150+
const apiFn = vi.fn(() => true)
151+
const serviceFn = vi.fn(() => true)
152+
const teardownFn = vi.fn()
153+
154+
interface APIFixture {
155+
api: boolean
156+
service: boolean
157+
}
158+
159+
const testAPI = test.extend<APIFixture>({
160+
api: async ({}, use) => {
161+
await use(apiFn())
162+
apiFn.mockClear()
163+
teardownFn()
164+
},
165+
service: async ({}, use) => {
166+
await use(serviceFn())
167+
serviceFn.mockClear()
168+
teardownFn()
169+
},
170+
})
171+
172+
beforeEach<APIFixture>(({ api, service }) => {
173+
expect(api).toBe(true)
174+
expect(service).toBe(true)
175+
})
176+
177+
testAPI('Should init1 time', ({ api }) => {
178+
expect(api).toBe(true)
179+
expect(apiFn).toBeCalledTimes(1)
180+
})
181+
182+
testAPI('Should init 1 time has multiple fixture', ({ api, service }) => {
183+
expect(api).toBe(true)
184+
expect(service).toBe(true)
185+
expect(serviceFn).toBeCalledTimes(1)
186+
expect(apiFn).toBeCalledTimes(1)
187+
})
188+
189+
afterEach<APIFixture>(({ api, service }) => {
190+
expect(api).toBe(true)
191+
expect(service).toBe(true)
192+
expect(apiFn).toBeCalledTimes(1)
193+
expect(serviceFn).toBeCalledTimes(1)
194+
})
195+
196+
afterAll(() => {
197+
expect(serviceFn).toBeCalledTimes(0)
198+
expect(apiFn).toBeCalledTimes(0)
199+
expect(teardownFn).toBeCalledTimes(2)
200+
})
201+
})
202+
203+
describe('fixture in nested describe', () => {
204+
interface Fixture {
205+
foo: number
206+
bar: number
207+
}
208+
209+
const fooFn = vi.fn(() => 0)
210+
const fooCleanup = vi.fn()
211+
212+
const barFn = vi.fn(() => 0)
213+
const barCleanup = vi.fn()
214+
215+
const nestedTest = test.extend<Fixture>({
216+
async foo({}, use) {
217+
await use(fooFn())
218+
fooCleanup()
219+
},
220+
async bar({}, use) {
221+
await use(barFn())
222+
barCleanup()
223+
},
224+
})
225+
226+
beforeEach<Fixture>(({ foo }) => {
227+
expect(foo).toBe(0)
228+
})
229+
230+
nestedTest('should only initialize foo', ({ foo }) => {
231+
expect(foo).toBe(0)
232+
expect(fooFn).toBeCalledTimes(1)
233+
expect(barFn).toBeCalledTimes(0)
234+
})
235+
236+
describe('level 2, using both foo and bar together', () => {
237+
beforeEach<Fixture>(({ foo, bar }) => {
238+
expect(foo).toBe(0)
239+
expect(bar).toBe(0)
240+
})
241+
242+
nestedTest('should only initialize bar', ({ foo, bar }) => {
243+
expect(foo).toBe(0)
244+
expect(bar).toBe(0)
245+
expect(fooFn).toBeCalledTimes(1)
246+
expect(barFn).toBeCalledTimes(1)
247+
})
248+
249+
afterEach<Fixture>(({ foo, bar }) => {
250+
expect(foo).toBe(0)
251+
expect(bar).toBe(0)
252+
})
253+
254+
afterAll(() => {
255+
// foo setup in outside describe
256+
// cleanup also called in outside describe
257+
expect(fooCleanup).toHaveBeenCalledTimes(0)
258+
// bar setup in inside describe
259+
// cleanup also called in inside describe
260+
expect(barCleanup).toHaveBeenCalledTimes(1)
261+
})
262+
})
263+
264+
nestedTest('level 2 will not call foo cleanup', ({ foo }) => {
265+
expect(foo).toBe(0)
266+
expect(fooFn).toBeCalledTimes(1)
267+
})
268+
269+
afterEach<Fixture>(({ foo }) => {
270+
expect(foo).toBe(0)
271+
})
272+
273+
afterAll(() => {
274+
// foo setup in this describe
275+
// cleanup also called in this describe
276+
expect(fooCleanup).toHaveBeenCalledTimes(1)
277+
expect(barCleanup).toHaveBeenCalledTimes(1)
278+
})
279+
})
153280
})

0 commit comments

Comments
 (0)