diff --git a/packages/browser/src/node/pool.ts b/packages/browser/src/node/pool.ts index b39a52b3cfa3..bca377c0d609 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${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/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, '') diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts index 959c6d625ddd..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 @@ -256,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) { @@ -264,7 +264,7 @@ export function resolveConfig( browser: { provider: browser.provider, name: browser.name, - instances: browser.instances, + instances: browser.instances?.map(i => ({ browser: i.browser })), }, } @@ -469,7 +469,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' @@ -897,7 +897,7 @@ export function resolveCoverageReporters(configReporters: NonNullable 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 (!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 5b77a4412cb7..c353b05d911c 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -40,7 +40,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 @@ -90,7 +90,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 +102,7 @@ export class Vitest { /** @internal */ vitenode: ViteNodeServer = undefined! /** @internal */ runner: ViteNodeRunner = undefined! /** @internal */ _testRun: TestRun = undefined! + /** @internal */ _projectFilters: RegExp[] = [] private isFirstRun = true private restartsCount = 0 @@ -211,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) @@ -272,14 +279,8 @@ 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.projects.length) { + throw new Error(`No projects matched the filter "${toArray(resolved.project).join('", "')}".`) } if (!this.coreWorkspaceProject) { this.coreWorkspaceProject = TestProject._createBasicProject(this) @@ -397,8 +398,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` + // returns 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<{ @@ -858,15 +866,15 @@ export class Vitest { /** @internal */ async changeProjectName(pattern: string): Promise { if (pattern === '') { - delete this.configOverride.project + this.configOverride.project = undefined + this._projectFilters = [] } else { - this.configOverride.project = pattern + this.configOverride.project = [pattern] + this._projectFilters = [wildcardPatternToRegExp(pattern)] } - 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 */ @@ -1247,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/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..cd251177cdab 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 @@ -145,6 +145,11 @@ export async function VitestPlugin( }, } + if (ctx.configOverride.project) { + // project filter was set by the user, so we need to filter the project + options.project = ctx.configOverride.project + } + config.customLogger = createViteLogger( ctx.logger, viteConfig.logLevel || 'warn', @@ -217,9 +222,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) { @@ -255,6 +260,13 @@ export async function VitestPlugin( enumerable: false, configurable: true, }) + + const originalName = options.name + if (options.browser?.enabled && options.browser?.instances) { + options.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/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/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts index a3e91bdcf29e..46d4f213ae7f 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,35 @@ 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 hasProject = workspaceNames.some((name) => { + return project.vitest._matchesProjectFilter(name) + }) + if (!hasProject) { + throw new VitestFilteredOutProjectError() + } + } + const config: ViteConfig = { root, resolve: { @@ -92,7 +122,7 @@ export function WorkspaceVitestPlugin( fs: { allow: resolveFsAllow( project.vitest.config.root, - project.vitest.server.config.configFile, + project.vitest.vite.config.configFile, ), }, }, @@ -138,7 +168,7 @@ export function WorkspaceVitestPlugin( } } config.customLogger = createViteLogger( - project.logger, + project.vitest.logger, viteConfig.logLevel || 'warn', { allowClearScreen: false, 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 7b66529a6ace..7a6369903b33 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.config.project[0] || '', }, ]) on() diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index 0f556b99fce2..8a8cfb35248a 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,7 @@ export interface ResolvedConfig api?: ApiConfig cliExclude?: string[] + project: string[] benchmark?: Required< Omit > & diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index bddd51486549..9150dcc6c732 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,10 @@ 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())) + const project = getDefaultTestProject(vitest) + if (project) { + projectPromises.push(Promise.resolve(project)) + } continue } @@ -98,12 +101,41 @@ 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. ', + vitest.config.project.length ? `The filter matched no projects: ${vitest.config.project.join(', ')}. ` : '', + `The workspace: ${JSON.stringify(workspaceDefinition, null, 4)}.`, + ].join(''), + ) } - 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. There were errors during workspace setup. See below for more details.', + ) + } + // project names are guaranteed to be unique for (const project of resolvedProjects) { const name = project.name @@ -136,17 +168,20 @@ export async function resolveBrowserWorkspace( names: Set, resolvedProjects: TestProject[], ) { - const filters = toArray(vitest.config.project).map(s => wildcardPatternToRegExp(s)) const removeProjects = new Set() resolvedProjects.forEach((project) => { 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 browser = project.config.browser.name // browser.name should be defined, otherwise the config fails in "resolveConfig" - configs.push({ browser: project.config.browser.name }) + instances.push({ + browser, + name: project.name ? `${project.name} (${browser})` : browser, + }) console.warn( withLabel( 'yellow', @@ -162,16 +197,17 @@ export async function resolveBrowserWorkspace( ) } const originalName = project.config.name - const filteredConfigs = !filters.length - ? configs - : configs.filter((config) => { - const browser = config.browser - const newName = config.name || (originalName ? `${originalName} (${browser})` : browser) - return filters.some(pattern => pattern.test(newName)) + // if original name is in the --project=name filter, keep all instances + const filteredInstances = !vitest._projectFilters.length || vitest._matchesProjectFilter(originalName) + ? instances + : instances.filter((instance) => { + const newName = instance.name! // name is set in "workspace" plugin + return vitest._matchesProjectFilter(newName) }) // every project was filtered out - if (!filteredConfigs.length) { + if (!filteredInstances.length) { + removeProjects.add(project) return } @@ -181,28 +217,31 @@ export async function resolveBrowserWorkspace( ) } - filteredConfigs.forEach((config, index) => { + filteredInstances.forEach((config, index) => { const browser = config.browser if (!browser) { const nth = index + 1 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 (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( [ - `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) }) @@ -411,3 +450,30 @@ 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 + } + // check for the project name and browser names + const hasProjects = getPotentialProjectNames(project).some(p => + vitest._matchesProjectFilter(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 +} 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') }) 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", ]; 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/browser-configs.test.ts b/test/config/test/browser-configs.test.ts index de0ad210685a..827594c034eb 100644 --- a/test/config/test/browser-configs.test.ts +++ b/test/config/test/browser-configs.test.ts @@ -198,6 +198,68 @@ 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', + 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( { 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/config/test/workspace.test.ts b/test/config/test/workspace.test.ts index 15ec6ac35faf..1e1e190a760e 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. 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(` diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts index 92bca066d713..f0f2f7550bf1 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 @@ -293,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') +})