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): test.extend doesn't work in hooks without test #4065

Merged
merged 11 commits into from Sep 14, 2023
57 changes: 33 additions & 24 deletions packages/runner/src/fixture.ts
@@ -1,4 +1,5 @@
import type { TestContext } from './types'
import { getFixture } from './map'
import { getCurrentTest } from './test-state'

export interface FixtureItem {
prop: string
Expand Down Expand Up @@ -43,32 +44,40 @@ export function mergeContextFixtures(fixtures: Record<string, any>, context: { f
return context
}

export function withFixtures(fn: Function, fixtures: FixtureItem[], context: TestContext & Record<string, any>) {
if (!fixtures.length)
return () => fn(context)

const usedProps = getUsedProps(fn)
if (!usedProps.length)
return () => fn(context)

const usedFixtures = fixtures.filter(({ prop }) => usedProps.includes(prop))
const pendingFixtures = resolveDeps(usedFixtures)
let cursor = 0
export function withFixtures(fn: Function) {
return () => {
const test = getCurrentTest()

if (!test)
return fn({})
const context = test.context as typeof test.context & { [key: string]: any }
const fixtures = getFixture(test)
if (!fixtures?.length)
return fn(context)

const usedProps = getUsedProps(fn)
if (!usedProps.length)
return fn(context)

const usedFixtures = fixtures.filter(({ prop }) => usedProps.includes(prop))
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)
}

async function use(fixtureValue: any) {
const { prop } = pendingFixtures[cursor++]
context[prop] = fixtureValue
if (cursor < pendingFixtures.length)
await next()
else await fn(context)
}
async function next() {
const { value } = pendingFixtures[cursor]
typeof value === 'function' ? await value(context, use) : await use(value)
}

async function next() {
const { value } = pendingFixtures[cursor]
typeof value === 'function' ? await value(context, use) : await use(value)
return next()
}

return () => next()
}

function resolveDeps(fixtures: FixtureItem[], depSet = new Set<FixtureItem>(), pendingFixtures: FixtureItem[] = []) {
Expand Down
5 changes: 3 additions & 2 deletions packages/runner/src/hooks.ts
Expand Up @@ -2,6 +2,7 @@ import type { OnTestFailedHandler, SuiteHooks, Test } from './types'
import { getCurrentSuite, getRunner } from './suite'
import { getCurrentTest } from './test-state'
import { withTimeout } from './context'
import { withFixtures } from './fixture'

function getDefaultHookTimeout() {
return getRunner().config.hookTimeout
Expand All @@ -15,10 +16,10 @@ export function afterAll(fn: SuiteHooks['afterAll'][0], timeout?: number) {
return getCurrentSuite().on('afterAll', withTimeout(fn, timeout ?? getDefaultHookTimeout(), true))
}
export function beforeEach<ExtraContext = {}>(fn: SuiteHooks<ExtraContext>['beforeEach'][0], timeout?: number) {
return getCurrentSuite<ExtraContext>().on('beforeEach', withTimeout(fn, timeout ?? getDefaultHookTimeout(), true))
return getCurrentSuite<ExtraContext>().on('beforeEach', withTimeout(withFixtures(fn), timeout ?? getDefaultHookTimeout(), true))
}
export function afterEach<ExtraContext = {}>(fn: SuiteHooks<ExtraContext>['afterEach'][0], timeout?: number) {
return getCurrentSuite<ExtraContext>().on('afterEach', withTimeout(fn, timeout ?? getDefaultHookTimeout(), true))
return getCurrentSuite<ExtraContext>().on('afterEach', withTimeout(withFixtures(fn), timeout ?? getDefaultHookTimeout(), true))
}

export const onTestFailed = createTestHook<OnTestFailedHandler>('onTestFailed', (test, handler) => {
Expand Down
10 changes: 10 additions & 0 deletions packages/runner/src/map.ts
@@ -1,8 +1,10 @@
import type { Awaitable } from '@vitest/utils'
import type { Suite, SuiteHooks, Test } from './types'
import type { FixtureItem } from './fixture'

// use WeakMap here to make the Test and Suite object serializable
const fnMap = new WeakMap()
const fixtureMap = new WeakMap()
const hooksMap = new WeakMap()

export function setFn(key: Test, fn: (() => Awaitable<void>)) {
Expand All @@ -13,6 +15,14 @@ export function getFn<Task = Test>(key: Task): (() => Awaitable<void>) {
return fnMap.get(key as any)
}

export function setFixture(key: Test, fixture: FixtureItem[] | undefined) {
fixtureMap.set(key, fixture)
}

export function getFixture<Task = Test>(key: Task): FixtureItem[] {
return fixtureMap.get(key as any)
}

export function setHooks(key: Suite, hooks: SuiteHooks) {
hooksMap.set(key, hooks)
}
Expand Down
7 changes: 3 additions & 4 deletions packages/runner/src/suite.ts
Expand Up @@ -3,7 +3,7 @@ import type { File, Fixtures, RunMode, Suite, SuiteAPI, SuiteCollector, SuiteFac
import type { VitestRunner } from './types/runner'
import { createChainable } from './utils/chain'
import { collectTask, collectorContext, createTestContext, runWithSuite, withTimeout } from './context'
import { getHooks, setFn, setHooks } from './map'
import { getHooks, setFixture, setFn, setHooks } from './map'
import type { FixtureItem } from './fixture'
import { mergeContextFixtures, withFixtures } from './fixture'

Expand Down Expand Up @@ -96,10 +96,9 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m
enumerable: false,
})

setFixture(test, this.fixtures)
setFn(test, withTimeout(
this.fixtures
? withFixtures(fn, this.fixtures, context)
: () => fn(context),
withFixtures(fn),
options?.timeout ?? runner.config.testTimeout,
))

Expand Down
22 changes: 22 additions & 0 deletions test/core/test/hooks-top-level.test.ts
@@ -0,0 +1,22 @@
import { afterEach, beforeEach, expect, test as originalTest } from 'vitest'

interface Fixture { foo: number }

const test = originalTest.extend<Fixture>({
sheremet-va marked this conversation as resolved.
Show resolved Hide resolved
// eslint-disable-next-line no-empty-pattern
foo: async ({}, use) => {
await use(1)
},
})

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

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

test('placeholder', ({ foo }) => {
expect(foo).toBe(1)
})
22 changes: 22 additions & 0 deletions test/core/test/test-extend-with-hooks.test.ts
@@ -0,0 +1,22 @@
import { afterEach, beforeEach, expect, test as originalTest } from 'vitest'

interface Fixture { foo: number }

const test = originalTest.extend<Fixture>({
// eslint-disable-next-line no-empty-pattern
foo: async ({}, use) => {
await use(1)
},
})

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

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

test('the foo should exists', ({ foo }) => {
expect(foo).toBe(1)
})