From ac3bd890836640ebf926d03c4b7469edbc94fbaf Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 12 Aug 2024 19:13:28 +0200 Subject: [PATCH] fix(workspace): correctly resolve workspace globs and file paths (#6316) --- packages/vitest/src/constants.ts | 2 +- packages/vitest/src/node/core.ts | 170 ++---------- packages/vitest/src/node/workspace.ts | 12 +- .../src/node/workspace/resolveWorkspace.ts | 250 ++++++++++++++++++ packages/vitest/src/public/config.ts | 2 +- .../vitest.workspace.ts | 6 + .../vitest1.config.js | 5 + .../vitest2.config.js | 5 + .../vitest.workspace.ts | 14 + .../vitest.workspace.ts | 5 + .../several-configs/test/1_test.test.ts | 5 + .../several-configs/test/2_test.test.ts | 5 + .../several-configs/test/vitest.config.one.ts | 8 + .../several-configs/test/vitest.config.two.ts | 8 + .../several-configs/vitest.workspace.ts | 3 + test/config/test/workspace.test.ts | 44 +++ test/test-utils/cli.ts | 2 +- test/test-utils/index.ts | 5 +- test/workspaces/vitest.workspace.ts | 3 +- 19 files changed, 390 insertions(+), 164 deletions(-) create mode 100644 packages/vitest/src/node/workspace/resolveWorkspace.ts create mode 100644 test/config/fixtures/workspace/invalid-duplicate-configs/vitest.workspace.ts create mode 100644 test/config/fixtures/workspace/invalid-duplicate-configs/vitest1.config.js create mode 100644 test/config/fixtures/workspace/invalid-duplicate-configs/vitest2.config.js create mode 100644 test/config/fixtures/workspace/invalid-duplicate-inline/vitest.workspace.ts create mode 100644 test/config/fixtures/workspace/invalid-non-existing-config/vitest.workspace.ts create mode 100644 test/config/fixtures/workspace/several-configs/test/1_test.test.ts create mode 100644 test/config/fixtures/workspace/several-configs/test/2_test.test.ts create mode 100644 test/config/fixtures/workspace/several-configs/test/vitest.config.one.ts create mode 100644 test/config/fixtures/workspace/several-configs/test/vitest.config.two.ts create mode 100644 test/config/fixtures/workspace/several-configs/vitest.workspace.ts diff --git a/packages/vitest/src/constants.ts b/packages/vitest/src/constants.ts index ed504323c41f..c1bbad185275 100644 --- a/packages/vitest/src/constants.ts +++ b/packages/vitest/src/constants.ts @@ -16,7 +16,7 @@ export const CONFIG_NAMES = ['vitest.config', 'vite.config'] const WORKSPACES_NAMES = ['vitest.workspace', 'vitest.projects'] -const CONFIG_EXTENSIONS = ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs'] +export const CONFIG_EXTENSIONS = ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs'] export const configFiles = CONFIG_NAMES.flatMap(name => CONFIG_EXTENSIONS.map(ext => name + ext), diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 4a191a9b6ab1..c940e63a73c7 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -1,10 +1,7 @@ import { existsSync, promises as fs } from 'node:fs' import type { Writable } from 'node:stream' -import { isMainThread } from 'node:worker_threads' import type { ViteDevServer } from 'vite' -import { mergeConfig } from 'vite' -import { basename, dirname, join, normalize, relative, resolve } from 'pathe' -import fg from 'fast-glob' +import { dirname, join, normalize, relative, resolve } from 'pathe' import mm from 'micromatch' import { ViteNodeRunner } from 'vite-node/client' import { SnapshotManager } from '@vitest/snapshot/manager' @@ -14,7 +11,7 @@ import type { defineWorkspace } from 'vitest/config' import { version } from '../../package.json' with { type: 'json' } import { getTasks, hasFailed, noop, slash, toArray, wildcardPatternToRegExp } from '../utils' import { getCoverageProvider } from '../integrations/coverage' -import { CONFIG_NAMES, configFiles, workspacesFiles as workspaceFiles } from '../constants' +import { workspacesFiles as workspaceFiles } from '../constants' import { rootDir } from '../paths' import { WebSocketReporter } from '../api/setup' import type { SerializedCoverageConfig } from '../runtime/config' @@ -27,13 +24,14 @@ import { StateManager } from './state' import { resolveConfig } from './config/resolveConfig' import { Logger } from './logger' import { VitestCache } from './cache' -import { WorkspaceProject, initializeProject } from './workspace' +import { WorkspaceProject } from './workspace' import { VitestPackageInstaller } from './packageInstaller' import { BlobReporter, readBlobs } from './reporters/blob' import { FilesNotFoundError, GitNotFoundError } from './errors' -import type { ResolvedConfig, UserConfig, UserWorkspaceConfig, VitestRunMode } from './types/config' +import type { ResolvedConfig, UserConfig, VitestRunMode } from './types/config' import type { Reporter } from './types/reporter' import type { CoverageProvider } from './types/coverage' +import { resolveWorkspace } from './workspace/resolveWorkspace' const WATCHER_DEBOUNCE = 100 @@ -192,7 +190,10 @@ export class Vitest { this.getCoreWorkspaceProject().provide(key, value) } - private async createCoreProject() { + /** + * @internal + */ + async _createCoreProject() { this.coreWorkspaceProject = await WorkspaceProject.createCoreProject(this) return this.coreWorkspaceProject } @@ -241,7 +242,7 @@ export class Vitest { const workspaceConfigPath = await this.getWorkspaceConfigPath() if (!workspaceConfigPath) { - return [await this.createCoreProject()] + return [await this._createCoreProject()] } const workspaceModule = await this.runner.executeFile(workspaceConfigPath) as { @@ -249,152 +250,15 @@ export class Vitest { } if (!workspaceModule.default || !Array.isArray(workspaceModule.default)) { - throw new Error(`Workspace config file ${workspaceConfigPath} must export a default array of project paths.`) - } - - const workspaceGlobMatches: string[] = [] - const projectsOptions: UserWorkspaceConfig[] = [] - - for (const project of workspaceModule.default) { - if (typeof project === 'string') { - workspaceGlobMatches.push(project.replace('', this.config.root)) - } - else if (typeof project === 'function') { - projectsOptions.push(await project({ - command: this.server.config.command, - mode: this.server.config.mode, - isPreview: false, - isSsrBuild: false, - })) - } - else { - projectsOptions.push(await project) - } - } - - const globOptions: fg.Options = { - absolute: true, - dot: true, - onlyFiles: false, - markDirectories: true, - cwd: this.config.root, - ignore: ['**/node_modules/**', '**/*.timestamp-*'], - } - - const workspacesFs = await fg(workspaceGlobMatches, globOptions) - const resolvedWorkspacesPaths = await Promise.all(workspacesFs.filter((file) => { - if (file.endsWith('/')) { - // if it's a directory, check that we don't already have a workspace with a config inside - const hasWorkspaceWithConfig = workspacesFs.some((file2) => { - return file2 !== file && `${dirname(file2)}/` === file - }) - return !hasWorkspaceWithConfig - } - const filename = basename(file) - return CONFIG_NAMES.some(configName => filename.startsWith(configName)) - }).map(async (filepath) => { - if (filepath.endsWith('/')) { - const filesInside = await fs.readdir(filepath) - const configFile = configFiles.find(config => filesInside.includes(config)) - return configFile ? join(filepath, configFile) : filepath - } - return filepath - })) - - const workspacesByFolder = resolvedWorkspacesPaths - .reduce((configByFolder, filepath) => { - const dir = filepath.endsWith('/') ? filepath.slice(0, -1) : dirname(filepath) - configByFolder[dir] ??= [] - configByFolder[dir].push(filepath) - return configByFolder - }, {} as Record) - - const filteredWorkspaces = Object.values(workspacesByFolder).map((configFiles) => { - if (configFiles.length === 1) { - return configFiles[0] - } - const vitestConfig = configFiles.find(configFile => basename(configFile).startsWith('vitest.config')) - return vitestConfig || configFiles[0] - }) - - const overridesOptions = [ - 'logHeapUsage', - 'allowOnly', - 'sequence', - 'testTimeout', - 'pool', - 'update', - 'globals', - 'expandSnapshotDiff', - 'disableConsoleIntercept', - 'retry', - 'testNamePattern', - 'passWithNoTests', - 'bail', - 'isolate', - 'printConsoleTrace', - ] as const - - const cliOverrides = overridesOptions.reduce((acc, name) => { - if (name in cliOptions) { - acc[name] = cliOptions[name] as any - } - return acc - }, {} as UserConfig) - - const cwd = process.cwd() - - const projects: WorkspaceProject[] = [] - - try { - // we have to resolve them one by one because CWD should depend on the project - for (const filepath of filteredWorkspaces) { - if (this.server.config.configFile === filepath) { - const project = await this.createCoreProject() - projects.push(project) - continue - } - const dir = filepath.endsWith('/') ? filepath.slice(0, -1) : dirname(filepath) - if (isMainThread) { - process.chdir(dir) - } - projects.push( - await initializeProject(filepath, this, { workspaceConfigPath, test: cliOverrides }), - ) - } - } - finally { - if (isMainThread) { - process.chdir(cwd) - } + throw new TypeError(`Workspace config file "${workspaceConfigPath}" must export a default array of project paths.`) } - const projectPromises: Promise[] = [] - - projectsOptions.forEach((options, index) => { - // we can resolve these in parallel because process.cwd() is not changed - projectPromises.push(initializeProject(index, this, mergeConfig(options, { workspaceConfigPath, test: cliOverrides }) as any)) - }) - - if (!projects.length && !projectPromises.length) { - return [await this.createCoreProject()] - } - - const resolvedProjects = await Promise.all([ - ...projects, - ...await Promise.all(projectPromises), - ]) - const names = new Set() - - for (const project of resolvedProjects) { - const name = project.getName() - if (names.has(name)) { - throw new Error(`Project name "${name}" is not unique. All projects in a workspace should have unique names.`) - } - names.add(name) - } - - return resolvedProjects + return resolveWorkspace( + this, + cliOptions, + workspaceConfigPath, + workspaceModule.default, + ) } private async initCoverageProvider() { diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index e85b5d1275e2..408f1da3f870 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -52,12 +52,6 @@ export async function initializeProject( ) { const project = new WorkspaceProject(workspacePath, ctx, options) - const configFile = options.extends - ? resolve(dirname(options.workspaceConfigPath), options.extends) - : typeof workspacePath === 'number' || workspacePath.endsWith('/') - ? false - : workspacePath - const root = options.root || (typeof workspacePath === 'number' @@ -66,6 +60,12 @@ export async function initializeProject( ? workspacePath : dirname(workspacePath)) + const configFile = options.extends + ? resolve(dirname(options.workspaceConfigPath), options.extends) + : typeof workspacePath === 'number' || workspacePath.endsWith('/') + ? false + : workspacePath + const config: ViteInlineConfig = { ...options, root, diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts new file mode 100644 index 000000000000..a84857be76d6 --- /dev/null +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -0,0 +1,250 @@ +import { existsSync, promises as fs } from 'node:fs' +import { isMainThread } from 'node:worker_threads' +import { dirname, relative, resolve } from 'pathe' +import { mergeConfig } from 'vite' +import fg from 'fast-glob' +import type { UserWorkspaceConfig, WorkspaceProjectConfiguration } from '../../public/config' +import type { Vitest } from '../core' +import type { UserConfig } from '../types/config' +import type { WorkspaceProject } from '../workspace' +import { initializeProject } from '../workspace' +import { configFiles as defaultConfigFiles } from '../../constants' + +export async function resolveWorkspace( + vitest: Vitest, + cliOptions: UserConfig, + workspaceConfigPath: string, + workspaceDefinition: WorkspaceProjectConfiguration[], +): Promise { + const { configFiles, projectConfigs, nonConfigDirectories } = await resolveWorkspaceProjectConfigs( + vitest, + workspaceConfigPath, + workspaceDefinition, + ) + + // cli options that affect the project config, + // not all options are allowed to be overridden + const overridesOptions = [ + 'logHeapUsage', + 'allowOnly', + 'sequence', + 'testTimeout', + 'pool', + 'update', + 'globals', + 'expandSnapshotDiff', + 'disableConsoleIntercept', + 'retry', + 'testNamePattern', + 'passWithNoTests', + 'bail', + 'isolate', + 'printConsoleTrace', + ] as const + + const cliOverrides = overridesOptions.reduce((acc, name) => { + if (name in cliOptions) { + acc[name] = cliOptions[name] as any + } + return acc + }, {} as UserConfig) + + const cwd = process.cwd() + + const projects: WorkspaceProject[] = [] + + try { + // we have to resolve them one by one because CWD should depend on the project + for (const filepath of [...configFiles, ...nonConfigDirectories]) { + // if file leads to the root config, then we can just reuse it because we already initialized it + if (vitest.server.config.configFile === filepath) { + const project = await vitest._createCoreProject() + projects.push(project) + continue + } + + const directory = filepath.endsWith('/') + ? filepath.slice(0, -1) + : dirname(filepath) + + if (isMainThread) { + process.chdir(directory) + } + projects.push( + await initializeProject( + filepath, + vitest, + { workspaceConfigPath, test: cliOverrides }, + ), + ) + } + } + finally { + if (isMainThread) { + process.chdir(cwd) + } + } + + const projectPromises: Promise[] = [] + + projectConfigs.forEach((options, index) => { + // we can resolve these in parallel because process.cwd() is not changed + projectPromises.push(initializeProject( + index, + vitest, + mergeConfig(options, { workspaceConfigPath, test: cliOverrides }) as any, + )) + }) + + // pretty rare case - the glob didn't match anything and there are no inline configs + if (!projects.length && !projectPromises.length) { + return [await vitest._createCoreProject()] + } + + const resolvedProjects = await Promise.all([ + ...projects, + ...projectPromises, + ]) + const names = new Set() + + // project names are guaranteed to be unique + for (const project of resolvedProjects) { + const name = project.getName() + if (names.has(name)) { + const duplicate = resolvedProjects.find(p => p.getName() === name && p !== project)! + throw new Error([ + `Project name "${name}"`, + project.server.config.configFile ? ` from "${relative(vitest.config.root, project.server.config.configFile)}"` : '', + ' is not unique.', + duplicate?.server.config.configFile ? ` The project is already defined by "${relative(vitest.config.root, duplicate.server.config.configFile)}".` : '', + ' All projects in a workspace should have unique names. Make sure your configuration is correct.', + ].join('')) + } + names.add(name) + } + + return resolvedProjects +} + +async function resolveWorkspaceProjectConfigs( + vitest: Vitest, + workspaceConfigPath: string, + workspaceDefinition: WorkspaceProjectConfiguration[], +) { + // project configurations that were specified directly + const projectsOptions: UserWorkspaceConfig[] = [] + + // custom config files that were specified directly or resolved from a directory + const workspaceConfigFiles: string[] = [] + + // custom glob matches that should be resolved as directories or config files + const workspaceGlobMatches: string[] = [] + + // directories that don't have a config file inside, but should be treated as projects + const nonConfigProjectDirectories: string[] = [] + + const relativeWorkpaceConfigPath = relative(vitest.config.root, workspaceConfigPath) + + for (const definition of workspaceDefinition) { + if (typeof definition === 'string') { + const stringOption = definition.replace('', vitest.config.root) + // if the string doesn't contain a glob, we can resolve it directly + // ['./vitest.config.js'] + if (!stringOption.includes('*')) { + const file = resolve(vitest.config.root, stringOption) + + if (!existsSync(file)) { + throw new Error(`Workspace config file "${relativeWorkpaceConfigPath}" references a non-existing file or a directory: ${file}`) + } + + const stats = await fs.stat(file) + // user can specify a config file directly + if (stats.isFile()) { + workspaceConfigFiles.push(file) + } + // user can specify a directory that should be used as a project + else if (stats.isDirectory()) { + const configFile = await resolveDirectoryConfig(file) + if (configFile) { + workspaceConfigFiles.push(configFile) + } + else { + const directory = file[file.length - 1] === '/' ? file : `${file}/` + nonConfigProjectDirectories.push(directory) + } + } + else { + // should never happen + throw new TypeError(`Unexpected file type: ${file}`) + } + } + // if the string is a glob pattern, resolve it later + // ['./packages/*'] + else { + workspaceGlobMatches.push(stringOption) + } + } + // if the config is inlined, we can resolve it immediately + else if (typeof definition === 'function') { + projectsOptions.push(await definition({ + command: vitest.server.config.command, + mode: vitest.server.config.mode, + isPreview: false, + isSsrBuild: false, + })) + } + // the config is an object or a Promise that returns an object + else { + projectsOptions.push(await definition) + } + + if (workspaceGlobMatches.length) { + const globOptions: fg.Options = { + absolute: true, + dot: true, + onlyFiles: false, + markDirectories: true, + cwd: vitest.config.root, + ignore: ['**/node_modules/**', '**/*.timestamp-*'], + } + + const workspacesFs = await fg(workspaceGlobMatches, globOptions) + + await Promise.all(workspacesFs.map(async (filepath) => { + // directories are allowed with a glob like `packages/*` + // in this case every directory is treated as a project + if (filepath.endsWith('/')) { + const configFile = await resolveDirectoryConfig(filepath) + if (configFile) { + workspaceConfigFiles.push(configFile) + } + else { + nonConfigProjectDirectories.push(filepath) + } + } + else { + workspaceConfigFiles.push(filepath) + } + })) + } + } + + const projectConfigFiles = Array.from(new Set(workspaceConfigFiles)) + + return { + projectConfigs: projectsOptions, + nonConfigDirectories: nonConfigProjectDirectories, + configFiles: projectConfigFiles, + } +} + +async function resolveDirectoryConfig(directory: string) { + const files = new Set(await fs.readdir(directory)) + // default resolution looks for vitest.config.* or vite.config.* files + // this simulates how `findUp` works in packages/vitest/src/node/create.ts:29 + const configFile = defaultConfigFiles.find(file => files.has(file)) + if (configFile) { + return resolve(directory, configFile) + } + return null +} diff --git a/packages/vitest/src/public/config.ts b/packages/vitest/src/public/config.ts index ee605beb662d..b8a7341e66ae 100644 --- a/packages/vitest/src/public/config.ts +++ b/packages/vitest/src/public/config.ts @@ -58,7 +58,7 @@ export function defineProject(config: UserProjectConfigExport): UserProjectConfi return config } -type WorkspaceProjectConfiguration = string | (UserProjectConfigExport & { +export type WorkspaceProjectConfiguration = string | (UserProjectConfigExport & { /** * Relative path to the extendable config. All other options will be merged with this config. * @example '../vite.config.ts' diff --git a/test/config/fixtures/workspace/invalid-duplicate-configs/vitest.workspace.ts b/test/config/fixtures/workspace/invalid-duplicate-configs/vitest.workspace.ts new file mode 100644 index 000000000000..1aae3649d451 --- /dev/null +++ b/test/config/fixtures/workspace/invalid-duplicate-configs/vitest.workspace.ts @@ -0,0 +1,6 @@ +import { defineWorkspace } from 'vitest/config' + +export default defineWorkspace([ + './vitest1.config.js', + './vitest2.config.js', +]) \ No newline at end of file diff --git a/test/config/fixtures/workspace/invalid-duplicate-configs/vitest1.config.js b/test/config/fixtures/workspace/invalid-duplicate-configs/vitest1.config.js new file mode 100644 index 000000000000..e7efee0ec43d --- /dev/null +++ b/test/config/fixtures/workspace/invalid-duplicate-configs/vitest1.config.js @@ -0,0 +1,5 @@ +export default { + test: { + name: 'test', + } +} diff --git a/test/config/fixtures/workspace/invalid-duplicate-configs/vitest2.config.js b/test/config/fixtures/workspace/invalid-duplicate-configs/vitest2.config.js new file mode 100644 index 000000000000..e7efee0ec43d --- /dev/null +++ b/test/config/fixtures/workspace/invalid-duplicate-configs/vitest2.config.js @@ -0,0 +1,5 @@ +export default { + test: { + name: 'test', + } +} diff --git a/test/config/fixtures/workspace/invalid-duplicate-inline/vitest.workspace.ts b/test/config/fixtures/workspace/invalid-duplicate-inline/vitest.workspace.ts new file mode 100644 index 000000000000..5ac17ebfad18 --- /dev/null +++ b/test/config/fixtures/workspace/invalid-duplicate-inline/vitest.workspace.ts @@ -0,0 +1,14 @@ +import { defineWorkspace } from 'vitest/config' + +export default defineWorkspace([ + { + test: { + name: 'test', + }, + }, + { + test: { + name: 'test', + }, + }, +]) \ No newline at end of file diff --git a/test/config/fixtures/workspace/invalid-non-existing-config/vitest.workspace.ts b/test/config/fixtures/workspace/invalid-non-existing-config/vitest.workspace.ts new file mode 100644 index 000000000000..abfecd280a0c --- /dev/null +++ b/test/config/fixtures/workspace/invalid-non-existing-config/vitest.workspace.ts @@ -0,0 +1,5 @@ +import { defineWorkspace } from 'vitest/config' + +export default defineWorkspace([ + './vitest.config.js' +]) \ No newline at end of file diff --git a/test/config/fixtures/workspace/several-configs/test/1_test.test.ts b/test/config/fixtures/workspace/several-configs/test/1_test.test.ts new file mode 100644 index 000000000000..93bab1515828 --- /dev/null +++ b/test/config/fixtures/workspace/several-configs/test/1_test.test.ts @@ -0,0 +1,5 @@ +import { expect, test } from 'vitest'; + +test('1 + 1 = 2', () => { + expect(1 + 1).toBe(2); +}) diff --git a/test/config/fixtures/workspace/several-configs/test/2_test.test.ts b/test/config/fixtures/workspace/several-configs/test/2_test.test.ts new file mode 100644 index 000000000000..46bfb80b3a71 --- /dev/null +++ b/test/config/fixtures/workspace/several-configs/test/2_test.test.ts @@ -0,0 +1,5 @@ +import { expect, test } from 'vitest'; + +test('2 + 2 = 4', () => { + expect(2 + 2).toBe(4); +}) diff --git a/test/config/fixtures/workspace/several-configs/test/vitest.config.one.ts b/test/config/fixtures/workspace/several-configs/test/vitest.config.one.ts new file mode 100644 index 000000000000..1ed79a0766c2 --- /dev/null +++ b/test/config/fixtures/workspace/several-configs/test/vitest.config.one.ts @@ -0,0 +1,8 @@ +import { defineProject } from 'vitest/config'; + +export default defineProject({ + test: { + name: '1_test', + include: ['./1_test.test.ts'], + } +}) diff --git a/test/config/fixtures/workspace/several-configs/test/vitest.config.two.ts b/test/config/fixtures/workspace/several-configs/test/vitest.config.two.ts new file mode 100644 index 000000000000..8633fc14f624 --- /dev/null +++ b/test/config/fixtures/workspace/several-configs/test/vitest.config.two.ts @@ -0,0 +1,8 @@ +import { defineProject } from 'vitest/config'; + +export default defineProject({ + test: { + name: '2_test', + include: ['./2_test.test.ts'], + } +}) diff --git a/test/config/fixtures/workspace/several-configs/vitest.workspace.ts b/test/config/fixtures/workspace/several-configs/vitest.workspace.ts new file mode 100644 index 000000000000..1efaf00d4482 --- /dev/null +++ b/test/config/fixtures/workspace/several-configs/vitest.workspace.ts @@ -0,0 +1,3 @@ +export default [ + './test/*.config.*.ts' +] \ No newline at end of file diff --git a/test/config/test/workspace.test.ts b/test/config/test/workspace.test.ts index f7edc226d12c..7640843f1ed6 100644 --- a/test/config/test/workspace.test.ts +++ b/test/config/test/workspace.test.ts @@ -1,4 +1,5 @@ import { expect, it } from 'vitest' +import { resolve } from 'pathe' import { runVitest } from '../../test-utils' it('correctly runs workspace tests when workspace config path is specified', async () => { @@ -10,3 +11,46 @@ it('correctly runs workspace tests when workspace config path is specified', asy expect(stdout).toContain('1 + 1 = 2') expect(stdout).not.toContain('2 + 2 = 4') }) + +it('runs the workspace if there are several vitest config files', async () => { + const { stderr, stdout } = await runVitest({ + root: 'fixtures/workspace/several-configs', + workspace: './fixtures/workspace/several-configs/vitest.workspace.ts', + }) + expect(stderr).toBe('') + expect(stdout).toContain('workspace/several-configs') + expect(stdout).toContain('| 1_test') + expect(stdout).toContain('| 2_test') + expect(stdout).toContain('1 + 1 = 2') + expect(stdout).toContain('2 + 2 = 4') +}) + +it('fails if project names are identical with a nice error message', async () => { + const { stderr } = await runVitest({ + root: 'fixtures/workspace/invalid-duplicate-configs', + workspace: './fixtures/workspace/invalid-duplicate-configs/vitest.workspace.ts', + }, [], 'test', {}, { fails: true }) + expect(stderr).toContain( + 'Project name "test" from "vitest2.config.js" is not unique. The project is already defined by "vitest1.config.js". All projects in a workspace should have unique names. Make sure your configuration is correct.', + ) +}) + +it('fails if project names are identical inside the inline config', async () => { + const { stderr } = await runVitest({ + root: 'fixtures/workspace/invalid-duplicate-inline', + workspace: './fixtures/workspace/invalid-duplicate-inline/vitest.workspace.ts', + }, [], 'test', {}, { fails: true }) + expect(stderr).toContain( + 'Project name "test" is not unique. All projects in a workspace should have unique names. Make sure your configuration is correct.', + ) +}) + +it('fails if referenced file doesnt exist', async () => { + const { stderr } = await runVitest({ + root: 'fixtures/workspace/invalid-non-existing-config', + workspace: './fixtures/workspace/invalid-non-existing-config/vitest.workspace.ts', + }, [], 'test', {}, { fails: true }) + expect(stderr).toContain( + `Workspace config file "vitest.workspace.ts" references a non-existing file or a directory: ${resolve('fixtures/workspace/invalid-non-existing-config/vitest.config.js')}`, + ) +}) diff --git a/test/test-utils/cli.ts b/test/test-utils/cli.ts index 44709d7c4a8c..087aa3c01716 100644 --- a/test/test-utils/cli.ts +++ b/test/test-utils/cli.ts @@ -76,7 +76,7 @@ export class Cli { } const timeout = setTimeout(() => { - error.message = `Timeout when waiting for error "${expected}".\nReceived:\n${this[source]}` + error.message = `Timeout when waiting for error "${expected}".\nReceived:\nstdout: ${this.stdout}\nstderr: ${this.stderr}` reject(error) }, process.env.CI ? 20_000 : 4_000) diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts index c0c0b34a352b..b8ccd158299b 100644 --- a/test/test-utils/index.ts +++ b/test/test-utils/index.ts @@ -12,6 +12,7 @@ import { Cli } from './cli' interface VitestRunnerCLIOptions { std?: 'inherit' + fails?: boolean } export async function runVitest( @@ -83,7 +84,9 @@ export async function runVitest( }) } catch (e: any) { - console.error(e) + if (runnerOptions.fails !== true) { + console.error(e) + } cli.stderr += e.stack } finally { diff --git a/test/workspaces/vitest.workspace.ts b/test/workspaces/vitest.workspace.ts index 038fed32aa47..f09cdee0f22f 100644 --- a/test/workspaces/vitest.workspace.ts +++ b/test/workspaces/vitest.workspace.ts @@ -5,7 +5,8 @@ import type { Plugin } from 'vite' export default defineWorkspace([ 'space_2', - './space_*/*.config.ts', + './space_*/vitest.config.ts', + './space_1/*.config.ts', async () => ({ test: { name: 'happy-dom',