diff --git a/docs/guide/test-context.md b/docs/guide/test-context.md index ea5f483dc78d..9d0473107425 100644 --- a/docs/guide/test-context.md +++ b/docs/guide/test-context.md @@ -176,9 +176,61 @@ const test = base.extend({ ], }) -test('', () => {}) +test('works correctly') ``` +#### Default fixture + +Since Vitest 2.2, you can provide different values in different [projects](/guide/workspace). To enable this feature, pass down `{ injected: true }` to the options. If the key is not specified in the [project configuration](/config/#provide), then the default value will be used. + +:::code-group +```ts [fixtures.test.ts] +import { test as base } from 'vitest' + +const test = base.extend({ + url: [ + // default value if "url" is not defined in the config + 'default', + // mark the fixure as "injected" to allow the override + { injected: true }, + ], +}) + +test('works correctly', ({ url }) => { + // url is "/default" in "project-new" + // url is "/full" in "project-full" + // url is "/empty" in "project-empty" +}) +``` +```ts [vitest.workspace.ts] +import { defineWorkspace } from 'vitest/config' + +export default defineWorkspace([ + { + test: { + name: 'project-new', + }, + }, + { + test: { + name: 'project-full', + provide: { + url: '/full', + }, + }, + }, + { + test: { + name: 'project-empty', + provide: { + url: '/empty', + }, + }, + }, +]) +``` +::: + #### TypeScript To provide fixture types for all your custom contexts, you can pass the fixtures type as a generic. @@ -194,7 +246,7 @@ const myTest = test.extend({ archive: [] }) -myTest('', (context) => { +myTest('types are defined correctly', (context) => { expectTypeOf(context.todos).toEqualTypeOf() expectTypeOf(context.archive).toEqualTypeOf() }) diff --git a/packages/runner/src/fixture.ts b/packages/runner/src/fixture.ts index 59ea15a021fa..cd073ec911e3 100644 --- a/packages/runner/src/fixture.ts +++ b/packages/runner/src/fixture.ts @@ -5,6 +5,10 @@ import { getFixture } from './map' export interface FixtureItem extends FixtureOptions { prop: string value: any + /** + * Indicated if the injected value should be preferred over the fixture value + */ + injected?: boolean /** * Indicates whether the fixture is a function */ @@ -17,11 +21,12 @@ export interface FixtureItem extends FixtureOptions { export function mergeContextFixtures( fixtures: Record, - context: { fixtures?: FixtureItem[] } = {}, + context: { fixtures?: FixtureItem[] }, + inject: (key: string) => unknown, ): { fixtures?: FixtureItem[] } { - const fixtureOptionKeys = ['auto'] + const fixtureOptionKeys = ['auto', 'injected'] const fixtureArray: FixtureItem[] = Object.entries(fixtures).map( ([prop, value]) => { const fixtureItem = { value } as FixtureItem @@ -34,7 +39,10 @@ export function mergeContextFixtures( ) { // fixture with options Object.assign(fixtureItem, value[1]) - fixtureItem.value = value[0] + const userValue = value[0] + fixtureItem.value = fixtureItem.injected + ? (inject(prop) ?? userValue) + : userValue } fixtureItem.prop = prop diff --git a/packages/runner/src/suite.ts b/packages/runner/src/suite.ts index 7f69844a7407..d3c972794072 100644 --- a/packages/runner/src/suite.ts +++ b/packages/runner/src/suite.ts @@ -710,7 +710,11 @@ export function createTaskCollector( } taskFn.extend = function (fixtures: Fixtures>) { - const _context = mergeContextFixtures(fixtures, context) + const _context = mergeContextFixtures( + fixtures, + context || {}, + (key: string) => getRunner().injectValue?.(key), + ) return createTest(function fn( name: string | Function, diff --git a/packages/runner/src/types/runner.ts b/packages/runner/src/types/runner.ts index 76a29419d3bd..6e5e1dbd7d4c 100644 --- a/packages/runner/src/types/runner.ts +++ b/packages/runner/src/types/runner.ts @@ -147,6 +147,10 @@ export interface VitestRunner { * Called when test and setup files are imported. Can be called in two situations: when collecting tests and when importing setup files. */ importFile: (filepath: string, source: VitestRunnerImportSource) => unknown + /** + * Function that is called when the runner attempts to get the value when `test.extend` is used with `{ injected: true }` + */ + injectValue?: (key: string) => unknown /** * Publicly available configuration. */ diff --git a/packages/vitest/src/runtime/runners/test.ts b/packages/vitest/src/runtime/runners/test.ts index 642fd526c8f2..952d27389981 100644 --- a/packages/vitest/src/runtime/runners/test.ts +++ b/packages/vitest/src/runtime/runners/test.ts @@ -16,6 +16,7 @@ import type { VitestExecutor } from '../execute' import { getState, GLOBAL_EXPECT, setState } from '@vitest/expect' import { getNames, getTestName, getTests } from '@vitest/runner/utils' import { createExpect } from '../../integrations/chai/index' +import { inject } from '../../integrations/inject' import { getSnapshotClient } from '../../integrations/snapshot/chai' import { vi } from '../../integrations/vi' import { rpc } from '../rpc' @@ -89,6 +90,12 @@ export class VitestTestRunner implements VitestRunner { this.cancelRun = true } + injectValue(key: string) { + // inject has a very limiting type controlled by ProvidedContext + // some tests override it which causes the build to fail + return (inject as any)(key) + } + async onBeforeRunTask(test: Task) { if (this.cancelRun) { test.mode = 'skip' diff --git a/test/workspaces/globalTest.ts b/test/workspaces/globalTest.ts index 0069891a748d..938d8dfb4f89 100644 --- a/test/workspaces/globalTest.ts +++ b/test/workspaces/globalTest.ts @@ -9,6 +9,8 @@ declare module 'vitest' { invalidValue: unknown projectConfigValue: boolean globalConfigValue: boolean + + providedConfigValue: string } } @@ -35,8 +37,8 @@ export async function teardown() { try { assert.ok(results.success) assert.equal(results.numTotalTestSuites, 28) - assert.equal(results.numTotalTests, 31) - assert.equal(results.numPassedTests, 31) + assert.equal(results.numTotalTests, 33) + assert.equal(results.numPassedTests, 33) assert.ok(results.coverageMap) const shared = results.testResults.filter((r: any) => r.name.includes('space_shared/test.spec.ts')) diff --git a/test/workspaces/space_shared/test.spec.ts b/test/workspaces/space_shared/test.spec.ts index c152931a1345..cbff4df5c2bf 100644 --- a/test/workspaces/space_shared/test.spec.ts +++ b/test/workspaces/space_shared/test.spec.ts @@ -4,6 +4,19 @@ declare global { const testValue: string } +const custom = it.extend({ + providedConfigValue: ['default value', { injected: true }], +}) + +custom('provided config value is injected', ({ providedConfigValue }) => { + expect(providedConfigValue).toBe( + // happy-dom provides the value in the workspace config + expect.getState().environment === 'node' + ? 'default value' + : 'actual config value', + ) +}) + it('the same file works with different projects', () => { expect(testValue).toBe(expect.getState().environment === 'node' ? 'node' : 'jsdom') }) diff --git a/test/workspaces/vitest.workspace.ts b/test/workspaces/vitest.workspace.ts index 49223e68f2bb..077de9814b7a 100644 --- a/test/workspaces/vitest.workspace.ts +++ b/test/workspaces/vitest.workspace.ts @@ -13,6 +13,9 @@ export default defineWorkspace([ root: './space_shared', environment: 'happy-dom', setupFiles: ['./setup.jsdom.ts'], + provide: { + providedConfigValue: 'actual config value', + }, }, }), Promise.resolve({