Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(runner): the fixture of test.extend should be init once time in all test #4168

Merged
merged 4 commits into from Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
72 changes: 59 additions & 13 deletions packages/runner/src/fixture.ts
Expand Up @@ -44,13 +44,33 @@ export function mergeContextFixtures(fixtures: Record<string, any>, context: { f
return context
}

const fixtureValueMap = new Map<FixtureItem, any>()
const fixtureCleanupFnMap = new Map<string, Array<() => void | Promise<void>>>()

export async function callFixtureCleanup(id: string) {
const cleanupFnArray = fixtureCleanupFnMap.get(id)
if (!cleanupFnArray)
return

for (const cleanup of cleanupFnArray.reverse())
await cleanup()

fixtureCleanupFnMap.delete(id)
}

export function withFixtures(fn: Function, testContext?: TestContext) {
return (hookContext?: TestContext) => {
const context: TestContext & { [key: string]: any } | undefined = hookContext || testContext

if (!context)
return fn({})

let cleanupFnArray = fixtureCleanupFnMap.get(context.task.suite.id)!
if (!cleanupFnArray) {
cleanupFnArray = []
fixtureCleanupFnMap.set(context.task.suite.id, cleanupFnArray)
}

const fixtures = getFixture(context)
if (!fixtures?.length)
return fn(context)
Expand All @@ -63,21 +83,47 @@ export function withFixtures(fn: Function, testContext?: TestContext) {
const pendingFixtures = resolveDeps(usedFixtures)
let cursor = 0

async function use(fixtureValue: any) {
const { prop } = pendingFixtures[cursor++]
context![prop] = fixtureValue

if (cursor < pendingFixtures.length)
await next()
else await fn(context)
}
return new Promise((resolve, reject) => {
async function use(fixtureValue: any) {
const fixture = pendingFixtures[cursor++]
context![fixture.prop] = fixtureValue

if (!fixtureValueMap.has(fixture)) {
fixtureValueMap.set(fixture, fixtureValue)
cleanupFnArray.unshift(() => {
fixtureValueMap.delete(fixture)
})
}

if (cursor < pendingFixtures.length) {
await next()
}
else {
// When all fixtures setup, call the test function
try {
resolve(await fn(context))
}
catch (err) {
reject(err)
}
return new Promise<void>((resolve) => {
cleanupFnArray.push(resolve)
})
}
}

async function next() {
const { value } = pendingFixtures[cursor]
typeof value === 'function' ? await value(context, use) : await use(value)
}
async function next() {
const fixture = pendingFixtures[cursor]
const { isFn, value } = fixture
if (fixtureValueMap.has(fixture))
return use(fixtureValueMap.get(fixture))
else
return isFn ? value(context, use) : use(value)
}

return next()
const setupFixturePromise = next()
cleanupFnArray.unshift(() => setupFixturePromise)
})
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/runner/src/run.ts
Expand Up @@ -10,6 +10,7 @@ import { collectTests } from './collect'
import { setCurrentTest } from './test-state'
import { hasFailed, hasTests } from './utils/tasks'
import { PendingError } from './errors'
import { callFixtureCleanup } from './fixture'

const now = Date.now

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

try {
await callFixtureCleanup(suite.id)
await callSuiteHook(suite, suite, 'afterAll', runner, [suite])
await callCleanupHooks(beforeAllCleanups)
}
Expand Down
181 changes: 154 additions & 27 deletions test/core/test/test-extend.test.ts
@@ -1,6 +1,6 @@
/* eslint-disable prefer-rest-params */
/* eslint-disable no-empty-pattern */
import { describe, expect, expectTypeOf, test, vi } from 'vitest'
import { afterAll, afterEach, beforeEach, describe, expect, expectTypeOf, test, vi } from 'vitest'

interface Fixtures {
todoList: number[]
Expand Down Expand Up @@ -38,39 +38,34 @@ const myTest = test
})

describe('test.extend()', () => {
myTest('todoList and doneList', ({ todoList, doneList, archiveList }) => {
expect(todoFn).toBeCalledTimes(1)
expect(doneFn).toBeCalledTimes(1)

expectTypeOf(todoList).toEqualTypeOf<number[]>()
expectTypeOf(doneList).toEqualTypeOf<number[]>()
expectTypeOf(doneList).toEqualTypeOf<number[]>()
describe('basic', () => {
myTest('todoList and doneList', ({ todoList, doneList, archiveList }) => {
expect(todoFn).toBeCalledTimes(1)
expect(doneFn).toBeCalledTimes(1)

expect(todoList).toEqual([1, 2, 3])
expect(doneList).toEqual([])
expect(archiveList).toEqual([])
expectTypeOf(todoList).toEqualTypeOf<number[]>()
expectTypeOf(doneList).toEqualTypeOf<number[]>()
expectTypeOf(doneList).toEqualTypeOf<number[]>()

doneList.push(todoList.shift()!)
expect(todoList).toEqual([2, 3])
expect(doneList).toEqual([1])
expect(todoList).toEqual([1, 2, 3])
expect(doneList).toEqual([])
expect(archiveList).toEqual([])

doneList.push(todoList.shift()!)
expect(todoList).toEqual([3])
expect(doneList).toEqual([1, 2])
doneList.push(todoList.shift()!)
expect(todoList).toEqual([2, 3])
expect(doneList).toEqual([1])

archiveList.push(todoList.shift()!)
expect(todoList).toEqual([])
expect(archiveList).toEqual([3])
doneList.push(todoList.shift()!)
expect(todoList).toEqual([3])
expect(doneList).toEqual([1, 2])

archiveList.pop()
})
archiveList.push(todoList.shift()!)
expect(todoList).toEqual([])
expect(archiveList).toEqual([3])

myTest('should called cleanup functions', ({ todoList, doneList, archiveList }) => {
expect(todoList).toEqual([1, 2, 3])
expect(doneList).toEqual([])
expect(archiveList).toEqual([])
archiveList.pop()
})
})

describe('smartly init fixtures', () => {
myTest('should not init any fixtures', function () {
expect(todoFn).not.toBeCalled()
Expand Down Expand Up @@ -150,4 +145,136 @@ describe('test.extend()', () => {
expect(archive).toEqual([])
})
})

describe('fixture call times', () => {
const apiFn = vi.fn(() => true)
const serviceFn = vi.fn(() => true)
const teardownFn = vi.fn()

interface APIFixture {
api: boolean
service: boolean
}

const testAPI = test.extend<APIFixture>({
api: async ({}, use) => {
await use(apiFn())
apiFn.mockClear()
teardownFn()
},
service: async ({}, use) => {
await use(serviceFn())
serviceFn.mockClear()
teardownFn()
},
})

beforeEach<APIFixture>(({ api, service }) => {
expect(api).toBe(true)
expect(service).toBe(true)
})

testAPI('Should init1 time', ({ api }) => {
expect(api).toBe(true)
expect(apiFn).toBeCalledTimes(1)
})

testAPI('Should init 1 time has multiple fixture', ({ api, service }) => {
expect(api).toBe(true)
expect(service).toBe(true)
expect(serviceFn).toBeCalledTimes(1)
expect(apiFn).toBeCalledTimes(1)
})

afterEach<APIFixture>(({ api, service }) => {
expect(api).toBe(true)
expect(service).toBe(true)
expect(apiFn).toBeCalledTimes(1)
expect(serviceFn).toBeCalledTimes(1)
})

afterAll(() => {
expect(serviceFn).toBeCalledTimes(0)
expect(apiFn).toBeCalledTimes(0)
expect(teardownFn).toBeCalledTimes(2)
})
})

describe('fixture in nested describe', () => {
interface Fixture {
foo: number
bar: number
}

const fooFn = vi.fn(() => 0)
const fooCleanup = vi.fn()

const barFn = vi.fn(() => 0)
const barCleanup = vi.fn()

const nestedTest = test.extend<Fixture>({
async foo({}, use) {
await use(fooFn())
fooCleanup()
},
async bar({}, use) {
await use(barFn())
barCleanup()
},
})

beforeEach<Fixture>(({ foo }) => {
expect(foo).toBe(0)
})

nestedTest('should only initialize foo', ({ foo }) => {
expect(foo).toBe(0)
expect(fooFn).toBeCalledTimes(1)
expect(barFn).toBeCalledTimes(0)
})

describe('level 2, using both foo and bar together', () => {
beforeEach<Fixture>(({ foo, bar }) => {
expect(foo).toBe(0)
expect(bar).toBe(0)
})

nestedTest('should only initialize bar', ({ foo, bar }) => {
expect(foo).toBe(0)
expect(bar).toBe(0)
expect(fooFn).toBeCalledTimes(1)
expect(barFn).toBeCalledTimes(1)
})

afterEach<Fixture>(({ foo, bar }) => {
expect(foo).toBe(0)
expect(bar).toBe(0)
})

afterAll(() => {
// foo setup in outside describe
// cleanup also called in outside describe
expect(fooCleanup).toHaveBeenCalledTimes(0)
// bar setup in inside describe
// cleanup also called in inside describe
expect(barCleanup).toHaveBeenCalledTimes(1)
})
})

nestedTest('level 2 will not call foo cleanup', ({ foo }) => {
expect(foo).toBe(0)
expect(fooFn).toBeCalledTimes(1)
})

afterEach<Fixture>(({ foo }) => {
expect(foo).toBe(0)
})

afterAll(() => {
// foo setup in this describe
// cleanup also called in this describe
expect(fooCleanup).toHaveBeenCalledTimes(1)
expect(barCleanup).toHaveBeenCalledTimes(1)
})
})
})