diff --git a/docs/guide/test-context.md b/docs/guide/test-context.md index 7de283ea2035..9887baf282c0 100644 --- a/docs/guide/test-context.md +++ b/docs/guide/test-context.md @@ -161,6 +161,32 @@ myTest('', ({ todos }) => {}) When using `test.extend()` with fixtures, you should always use the object destructuring pattern `{ todos }` to access context both in fixture function and test function. ::: +#### Automatic fixture + +::: warning +This feature is available since Vitest 1.3.0. +::: + +Vitest also supports the tuple syntax for fixtures, allowing you to pass options for each fixture. For example, you can use it to explicitly initialize a fixture, even if it's not being used in tests. + +```ts +import { test as base } from 'vitest' + +const test = base.extend({ + fixture: [ + async ({}, use) => { + // this function will run + setup() + await use() + teardown() + }, + { auto: true } // Mark as an automatic fixture + ], +}) + +test('', () => {}) +``` + #### TypeScript To provide fixture types for all your custom contexts, you can pass the fixtures type as a generic. diff --git a/packages/runner/src/fixture.ts b/packages/runner/src/fixture.ts index f089e1ae61be..eacc31d658a2 100644 --- a/packages/runner/src/fixture.ts +++ b/packages/runner/src/fixture.ts @@ -1,11 +1,10 @@ -import { createDefer } from '@vitest/utils' +import { createDefer, isObject } from '@vitest/utils' import { getFixture } from './map' -import type { TestContext } from './types' +import type { FixtureOptions, TestContext } from './types' -export interface FixtureItem { +export interface FixtureItem extends FixtureOptions { prop: string value: any - index: number /** * Indicates whether the fixture is a function */ @@ -17,15 +16,24 @@ export interface FixtureItem { } export function mergeContextFixtures(fixtures: Record, context: { fixtures?: FixtureItem[] } = {}) { + const fixtureOptionKeys = ['auto'] const fixtureArray: FixtureItem[] = Object.entries(fixtures) - .map(([prop, value], index) => { - const isFn = typeof value === 'function' - return { - prop, - value, - index, - isFn, + .map(([prop, value]) => { + const fixtureItem = { value } as FixtureItem + + if ( + Array.isArray(value) && value.length >= 2 + && isObject(value[1]) + && Object.keys(value[1]).some(key => fixtureOptionKeys.includes(key)) + ) { + // fixture with options + Object.assign(fixtureItem, value[1]) + fixtureItem.value = value[0] } + + fixtureItem.prop = prop + fixtureItem.isFn = typeof fixtureItem.value === 'function' + return fixtureItem }) if (Array.isArray(context.fixtures)) @@ -67,7 +75,8 @@ export function withFixtures(fn: Function, testContext?: TestContext) { return fn(context) const usedProps = getUsedProps(fn) - if (!usedProps.length) + const hasAutoFixture = fixtures.some(({ auto }) => auto) + if (!usedProps.length && !hasAutoFixture) return fn(context) if (!fixtureValueMaps.get(context)) @@ -78,7 +87,7 @@ export function withFixtures(fn: Function, testContext?: TestContext) { cleanupFnArrayMap.set(context, []) const cleanupFnArray = cleanupFnArrayMap.get(context)! - const usedFixtures = fixtures.filter(({ prop }) => usedProps.includes(prop)) + const usedFixtures = fixtures.filter(({ prop, auto }) => auto || usedProps.includes(prop)) const pendingFixtures = resolveDeps(usedFixtures) if (!pendingFixtures.length) diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts index bc6be12fbdd2..e2ceddfe7a50 100644 --- a/packages/runner/src/types/tasks.ts +++ b/packages/runner/src/types/tasks.ts @@ -223,6 +223,13 @@ export type TestAPI = ChainableTestAPI & Extend K extends keyof ExtraContext ? ExtraContext[K] : never }> } +export interface FixtureOptions { + /** + * Whether to automatically set up current fixture, even though it's not being used in tests. + */ + auto?: boolean +} + export type Use = (value: T) => Promise export type FixtureFn = (context: Omit & ExtraContext, use: Use) => Promise @@ -231,7 +238,7 @@ export type Fixture = ? (T[K] extends any ? FixtureFn>> : never) : T[K] | (T[K] extends any ? FixtureFn>> : never) export type Fixtures, ExtraContext = {}> = { - [K in keyof T]: Fixture> + [K in keyof T]: Fixture> | [Fixture>, FixtureOptions?] } export type InferFixturesTypes = T extends TestAPI ? C : T diff --git a/test/core/test/fixture-options.test.ts b/test/core/test/fixture-options.test.ts new file mode 100644 index 000000000000..fb3e56d67a8b --- /dev/null +++ b/test/core/test/fixture-options.test.ts @@ -0,0 +1,43 @@ +import { afterAll, beforeEach, describe, expect, test, vi } from 'vitest' + +const mockServer = { setup: vi.fn(), teardown: vi.fn() } +const FnA = vi.fn() + +const myTest = test.extend<{ + autoFixture: void + normalFixture: any[] +}>({ + autoFixture: [async ({}, use) => { + await mockServer.setup() + await use() + await mockServer.teardown() + }, { auto: true }], + + normalFixture: [async () => { + await FnA() + }, {}], +}) + +describe('fixture with options', () => { + describe('automatic fixture', () => { + beforeEach(() => { + expect(mockServer.setup).toBeCalledTimes(1) + }) + + afterAll(() => { + expect(mockServer.setup).toBeCalledTimes(1) + expect(mockServer.teardown).toBeCalledTimes(1) + }) + + myTest('should setup mock server', () => { + expect(mockServer.setup).toBeCalledTimes(1) + }) + }) + + describe('normal fixture', () => { + myTest('it is not a fixture with options', ({ normalFixture }) => { + expect(FnA).not.toBeCalled() + expect(normalFixture).toBeInstanceOf(Array) + }) + }) +})