From bd8a7dffefb2c0568eec7252091432e08df048e5 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 20 Jan 2025 17:24:11 +0100 Subject: [PATCH 01/21] fix: filter projects eagerly during config resolution --- .../vitest/src/node/config/resolveConfig.ts | 7 +-- packages/vitest/src/node/core.ts | 29 +++++----- packages/vitest/src/node/errors.ts | 8 +++ packages/vitest/src/node/plugins/index.ts | 13 ++++- packages/vitest/src/node/plugins/workspace.ts | 23 +++++++- packages/vitest/src/node/stdin.ts | 3 +- packages/vitest/src/node/types/config.ts | 3 ++ .../src/node/workspace/resolveWorkspace.ts | 54 ++++++++++++++----- test/config/test/browser-configs.test.ts | 33 ++++++++++++ 9 files changed, 138 insertions(+), 35 deletions(-) diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts index a72f44261dc6..f15c25927f3d 100644 --- a/packages/vitest/src/node/config/resolveConfig.ts +++ b/packages/vitest/src/node/config/resolveConfig.ts @@ -231,6 +231,8 @@ export function resolveConfig( } } + resolved.project = toArray(options.project || []).map(project => wildcardPatternToRegExp(project)) + const browser = resolved.browser if (browser.enabled) { @@ -469,7 +471,7 @@ export function resolveConfig( resolved.forceRerunTriggers.push(...resolved.snapshotSerializers) if (options.resolveSnapshotPath) { - delete (resolved as UserConfig).resolveSnapshotPath + delete (resolved as any).resolveSnapshotPath } resolved.pool ??= 'threads' @@ -908,11 +910,10 @@ function isPlaywrightChromiumOnly(config: ResolvedConfig) { if (!browser.instances) { return false } - const filteredProjects = toArray(config.project).map(p => wildcardPatternToRegExp(p)) for (const instance of browser.instances) { const name = instance.name || (config.name ? `${config.name} (${instance.browser})` : instance.browser) // browser config is filtered out - if (filteredProjects.length && !filteredProjects.every(p => p.test(name))) { + if (config.project.length && !config.project.every(p => p.test(name))) { continue } if (instance.browser !== 'chromium') { diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 5b77a4412cb7..3bfd20c6cd4c 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -23,7 +23,6 @@ import { WebSocketReporter } from '../api/setup' import { defaultBrowserPort, workspacesFiles as workspaceFiles } from '../constants' import { getCoverageProvider } from '../integrations/coverage' import { distDir } from '../paths' -import { wildcardPatternToRegExp } from '../utils/base' import { convertTasksToEvents } from '../utils/tasks' import { BrowserSessions } from './browser/sessions' import { VitestCache } from './cache' @@ -90,7 +89,11 @@ export class Vitest { /** @internal */ closingPromise?: Promise /** @internal */ isCancelling = false /** @internal */ coreWorkspaceProject: TestProject | undefined - /** @internal */ resolvedProjects: TestProject[] = [] + /** + * @internal + * @deprecated + */ + resolvedProjects: TestProject[] = [] /** @internal */ _browserLastPort = defaultBrowserPort /** @internal */ _browserSessions = new BrowserSessions() /** @internal */ _options: UserConfig = {} @@ -98,6 +101,7 @@ export class Vitest { /** @internal */ vitenode: ViteNodeServer = undefined! /** @internal */ runner: ViteNodeRunner = undefined! /** @internal */ _testRun: TestRun = undefined! + /** @internal */ _projectFilter: string | undefined private isFirstRun = true private restartsCount = 0 @@ -272,15 +276,15 @@ export class Vitest { const projects = await this.resolveWorkspace(cliOptions) this.resolvedProjects = projects this.projects = projects - const filters = toArray(resolved.project).map(s => wildcardPatternToRegExp(s)) - if (filters.length > 0) { - this.projects = this.projects.filter(p => - filters.some(pattern => pattern.test(p.name)), - ) - if (!this.projects.length) { - throw new Error(`No projects matched the filter "${toArray(resolved.project).join('", "')}".`) - } + // const filters = toArray(resolved.project).map(s => wildcardPatternToRegExp(s)) + // if (filters.length > 0) { + // this.projects = this.projects.filter(p => + // filters.some(pattern => pattern.test(p.name)), + // ) + if (!this.projects.length) { + throw new Error(`No projects matched the filter "${toArray(resolved.project).join('", "')}".`) } + // } if (!this.coreWorkspaceProject) { this.coreWorkspaceProject = TestProject._createBasicProject(this) } @@ -858,12 +862,13 @@ export class Vitest { /** @internal */ async changeProjectName(pattern: string): Promise { if (pattern === '') { - delete this.configOverride.project + delete this._projectFilter } else { - this.configOverride.project = pattern + this._projectFilter = pattern } + // TODO: restart the whole vitest server, not just filter projects this.projects = this.resolvedProjects.filter(p => p.name === pattern) const files = (await this.globTestSpecifications()).map(spec => spec.moduleId) await this.rerunFiles(files, 'change project filter', pattern === '') diff --git a/packages/vitest/src/node/errors.ts b/packages/vitest/src/node/errors.ts index 56dac30d76dc..00db3293cf1c 100644 --- a/packages/vitest/src/node/errors.ts +++ b/packages/vitest/src/node/errors.ts @@ -39,3 +39,11 @@ export class RangeLocationFilterProvidedError extends Error { + `are not supported. Consider specifying the exact line numbers of your tests.`) } } + +export class VitestFilteredOutProjectError extends Error { + code = 'VITEST_FILTERED_OUT_PROJECT' + + constructor() { + super('VITEST_FILTERED_OUT_PROJECT') + } +} diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index 1bc182c542b5..e3cdd5d1d33e 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -63,9 +63,9 @@ export async function VitestPlugin( // store defines for globalThis to make them // reassignable when running in worker in src/runtime/setup.ts - const defines: Record = deleteDefineConfig(viteConfig); + const defines: Record = deleteDefineConfig(viteConfig) - (options as ResolvedConfig).defines = defines + ;(options as unknown as ResolvedConfig).defines = defines let open: string | boolean | undefined = false @@ -73,6 +73,15 @@ export async function VitestPlugin( open = testConfig.uiBase ?? '/__vitest__/' } + const originalName = testConfig.name + const workspaceNames = originalName ? [originalName] : [] + if (testConfig.browser?.enabled && testConfig.browser?.instances) { + testConfig.browser.instances.forEach((instance) => { + instance.name ??= originalName ? `${originalName} (${instance.browser})` : instance.browser + workspaceNames.push(instance.name) + }) + } + const config: ViteConfig = { root: viteConfig.test?.root || options.root, esbuild: diff --git a/packages/vitest/src/node/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts index a3e91bdcf29e..19d4f9d4c4e0 100644 --- a/packages/vitest/src/node/plugins/workspace.ts +++ b/packages/vitest/src/node/plugins/workspace.ts @@ -6,6 +6,7 @@ import { deepMerge } from '@vitest/utils' import { basename, dirname, relative, resolve } from 'pathe' import { configDefaults } from '../../defaults' import { generateScopedClassName } from '../../integrations/css/css-modules' +import { VitestFilteredOutProjectError } from '../errors' import { createViteLogger, silenceImportViteIgnoreWarning } from '../viteLogger' import { CoverageTransform } from './coverageTransform' import { CSSEnablerPlugin } from './cssEnabler' @@ -62,6 +63,24 @@ export function WorkspaceVitestPlugin( } } + const workspaceNames = [name] + if (viteConfig.test?.browser?.enabled && viteConfig.test?.browser?.instances) { + viteConfig.test.browser.instances.forEach((instance) => { + instance.name ??= name ? `${name} (${instance.browser})` : instance.browser + workspaceNames.push(instance.name) + }) + } + + const filters = project.vitest.config.project + if (filters.length) { + const filteredNames = workspaceNames.filter((name) => { + return filters.some(n => n.test(name)) + }) + if (!filteredNames.length) { + throw new VitestFilteredOutProjectError() + } + } + const config: ViteConfig = { root, resolve: { @@ -92,7 +111,7 @@ export function WorkspaceVitestPlugin( fs: { allow: resolveFsAllow( project.vitest.config.root, - project.vitest.server.config.configFile, + project.vitest.vite.config.configFile, ), }, }, @@ -138,7 +157,7 @@ export function WorkspaceVitestPlugin( } } config.customLogger = createViteLogger( - project.logger, + project.vitest.logger, viteConfig.logLevel || 'warn', { allowClearScreen: false, diff --git a/packages/vitest/src/node/stdin.ts b/packages/vitest/src/node/stdin.ts index 7b66529a6ace..cdb8e30ae052 100644 --- a/packages/vitest/src/node/stdin.ts +++ b/packages/vitest/src/node/stdin.ts @@ -2,7 +2,6 @@ import type { Writable } from 'node:stream' import type { Vitest } from './core' import readline from 'node:readline' import { getTests } from '@vitest/runner/utils' -import { toArray } from '@vitest/utils' import { relative, resolve } from 'pathe' import prompt from 'prompts' import c from 'tinyrainbow' @@ -182,7 +181,7 @@ export function registerConsoleShortcuts( name: 'filter', type: 'text', message: 'Input a single project name', - initial: toArray(ctx.configOverride.project)[0] || '', + initial: ctx._projectFilter || '', }, ]) on() diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index 0f556b99fce2..bbc2a1c4432c 100644 --- a/packages/vitest/src/node/types/config.ts +++ b/packages/vitest/src/node/types/config.ts @@ -965,6 +965,7 @@ export interface UserConfig extends InlineConfig { export interface ResolvedConfig extends Omit< Required, + | 'project' | 'config' | 'filters' | 'browser' @@ -1016,6 +1017,8 @@ export interface ResolvedConfig api?: ApiConfig cliExclude?: string[] + project: RegExp[] + benchmark?: Required< Omit > & diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index f6a31b3cd716..2891bfa6f6bb 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -3,13 +3,13 @@ import type { BrowserInstanceOption, ResolvedConfig, TestProjectConfiguration, U import { existsSync, promises as fs } from 'node:fs' import os from 'node:os' import { limitConcurrency } from '@vitest/runner/utils' -import { deepClone, toArray } from '@vitest/utils' +import { deepClone } from '@vitest/utils' import fg from 'fast-glob' import { dirname, relative, resolve } from 'pathe' import { mergeConfig } from 'vite' import { configFiles as defaultConfigFiles } from '../../constants' -import { wildcardPatternToRegExp } from '../../utils/base' import { isTTY } from '../../utils/env' +import { VitestFilteredOutProjectError } from '../errors' import { initializeProject, TestProject } from '../project' import { withLabel } from '../reporters/renderers/utils' import { isDynamicPattern } from './fast-glob-pattern' @@ -80,7 +80,13 @@ export async function resolveWorkspace( for (const path of fileProjects) { // if file leads to the root config, then we can just reuse it because we already initialized it if (vitest.vite.config.configFile === path) { - projectPromises.push(Promise.resolve(vitest._ensureRootProject())) + if ( + !vitest.config.project.length + // only include the root project if it wasn't filtered out + || vitest.config.project.some(pattern => pattern.test(vitest.config.name || '')) + ) { + projectPromises.push(Promise.resolve(vitest._ensureRootProject())) + } continue } @@ -101,9 +107,29 @@ export async function resolveWorkspace( return resolveBrowserWorkspace(vitest, new Set(), [vitest._ensureRootProject()]) } - const resolvedProjects = await Promise.all(projectPromises) + const resolvedProjectsPromises = await Promise.allSettled(projectPromises) const names = new Set() + const errors: Error[] = [] + const resolvedProjects: TestProject[] = [] + + for (const result of resolvedProjectsPromises) { + if (result.status === 'rejected') { + if (result.reason instanceof VitestFilteredOutProjectError) { + // filter out filtered out projects + continue + } + errors.push(result.reason) + } + else { + resolvedProjects.push(result.value) + } + } + + if (errors.length) { + throw new AggregateError(errors, 'Failed to initialize projects') + } + // project names are guaranteed to be unique for (const project of resolvedProjects) { const name = project.name @@ -136,7 +162,7 @@ export async function resolveBrowserWorkspace( names: Set, resolvedProjects: TestProject[], ) { - const filters = toArray(vitest.config.project).map(s => wildcardPatternToRegExp(s)) + const filters = vitest.config.project const removeProjects = new Set() resolvedProjects.forEach((project) => { @@ -162,16 +188,17 @@ export async function resolveBrowserWorkspace( ) } const originalName = project.config.name - const filteredConfigs = !filters.length + // if original name is in the --project=name filter, keep all instances + const filteredConfigs = !filters.length || filters.some(pattern => pattern.test(originalName)) ? configs : configs.filter((config) => { - const browser = config.browser - const newName = config.name || (originalName ? `${originalName} (${browser})` : browser) + const newName = config.name! // name is set in "workspace" plugin return filters.some(pattern => pattern.test(newName)) }) // every project was filtered out if (!filteredConfigs.length) { + removeProjects.add(project) return } @@ -188,21 +215,20 @@ export async function resolveBrowserWorkspace( const ending = nth === 2 ? 'nd' : nth === 3 ? 'rd' : 'th' throw new Error(`The browser configuration must have a "browser" property. The ${nth}${ending} item in "browser.instances" doesn't have it. Make sure your${originalName ? ` "${originalName}"` : ''} configuration is correct.`) } - const name = config.name - const newName = name || (originalName ? `${originalName} (${browser})` : browser) + const name = config.name! - if (names.has(newName)) { + if (names.has(name)) { throw new Error( [ - `Cannot define a nested project for a ${browser} browser. The project name "${newName}" was already defined. `, + `Cannot define a nested project for a ${browser} browser. The project name "${name}" was already defined. `, 'If you have multiple instances for the same browser, make sure to define a custom "name". ', 'All projects in a workspace should have unique names. Make sure your configuration is correct.', ].join(''), ) } - names.add(newName) + names.add(name) const clonedConfig = cloneConfig(project, config) - clonedConfig.name = newName + clonedConfig.name = name const clone = TestProject._cloneBrowserProject(project, clonedConfig) resolvedProjects.push(clone) }) diff --git a/test/config/test/browser-configs.test.ts b/test/config/test/browser-configs.test.ts index de0ad210685a..f6eedff8f90c 100644 --- a/test/config/test/browser-configs.test.ts +++ b/test/config/test/browser-configs.test.ts @@ -198,6 +198,39 @@ test('coverage provider v8 works correctly in browser mode if instances are filt ]) }) +test.only('filter for the global browser project includes all browser instances', async () => { + const { projects } = await vitest({ + project: 'myproject', + workspace: [ + { + test: { + name: 'myproject', + browser: { + enabled: true, + provider: 'playwright', + headless: true, + instances: [ + { browser: 'chromium' }, + { browser: 'firefox' }, + { browser: 'webkit' }, + ], + }, + }, + }, + { + test: { + name: 'skip', + }, + }, + ], + }) + expect(projects.map(p => p.name)).toEqual([ + 'myproject (chromium)', + 'myproject (firefox)', + 'myproject (webkit)', + ]) +}) + test('can enable browser-cli options for multi-project workspace', async () => { const { projects } = await vitest( { From 570e4031e9e3b6f6caefcd10117a67cab221395e Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 20 Jan 2025 17:28:00 +0100 Subject: [PATCH 02/21] chore: cleanup --- packages/vitest/src/node/workspace/resolveWorkspace.ts | 2 ++ test/config/test/browser-configs.test.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 2891bfa6f6bb..1b1a9bed0fb4 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -83,6 +83,8 @@ export async function resolveWorkspace( if ( !vitest.config.project.length // only include the root project if it wasn't filtered out + // TODO: browser.instances might not be filtered out -- test it out + // workspaces: [{ extends: true }] || vitest.config.project.some(pattern => pattern.test(vitest.config.name || '')) ) { projectPromises.push(Promise.resolve(vitest._ensureRootProject())) diff --git a/test/config/test/browser-configs.test.ts b/test/config/test/browser-configs.test.ts index f6eedff8f90c..5cd9ba3f1adc 100644 --- a/test/config/test/browser-configs.test.ts +++ b/test/config/test/browser-configs.test.ts @@ -198,7 +198,7 @@ test('coverage provider v8 works correctly in browser mode if instances are filt ]) }) -test.only('filter for the global browser project includes all browser instances', async () => { +test('filter for the global browser project includes all browser instances', async () => { const { projects } = await vitest({ project: 'myproject', workspace: [ From 48209a6fba814fa02f95b108fb2114aa6d901541 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 20 Jan 2025 17:54:54 +0100 Subject: [PATCH 03/21] chore: cleanup --- packages/browser/src/node/pool.ts | 3 +++ packages/vitest/src/node/plugins/index.ts | 20 +++++++++---------- packages/vitest/src/node/plugins/workspace.ts | 9 +++++++-- .../src/node/workspace/resolveWorkspace.ts | 6 +++++- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/browser/src/node/pool.ts b/packages/browser/src/node/pool.ts index b39a52b3cfa3..b4768373822b 100644 --- a/packages/browser/src/node/pool.ts +++ b/packages/browser/src/node/pool.ts @@ -133,6 +133,9 @@ export function createBrowserPool(vitest: Vitest): ProcessPool { } await project._initBrowserProvider() + if (!project.browser) { + throw new TypeError(`The browser server was not initialized for the ${project.name} project. This is a bug in Vitest. Please, open a new issue with reproduction.`) + } await executeTests(method, project, files) } } diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index e3cdd5d1d33e..f7bef65ea4ca 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -73,15 +73,6 @@ export async function VitestPlugin( open = testConfig.uiBase ?? '/__vitest__/' } - const originalName = testConfig.name - const workspaceNames = originalName ? [originalName] : [] - if (testConfig.browser?.enabled && testConfig.browser?.instances) { - testConfig.browser.instances.forEach((instance) => { - instance.name ??= originalName ? `${originalName} (${instance.browser})` : instance.browser - workspaceNames.push(instance.name) - }) - } - const config: ViteConfig = { root: viteConfig.test?.root || options.root, esbuild: @@ -226,9 +217,9 @@ export async function VitestPlugin( return config }, async configResolved(viteConfig) { - const viteConfigTest = (viteConfig.test as any) || {} + const viteConfigTest = (viteConfig.test as UserConfig) || {} if (viteConfigTest.watch === false) { - viteConfigTest.run = true + ;(viteConfigTest as any).run = true } if ('alias' in viteConfigTest) { @@ -264,6 +255,13 @@ export async function VitestPlugin( enumerable: false, configurable: true, }) + + const originalName = viteConfigTest.name + if (viteConfigTest.browser?.enabled && viteConfigTest.browser?.instances) { + viteConfigTest.browser.instances.forEach((instance) => { + instance.name ??= originalName ? `${originalName} (${instance.browser})` : instance.browser + }) + } }, configureServer: { // runs after vite:import-analysis as it relies on `server` instance on Vite 5 diff --git a/packages/vitest/src/node/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts index 19d4f9d4c4e0..ad8d9f3e7307 100644 --- a/packages/vitest/src/node/plugins/workspace.ts +++ b/packages/vitest/src/node/plugins/workspace.ts @@ -64,8 +64,13 @@ export function WorkspaceVitestPlugin( } const workspaceNames = [name] - if (viteConfig.test?.browser?.enabled && viteConfig.test?.browser?.instances) { - viteConfig.test.browser.instances.forEach((instance) => { + if (viteConfig.test?.browser?.enabled) { + if (viteConfig.test.browser.name) { + const browser = viteConfig.test.browser.name + workspaceNames.push(name ? `${name} (${browser})` : browser) + } + + viteConfig.test.browser.instances?.forEach((instance) => { instance.name ??= name ? `${name} (${instance.browser})` : instance.browser workspaceNames.push(instance.name) }) diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 1b1a9bed0fb4..0da4ea5c02f3 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -173,8 +173,12 @@ export async function resolveBrowserWorkspace( } const configs = project.config.browser.instances || [] if (configs.length === 0) { + const name = project.config.browser.name // browser.name should be defined, otherwise the config fails in "resolveConfig" - configs.push({ browser: project.config.browser.name }) + configs.push({ + browser: name, + name: project.name ? `${project.name} (${name})` : name, + }) console.warn( withLabel( 'yellow', From a9dd4d79bbdc3133e8bd2348f4c7adbb9ac108d7 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 21 Jan 2025 14:55:09 +0100 Subject: [PATCH 04/21] chore: cleanup --- packages/vitest/src/node/core.ts | 11 +++++- .../src/node/workspace/resolveWorkspace.ts | 37 ++++++++++++++----- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 3bfd20c6cd4c..61cc1421e431 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -39,7 +39,7 @@ import { VitestSpecifications } from './specifications' import { StateManager } from './state' import { TestRun } from './test-run' import { VitestWatcher } from './watcher' -import { resolveBrowserWorkspace, resolveWorkspace } from './workspace/resolveWorkspace' +import { getDefaultTestProject, resolveBrowserWorkspace, resolveWorkspace } from './workspace/resolveWorkspace' const WATCHER_DEBOUNCE = 100 @@ -401,8 +401,15 @@ export class Vitest { this._workspaceConfigPath = workspaceConfigPath + // user doesn't have a workspace config, return default project if (!workspaceConfigPath) { - return resolveBrowserWorkspace(this, new Set(), [this._ensureRootProject()]) + // user can filter projects with --project flag, `getDefaultTestProject` + // return the project only if it matches the filter + const project = getDefaultTestProject(this) + if (!project) { + return [] + } + return resolveBrowserWorkspace(this, new Set(), [project]) } const workspaceModule = await this.import<{ diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 0da4ea5c02f3..3c2fca771409 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -80,14 +80,9 @@ export async function resolveWorkspace( for (const path of fileProjects) { // if file leads to the root config, then we can just reuse it because we already initialized it if (vitest.vite.config.configFile === path) { - if ( - !vitest.config.project.length - // only include the root project if it wasn't filtered out - // TODO: browser.instances might not be filtered out -- test it out - // workspaces: [{ extends: true }] - || vitest.config.project.some(pattern => pattern.test(vitest.config.name || '')) - ) { - projectPromises.push(Promise.resolve(vitest._ensureRootProject())) + const project = getDefaultTestProject(vitest) + if (project) { + projectPromises.push(Promise.resolve(project)) } continue } @@ -106,7 +101,7 @@ export async function resolveWorkspace( // pretty rare case - the glob didn't match anything and there are no inline configs if (!projectPromises.length) { - return resolveBrowserWorkspace(vitest, new Set(), [vitest._ensureRootProject()]) + throw new Error(`No projects were found. Make sure your configuration is correct. The workspace: ${JSON.stringify(workspaceDefinition)}`) } const resolvedProjectsPromises = await Promise.allSettled(projectPromises) @@ -443,3 +438,27 @@ async function resolveDirectoryConfig(directory: string) { } return null } + +export function getDefaultTestProject(vitest: Vitest): TestProject | null { + const filter = vitest.config.project + const project = vitest._ensureRootProject() + if (!filter.length) { + return project + } + const hasProjects = getPotentialProjectNames(project).some(p => filter.some(pattern => pattern.test(p))) + if (hasProjects) { + return project + } + return null +} + +function getPotentialProjectNames(project: TestProject) { + const names = [project.name] + if (project.config.browser.instances) { + names.push(...project.config.browser.instances.map(i => i.name!)) + } + else if (project.config.browser.name) { + names.push(project.config.browser.name) + } + return names +} From 8c28185a64b14b56ee18447e29b06f337e5f8878 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 21 Jan 2025 15:44:12 +0100 Subject: [PATCH 05/21] chore: improve error message --- packages/browser/src/node/pool.ts | 2 +- packages/vitest/src/node/core.ts | 8 +------ .../src/node/workspace/resolveWorkspace.ts | 13 ++++++++++-- .../workspace/config-empty/vitest.config.js | 3 +++ test/config/test/workspace.test.ts | 21 +++++++++++++++++++ 5 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 test/config/fixtures/workspace/config-empty/vitest.config.js diff --git a/packages/browser/src/node/pool.ts b/packages/browser/src/node/pool.ts index b4768373822b..bca377c0d609 100644 --- a/packages/browser/src/node/pool.ts +++ b/packages/browser/src/node/pool.ts @@ -134,7 +134,7 @@ export function createBrowserPool(vitest: Vitest): ProcessPool { await project._initBrowserProvider() if (!project.browser) { - throw new TypeError(`The browser server was not initialized for the ${project.name} project. This is a bug in Vitest. Please, open a new issue with reproduction.`) + throw new TypeError(`The browser server was not initialized${project.name ? ` for the "${project.name}" project` : ''}. This is a bug in Vitest. Please, open a new issue with reproduction.`) } await executeTests(method, project, files) } diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 61cc1421e431..1482dd741c6e 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -276,15 +276,9 @@ export class Vitest { const projects = await this.resolveWorkspace(cliOptions) this.resolvedProjects = projects this.projects = projects - // const filters = toArray(resolved.project).map(s => wildcardPatternToRegExp(s)) - // if (filters.length > 0) { - // this.projects = this.projects.filter(p => - // filters.some(pattern => pattern.test(p.name)), - // ) if (!this.projects.length) { throw new Error(`No projects matched the filter "${toArray(resolved.project).join('", "')}".`) } - // } if (!this.coreWorkspaceProject) { this.coreWorkspaceProject = TestProject._createBasicProject(this) } @@ -404,7 +398,7 @@ export class Vitest { // user doesn't have a workspace config, return default project if (!workspaceConfigPath) { // user can filter projects with --project flag, `getDefaultTestProject` - // return the project only if it matches the filter + // returns the project only if it matches the filter const project = getDefaultTestProject(this) if (!project) { return [] diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 3c2fca771409..1f5cc43c52e6 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -101,7 +101,13 @@ export async function resolveWorkspace( // pretty rare case - the glob didn't match anything and there are no inline configs if (!projectPromises.length) { - throw new Error(`No projects were found. Make sure your configuration is correct. The workspace: ${JSON.stringify(workspaceDefinition)}`) + throw new Error( + [ + 'No projects were found. Make sure your configuration is correct. ', + vitest.config.project.length ? `The filter matched no projects: ${vitest.config.project.map(p => p.toString()).join(', ')}. ` : '', + `The workspace: ${JSON.stringify(workspaceDefinition, null, 4)}.`, + ].join(''), + ) } const resolvedProjectsPromises = await Promise.allSettled(projectPromises) @@ -445,7 +451,10 @@ export function getDefaultTestProject(vitest: Vitest): TestProject | null { if (!filter.length) { return project } - const hasProjects = getPotentialProjectNames(project).some(p => filter.some(pattern => pattern.test(p))) + // check for the project name and browser names + const hasProjects = getPotentialProjectNames(project).some(p => + filter.some(pattern => pattern.test(p)), + ) if (hasProjects) { return project } diff --git a/test/config/fixtures/workspace/config-empty/vitest.config.js b/test/config/fixtures/workspace/config-empty/vitest.config.js new file mode 100644 index 000000000000..f5913ebd5a39 --- /dev/null +++ b/test/config/fixtures/workspace/config-empty/vitest.config.js @@ -0,0 +1,3 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({}) diff --git a/test/config/test/workspace.test.ts b/test/config/test/workspace.test.ts index 15ec6ac35faf..6f5d73421a07 100644 --- a/test/config/test/workspace.test.ts +++ b/test/config/test/workspace.test.ts @@ -134,3 +134,24 @@ it('correctly inherits the root config', async () => { expect(stderr).toBe('') expect(stdout).toContain('repro.test.js > importing a virtual module') }) + +it('fails if workspace is empty', async () => { + const { stderr } = await runVitest({ + workspace: [], + }) + expect(stderr).toContain('No projects were found. Make sure your configuration is correct. The workspace: [].') +}) + +it('fails if workspace is filtered by the project', async () => { + const { stderr } = await runVitest({ + project: 'non-existing', + root: 'fixtures/workspace/config-empty', + config: './vitest.config.js', + workspace: [ + './vitest.config.js', + ], + }) + expect(stderr).toContain(`No projects were found. Make sure your configuration is correct. The filter matched no projects: /^non-existing$/i. The workspace: [ + "./vitest.config.js" +].`) +}) From 79b6d78a4ed7f58906b924ece0c917280d88fb0b Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 21 Jan 2025 15:51:27 +0100 Subject: [PATCH 06/21] chore: fix glob --- test/cli/fixtures/git-changed/workspace/vitest.workspace.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cli/fixtures/git-changed/workspace/vitest.workspace.js b/test/cli/fixtures/git-changed/workspace/vitest.workspace.js index f78425f85b00..f63e728f92e2 100644 --- a/test/cli/fixtures/git-changed/workspace/vitest.workspace.js +++ b/test/cli/fixtures/git-changed/workspace/vitest.workspace.js @@ -1,3 +1,3 @@ export default [ - "packages/*/vitest.config.js", + "packages/*/vitest.config.mjs", ]; From bad745f392509507c0db45a17bd1c86ebed9df13 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 21 Jan 2025 16:01:34 +0100 Subject: [PATCH 07/21] fix: restart server on project name change --- packages/vitest/src/node/core.ts | 5 +---- packages/vitest/src/node/plugins/index.ts | 5 +++++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 1482dd741c6e..0879e0be1b94 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -869,10 +869,7 @@ export class Vitest { this._projectFilter = pattern } - // TODO: restart the whole vitest server, not just filter projects - this.projects = this.resolvedProjects.filter(p => p.name === pattern) - const files = (await this.globTestSpecifications()).map(spec => spec.moduleId) - await this.rerunFiles(files, 'change project filter', pattern === '') + await this.vite.restart() } /** @internal */ diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index f7bef65ea4ca..549d13293de7 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -145,6 +145,11 @@ export async function VitestPlugin( }, } + if (ctx._projectFilter) { + // project filter was set by the user, so we need to filter the project + options.project = ctx._projectFilter + } + config.customLogger = createViteLogger( ctx.logger, viteConfig.logLevel || 'warn', From 96a609ff942e57c943a3d3e7d40903210c98c1ae Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 21 Jan 2025 16:11:12 +0100 Subject: [PATCH 08/21] fix: correctly set the name for browser instances --- packages/vitest/src/node/plugins/index.ts | 4 ++-- .../vitest/src/node/workspace/resolveWorkspace.ts | 15 +++++++++------ test/browser/specs/browser-crash.test.ts | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index 549d13293de7..35eda71c5aa0 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -262,8 +262,8 @@ export async function VitestPlugin( }) const originalName = viteConfigTest.name - if (viteConfigTest.browser?.enabled && viteConfigTest.browser?.instances) { - viteConfigTest.browser.instances.forEach((instance) => { + if (options.browser?.enabled && options.browser?.instances) { + options.browser.instances.forEach((instance) => { instance.name ??= originalName ? `${originalName} (${instance.browser})` : instance.browser }) } diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 1f5cc43c52e6..85fce33022c5 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -172,11 +172,11 @@ export async function resolveBrowserWorkspace( if (!project.config.browser.enabled) { return } - const configs = project.config.browser.instances || [] - if (configs.length === 0) { + const instances = project.config.browser.instances || [] + if (instances.length === 0) { const name = project.config.browser.name // browser.name should be defined, otherwise the config fails in "resolveConfig" - configs.push({ + instances.push({ browser: name, name: project.name ? `${project.name} (${name})` : name, }) @@ -197,8 +197,8 @@ export async function resolveBrowserWorkspace( const originalName = project.config.name // if original name is in the --project=name filter, keep all instances const filteredConfigs = !filters.length || filters.some(pattern => pattern.test(originalName)) - ? configs - : configs.filter((config) => { + ? instances + : instances.filter((config) => { const newName = config.name! // name is set in "workspace" plugin return filters.some(pattern => pattern.test(newName)) }) @@ -224,6 +224,10 @@ export async function resolveBrowserWorkspace( } const name = config.name! + if (name == null) { + throw new Error(`The browser configuration must have a "name" property. This is a bug in Vitest. Please, open a new issue with reproduction`) + } + if (names.has(name)) { throw new Error( [ @@ -235,7 +239,6 @@ export async function resolveBrowserWorkspace( } names.add(name) const clonedConfig = cloneConfig(project, config) - clonedConfig.name = name const clone = TestProject._cloneBrowserProject(project, clonedConfig) resolvedProjects.push(clone) }) diff --git a/test/browser/specs/browser-crash.test.ts b/test/browser/specs/browser-crash.test.ts index 76428b0e23f5..89d0b950e64e 100644 --- a/test/browser/specs/browser-crash.test.ts +++ b/test/browser/specs/browser-crash.test.ts @@ -13,5 +13,5 @@ test.runIf(provider === 'playwright')('fails gracefully when browser crashes', a }, }) - expect(stderr).contains('Page crashed when executing tests') + expect(stderr).toContain('Page crashed when executing tests') }) From 0fdaf8fe05d6cf38dbfb1aeb95130a6500e22cc2 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 21 Jan 2025 16:29:22 +0100 Subject: [PATCH 09/21] fix: correctly inject name --- .../src/node/workspace/resolveWorkspace.ts | 17 +++++++++-------- test/cli/test/list.test.ts | 11 ++++++----- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 85fce33022c5..2569b042b93f 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -174,11 +174,11 @@ export async function resolveBrowserWorkspace( } const instances = project.config.browser.instances || [] if (instances.length === 0) { - const name = project.config.browser.name + const browser = project.config.browser.name // browser.name should be defined, otherwise the config fails in "resolveConfig" instances.push({ - browser: name, - name: project.name ? `${project.name} (${name})` : name, + browser, + name: project.name ? `${project.name} (${browser})` : browser, }) console.warn( withLabel( @@ -196,15 +196,15 @@ export async function resolveBrowserWorkspace( } const originalName = project.config.name // if original name is in the --project=name filter, keep all instances - const filteredConfigs = !filters.length || filters.some(pattern => pattern.test(originalName)) + const filteredInstances = !filters.length || filters.some(pattern => pattern.test(originalName)) ? instances - : instances.filter((config) => { - const newName = config.name! // name is set in "workspace" plugin + : instances.filter((instance) => { + const newName = instance.name! // name is set in "workspace" plugin return filters.some(pattern => pattern.test(newName)) }) // every project was filtered out - if (!filteredConfigs.length) { + if (!filteredInstances.length) { removeProjects.add(project) return } @@ -215,7 +215,7 @@ export async function resolveBrowserWorkspace( ) } - filteredConfigs.forEach((config, index) => { + filteredInstances.forEach((config, index) => { const browser = config.browser if (!browser) { const nth = index + 1 @@ -239,6 +239,7 @@ export async function resolveBrowserWorkspace( } names.add(name) const clonedConfig = cloneConfig(project, config) + clonedConfig.name = name const clone = TestProject._cloneBrowserProject(project, clonedConfig) resolvedProjects.push(clone) }) diff --git a/test/cli/test/list.test.ts b/test/cli/test/list.test.ts index 545e0ac8d1fe..848dd2260f66 100644 --- a/test/cli/test/list.test.ts +++ b/test/cli/test/list.test.ts @@ -2,13 +2,14 @@ import { readFileSync, rmSync } from 'node:fs' import { expect, onTestFinished, test } from 'vitest' import { runVitestCli } from '../../test-utils' -test.each([ - ['--pool=threads'], - ['--pool=forks'], - ['--pool=vmForks'], +test.only.each([ + // ['--pool=threads'], + // ['--pool=forks'], + // ['--pool=vmForks'], ['--browser.enabled'], ])('correctly outputs all tests with args: "%s"', async (...args) => { - const { stdout, exitCode } = await runVitestCli('list', '-r=./fixtures/list', ...args) + const { stdout, stderr, exitCode } = await runVitestCli('list', '-r=./fixtures/list', ...args) + console.log(stderr) expect(stdout).toMatchSnapshot() expect(exitCode).toBe(0) }) From 4eea6be78e121454a989a6cc45f919c894df3be4 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 21 Jan 2025 16:34:53 +0100 Subject: [PATCH 10/21] test: cleanup --- test/cli/test/list.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/cli/test/list.test.ts b/test/cli/test/list.test.ts index 848dd2260f66..26489b797c0f 100644 --- a/test/cli/test/list.test.ts +++ b/test/cli/test/list.test.ts @@ -2,10 +2,10 @@ import { readFileSync, rmSync } from 'node:fs' import { expect, onTestFinished, test } from 'vitest' import { runVitestCli } from '../../test-utils' -test.only.each([ - // ['--pool=threads'], - // ['--pool=forks'], - // ['--pool=vmForks'], +test.each([ + ['--pool=threads'], + ['--pool=forks'], + ['--pool=vmForks'], ['--browser.enabled'], ])('correctly outputs all tests with args: "%s"', async (...args) => { const { stdout, stderr, exitCode } = await runVitestCli('list', '-r=./fixtures/list', ...args) From e8f22512ad8ba80c1af436002ba684145c924660 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 21 Jan 2025 16:35:03 +0100 Subject: [PATCH 11/21] chore: cleanup --- test/cli/test/list.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/cli/test/list.test.ts b/test/cli/test/list.test.ts index 26489b797c0f..545e0ac8d1fe 100644 --- a/test/cli/test/list.test.ts +++ b/test/cli/test/list.test.ts @@ -8,8 +8,7 @@ test.each([ ['--pool=vmForks'], ['--browser.enabled'], ])('correctly outputs all tests with args: "%s"', async (...args) => { - const { stdout, stderr, exitCode } = await runVitestCli('list', '-r=./fixtures/list', ...args) - console.log(stderr) + const { stdout, exitCode } = await runVitestCli('list', '-r=./fixtures/list', ...args) expect(stdout).toMatchSnapshot() expect(exitCode).toBe(0) }) From dde22a61459fc750fd8ed9c9dc40911ffde8cfe8 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 21 Jan 2025 16:41:49 +0100 Subject: [PATCH 12/21] chore: add comment --- packages/vitest/src/node/plugins/workspace.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/vitest/src/node/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts index ad8d9f3e7307..debbb7e5d130 100644 --- a/packages/vitest/src/node/plugins/workspace.ts +++ b/packages/vitest/src/node/plugins/workspace.ts @@ -63,20 +63,26 @@ export function WorkspaceVitestPlugin( } } + // keep project names to potentially filter it out const workspaceNames = [name] if (viteConfig.test?.browser?.enabled) { if (viteConfig.test.browser.name) { const browser = viteConfig.test.browser.name + // vitest injects `instances` in this case later on workspaceNames.push(name ? `${name} (${browser})` : browser) } viteConfig.test.browser.instances?.forEach((instance) => { + // every instance is a potential project instance.name ??= name ? `${name} (${instance.browser})` : instance.browser workspaceNames.push(instance.name) }) } const filters = project.vitest.config.project + // if there is `--project=...` filter, check if any of the potential projects match + // if projects don't match, we ignore the test project altogether + // if some of them match, they will later be filtered again by `resolveWorkspace` if (filters.length) { const filteredNames = workspaceNames.filter((name) => { return filters.some(n => n.test(name)) From a30a5d47b8398ecc38b1410e765fecfba401f603 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 21 Jan 2025 16:42:59 +0100 Subject: [PATCH 13/21] chore: cleanup error msg --- packages/vitest/src/node/config/resolveConfig.ts | 2 +- test/config/test/failures.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts index f15c25927f3d..352a57f5d3fc 100644 --- a/packages/vitest/src/node/config/resolveConfig.ts +++ b/packages/vitest/src/node/config/resolveConfig.ts @@ -266,7 +266,7 @@ export function resolveConfig( browser: { provider: browser.provider, name: browser.name, - instances: browser.instances, + instances: browser.instances?.map(i => ({ browser: i.browser })), }, } diff --git a/test/config/test/failures.test.ts b/test/config/test/failures.test.ts index fd453927dc88..1e9d34a87475 100644 --- a/test/config/test/failures.test.ts +++ b/test/config/test/failures.test.ts @@ -175,7 +175,7 @@ Use either: } }) -test('v8 coverage provider throws when not playwright + chromium (browser.instances)', async () => { +test.only('v8 coverage provider throws when not playwright + chromium (browser.instances)', async () => { for (const { provider, name } of browsers) { if (provider === 'playwright' && name === 'chromium') { continue From e5de674885a8858b954e5c92c0cc5d3ce80c3050 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 21 Jan 2025 16:47:54 +0100 Subject: [PATCH 14/21] test: oops --- test/config/test/failures.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/config/test/failures.test.ts b/test/config/test/failures.test.ts index 1e9d34a87475..fd453927dc88 100644 --- a/test/config/test/failures.test.ts +++ b/test/config/test/failures.test.ts @@ -175,7 +175,7 @@ Use either: } }) -test.only('v8 coverage provider throws when not playwright + chromium (browser.instances)', async () => { +test('v8 coverage provider throws when not playwright + chromium (browser.instances)', async () => { for (const { provider, name } of browsers) { if (provider === 'playwright' && name === 'chromium') { continue From 64b039db202a506baee80cbc7f827a0589d3c7cc Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 22 Jan 2025 14:46:26 +0100 Subject: [PATCH 15/21] refactor: keep project filter in a hidden vitest field --- .../vitest/src/node/config/resolveConfig.ts | 18 +++++------ packages/vitest/src/node/core.ts | 27 +++++++++++++---- packages/vitest/src/node/plugins/index.ts | 6 ++-- .../vitest/src/node/plugins/publicConfig.ts | 3 +- packages/vitest/src/node/project.ts | 3 +- packages/vitest/src/node/stdin.ts | 2 +- packages/vitest/src/node/types/config.ts | 3 +- .../src/node/workspace/resolveWorkspace.ts | 18 ++++++----- test/config/test/failures.test.ts | 30 +++++++++++-------- test/test-utils/index.ts | 5 ++-- 10 files changed, 67 insertions(+), 48 deletions(-) diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts index b4096f01e0b7..05045f7e833e 100644 --- a/packages/vitest/src/node/config/resolveConfig.ts +++ b/packages/vitest/src/node/config/resolveConfig.ts @@ -1,11 +1,10 @@ import type { ResolvedConfig as ResolvedViteConfig } from 'vite' -import type { Logger } from '../logger' +import type { Vitest } from '../core' import type { BenchmarkBuiltinReporters } from '../reporters' import type { ApiConfig, ResolvedConfig, UserConfig, - VitestRunMode, } from '../types/config' import type { BaseCoverageOptions, CoverageReporterWithOptions } from '../types/coverage' import type { BuiltinPool, ForksOptions, PoolOptions, ThreadsOptions } from '../types/pool-options' @@ -20,7 +19,6 @@ import { extraInlineDeps, } from '../../constants' import { benchmarkConfigDefaults, configDefaults } from '../../defaults' -import { wildcardPatternToRegExp } from '../../utils/base' import { isCI, stdProvider } from '../../utils/env' import { getWorkersCountByPercentage } from '../../utils/workers' import { VitestCache } from '../cache' @@ -111,11 +109,12 @@ function resolveInlineWorkerOption(value: string | number): number { } export function resolveConfig( - mode: VitestRunMode, + vitest: Vitest, options: UserConfig, viteConfig: ResolvedViteConfig, - logger: Logger, ): ResolvedConfig { + const mode = vitest.mode + const logger = vitest.logger if (options.dom) { if ( viteConfig.test?.environment != null @@ -142,6 +141,7 @@ export function resolveConfig( mode, } as any as ResolvedConfig + resolved.project = toArray(resolved.project) resolved.provide ??= {} const inspector = resolved.inspect || resolved.inspectBrk @@ -231,8 +231,6 @@ export function resolveConfig( } } - resolved.project = toArray(options.project || []).map(project => wildcardPatternToRegExp(project)) - const browser = resolved.browser if (browser.enabled) { @@ -258,7 +256,7 @@ export function resolveConfig( } } - const playwrightChromiumOnly = isPlaywrightChromiumOnly(resolved) + const playwrightChromiumOnly = isPlaywrightChromiumOnly(vitest, resolved) // Browser-mode "Playwright + Chromium" only features: if (browser.enabled && !playwrightChromiumOnly) { @@ -899,7 +897,7 @@ export function resolveCoverageReporters(configReporters: NonNullable p.test(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 0879e0be1b94..c353b05d911c 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -23,6 +23,7 @@ import { WebSocketReporter } from '../api/setup' import { defaultBrowserPort, workspacesFiles as workspaceFiles } from '../constants' import { getCoverageProvider } from '../integrations/coverage' import { distDir } from '../paths' +import { wildcardPatternToRegExp } from '../utils/base' import { convertTasksToEvents } from '../utils/tasks' import { BrowserSessions } from './browser/sessions' import { VitestCache } from './cache' @@ -101,7 +102,7 @@ export class Vitest { /** @internal */ vitenode: ViteNodeServer = undefined! /** @internal */ runner: ViteNodeRunner = undefined! /** @internal */ _testRun: TestRun = undefined! - /** @internal */ _projectFilter: string | undefined + /** @internal */ _projectFilters: RegExp[] = [] private isFirstRun = true private restartsCount = 0 @@ -215,9 +216,11 @@ export class Vitest { this.specifications.clearCache() this._onUserTestsRerun = [] - const resolved = resolveConfig(this.mode, options, server.config, this.logger) - + this._projectFilters = toArray(options.project || []).map(project => wildcardPatternToRegExp(project)) this._vite = server + + const resolved = resolveConfig(this, options, server.config) + this._config = resolved this._state = new StateManager() this._cache = new VitestCache(this.version) @@ -863,10 +866,12 @@ export class Vitest { /** @internal */ async changeProjectName(pattern: string): Promise { if (pattern === '') { - delete this._projectFilter + this.configOverride.project = undefined + this._projectFilters = [] } else { - this._projectFilter = pattern + this.configOverride.project = [pattern] + this._projectFilters = [wildcardPatternToRegExp(pattern)] } await this.vite.restart() @@ -1250,6 +1255,18 @@ export class Vitest { onAfterSetServer(fn: OnServerRestartHandler): void { this._onSetServer.push(fn) } + + /** + * Check if the project with a given name should be included. + * @internal + */ + _matchesProjectFilter(name: string): boolean { + // no filters applied, any project can be included + if (!this._projectFilters.length) { + return true + } + return this._projectFilters.some(filter => filter.test(name)) + } } function assert(condition: unknown, property: string, name: string = property): asserts condition { diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index 35eda71c5aa0..cd251177cdab 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -145,9 +145,9 @@ export async function VitestPlugin( }, } - if (ctx._projectFilter) { + if (ctx.configOverride.project) { // project filter was set by the user, so we need to filter the project - options.project = ctx._projectFilter + options.project = ctx.configOverride.project } config.customLogger = createViteLogger( @@ -261,7 +261,7 @@ export async function VitestPlugin( configurable: true, }) - const originalName = viteConfigTest.name + const originalName = options.name if (options.browser?.enabled && options.browser?.instances) { options.browser.instances.forEach((instance) => { instance.name ??= originalName ? `${originalName} (${instance.browser})` : instance.browser diff --git a/packages/vitest/src/node/plugins/publicConfig.ts b/packages/vitest/src/node/plugins/publicConfig.ts index 2365a56fbe66..fc34b7accafa 100644 --- a/packages/vitest/src/node/plugins/publicConfig.ts +++ b/packages/vitest/src/node/plugins/publicConfig.ts @@ -45,10 +45,9 @@ export async function resolveConfig( // Reflect just to avoid type error const updatedOptions = Reflect.get(config, '_vitest') as UserConfig const vitestConfig = resolveVitestConfig( - 'test', + vitest, updatedOptions, config, - vitest.logger, ) return { viteConfig: config, diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index e5235cd7d7a2..daf34d32c459 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -577,13 +577,12 @@ export class TestProject { /** @internal */ async _configureServer(options: UserConfig, server: ViteDevServer): Promise { this._config = resolveConfig( - this.vitest.mode, + this.vitest, { ...options, coverage: this.vitest.config.coverage, }, server.config, - this.vitest.logger, ) for (const _providedKey in this.config.provide) { const providedKey = _providedKey as keyof ProvidedContext diff --git a/packages/vitest/src/node/stdin.ts b/packages/vitest/src/node/stdin.ts index cdb8e30ae052..7a6369903b33 100644 --- a/packages/vitest/src/node/stdin.ts +++ b/packages/vitest/src/node/stdin.ts @@ -181,7 +181,7 @@ export function registerConsoleShortcuts( name: 'filter', type: 'text', message: 'Input a single project name', - initial: ctx._projectFilter || '', + initial: ctx.config.project[0] || '', }, ]) on() diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index bbc2a1c4432c..8a8cfb35248a 100644 --- a/packages/vitest/src/node/types/config.ts +++ b/packages/vitest/src/node/types/config.ts @@ -1017,8 +1017,7 @@ export interface ResolvedConfig api?: ApiConfig cliExclude?: string[] - project: RegExp[] - + project: string[] benchmark?: Required< Omit > & diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 2569b042b93f..9150dcc6c732 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -104,7 +104,7 @@ export async function resolveWorkspace( throw new Error( [ 'No projects were found. Make sure your configuration is correct. ', - vitest.config.project.length ? `The filter matched no projects: ${vitest.config.project.map(p => p.toString()).join(', ')}. ` : '', + vitest.config.project.length ? `The filter matched no projects: ${vitest.config.project.join(', ')}. ` : '', `The workspace: ${JSON.stringify(workspaceDefinition, null, 4)}.`, ].join(''), ) @@ -130,7 +130,10 @@ export async function resolveWorkspace( } if (errors.length) { - throw new AggregateError(errors, 'Failed to initialize projects') + throw new AggregateError( + errors, + 'Failed to initialize projects. There were errors during workspace setup. See below for more details.', + ) } // project names are guaranteed to be unique @@ -165,7 +168,6 @@ export async function resolveBrowserWorkspace( names: Set, resolvedProjects: TestProject[], ) { - const filters = vitest.config.project const removeProjects = new Set() resolvedProjects.forEach((project) => { @@ -196,12 +198,12 @@ export async function resolveBrowserWorkspace( } const originalName = project.config.name // if original name is in the --project=name filter, keep all instances - const filteredInstances = !filters.length || filters.some(pattern => pattern.test(originalName)) + const filteredInstances = !vitest._projectFilters.length || vitest._matchesProjectFilter(originalName) ? instances : instances.filter((instance) => { - const newName = instance.name! // name is set in "workspace" plugin - return filters.some(pattern => pattern.test(newName)) - }) + const newName = instance.name! // name is set in "workspace" plugin + return vitest._matchesProjectFilter(newName) + }) // every project was filtered out if (!filteredInstances.length) { @@ -457,7 +459,7 @@ export function getDefaultTestProject(vitest: Vitest): TestProject | null { } // check for the project name and browser names const hasProjects = getPotentialProjectNames(project).some(p => - filter.some(pattern => pattern.test(p)), + vitest._matchesProjectFilter(p), ) if (hasProjects) { return project diff --git a/test/config/test/failures.test.ts b/test/config/test/failures.test.ts index fd453927dc88..a2ac65214a3f 100644 --- a/test/config/test/failures.test.ts +++ b/test/config/test/failures.test.ts @@ -1,7 +1,8 @@ import type { UserConfig } from 'vitest/node' +import type { VitestRunnerCLIOptions } from '../../test-utils' import { normalize, resolve } from 'pathe' -import { beforeEach, expect, test } from 'vitest' +import { beforeEach, expect, test } from 'vitest' import { version } from 'vitest/package.json' import * as testUtils from '../../test-utils' @@ -9,8 +10,8 @@ const providers = ['playwright', 'webdriverio', 'preview'] as const const names = ['edge', 'chromium', 'webkit', 'chrome', 'firefox', 'safari'] as const const browsers = providers.map(provider => names.map(name => ({ name, provider }))).flat() -function runVitest(config: NonNullable & { shard?: any }) { - return testUtils.runVitest({ root: './fixtures/test', ...config }, []) +function runVitest(config: NonNullable & { shard?: any }, runnerOptions?: VitestRunnerCLIOptions) { + return testUtils.runVitest({ root: './fixtures/test', ...config }, [], undefined, {}, runnerOptions) } function runVitestCli(...cliArgs: string[]) { @@ -286,19 +287,22 @@ Use either: }) test('v8 coverage provider cannot be used in workspace without playwright + chromium', async () => { - const { stderr } = await runVitest({ coverage: { enabled: true }, workspace: './fixtures/workspace/browser/workspace-with-browser.ts' }) + const { stderr } = await runVitest({ + coverage: { enabled: true }, + workspace: './fixtures/workspace/browser/workspace-with-browser.ts', + }, { fails: true }) expect(stderr).toMatch( `Error: @vitest/coverage-v8 does not work with -{ - "browser": { - "provider": "webdriverio", - "instances": [ - { - "browser": "chrome" + { + "browser": { + "provider": "webdriverio", + "instances": [ + { + "browser": "chrome" + } + ] } - ] - } -}`, + }`, ) }) diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts index 92bca066d713..a9250df797af 100644 --- a/test/test-utils/index.ts +++ b/test/test-utils/index.ts @@ -6,6 +6,7 @@ import { webcrypto as crypto } from 'node:crypto' import fs from 'node:fs' import { Readable, Writable } from 'node:stream' import { fileURLToPath } from 'node:url' +import { inspect } from 'node:util' import { dirname, resolve } from 'pathe' import { x } from 'tinyexec' import * as tinyrainbow from 'tinyrainbow' @@ -17,7 +18,7 @@ import { Cli } from './cli' // override default colors to disable them in tests Object.assign(tinyrainbow.default, tinyrainbow.getDefaultColors()) -interface VitestRunnerCLIOptions { +export interface VitestRunnerCLIOptions { std?: 'inherit' fails?: boolean preserveAnsi?: boolean @@ -101,7 +102,7 @@ export async function runVitest( console.error(e) } thrown = true - cli.stderr += e.stack + cli.stderr += inspect(e) } finally { exitCode = process.exitCode From dfab725656310065dad1db0fac94ff43288f1569 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 22 Jan 2025 14:50:06 +0100 Subject: [PATCH 16/21] chore: fix check --- packages/vitest/src/node/plugins/workspace.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vitest/src/node/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts index debbb7e5d130..118316717e79 100644 --- a/packages/vitest/src/node/plugins/workspace.ts +++ b/packages/vitest/src/node/plugins/workspace.ts @@ -85,7 +85,7 @@ export function WorkspaceVitestPlugin( // if some of them match, they will later be filtered again by `resolveWorkspace` if (filters.length) { const filteredNames = workspaceNames.filter((name) => { - return filters.some(n => n.test(name)) + return project.vitest._matchesProjectFilter(name) }) if (!filteredNames.length) { throw new VitestFilteredOutProjectError() From eeac4bd34afb9f949f470e64a530f4fff8c8999f Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 22 Jan 2025 15:01:26 +0100 Subject: [PATCH 17/21] test: cleanup --- test/config/test/workspace.test.ts | 2 +- test/core/test/cli-test.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/config/test/workspace.test.ts b/test/config/test/workspace.test.ts index 6f5d73421a07..1e1e190a760e 100644 --- a/test/config/test/workspace.test.ts +++ b/test/config/test/workspace.test.ts @@ -151,7 +151,7 @@ it('fails if workspace is filtered by the project', async () => { './vitest.config.js', ], }) - expect(stderr).toContain(`No projects were found. Make sure your configuration is correct. The filter matched no projects: /^non-existing$/i. The workspace: [ + expect(stderr).toContain(`No projects were found. Make sure your configuration is correct. The filter matched no projects: non-existing. The workspace: [ "./vitest.config.js" ].`) }) diff --git a/test/core/test/cli-test.test.ts b/test/core/test/cli-test.test.ts index 98ad0e2dd12f..f6f1869cc7c4 100644 --- a/test/core/test/cli-test.test.ts +++ b/test/core/test/cli-test.test.ts @@ -292,7 +292,7 @@ test('clearScreen', async () => { clearScreen: viteClearScreen, } const vitestConfig = getCLIOptions(vitestClearScreen) - const config = resolveConfig('test', vitestConfig, viteConfig, undefined as any) + const config = resolveConfig({ logger: undefined, mode: 'test' } as any, vitestConfig, viteConfig) return config.clearScreen }) expect(results).toMatchInlineSnapshot(` From 572a9d847bfd1262641fb61f979e83ead64d15ac Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 22 Jan 2025 15:35:40 +0100 Subject: [PATCH 18/21] test: add workspace test for filtering --- test/config/test/browser-configs.test.ts | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/config/test/browser-configs.test.ts b/test/config/test/browser-configs.test.ts index 5cd9ba3f1adc..827594c034eb 100644 --- a/test/config/test/browser-configs.test.ts +++ b/test/config/test/browser-configs.test.ts @@ -198,6 +198,35 @@ test('coverage provider v8 works correctly in browser mode if instances are filt ]) }) +test('coverage provider v8 works correctly in workspaced browser mode if instances are filtered', async () => { + const { projects } = await vitest({ + project: 'browser (chromium)', + workspace: [ + { + test: { + name: 'browser', + browser: { + enabled: true, + provider: 'playwright', + instances: [ + { browser: 'chromium' }, + { browser: 'firefox' }, + { browser: 'webkit' }, + ], + }, + }, + }, + ], + coverage: { + enabled: true, + provider: 'v8', + }, + }) + expect(projects.map(p => p.name)).toEqual([ + 'browser (chromium)', + ]) +}) + test('filter for the global browser project includes all browser instances', async () => { const { projects } = await vitest({ project: 'myproject', From 07331bab281854f1dcd138ca0778d8bb44c60448 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 22 Jan 2025 15:56:40 +0100 Subject: [PATCH 19/21] fix: don't resolve `node:` and `internal:` in the error stack --- packages/utils/src/source-map.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/utils/src/source-map.ts b/packages/utils/src/source-map.ts index 7182d46e7eb5..8e2a630fe28c 100644 --- a/packages/utils/src/source-map.ts +++ b/packages/utils/src/source-map.ts @@ -167,7 +167,9 @@ export function parseSingleV8Stack(raw: string): ParsedStack | null { } // normalize Windows path (\ -> /) - file = resolve(file) + file = file.startsWith('node:') || file.startsWith('internal:') + ? file + : resolve(file) if (method) { method = method.replace(/__vite_ssr_import_\d+__\./g, '') From 4457ad07679ed88bacc32469b18d48a4b333dcf8 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 22 Jan 2025 15:57:18 +0100 Subject: [PATCH 20/21] test: add test for project change --- test/test-utils/index.ts | 3 +- test/watch/test/change-project.test.ts | 44 ++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 test/watch/test/change-project.test.ts diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts index a9250df797af..f0f2f7550bf1 100644 --- a/test/test-utils/index.ts +++ b/test/test-utils/index.ts @@ -294,13 +294,14 @@ export function useFS(root: string, structure: Record, config?: UserConfig, + options?: VitestRunnerCLIOptions, ) { const root = resolve(process.cwd(), `vitest-test-${crypto.randomUUID()}`) const fs = useFS(root, structure) const vitest = await runVitest({ root, ...config, - }) + }, [], 'test', {}, options) return { fs, root, diff --git a/test/watch/test/change-project.test.ts b/test/watch/test/change-project.test.ts new file mode 100644 index 000000000000..d38ee1544f6d --- /dev/null +++ b/test/watch/test/change-project.test.ts @@ -0,0 +1,44 @@ +import { expect, test } from 'vitest' +import { runInlineTests } from '../../test-utils' + +test('reruns tests when config changes', async () => { + const { vitest, ctx } = await runInlineTests({ + 'vitest.config.ts': ` + + process.stdin.isTTY = true + process.stdin.setRawMode = () => process.stdin + + export default { + test: { + workspace: [ + './project-1', + './project-2', + ], + }, + }`, + 'project-1/vitest.config.ts': { test: { name: 'project-1' } }, + 'project-1/basic-1.test.ts': /* ts */` + import { test } from 'vitest' + test('basic test 1', () => {}) + `, + 'project-2/vitest.config.ts': { test: { name: 'project-2' } }, + 'project-2/basic-2.test.ts': /* ts */` + import { test } from 'vitest' + test('basic test 2', () => {}) + `, + }, { watch: true }) + + await vitest.waitForStdout('Waiting for file changes') + + expect(vitest.stdout).toContain('2 passed') + expect(vitest.stdout).toContain('basic-1.test.ts') + expect(vitest.stdout).toContain('basic-2.test.ts') + vitest.resetOutput() + + await ctx!.changeProjectName('project-2') + + await vitest.waitForStdout('Waiting for file changes') + + expect(vitest.stdout).toContain('1 passed') + expect(vitest.stdout).toContain('basic-2.test.ts') +}) From 86f9758ec1a772cdf3f2a432414a2c88ef32e0aa Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 22 Jan 2025 17:17:22 +0100 Subject: [PATCH 21/21] chore: cleanup --- packages/vitest/src/node/plugins/workspace.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vitest/src/node/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts index 118316717e79..46d4f213ae7f 100644 --- a/packages/vitest/src/node/plugins/workspace.ts +++ b/packages/vitest/src/node/plugins/workspace.ts @@ -84,10 +84,10 @@ export function WorkspaceVitestPlugin( // if projects don't match, we ignore the test project altogether // if some of them match, they will later be filtered again by `resolveWorkspace` if (filters.length) { - const filteredNames = workspaceNames.filter((name) => { + const hasProject = workspaceNames.some((name) => { return project.vitest._matchesProjectFilter(name) }) - if (!filteredNames.length) { + if (!hasProject) { throw new VitestFilteredOutProjectError() } }