diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 66f94571d..12f617a12 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -350,6 +350,10 @@ export default ({ mode }: { mode: string }) => { }, ], }, + { + text: 'Plugin API', + link: '/advanced/api/plugin', + }, { text: 'Runner API', link: '/advanced/runner', diff --git a/docs/advanced/api/plugin.md b/docs/advanced/api/plugin.md new file mode 100644 index 000000000..f609d1185 --- /dev/null +++ b/docs/advanced/api/plugin.md @@ -0,0 +1,125 @@ +--- +title: Plugin API +outline: deep +--- + +# Plugin API 3.1.0 {#plugin-api} + +::: warning +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. + +This guide assumes you know how to work with [Vite plugins](https://vite.dev/guide/api-plugin.html). +::: + +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). + +::: code-group +```ts [only vitest] +import type { Vite, VitestPluginContext } from 'vitest/node' + +export function plugin(): Vite.Plugin { + return { + name: 'vitest:my-plugin', + configureVitest(context: VitestPluginContext) { + // ... + } + } +} +``` +```ts [vite and vitest] +/// + +import type { Plugin } from 'vite' + +export function plugin(): Plugin { + return { + name: 'vitest:my-plugin', + transform() { + // ... + }, + configureVitest(context) { + // ... + } + } +} +``` +::: + +::: tip TypeScript +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: + +```ts +/// +``` +::: + +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. + +## Context + +### project + +The current [test project](./test-project) that the plugin belongs to. + +::: warning Browser Mode +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. +::: + +### vitest + +The global [Vitest](./vitest) instance. You can change the global configuration by directly mutating the `vitest.config` property: + +```ts +vitest.config.coverage.enabled = false +vitest.config.reporters.push([['my-reporter', {}]]) +``` + +::: warning Config is Resolved +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. + +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. +::: + +### injectTestProjects + +```ts +function injectTestProjects( + config: TestProjectConfiguration | TestProjectConfiguration[] +): Promise +``` + +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). + +```ts +// inject a single project with a custom alias +const newProjects = await injectTestProjects({ + // you can inherit the current project config by referencing `configFile` + // note that you cannot have a project with the name that already exists, + // so it's a good practice to define a custom name + configFile: project.vite.config.configFile, + test: { + name: 'my-custom-alias', + alias: { + customAlias: resolve('./custom-path.js'), + }, + }, +}) +``` + +::: warning Projects are Filtered +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: + +```ts +vitest.config.project.push('my-project-name') +``` + +Note that this will only affect projects injected with [`injectTestProjects`](#injecttestprojects) method. +::: + +::: tip Referencing the Current Config +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. + +The project's `configFile` can be accessed in Vite's config: `project.vite.config.configFile`. + +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. +::: diff --git a/docs/advanced/api/vitest.md b/docs/advanced/api/vitest.md index 5043c56bb..5933d6371 100644 --- a/docs/advanced/api/vitest.md +++ b/docs/advanced/api/vitest.md @@ -518,3 +518,13 @@ vitest.onFilterWatchedSpecification(specification => ``` 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. + +## matchesProjectFilter 3.1.0 {#matchesprojectfilter} + +```ts +function matchesProjectFilter(name: string): boolean +``` + +Check if the name matches the current [project filter](/guide/cli#project). If there is no project filter, this will always return `true`. + +It is not possible to programmatically change the `--project` CLI option. diff --git a/packages/browser/src/node/pool.ts b/packages/browser/src/node/pool.ts index be0ad6e60..86dcf7a8b 100644 --- a/packages/browser/src/node/pool.ts +++ b/packages/browser/src/node/pool.ts @@ -169,7 +169,7 @@ export function createBrowserPool(vitest: Vitest): ProcessPool { async close() { await Promise.all([...providers].map(provider => provider.close())) providers.clear() - vitest.resolvedProjects.forEach((project) => { + vitest.projects.forEach((project) => { project.browser?.state.orchestrators.forEach((orchestrator) => { orchestrator.$close() }) diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts index eb32b41bc..9bc308324 100644 --- a/packages/vitest/src/api/setup.ts +++ b/packages/vitest/src/api/setup.ts @@ -88,7 +88,7 @@ export function setup(ctx: Vitest, _server?: ViteDevServer): void { return ctx.getRootProject().serializedConfig }, getResolvedProjectNames(): string[] { - return ctx.resolvedProjects.map(p => p.name) + return ctx.projects.map(p => p.name) }, async getTransformResult(projectName: string, id, browser = false) { const project = ctx.getProjectByName(projectName) diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts index 804f16591..4c319cc25 100644 --- a/packages/vitest/src/node/config/resolveConfig.ts +++ b/packages/vitest/src/node/config/resolveConfig.ts @@ -913,7 +913,7 @@ function isPlaywrightChromiumOnly(vitest: Vitest, config: ResolvedConfig) { for (const instance of browser.instances) { const name = instance.name || (config.name ? `${config.name} (${instance.browser})` : instance.browser) // browser config is filtered out - if (!vitest._matchesProjectFilter(name)) { + if (!vitest.matchesProjectFilter(name)) { continue } if (instance.browser !== 'chromium') { diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 9d3b5e69b..d3b2820f6 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -7,7 +7,7 @@ import type { SerializedCoverageConfig } from '../runtime/config' import type { ArgumentsType, ProvidedContext, UserConsoleLog } from '../types/general' import type { ProcessPool, WorkspaceSpec } from './pool' import type { TestSpecification } from './spec' -import type { ResolvedConfig, UserConfig, VitestRunMode } from './types/config' +import type { ResolvedConfig, TestProjectConfiguration, UserConfig, VitestRunMode } from './types/config' import type { CoverageProvider } from './types/coverage' import type { Reporter } from './types/reporter' import type { TestRunResult } from './types/tests' @@ -98,11 +98,10 @@ export class Vitest { /** @internal */ _browserLastPort = defaultBrowserPort /** @internal */ _browserSessions = new BrowserSessions() /** @internal */ _options: UserConfig = {} - /** @internal */ reporters: Reporter[] = undefined! + /** @internal */ reporters: Reporter[] = [] /** @internal */ vitenode: ViteNodeServer = undefined! /** @internal */ runner: ViteNodeRunner = undefined! /** @internal */ _testRun: TestRun = undefined! - /** @internal */ _projectFilters: RegExp[] = [] private isFirstRun = true private restartsCount = 0 @@ -216,7 +215,6 @@ export class Vitest { this.specifications.clearCache() this._onUserTestsRerun = [] - this._projectFilters = toArray(options.project || []).map(project => wildcardPatternToRegExp(project)) this._vite = server const resolved = resolveConfig(this, options, server.config) @@ -259,7 +257,7 @@ export class Vitest { server.watcher.on('change', async (file) => { file = normalize(file) const isConfig = file === server.config.configFile - || this.resolvedProjects.some(p => p.vite.config.configFile === file) + || this.projects.some(p => p.vite.config.configFile === file) || file === this._workspaceConfigPath if (isConfig) { await Promise.all(this._onRestartListeners.map(fn => fn('config'))) @@ -279,6 +277,16 @@ export class Vitest { const projects = await this.resolveWorkspace(cliOptions) this.resolvedProjects = projects this.projects = projects + + await Promise.all(projects.flatMap((project) => { + const hooks = project.vite.config.getSortedPluginHooks('configureVitest') + return hooks.map(hook => hook({ + project, + vitest: this, + injectTestProjects: this.injectTestProject, + })) + })) + if (!this.projects.length) { throw new Error(`No projects matched the filter "${toArray(resolved.project).join('", "')}".`) } @@ -297,6 +305,24 @@ export class Vitest { await Promise.all(this._onSetServer.map(fn => fn())) } + /** + * Inject new test projects into the workspace. + * @param config Glob, config path or a custom config options. + * @returns An array of new test projects. Can be empty if the name was filtered out. + */ + private injectTestProject = async (config: TestProjectConfiguration | TestProjectConfiguration[]): Promise => { + const currentNames = new Set(this.projects.map(p => p.name)) + const workspace = await resolveWorkspace( + this, + this._options, + undefined, + Array.isArray(config) ? config : [config], + currentNames, + ) + this.projects.push(...workspace) + return workspace + } + /** * Provide a value to the test context. This value will be available to all tests with `inject`. */ @@ -385,12 +411,15 @@ export class Vitest { } private async resolveWorkspace(cliOptions: UserConfig): Promise { + const names = new Set() + if (Array.isArray(this.config.workspace)) { return resolveWorkspace( this, cliOptions, undefined, this.config.workspace, + names, ) } @@ -406,7 +435,7 @@ export class Vitest { if (!project) { return [] } - return resolveBrowserWorkspace(this, new Set(), [project]) + return resolveBrowserWorkspace(this, new Set([project.name]), [project]) } const workspaceModule = await this.import<{ @@ -422,6 +451,7 @@ export class Vitest { cliOptions, workspaceConfigPath, workspaceModule.default, + names, ) } @@ -861,11 +891,9 @@ export class Vitest { async changeProjectName(pattern: string): Promise { if (pattern === '') { this.configOverride.project = undefined - this._projectFilters = [] } else { this.configOverride.project = [pattern] - this._projectFilters = [wildcardPatternToRegExp(pattern)] } await this.vite.restart() @@ -1096,10 +1124,10 @@ export class Vitest { await project._teardownGlobalSetup() } - const closePromises: unknown[] = this.resolvedProjects.map(w => w.close()) + const closePromises: unknown[] = this.projects.map(w => w.close()) // close the core workspace server only once // it's possible that it's not initialized at all because it's not running any tests - if (this.coreWorkspaceProject && !this.resolvedProjects.includes(this.coreWorkspaceProject)) { + if (this.coreWorkspaceProject && !this.projects.includes(this.coreWorkspaceProject)) { closePromises.push(this.coreWorkspaceProject.close().then(() => this._vite = undefined as any)) } @@ -1136,7 +1164,7 @@ export class Vitest { this.state.getProcessTimeoutCauses().forEach(cause => console.warn(cause)) if (!this.pool) { - const runningServers = [this._vite, ...this.resolvedProjects.map(p => p._vite)].filter(Boolean).length + const runningServers = [this._vite, ...this.projects.map(p => p._vite)].filter(Boolean).length if (runningServers === 1) { console.warn('Tests closed successfully but something prevents Vite server from exiting') @@ -1252,20 +1280,23 @@ export class Vitest { /** * Check if the project with a given name should be included. - * @internal */ - _matchesProjectFilter(name: string): boolean { + matchesProjectFilter(name: string): boolean { + const projects = this._config?.project || this._options?.project // no filters applied, any project can be included - if (!this._projectFilters.length) { + if (!projects || !projects.length) { return true } - return this._projectFilters.some(filter => filter.test(name)) + return toArray(projects).some((project) => { + const regexp = wildcardPatternToRegExp(project) + return regexp.test(name) + }) } } function assert(condition: unknown, property: string, name: string = property): asserts condition { if (!condition) { - 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}\`.`) + 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}\`.`) } } diff --git a/packages/vitest/src/node/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts index 8b3395cb0..40f2cfba5 100644 --- a/packages/vitest/src/node/plugins/workspace.ts +++ b/packages/vitest/src/node/plugins/workspace.ts @@ -1,6 +1,6 @@ import type { UserConfig as ViteConfig, Plugin as VitePlugin } from 'vite' import type { TestProject } from '../project' -import type { ResolvedConfig, UserWorkspaceConfig } from '../types/config' +import type { ResolvedConfig, TestProjectInlineConfiguration } from '../types/config' import { existsSync, readFileSync } from 'node:fs' import { deepMerge } from '@vitest/utils' import { basename, dirname, relative, resolve } from 'pathe' @@ -21,7 +21,7 @@ import { } from './utils' import { VitestProjectResolver } from './vitestResolver' -interface WorkspaceOptions extends UserWorkspaceConfig { +interface WorkspaceOptions extends TestProjectInlineConfiguration { root?: string workspacePath: string | number } @@ -85,7 +85,7 @@ export function WorkspaceVitestPlugin( // if some of them match, they will later be filtered again by `resolveWorkspace` if (filters.length) { const hasProject = workspaceNames.some((name) => { - return project.vitest._matchesProjectFilter(name) + return project.vitest.matchesProjectFilter(name) }) if (!hasProject) { throw new VitestFilteredOutProjectError() diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index 9fe135e72..3a41fb743 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -16,8 +16,8 @@ import type { ParentProjectBrowser, ProjectBrowser } from './types/browser' import type { ResolvedConfig, SerializedConfig, + TestProjectInlineConfiguration, UserConfig, - UserWorkspaceConfig, } from './types/config' import { promises as fs, readFileSync } from 'node:fs' import { rm } from 'node:fs/promises' @@ -726,7 +726,7 @@ export interface SerializedTestProject { context: ProvidedContext } -interface InitializeProjectOptions extends UserWorkspaceConfig { +interface InitializeProjectOptions extends TestProjectInlineConfiguration { configFile: string | false } diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index 73956ee5a..62c12ddf3 100644 --- a/packages/vitest/src/node/types/config.ts +++ b/packages/vitest/src/node/types/config.ts @@ -1125,7 +1125,7 @@ export type UserProjectConfigExport = | Promise | UserProjectConfigFn -export type TestProjectConfiguration = string | (UserProjectConfigExport & { +export type TestProjectInlineConfiguration = (UserWorkspaceConfig & { /** * Relative path to the extendable config. All other options will be merged with this config. * If `true`, the project will inherit all options from the root config. @@ -1134,5 +1134,11 @@ export type TestProjectConfiguration = string | (UserProjectConfigExport & { extends?: string | true }) +export type TestProjectConfiguration = + string + | TestProjectInlineConfiguration + | Promise + | UserProjectConfigFn + /** @deprecated use `TestProjectConfiguration` instead */ export type WorkspaceProjectConfiguration = TestProjectConfiguration diff --git a/packages/vitest/src/node/types/plugin.ts b/packages/vitest/src/node/types/plugin.ts new file mode 100644 index 000000000..012084d84 --- /dev/null +++ b/packages/vitest/src/node/types/plugin.ts @@ -0,0 +1,9 @@ +import type { Vitest } from '../core' +import type { TestProject } from '../project' +import type { TestProjectConfiguration } from './config' + +export interface VitestPluginContext { + vitest: Vitest + project: TestProject + injectTestProjects: (config: TestProjectConfiguration | TestProjectConfiguration[]) => Promise +} diff --git a/packages/vitest/src/node/types/vite.ts b/packages/vitest/src/node/types/vite.ts index 60d9b7764..3de960bce 100644 --- a/packages/vitest/src/node/types/vite.ts +++ b/packages/vitest/src/node/types/vite.ts @@ -1,4 +1,8 @@ +/* eslint-disable unused-imports/no-unused-vars */ + +import type { HookHandler } from 'vite' import type { InlineConfig } from './config' +import type { VitestPluginContext } from './plugin' type VitestInlineConfig = InlineConfig @@ -9,6 +13,10 @@ declare module 'vite' { */ test?: VitestInlineConfig } + + interface Plugin { + configureVitest?: HookHandler<(context: VitestPluginContext) => void> + } } export {} diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index fb516a8b3..2d7ec50f2 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -19,6 +19,7 @@ export async function resolveWorkspace( cliOptions: UserConfig, workspaceConfigPath: string | undefined, workspaceDefinition: TestProjectConfiguration[], + names: Set, ): Promise { const { configFiles, projectConfigs, nonConfigDirectories } = await resolveTestProjectConfigs( vitest, @@ -114,7 +115,6 @@ export async function resolveWorkspace( } const resolvedProjectsPromises = await Promise.allSettled(projectPromises) - const names = new Set() const errors: Error[] = [] const resolvedProjects: TestProject[] = [] @@ -201,11 +201,11 @@ export async function resolveBrowserWorkspace( } const originalName = project.config.name // if original name is in the --project=name filter, keep all instances - const filteredInstances = !vitest._projectFilters.length || vitest._matchesProjectFilter(originalName) + const filteredInstances = vitest.matchesProjectFilter(originalName) ? instances : instances.filter((instance) => { const newName = instance.name! // name is set in "workspace" plugin - return vitest._matchesProjectFilter(newName) + return vitest.matchesProjectFilter(newName) }) // every project was filtered out @@ -460,7 +460,7 @@ export function getDefaultTestProject(vitest: Vitest): TestProject | null { } // check for the project name and browser names const hasProjects = getPotentialProjectNames(project).some(p => - vitest._matchesProjectFilter(p), + vitest.matchesProjectFilter(p), ) if (hasProjects) { return project diff --git a/packages/vitest/src/public/config.ts b/packages/vitest/src/public/config.ts index fd906108c..6b7374d52 100644 --- a/packages/vitest/src/public/config.ts +++ b/packages/vitest/src/public/config.ts @@ -1,6 +1,13 @@ import type { ConfigEnv, UserConfig as ViteUserConfig } from 'vite' -import type { TestProjectConfiguration, UserProjectConfigExport, UserProjectConfigFn, UserWorkspaceConfig, WorkspaceProjectConfiguration } from '../node/types/config' +import type { + TestProjectConfiguration, + TestProjectInlineConfiguration, + UserProjectConfigExport, + UserProjectConfigFn, + UserWorkspaceConfig, + WorkspaceProjectConfiguration, +} from '../node/types/config' import '../node/types/vite' export { extraInlineDeps } from '../constants' @@ -20,7 +27,14 @@ export type { ConfigEnv, ViteUserConfig } * @deprecated Use `ViteUserConfig` instead */ export type UserConfig = ViteUserConfig -export type { TestProjectConfiguration, UserProjectConfigExport, UserProjectConfigFn, UserWorkspaceConfig, WorkspaceProjectConfiguration } +export type { + TestProjectConfiguration, + TestProjectInlineConfiguration, + UserProjectConfigExport, + UserProjectConfigFn, + UserWorkspaceConfig, + WorkspaceProjectConfiguration, +} export type UserConfigFnObject = (env: ConfigEnv) => ViteUserConfig export type UserConfigFnPromise = (env: ConfigEnv) => Promise export type UserConfigFn = ( diff --git a/packages/vitest/src/public/node.ts b/packages/vitest/src/public/node.ts index 716250bb3..b44ddd3fb 100644 --- a/packages/vitest/src/public/node.ts +++ b/packages/vitest/src/public/node.ts @@ -121,6 +121,7 @@ export type { ResolvedCoverageOptions, } from '../node/types/coverage' +export type { VitestPluginContext } from '../node/types/plugin' export type { TestRunResult } from '../node/types/tests' /** * @deprecated Use `TestModule` instead diff --git a/test/config/test/configureVitest.test.ts b/test/config/test/configureVitest.test.ts new file mode 100644 index 000000000..41bd27a4f --- /dev/null +++ b/test/config/test/configureVitest.test.ts @@ -0,0 +1,250 @@ +import type { ViteUserConfig } from 'vitest/config' +import type { TestProject, UserConfig, VitestOptions } from 'vitest/node' +import { expect, onTestFinished, test } from 'vitest' +import { createVitest } from 'vitest/node' + +async function vitest(cliOptions: UserConfig, configValue: UserConfig = {}, viteConfig: ViteUserConfig = {}, vitestOptions: VitestOptions = {}) { + const vitest = await createVitest('test', { ...cliOptions, watch: false }, { ...viteConfig, test: configValue as any }, vitestOptions) + onTestFinished(() => vitest.close()) + return vitest +} + +test('can change global configuration', async () => { + const v = await vitest({}, {}, { + plugins: [ + { + name: 'test', + configureVitest({ vitest }) { + vitest.config.coverage.enabled = true + vitest.config.coverage.exclude = ['**/*'] + vitest.config.setupFiles.push('test/setup.ts') + }, + }, + ], + }) + expect(v.config.coverage.enabled).toBe(true) + expect(v.config.coverage.exclude).toEqual(['**/*']) + // setup is not resolved + expect(v.config.setupFiles).toEqual(['test/setup.ts']) +}) + +test('can change the project and the global configurations', async () => { + const v = await vitest({}, { + workspace: [ + { + plugins: [ + { + name: 'test', + configureVitest({ vitest, project }) { + vitest.config.setupFiles.push('test/setup.ts') + project.config.setupFiles.push('test/project-setup.ts') + }, + }, + ], + }, + ], + }) + + expect(v.config.setupFiles).toEqual(['test/setup.ts']) + const rootProject = v.getRootProject() + + expect(v.projects).toHaveLength(1) + + const project = v.projects[0] + expect(project).not.toBe(rootProject) + expect(project.config.setupFiles).toEqual(['test/project-setup.ts']) +}) + +test('plugin is not called if the project is filtered out', async () => { + const { projects } = await vitest({ + project: 'project-2', + }, { + workspace: [ + { + test: { + name: 'project-1', + }, + plugins: [ + { + name: 'test', + configureVitest() { + expect.unreachable() + }, + }, + ], + }, + { + test: { + name: 'project-2', + }, + }, + ], + }) + expect(projects).toHaveLength(1) + expect(projects[0].name).toBe('project-2') +}) + +test('can inject the plugin', async () => { + let newWorkspace: TestProject[] = [] + const v = await vitest({}, {}, { + plugins: [ + { + name: 'test', + async configureVitest({ injectTestProjects }) { + newWorkspace = await injectTestProjects({ + test: { + name: 'project-1', + }, + }) + }, + }, + ], + }) + expect(v.projects).toHaveLength(2) + // the default project that called configureVitest + expect(v.projects[0].name).toBe('') + expect(v.projects[1].name).toBe('project-1') + + expect(newWorkspace).toHaveLength(1) + expect(newWorkspace[0].name).toBe('project-1') +}) + +test('injected plugin is filtered by the --project filter', async () => { + let newWorkspace: TestProject[] = [] + const { projects } = await vitest({ + project: 'project-1', + workspace: [ + { + test: { + name: 'project-1', + }, + plugins: [ + { + name: 'test', + async configureVitest({ injectTestProjects }) { + newWorkspace = await injectTestProjects({ + test: { + name: 'project-2', + }, + }) + }, + }, + ], + }, + ], + }) + expect(projects).toHaveLength(1) + expect(projects[0].name).toBe('project-1') + + expect(newWorkspace).toHaveLength(0) +}) + +test('injected plugin is not filtered by the --project filter when it\'s overriden', async () => { + let newWorkspace: TestProject[] = [] + const { projects } = await vitest({ + project: 'project-1', + workspace: [ + { + test: { + name: 'project-1', + }, + plugins: [ + { + name: 'test', + async configureVitest({ vitest, injectTestProjects }) { + vitest.config.project.push('project-2') + newWorkspace = await injectTestProjects({ + test: { + name: 'project-2', + }, + }) + }, + }, + ], + }, + ], + }) + expect(projects).toHaveLength(2) + expect(projects[0].name).toBe('project-1') + expect(projects[1].name).toBe('project-2') + + expect(newWorkspace).toHaveLength(1) + expect(newWorkspace[0].name).toBe('project-2') +}) + +test('adding a plugin with existing name throws and error', async () => { + await expect(() => vitest({ + workspace: [ + { + test: { + name: 'project-1', + }, + plugins: [ + { + name: 'test', + async configureVitest({ injectTestProjects }) { + await injectTestProjects({ + test: { + name: 'project-1', + }, + }) + }, + }, + ], + }, + ], + }), + ).rejects.toThrowError('Project name "project-1" is not unique. All projects in a workspace should have unique names. Make sure your configuration is correct.') + + await expect(() => vitest({ + workspace: [ + { + plugins: [ + { + name: 'test', + async configureVitest({ injectTestProjects }) { + await injectTestProjects({ + test: { + name: 'project-1', + }, + }) + await injectTestProjects({ + test: { + name: 'project-1', + }, + }) + }, + }, + ], + }, + ], + }), + ).rejects.toThrowError('Project name "project-1" is not unique. All projects in a workspace should have unique names. Make sure your configuration is correct.') + + await expect(() => vitest({ + workspace: [ + { + plugins: [ + { + name: 'test', + async configureVitest({ injectTestProjects }) { + await injectTestProjects([ + { + test: { + name: 'project-1', + }, + }, + { + test: { + name: 'project-1', + }, + }, + ]) + }, + }, + ], + }, + ], + }), + ).rejects.toThrowError('Project name "project-1" is not unique. All projects in a workspace should have unique names. Make sure your configuration is correct.') +})