Skip to content

Commit 20a5d4b

Browse files
sheremet-vaAriPerkkio
andauthoredMar 19, 2025··
feat: add "configureVitest" plugin hook (#7349)
Co-authored-by: Ari Perkkiö <ari.perkkio@gmail.com>
1 parent ba9b51c commit 20a5d4b

File tree

16 files changed

+489
-31
lines changed

16 files changed

+489
-31
lines changed
 

‎docs/.vitepress/config.ts

+4
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,10 @@ export default ({ mode }: { mode: string }) => {
350350
},
351351
],
352352
},
353+
{
354+
text: 'Plugin API',
355+
link: '/advanced/api/plugin',
356+
},
353357
{
354358
text: 'Runner API',
355359
link: '/advanced/runner',

‎docs/advanced/api/plugin.md

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
---
2+
title: Plugin API
3+
outline: deep
4+
---
5+
6+
# Plugin API <Version>3.1.0</Version> {#plugin-api}
7+
8+
::: warning
9+
This is an advanced API. If you just want to [run tests](/guide/), you probably don't need this. It is primarily used by library authors.
10+
11+
This guide assumes you know how to work with [Vite plugins](https://vite.dev/guide/api-plugin.html).
12+
:::
13+
14+
Vitest supports an experimental `configureVitest` [plugin](https://vite.dev/guide/api-plugin.html) hook hook since version 3.1. Any feedback regarding this API is welcome in [GitHub](https://github.com/vitest-dev/vitest/discussions/7104).
15+
16+
::: code-group
17+
```ts [only vitest]
18+
import type { Vite, VitestPluginContext } from 'vitest/node'
19+
20+
export function plugin(): Vite.Plugin {
21+
return {
22+
name: 'vitest:my-plugin',
23+
configureVitest(context: VitestPluginContext) {
24+
// ...
25+
}
26+
}
27+
}
28+
```
29+
```ts [vite and vitest]
30+
/// <reference types="vitest/config" />
31+
32+
import type { Plugin } from 'vite'
33+
34+
export function plugin(): Plugin {
35+
return {
36+
name: 'vitest:my-plugin',
37+
transform() {
38+
// ...
39+
},
40+
configureVitest(context) {
41+
// ...
42+
}
43+
}
44+
}
45+
```
46+
:::
47+
48+
::: tip TypeScript
49+
Vitest re-exports all Vite type-only imports via a `Vite` namespace, which you can use to keep your versions in sync. However, if you are writing a plugin for both Vite and Vitest, you can continue using the `Plugin` type from the `vite` entrypoint. Just make sure you have `vitest/config` referenced somewhere so that `configureVitest` is augmented correctly:
50+
51+
```ts
52+
/// <reference types="vitest/config" />
53+
```
54+
:::
55+
56+
Unlike [`reporter.onInit`](/advanced/api/reporters#oninit), this hooks runs early in Vitest lifecycle allowing you to make changes to configuration like `coverage` and `reporters`. A more notable change is that you can manipulate the global config from a [workspace project](/guide/workspace) if your plugin is defined in the project and not in the global config.
57+
58+
## Context
59+
60+
### project
61+
62+
The current [test project](./test-project) that the plugin belongs to.
63+
64+
::: warning Browser Mode
65+
Note that if you are relying on a browser feature, the `project.browser` field is not set yet. Use [`reporter.onBrowserInit`](./reporters#onbrowserinit) event instead.
66+
:::
67+
68+
### vitest
69+
70+
The global [Vitest](./vitest) instance. You can change the global configuration by directly mutating the `vitest.config` property:
71+
72+
```ts
73+
vitest.config.coverage.enabled = false
74+
vitest.config.reporters.push([['my-reporter', {}]])
75+
```
76+
77+
::: warning Config is Resolved
78+
Note that Vitest already resolved the config, so some types might be different from the usual user configuration. This also means that some properties will not be resolved again, like `setupFile`. If you are adding new files, make sure to resolve it first.
79+
80+
At this point reporters are not created yet, so modifying `vitest.reporters` will have no effect because it will be overwritten. If you need to inject your own reporter, modify the config instead.
81+
:::
82+
83+
### injectTestProjects
84+
85+
```ts
86+
function injectTestProjects(
87+
config: TestProjectConfiguration | TestProjectConfiguration[]
88+
): Promise<TestProject[]>
89+
```
90+
91+
This methods accepts a config glob pattern, a filepath to the config or an inline configuration. It returns an array of resolved [test projects](./test-project).
92+
93+
```ts
94+
// inject a single project with a custom alias
95+
const newProjects = await injectTestProjects({
96+
// you can inherit the current project config by referencing `configFile`
97+
// note that you cannot have a project with the name that already exists,
98+
// so it's a good practice to define a custom name
99+
configFile: project.vite.config.configFile,
100+
test: {
101+
name: 'my-custom-alias',
102+
alias: {
103+
customAlias: resolve('./custom-path.js'),
104+
},
105+
},
106+
})
107+
```
108+
109+
::: warning Projects are Filtered
110+
Vitest filters projects during the config resolution, so if the user defined a filter, injected project might not be resolved unless it [matches the filter](./vitest#matchesprojectfilter). You can update the filter via the `vitest.config.project` option to always include your workspace project:
111+
112+
```ts
113+
vitest.config.project.push('my-project-name')
114+
```
115+
116+
Note that this will only affect projects injected with [`injectTestProjects`](#injecttestprojects) method.
117+
:::
118+
119+
::: tip Referencing the Current Config
120+
If you want to keep the user configuration, you can specify the `configFile` property. All other properties will be merged with the user defined config.
121+
122+
The project's `configFile` can be accessed in Vite's config: `project.vite.config.configFile`.
123+
124+
Note that this will also inherit the `name` - Vitest doesn't allow multiple projects with the same name, so this will throw an error. Make sure you specified a different name. You can access the current name via the `project.name` property and all used names are available in the `vitest.projects` array.
125+
:::

‎docs/advanced/api/vitest.md

+10
Original file line numberDiff line numberDiff line change
@@ -518,3 +518,13 @@ vitest.onFilterWatchedSpecification(specification =>
518518
```
519519

520520
Vitest can create different specifications for the same file depending on the `pool` or `locations` options, so do not rely on the reference. Vitest can also return cached specification from [`vitest.getModuleSpecifications`](#getmodulespecifications) - the cache is based on the `moduleId` and `pool`. Note that [`project.createSpecification`](/advanced/api/test-project#createspecification) always returns a new instance.
521+
522+
## matchesProjectFilter <Version>3.1.0</Version> {#matchesprojectfilter}
523+
524+
```ts
525+
function matchesProjectFilter(name: string): boolean
526+
```
527+
528+
Check if the name matches the current [project filter](/guide/cli#project). If there is no project filter, this will always return `true`.
529+
530+
It is not possible to programmatically change the `--project` CLI option.

‎packages/browser/src/node/pool.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ export function createBrowserPool(vitest: Vitest): ProcessPool {
169169
async close() {
170170
await Promise.all([...providers].map(provider => provider.close()))
171171
providers.clear()
172-
vitest.resolvedProjects.forEach((project) => {
172+
vitest.projects.forEach((project) => {
173173
project.browser?.state.orchestrators.forEach((orchestrator) => {
174174
orchestrator.$close()
175175
})

‎packages/vitest/src/api/setup.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export function setup(ctx: Vitest, _server?: ViteDevServer): void {
8888
return ctx.getRootProject().serializedConfig
8989
},
9090
getResolvedProjectNames(): string[] {
91-
return ctx.resolvedProjects.map(p => p.name)
91+
return ctx.projects.map(p => p.name)
9292
},
9393
async getTransformResult(projectName: string, id, browser = false) {
9494
const project = ctx.getProjectByName(projectName)

‎packages/vitest/src/node/config/resolveConfig.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -913,7 +913,7 @@ function isPlaywrightChromiumOnly(vitest: Vitest, config: ResolvedConfig) {
913913
for (const instance of browser.instances) {
914914
const name = instance.name || (config.name ? `${config.name} (${instance.browser})` : instance.browser)
915915
// browser config is filtered out
916-
if (!vitest._matchesProjectFilter(name)) {
916+
if (!vitest.matchesProjectFilter(name)) {
917917
continue
918918
}
919919
if (instance.browser !== 'chromium') {

‎packages/vitest/src/node/core.ts

+47-16
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { SerializedCoverageConfig } from '../runtime/config'
77
import type { ArgumentsType, ProvidedContext, UserConsoleLog } from '../types/general'
88
import type { ProcessPool, WorkspaceSpec } from './pool'
99
import type { TestSpecification } from './spec'
10-
import type { ResolvedConfig, UserConfig, VitestRunMode } from './types/config'
10+
import type { ResolvedConfig, TestProjectConfiguration, UserConfig, VitestRunMode } from './types/config'
1111
import type { CoverageProvider } from './types/coverage'
1212
import type { Reporter } from './types/reporter'
1313
import type { TestRunResult } from './types/tests'
@@ -98,11 +98,10 @@ export class Vitest {
9898
/** @internal */ _browserLastPort = defaultBrowserPort
9999
/** @internal */ _browserSessions = new BrowserSessions()
100100
/** @internal */ _options: UserConfig = {}
101-
/** @internal */ reporters: Reporter[] = undefined!
101+
/** @internal */ reporters: Reporter[] = []
102102
/** @internal */ vitenode: ViteNodeServer = undefined!
103103
/** @internal */ runner: ViteNodeRunner = undefined!
104104
/** @internal */ _testRun: TestRun = undefined!
105-
/** @internal */ _projectFilters: RegExp[] = []
106105

107106
private isFirstRun = true
108107
private restartsCount = 0
@@ -216,7 +215,6 @@ export class Vitest {
216215
this.specifications.clearCache()
217216
this._onUserTestsRerun = []
218217

219-
this._projectFilters = toArray(options.project || []).map(project => wildcardPatternToRegExp(project))
220218
this._vite = server
221219

222220
const resolved = resolveConfig(this, options, server.config)
@@ -259,7 +257,7 @@ export class Vitest {
259257
server.watcher.on('change', async (file) => {
260258
file = normalize(file)
261259
const isConfig = file === server.config.configFile
262-
|| this.resolvedProjects.some(p => p.vite.config.configFile === file)
260+
|| this.projects.some(p => p.vite.config.configFile === file)
263261
|| file === this._workspaceConfigPath
264262
if (isConfig) {
265263
await Promise.all(this._onRestartListeners.map(fn => fn('config')))
@@ -279,6 +277,16 @@ export class Vitest {
279277
const projects = await this.resolveWorkspace(cliOptions)
280278
this.resolvedProjects = projects
281279
this.projects = projects
280+
281+
await Promise.all(projects.flatMap((project) => {
282+
const hooks = project.vite.config.getSortedPluginHooks('configureVitest')
283+
return hooks.map(hook => hook({
284+
project,
285+
vitest: this,
286+
injectTestProjects: this.injectTestProject,
287+
}))
288+
}))
289+
282290
if (!this.projects.length) {
283291
throw new Error(`No projects matched the filter "${toArray(resolved.project).join('", "')}".`)
284292
}
@@ -297,6 +305,24 @@ export class Vitest {
297305
await Promise.all(this._onSetServer.map(fn => fn()))
298306
}
299307

308+
/**
309+
* Inject new test projects into the workspace.
310+
* @param config Glob, config path or a custom config options.
311+
* @returns An array of new test projects. Can be empty if the name was filtered out.
312+
*/
313+
private injectTestProject = async (config: TestProjectConfiguration | TestProjectConfiguration[]): Promise<TestProject[]> => {
314+
const currentNames = new Set(this.projects.map(p => p.name))
315+
const workspace = await resolveWorkspace(
316+
this,
317+
this._options,
318+
undefined,
319+
Array.isArray(config) ? config : [config],
320+
currentNames,
321+
)
322+
this.projects.push(...workspace)
323+
return workspace
324+
}
325+
300326
/**
301327
* Provide a value to the test context. This value will be available to all tests with `inject`.
302328
*/
@@ -385,12 +411,15 @@ export class Vitest {
385411
}
386412

387413
private async resolveWorkspace(cliOptions: UserConfig): Promise<TestProject[]> {
414+
const names = new Set<string>()
415+
388416
if (Array.isArray(this.config.workspace)) {
389417
return resolveWorkspace(
390418
this,
391419
cliOptions,
392420
undefined,
393421
this.config.workspace,
422+
names,
394423
)
395424
}
396425

@@ -406,7 +435,7 @@ export class Vitest {
406435
if (!project) {
407436
return []
408437
}
409-
return resolveBrowserWorkspace(this, new Set(), [project])
438+
return resolveBrowserWorkspace(this, new Set([project.name]), [project])
410439
}
411440

412441
const workspaceModule = await this.import<{
@@ -422,6 +451,7 @@ export class Vitest {
422451
cliOptions,
423452
workspaceConfigPath,
424453
workspaceModule.default,
454+
names,
425455
)
426456
}
427457

@@ -861,11 +891,9 @@ export class Vitest {
861891
async changeProjectName(pattern: string): Promise<void> {
862892
if (pattern === '') {
863893
this.configOverride.project = undefined
864-
this._projectFilters = []
865894
}
866895
else {
867896
this.configOverride.project = [pattern]
868-
this._projectFilters = [wildcardPatternToRegExp(pattern)]
869897
}
870898

871899
await this.vite.restart()
@@ -1096,10 +1124,10 @@ export class Vitest {
10961124
await project._teardownGlobalSetup()
10971125
}
10981126

1099-
const closePromises: unknown[] = this.resolvedProjects.map(w => w.close())
1127+
const closePromises: unknown[] = this.projects.map(w => w.close())
11001128
// close the core workspace server only once
11011129
// it's possible that it's not initialized at all because it's not running any tests
1102-
if (this.coreWorkspaceProject && !this.resolvedProjects.includes(this.coreWorkspaceProject)) {
1130+
if (this.coreWorkspaceProject && !this.projects.includes(this.coreWorkspaceProject)) {
11031131
closePromises.push(this.coreWorkspaceProject.close().then(() => this._vite = undefined as any))
11041132
}
11051133

@@ -1136,7 +1164,7 @@ export class Vitest {
11361164
this.state.getProcessTimeoutCauses().forEach(cause => console.warn(cause))
11371165

11381166
if (!this.pool) {
1139-
const runningServers = [this._vite, ...this.resolvedProjects.map(p => p._vite)].filter(Boolean).length
1167+
const runningServers = [this._vite, ...this.projects.map(p => p._vite)].filter(Boolean).length
11401168

11411169
if (runningServers === 1) {
11421170
console.warn('Tests closed successfully but something prevents Vite server from exiting')
@@ -1252,20 +1280,23 @@ export class Vitest {
12521280

12531281
/**
12541282
* Check if the project with a given name should be included.
1255-
* @internal
12561283
*/
1257-
_matchesProjectFilter(name: string): boolean {
1284+
matchesProjectFilter(name: string): boolean {
1285+
const projects = this._config?.project || this._options?.project
12581286
// no filters applied, any project can be included
1259-
if (!this._projectFilters.length) {
1287+
if (!projects || !projects.length) {
12601288
return true
12611289
}
1262-
return this._projectFilters.some(filter => filter.test(name))
1290+
return toArray(projects).some((project) => {
1291+
const regexp = wildcardPatternToRegExp(project)
1292+
return regexp.test(name)
1293+
})
12631294
}
12641295
}
12651296

12661297
function assert(condition: unknown, property: string, name: string = property): asserts condition {
12671298
if (!condition) {
1268-
throw new Error(`The ${name} was not set. It means that \`vitest.${property}\` was called before the Vite server was established. Either await the Vitest promise or check that it is initialized with \`vitest.ready()\` before accessing \`vitest.${property}\`.`)
1299+
throw new Error(`The ${name} was not set. It means that \`vitest.${property}\` was called before the Vite server was established. Await the Vitest promise before accessing \`vitest.${property}\`.`)
12691300
}
12701301
}
12711302

‎packages/vitest/src/node/plugins/workspace.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { UserConfig as ViteConfig, Plugin as VitePlugin } from 'vite'
22
import type { TestProject } from '../project'
3-
import type { ResolvedConfig, UserWorkspaceConfig } from '../types/config'
3+
import type { ResolvedConfig, TestProjectInlineConfiguration } from '../types/config'
44
import { existsSync, readFileSync } from 'node:fs'
55
import { deepMerge } from '@vitest/utils'
66
import { basename, dirname, relative, resolve } from 'pathe'
@@ -21,7 +21,7 @@ import {
2121
} from './utils'
2222
import { VitestProjectResolver } from './vitestResolver'
2323

24-
interface WorkspaceOptions extends UserWorkspaceConfig {
24+
interface WorkspaceOptions extends TestProjectInlineConfiguration {
2525
root?: string
2626
workspacePath: string | number
2727
}
@@ -85,7 +85,7 @@ export function WorkspaceVitestPlugin(
8585
// if some of them match, they will later be filtered again by `resolveWorkspace`
8686
if (filters.length) {
8787
const hasProject = workspaceNames.some((name) => {
88-
return project.vitest._matchesProjectFilter(name)
88+
return project.vitest.matchesProjectFilter(name)
8989
})
9090
if (!hasProject) {
9191
throw new VitestFilteredOutProjectError()

‎packages/vitest/src/node/project.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import type { ParentProjectBrowser, ProjectBrowser } from './types/browser'
1616
import type {
1717
ResolvedConfig,
1818
SerializedConfig,
19+
TestProjectInlineConfiguration,
1920
UserConfig,
20-
UserWorkspaceConfig,
2121
} from './types/config'
2222
import { promises as fs, readFileSync } from 'node:fs'
2323
import { rm } from 'node:fs/promises'
@@ -726,7 +726,7 @@ export interface SerializedTestProject {
726726
context: ProvidedContext
727727
}
728728

729-
interface InitializeProjectOptions extends UserWorkspaceConfig {
729+
interface InitializeProjectOptions extends TestProjectInlineConfiguration {
730730
configFile: string | false
731731
}
732732

‎packages/vitest/src/node/types/config.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -1125,7 +1125,7 @@ export type UserProjectConfigExport =
11251125
| Promise<UserWorkspaceConfig>
11261126
| UserProjectConfigFn
11271127

1128-
export type TestProjectConfiguration = string | (UserProjectConfigExport & {
1128+
export type TestProjectInlineConfiguration = (UserWorkspaceConfig & {
11291129
/**
11301130
* Relative path to the extendable config. All other options will be merged with this config.
11311131
* If `true`, the project will inherit all options from the root config.
@@ -1134,5 +1134,11 @@ export type TestProjectConfiguration = string | (UserProjectConfigExport & {
11341134
extends?: string | true
11351135
})
11361136

1137+
export type TestProjectConfiguration =
1138+
string
1139+
| TestProjectInlineConfiguration
1140+
| Promise<UserWorkspaceConfig>
1141+
| UserProjectConfigFn
1142+
11371143
/** @deprecated use `TestProjectConfiguration` instead */
11381144
export type WorkspaceProjectConfiguration = TestProjectConfiguration
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { Vitest } from '../core'
2+
import type { TestProject } from '../project'
3+
import type { TestProjectConfiguration } from './config'
4+
5+
export interface VitestPluginContext {
6+
vitest: Vitest
7+
project: TestProject
8+
injectTestProjects: (config: TestProjectConfiguration | TestProjectConfiguration[]) => Promise<TestProject[]>
9+
}

‎packages/vitest/src/node/types/vite.ts

+8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
/* eslint-disable unused-imports/no-unused-vars */
2+
3+
import type { HookHandler } from 'vite'
14
import type { InlineConfig } from './config'
5+
import type { VitestPluginContext } from './plugin'
26

37
type VitestInlineConfig = InlineConfig
48

@@ -9,6 +13,10 @@ declare module 'vite' {
913
*/
1014
test?: VitestInlineConfig
1115
}
16+
17+
interface Plugin<A = any> {
18+
configureVitest?: HookHandler<(context: VitestPluginContext) => void>
19+
}
1220
}
1321

1422
export {}

‎packages/vitest/src/node/workspace/resolveWorkspace.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export async function resolveWorkspace(
1919
cliOptions: UserConfig,
2020
workspaceConfigPath: string | undefined,
2121
workspaceDefinition: TestProjectConfiguration[],
22+
names: Set<string>,
2223
): Promise<TestProject[]> {
2324
const { configFiles, projectConfigs, nonConfigDirectories } = await resolveTestProjectConfigs(
2425
vitest,
@@ -114,7 +115,6 @@ export async function resolveWorkspace(
114115
}
115116

116117
const resolvedProjectsPromises = await Promise.allSettled(projectPromises)
117-
const names = new Set<string>()
118118

119119
const errors: Error[] = []
120120
const resolvedProjects: TestProject[] = []
@@ -201,11 +201,11 @@ export async function resolveBrowserWorkspace(
201201
}
202202
const originalName = project.config.name
203203
// if original name is in the --project=name filter, keep all instances
204-
const filteredInstances = !vitest._projectFilters.length || vitest._matchesProjectFilter(originalName)
204+
const filteredInstances = vitest.matchesProjectFilter(originalName)
205205
? instances
206206
: instances.filter((instance) => {
207207
const newName = instance.name! // name is set in "workspace" plugin
208-
return vitest._matchesProjectFilter(newName)
208+
return vitest.matchesProjectFilter(newName)
209209
})
210210

211211
// every project was filtered out
@@ -460,7 +460,7 @@ export function getDefaultTestProject(vitest: Vitest): TestProject | null {
460460
}
461461
// check for the project name and browser names
462462
const hasProjects = getPotentialProjectNames(project).some(p =>
463-
vitest._matchesProjectFilter(p),
463+
vitest.matchesProjectFilter(p),
464464
)
465465
if (hasProjects) {
466466
return project

‎packages/vitest/src/public/config.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import type { ConfigEnv, UserConfig as ViteUserConfig } from 'vite'
22

3-
import type { TestProjectConfiguration, UserProjectConfigExport, UserProjectConfigFn, UserWorkspaceConfig, WorkspaceProjectConfiguration } from '../node/types/config'
3+
import type {
4+
TestProjectConfiguration,
5+
TestProjectInlineConfiguration,
6+
UserProjectConfigExport,
7+
UserProjectConfigFn,
8+
UserWorkspaceConfig,
9+
WorkspaceProjectConfiguration,
10+
} from '../node/types/config'
411
import '../node/types/vite'
512

613
export { extraInlineDeps } from '../constants'
@@ -20,7 +27,14 @@ export type { ConfigEnv, ViteUserConfig }
2027
* @deprecated Use `ViteUserConfig` instead
2128
*/
2229
export type UserConfig = ViteUserConfig
23-
export type { TestProjectConfiguration, UserProjectConfigExport, UserProjectConfigFn, UserWorkspaceConfig, WorkspaceProjectConfiguration }
30+
export type {
31+
TestProjectConfiguration,
32+
TestProjectInlineConfiguration,
33+
UserProjectConfigExport,
34+
UserProjectConfigFn,
35+
UserWorkspaceConfig,
36+
WorkspaceProjectConfiguration,
37+
}
2438
export type UserConfigFnObject = (env: ConfigEnv) => ViteUserConfig
2539
export type UserConfigFnPromise = (env: ConfigEnv) => Promise<ViteUserConfig>
2640
export type UserConfigFn = (

‎packages/vitest/src/public/node.ts

+1
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export type {
121121
ResolvedCoverageOptions,
122122
} from '../node/types/coverage'
123123

124+
export type { VitestPluginContext } from '../node/types/plugin'
124125
export type { TestRunResult } from '../node/types/tests'
125126
/**
126127
* @deprecated Use `TestModule` instead
+250
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import type { ViteUserConfig } from 'vitest/config'
2+
import type { TestProject, UserConfig, VitestOptions } from 'vitest/node'
3+
import { expect, onTestFinished, test } from 'vitest'
4+
import { createVitest } from 'vitest/node'
5+
6+
async function vitest(cliOptions: UserConfig, configValue: UserConfig = {}, viteConfig: ViteUserConfig = {}, vitestOptions: VitestOptions = {}) {
7+
const vitest = await createVitest('test', { ...cliOptions, watch: false }, { ...viteConfig, test: configValue as any }, vitestOptions)
8+
onTestFinished(() => vitest.close())
9+
return vitest
10+
}
11+
12+
test('can change global configuration', async () => {
13+
const v = await vitest({}, {}, {
14+
plugins: [
15+
{
16+
name: 'test',
17+
configureVitest({ vitest }) {
18+
vitest.config.coverage.enabled = true
19+
vitest.config.coverage.exclude = ['**/*']
20+
vitest.config.setupFiles.push('test/setup.ts')
21+
},
22+
},
23+
],
24+
})
25+
expect(v.config.coverage.enabled).toBe(true)
26+
expect(v.config.coverage.exclude).toEqual(['**/*'])
27+
// setup is not resolved
28+
expect(v.config.setupFiles).toEqual(['test/setup.ts'])
29+
})
30+
31+
test('can change the project and the global configurations', async () => {
32+
const v = await vitest({}, {
33+
workspace: [
34+
{
35+
plugins: [
36+
{
37+
name: 'test',
38+
configureVitest({ vitest, project }) {
39+
vitest.config.setupFiles.push('test/setup.ts')
40+
project.config.setupFiles.push('test/project-setup.ts')
41+
},
42+
},
43+
],
44+
},
45+
],
46+
})
47+
48+
expect(v.config.setupFiles).toEqual(['test/setup.ts'])
49+
const rootProject = v.getRootProject()
50+
51+
expect(v.projects).toHaveLength(1)
52+
53+
const project = v.projects[0]
54+
expect(project).not.toBe(rootProject)
55+
expect(project.config.setupFiles).toEqual(['test/project-setup.ts'])
56+
})
57+
58+
test('plugin is not called if the project is filtered out', async () => {
59+
const { projects } = await vitest({
60+
project: 'project-2',
61+
}, {
62+
workspace: [
63+
{
64+
test: {
65+
name: 'project-1',
66+
},
67+
plugins: [
68+
{
69+
name: 'test',
70+
configureVitest() {
71+
expect.unreachable()
72+
},
73+
},
74+
],
75+
},
76+
{
77+
test: {
78+
name: 'project-2',
79+
},
80+
},
81+
],
82+
})
83+
expect(projects).toHaveLength(1)
84+
expect(projects[0].name).toBe('project-2')
85+
})
86+
87+
test('can inject the plugin', async () => {
88+
let newWorkspace: TestProject[] = []
89+
const v = await vitest({}, {}, {
90+
plugins: [
91+
{
92+
name: 'test',
93+
async configureVitest({ injectTestProjects }) {
94+
newWorkspace = await injectTestProjects({
95+
test: {
96+
name: 'project-1',
97+
},
98+
})
99+
},
100+
},
101+
],
102+
})
103+
expect(v.projects).toHaveLength(2)
104+
// the default project that called configureVitest
105+
expect(v.projects[0].name).toBe('')
106+
expect(v.projects[1].name).toBe('project-1')
107+
108+
expect(newWorkspace).toHaveLength(1)
109+
expect(newWorkspace[0].name).toBe('project-1')
110+
})
111+
112+
test('injected plugin is filtered by the --project filter', async () => {
113+
let newWorkspace: TestProject[] = []
114+
const { projects } = await vitest({
115+
project: 'project-1',
116+
workspace: [
117+
{
118+
test: {
119+
name: 'project-1',
120+
},
121+
plugins: [
122+
{
123+
name: 'test',
124+
async configureVitest({ injectTestProjects }) {
125+
newWorkspace = await injectTestProjects({
126+
test: {
127+
name: 'project-2',
128+
},
129+
})
130+
},
131+
},
132+
],
133+
},
134+
],
135+
})
136+
expect(projects).toHaveLength(1)
137+
expect(projects[0].name).toBe('project-1')
138+
139+
expect(newWorkspace).toHaveLength(0)
140+
})
141+
142+
test('injected plugin is not filtered by the --project filter when it\'s overriden', async () => {
143+
let newWorkspace: TestProject[] = []
144+
const { projects } = await vitest({
145+
project: 'project-1',
146+
workspace: [
147+
{
148+
test: {
149+
name: 'project-1',
150+
},
151+
plugins: [
152+
{
153+
name: 'test',
154+
async configureVitest({ vitest, injectTestProjects }) {
155+
vitest.config.project.push('project-2')
156+
newWorkspace = await injectTestProjects({
157+
test: {
158+
name: 'project-2',
159+
},
160+
})
161+
},
162+
},
163+
],
164+
},
165+
],
166+
})
167+
expect(projects).toHaveLength(2)
168+
expect(projects[0].name).toBe('project-1')
169+
expect(projects[1].name).toBe('project-2')
170+
171+
expect(newWorkspace).toHaveLength(1)
172+
expect(newWorkspace[0].name).toBe('project-2')
173+
})
174+
175+
test('adding a plugin with existing name throws and error', async () => {
176+
await expect(() => vitest({
177+
workspace: [
178+
{
179+
test: {
180+
name: 'project-1',
181+
},
182+
plugins: [
183+
{
184+
name: 'test',
185+
async configureVitest({ injectTestProjects }) {
186+
await injectTestProjects({
187+
test: {
188+
name: 'project-1',
189+
},
190+
})
191+
},
192+
},
193+
],
194+
},
195+
],
196+
}),
197+
).rejects.toThrowError('Project name "project-1" is not unique. All projects in a workspace should have unique names. Make sure your configuration is correct.')
198+
199+
await expect(() => vitest({
200+
workspace: [
201+
{
202+
plugins: [
203+
{
204+
name: 'test',
205+
async configureVitest({ injectTestProjects }) {
206+
await injectTestProjects({
207+
test: {
208+
name: 'project-1',
209+
},
210+
})
211+
await injectTestProjects({
212+
test: {
213+
name: 'project-1',
214+
},
215+
})
216+
},
217+
},
218+
],
219+
},
220+
],
221+
}),
222+
).rejects.toThrowError('Project name "project-1" is not unique. All projects in a workspace should have unique names. Make sure your configuration is correct.')
223+
224+
await expect(() => vitest({
225+
workspace: [
226+
{
227+
plugins: [
228+
{
229+
name: 'test',
230+
async configureVitest({ injectTestProjects }) {
231+
await injectTestProjects([
232+
{
233+
test: {
234+
name: 'project-1',
235+
},
236+
},
237+
{
238+
test: {
239+
name: 'project-1',
240+
},
241+
},
242+
])
243+
},
244+
},
245+
],
246+
},
247+
],
248+
}),
249+
).rejects.toThrowError('Project name "project-1" is not unique. All projects in a workspace should have unique names. Make sure your configuration is correct.')
250+
})

0 commit comments

Comments
 (0)
Please sign in to comment.