Skip to content

Commit afdcb8f

Browse files
authoredAug 12, 2024··
fix(workspace): correctly resolve workspace globs and file paths (#6316)
1 parent abd85e3 commit afdcb8f

File tree

19 files changed

+390
-164
lines changed

19 files changed

+390
-164
lines changed
 

‎packages/vitest/src/constants.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const CONFIG_NAMES = ['vitest.config', 'vite.config']
1616

1717
const WORKSPACES_NAMES = ['vitest.workspace', 'vitest.projects']
1818

19-
const CONFIG_EXTENSIONS = ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs']
19+
export const CONFIG_EXTENSIONS = ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs']
2020

2121
export const configFiles = CONFIG_NAMES.flatMap(name =>
2222
CONFIG_EXTENSIONS.map(ext => name + ext),

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

+17-153
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import { existsSync, promises as fs } from 'node:fs'
22
import type { Writable } from 'node:stream'
3-
import { isMainThread } from 'node:worker_threads'
43
import type { ViteDevServer } from 'vite'
5-
import { mergeConfig } from 'vite'
6-
import { basename, dirname, join, normalize, relative, resolve } from 'pathe'
7-
import fg from 'fast-glob'
4+
import { dirname, join, normalize, relative, resolve } from 'pathe'
85
import mm from 'micromatch'
96
import { ViteNodeRunner } from 'vite-node/client'
107
import { SnapshotManager } from '@vitest/snapshot/manager'
@@ -14,7 +11,7 @@ import type { defineWorkspace } from 'vitest/config'
1411
import { version } from '../../package.json' with { type: 'json' }
1512
import { getTasks, hasFailed, noop, slash, toArray, wildcardPatternToRegExp } from '../utils'
1613
import { getCoverageProvider } from '../integrations/coverage'
17-
import { CONFIG_NAMES, configFiles, workspacesFiles as workspaceFiles } from '../constants'
14+
import { workspacesFiles as workspaceFiles } from '../constants'
1815
import { rootDir } from '../paths'
1916
import { WebSocketReporter } from '../api/setup'
2017
import type { SerializedCoverageConfig } from '../runtime/config'
@@ -27,13 +24,14 @@ import { StateManager } from './state'
2724
import { resolveConfig } from './config/resolveConfig'
2825
import { Logger } from './logger'
2926
import { VitestCache } from './cache'
30-
import { WorkspaceProject, initializeProject } from './workspace'
27+
import { WorkspaceProject } from './workspace'
3128
import { VitestPackageInstaller } from './packageInstaller'
3229
import { BlobReporter, readBlobs } from './reporters/blob'
3330
import { FilesNotFoundError, GitNotFoundError } from './errors'
34-
import type { ResolvedConfig, UserConfig, UserWorkspaceConfig, VitestRunMode } from './types/config'
31+
import type { ResolvedConfig, UserConfig, VitestRunMode } from './types/config'
3532
import type { Reporter } from './types/reporter'
3633
import type { CoverageProvider } from './types/coverage'
34+
import { resolveWorkspace } from './workspace/resolveWorkspace'
3735

3836
const WATCHER_DEBOUNCE = 100
3937

@@ -192,7 +190,10 @@ export class Vitest {
192190
this.getCoreWorkspaceProject().provide(key, value)
193191
}
194192

195-
private async createCoreProject() {
193+
/**
194+
* @internal
195+
*/
196+
async _createCoreProject() {
196197
this.coreWorkspaceProject = await WorkspaceProject.createCoreProject(this)
197198
return this.coreWorkspaceProject
198199
}
@@ -241,160 +242,23 @@ export class Vitest {
241242
const workspaceConfigPath = await this.getWorkspaceConfigPath()
242243

243244
if (!workspaceConfigPath) {
244-
return [await this.createCoreProject()]
245+
return [await this._createCoreProject()]
245246
}
246247

247248
const workspaceModule = await this.runner.executeFile(workspaceConfigPath) as {
248249
default: ReturnType<typeof defineWorkspace>
249250
}
250251

251252
if (!workspaceModule.default || !Array.isArray(workspaceModule.default)) {
252-
throw new Error(`Workspace config file ${workspaceConfigPath} must export a default array of project paths.`)
253-
}
254-
255-
const workspaceGlobMatches: string[] = []
256-
const projectsOptions: UserWorkspaceConfig[] = []
257-
258-
for (const project of workspaceModule.default) {
259-
if (typeof project === 'string') {
260-
workspaceGlobMatches.push(project.replace('<rootDir>', this.config.root))
261-
}
262-
else if (typeof project === 'function') {
263-
projectsOptions.push(await project({
264-
command: this.server.config.command,
265-
mode: this.server.config.mode,
266-
isPreview: false,
267-
isSsrBuild: false,
268-
}))
269-
}
270-
else {
271-
projectsOptions.push(await project)
272-
}
273-
}
274-
275-
const globOptions: fg.Options = {
276-
absolute: true,
277-
dot: true,
278-
onlyFiles: false,
279-
markDirectories: true,
280-
cwd: this.config.root,
281-
ignore: ['**/node_modules/**', '**/*.timestamp-*'],
282-
}
283-
284-
const workspacesFs = await fg(workspaceGlobMatches, globOptions)
285-
const resolvedWorkspacesPaths = await Promise.all(workspacesFs.filter((file) => {
286-
if (file.endsWith('/')) {
287-
// if it's a directory, check that we don't already have a workspace with a config inside
288-
const hasWorkspaceWithConfig = workspacesFs.some((file2) => {
289-
return file2 !== file && `${dirname(file2)}/` === file
290-
})
291-
return !hasWorkspaceWithConfig
292-
}
293-
const filename = basename(file)
294-
return CONFIG_NAMES.some(configName => filename.startsWith(configName))
295-
}).map(async (filepath) => {
296-
if (filepath.endsWith('/')) {
297-
const filesInside = await fs.readdir(filepath)
298-
const configFile = configFiles.find(config => filesInside.includes(config))
299-
return configFile ? join(filepath, configFile) : filepath
300-
}
301-
return filepath
302-
}))
303-
304-
const workspacesByFolder = resolvedWorkspacesPaths
305-
.reduce((configByFolder, filepath) => {
306-
const dir = filepath.endsWith('/') ? filepath.slice(0, -1) : dirname(filepath)
307-
configByFolder[dir] ??= []
308-
configByFolder[dir].push(filepath)
309-
return configByFolder
310-
}, {} as Record<string, string[]>)
311-
312-
const filteredWorkspaces = Object.values(workspacesByFolder).map((configFiles) => {
313-
if (configFiles.length === 1) {
314-
return configFiles[0]
315-
}
316-
const vitestConfig = configFiles.find(configFile => basename(configFile).startsWith('vitest.config'))
317-
return vitestConfig || configFiles[0]
318-
})
319-
320-
const overridesOptions = [
321-
'logHeapUsage',
322-
'allowOnly',
323-
'sequence',
324-
'testTimeout',
325-
'pool',
326-
'update',
327-
'globals',
328-
'expandSnapshotDiff',
329-
'disableConsoleIntercept',
330-
'retry',
331-
'testNamePattern',
332-
'passWithNoTests',
333-
'bail',
334-
'isolate',
335-
'printConsoleTrace',
336-
] as const
337-
338-
const cliOverrides = overridesOptions.reduce((acc, name) => {
339-
if (name in cliOptions) {
340-
acc[name] = cliOptions[name] as any
341-
}
342-
return acc
343-
}, {} as UserConfig)
344-
345-
const cwd = process.cwd()
346-
347-
const projects: WorkspaceProject[] = []
348-
349-
try {
350-
// we have to resolve them one by one because CWD should depend on the project
351-
for (const filepath of filteredWorkspaces) {
352-
if (this.server.config.configFile === filepath) {
353-
const project = await this.createCoreProject()
354-
projects.push(project)
355-
continue
356-
}
357-
const dir = filepath.endsWith('/') ? filepath.slice(0, -1) : dirname(filepath)
358-
if (isMainThread) {
359-
process.chdir(dir)
360-
}
361-
projects.push(
362-
await initializeProject(filepath, this, { workspaceConfigPath, test: cliOverrides }),
363-
)
364-
}
365-
}
366-
finally {
367-
if (isMainThread) {
368-
process.chdir(cwd)
369-
}
253+
throw new TypeError(`Workspace config file "${workspaceConfigPath}" must export a default array of project paths.`)
370254
}
371255

372-
const projectPromises: Promise<WorkspaceProject>[] = []
373-
374-
projectsOptions.forEach((options, index) => {
375-
// we can resolve these in parallel because process.cwd() is not changed
376-
projectPromises.push(initializeProject(index, this, mergeConfig(options, { workspaceConfigPath, test: cliOverrides }) as any))
377-
})
378-
379-
if (!projects.length && !projectPromises.length) {
380-
return [await this.createCoreProject()]
381-
}
382-
383-
const resolvedProjects = await Promise.all([
384-
...projects,
385-
...await Promise.all(projectPromises),
386-
])
387-
const names = new Set<string>()
388-
389-
for (const project of resolvedProjects) {
390-
const name = project.getName()
391-
if (names.has(name)) {
392-
throw new Error(`Project name "${name}" is not unique. All projects in a workspace should have unique names.`)
393-
}
394-
names.add(name)
395-
}
396-
397-
return resolvedProjects
256+
return resolveWorkspace(
257+
this,
258+
cliOptions,
259+
workspaceConfigPath,
260+
workspaceModule.default,
261+
)
398262
}
399263

400264
private async initCoverageProvider() {

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

+6-6
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,6 @@ export async function initializeProject(
5252
) {
5353
const project = new WorkspaceProject(workspacePath, ctx, options)
5454

55-
const configFile = options.extends
56-
? resolve(dirname(options.workspaceConfigPath), options.extends)
57-
: typeof workspacePath === 'number' || workspacePath.endsWith('/')
58-
? false
59-
: workspacePath
60-
6155
const root
6256
= options.root
6357
|| (typeof workspacePath === 'number'
@@ -66,6 +60,12 @@ export async function initializeProject(
6660
? workspacePath
6761
: dirname(workspacePath))
6862

63+
const configFile = options.extends
64+
? resolve(dirname(options.workspaceConfigPath), options.extends)
65+
: typeof workspacePath === 'number' || workspacePath.endsWith('/')
66+
? false
67+
: workspacePath
68+
6969
const config: ViteInlineConfig = {
7070
...options,
7171
root,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import { existsSync, promises as fs } from 'node:fs'
2+
import { isMainThread } from 'node:worker_threads'
3+
import { dirname, relative, resolve } from 'pathe'
4+
import { mergeConfig } from 'vite'
5+
import fg from 'fast-glob'
6+
import type { UserWorkspaceConfig, WorkspaceProjectConfiguration } from '../../public/config'
7+
import type { Vitest } from '../core'
8+
import type { UserConfig } from '../types/config'
9+
import type { WorkspaceProject } from '../workspace'
10+
import { initializeProject } from '../workspace'
11+
import { configFiles as defaultConfigFiles } from '../../constants'
12+
13+
export async function resolveWorkspace(
14+
vitest: Vitest,
15+
cliOptions: UserConfig,
16+
workspaceConfigPath: string,
17+
workspaceDefinition: WorkspaceProjectConfiguration[],
18+
): Promise<WorkspaceProject[]> {
19+
const { configFiles, projectConfigs, nonConfigDirectories } = await resolveWorkspaceProjectConfigs(
20+
vitest,
21+
workspaceConfigPath,
22+
workspaceDefinition,
23+
)
24+
25+
// cli options that affect the project config,
26+
// not all options are allowed to be overridden
27+
const overridesOptions = [
28+
'logHeapUsage',
29+
'allowOnly',
30+
'sequence',
31+
'testTimeout',
32+
'pool',
33+
'update',
34+
'globals',
35+
'expandSnapshotDiff',
36+
'disableConsoleIntercept',
37+
'retry',
38+
'testNamePattern',
39+
'passWithNoTests',
40+
'bail',
41+
'isolate',
42+
'printConsoleTrace',
43+
] as const
44+
45+
const cliOverrides = overridesOptions.reduce((acc, name) => {
46+
if (name in cliOptions) {
47+
acc[name] = cliOptions[name] as any
48+
}
49+
return acc
50+
}, {} as UserConfig)
51+
52+
const cwd = process.cwd()
53+
54+
const projects: WorkspaceProject[] = []
55+
56+
try {
57+
// we have to resolve them one by one because CWD should depend on the project
58+
for (const filepath of [...configFiles, ...nonConfigDirectories]) {
59+
// if file leads to the root config, then we can just reuse it because we already initialized it
60+
if (vitest.server.config.configFile === filepath) {
61+
const project = await vitest._createCoreProject()
62+
projects.push(project)
63+
continue
64+
}
65+
66+
const directory = filepath.endsWith('/')
67+
? filepath.slice(0, -1)
68+
: dirname(filepath)
69+
70+
if (isMainThread) {
71+
process.chdir(directory)
72+
}
73+
projects.push(
74+
await initializeProject(
75+
filepath,
76+
vitest,
77+
{ workspaceConfigPath, test: cliOverrides },
78+
),
79+
)
80+
}
81+
}
82+
finally {
83+
if (isMainThread) {
84+
process.chdir(cwd)
85+
}
86+
}
87+
88+
const projectPromises: Promise<WorkspaceProject>[] = []
89+
90+
projectConfigs.forEach((options, index) => {
91+
// we can resolve these in parallel because process.cwd() is not changed
92+
projectPromises.push(initializeProject(
93+
index,
94+
vitest,
95+
mergeConfig(options, { workspaceConfigPath, test: cliOverrides }) as any,
96+
))
97+
})
98+
99+
// pretty rare case - the glob didn't match anything and there are no inline configs
100+
if (!projects.length && !projectPromises.length) {
101+
return [await vitest._createCoreProject()]
102+
}
103+
104+
const resolvedProjects = await Promise.all([
105+
...projects,
106+
...projectPromises,
107+
])
108+
const names = new Set<string>()
109+
110+
// project names are guaranteed to be unique
111+
for (const project of resolvedProjects) {
112+
const name = project.getName()
113+
if (names.has(name)) {
114+
const duplicate = resolvedProjects.find(p => p.getName() === name && p !== project)!
115+
throw new Error([
116+
`Project name "${name}"`,
117+
project.server.config.configFile ? ` from "${relative(vitest.config.root, project.server.config.configFile)}"` : '',
118+
' is not unique.',
119+
duplicate?.server.config.configFile ? ` The project is already defined by "${relative(vitest.config.root, duplicate.server.config.configFile)}".` : '',
120+
' All projects in a workspace should have unique names. Make sure your configuration is correct.',
121+
].join(''))
122+
}
123+
names.add(name)
124+
}
125+
126+
return resolvedProjects
127+
}
128+
129+
async function resolveWorkspaceProjectConfigs(
130+
vitest: Vitest,
131+
workspaceConfigPath: string,
132+
workspaceDefinition: WorkspaceProjectConfiguration[],
133+
) {
134+
// project configurations that were specified directly
135+
const projectsOptions: UserWorkspaceConfig[] = []
136+
137+
// custom config files that were specified directly or resolved from a directory
138+
const workspaceConfigFiles: string[] = []
139+
140+
// custom glob matches that should be resolved as directories or config files
141+
const workspaceGlobMatches: string[] = []
142+
143+
// directories that don't have a config file inside, but should be treated as projects
144+
const nonConfigProjectDirectories: string[] = []
145+
146+
const relativeWorkpaceConfigPath = relative(vitest.config.root, workspaceConfigPath)
147+
148+
for (const definition of workspaceDefinition) {
149+
if (typeof definition === 'string') {
150+
const stringOption = definition.replace('<rootDir>', vitest.config.root)
151+
// if the string doesn't contain a glob, we can resolve it directly
152+
// ['./vitest.config.js']
153+
if (!stringOption.includes('*')) {
154+
const file = resolve(vitest.config.root, stringOption)
155+
156+
if (!existsSync(file)) {
157+
throw new Error(`Workspace config file "${relativeWorkpaceConfigPath}" references a non-existing file or a directory: ${file}`)
158+
}
159+
160+
const stats = await fs.stat(file)
161+
// user can specify a config file directly
162+
if (stats.isFile()) {
163+
workspaceConfigFiles.push(file)
164+
}
165+
// user can specify a directory that should be used as a project
166+
else if (stats.isDirectory()) {
167+
const configFile = await resolveDirectoryConfig(file)
168+
if (configFile) {
169+
workspaceConfigFiles.push(configFile)
170+
}
171+
else {
172+
const directory = file[file.length - 1] === '/' ? file : `${file}/`
173+
nonConfigProjectDirectories.push(directory)
174+
}
175+
}
176+
else {
177+
// should never happen
178+
throw new TypeError(`Unexpected file type: ${file}`)
179+
}
180+
}
181+
// if the string is a glob pattern, resolve it later
182+
// ['./packages/*']
183+
else {
184+
workspaceGlobMatches.push(stringOption)
185+
}
186+
}
187+
// if the config is inlined, we can resolve it immediately
188+
else if (typeof definition === 'function') {
189+
projectsOptions.push(await definition({
190+
command: vitest.server.config.command,
191+
mode: vitest.server.config.mode,
192+
isPreview: false,
193+
isSsrBuild: false,
194+
}))
195+
}
196+
// the config is an object or a Promise that returns an object
197+
else {
198+
projectsOptions.push(await definition)
199+
}
200+
201+
if (workspaceGlobMatches.length) {
202+
const globOptions: fg.Options = {
203+
absolute: true,
204+
dot: true,
205+
onlyFiles: false,
206+
markDirectories: true,
207+
cwd: vitest.config.root,
208+
ignore: ['**/node_modules/**', '**/*.timestamp-*'],
209+
}
210+
211+
const workspacesFs = await fg(workspaceGlobMatches, globOptions)
212+
213+
await Promise.all(workspacesFs.map(async (filepath) => {
214+
// directories are allowed with a glob like `packages/*`
215+
// in this case every directory is treated as a project
216+
if (filepath.endsWith('/')) {
217+
const configFile = await resolveDirectoryConfig(filepath)
218+
if (configFile) {
219+
workspaceConfigFiles.push(configFile)
220+
}
221+
else {
222+
nonConfigProjectDirectories.push(filepath)
223+
}
224+
}
225+
else {
226+
workspaceConfigFiles.push(filepath)
227+
}
228+
}))
229+
}
230+
}
231+
232+
const projectConfigFiles = Array.from(new Set(workspaceConfigFiles))
233+
234+
return {
235+
projectConfigs: projectsOptions,
236+
nonConfigDirectories: nonConfigProjectDirectories,
237+
configFiles: projectConfigFiles,
238+
}
239+
}
240+
241+
async function resolveDirectoryConfig(directory: string) {
242+
const files = new Set(await fs.readdir(directory))
243+
// default resolution looks for vitest.config.* or vite.config.* files
244+
// this simulates how `findUp` works in packages/vitest/src/node/create.ts:29
245+
const configFile = defaultConfigFiles.find(file => files.has(file))
246+
if (configFile) {
247+
return resolve(directory, configFile)
248+
}
249+
return null
250+
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export function defineProject(config: UserProjectConfigExport): UserProjectConfi
5858
return config
5959
}
6060

61-
type WorkspaceProjectConfiguration = string | (UserProjectConfigExport & {
61+
export type WorkspaceProjectConfiguration = string | (UserProjectConfigExport & {
6262
/**
6363
* Relative path to the extendable config. All other options will be merged with this config.
6464
* @example '../vite.config.ts'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { defineWorkspace } from 'vitest/config'
2+
3+
export default defineWorkspace([
4+
'./vitest1.config.js',
5+
'./vitest2.config.js',
6+
])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default {
2+
test: {
3+
name: 'test',
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default {
2+
test: {
3+
name: 'test',
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { defineWorkspace } from 'vitest/config'
2+
3+
export default defineWorkspace([
4+
{
5+
test: {
6+
name: 'test',
7+
},
8+
},
9+
{
10+
test: {
11+
name: 'test',
12+
},
13+
},
14+
])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { defineWorkspace } from 'vitest/config'
2+
3+
export default defineWorkspace([
4+
'./vitest.config.js'
5+
])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { expect, test } from 'vitest';
2+
3+
test('1 + 1 = 2', () => {
4+
expect(1 + 1).toBe(2);
5+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { expect, test } from 'vitest';
2+
3+
test('2 + 2 = 4', () => {
4+
expect(2 + 2).toBe(4);
5+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { defineProject } from 'vitest/config';
2+
3+
export default defineProject({
4+
test: {
5+
name: '1_test',
6+
include: ['./1_test.test.ts'],
7+
}
8+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { defineProject } from 'vitest/config';
2+
3+
export default defineProject({
4+
test: {
5+
name: '2_test',
6+
include: ['./2_test.test.ts'],
7+
}
8+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default [
2+
'./test/*.config.*.ts'
3+
]

‎test/config/test/workspace.test.ts

+44
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect, it } from 'vitest'
2+
import { resolve } from 'pathe'
23
import { runVitest } from '../../test-utils'
34

45
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
1011
expect(stdout).toContain('1 + 1 = 2')
1112
expect(stdout).not.toContain('2 + 2 = 4')
1213
})
14+
15+
it('runs the workspace if there are several vitest config files', async () => {
16+
const { stderr, stdout } = await runVitest({
17+
root: 'fixtures/workspace/several-configs',
18+
workspace: './fixtures/workspace/several-configs/vitest.workspace.ts',
19+
})
20+
expect(stderr).toBe('')
21+
expect(stdout).toContain('workspace/several-configs')
22+
expect(stdout).toContain('| 1_test')
23+
expect(stdout).toContain('| 2_test')
24+
expect(stdout).toContain('1 + 1 = 2')
25+
expect(stdout).toContain('2 + 2 = 4')
26+
})
27+
28+
it('fails if project names are identical with a nice error message', async () => {
29+
const { stderr } = await runVitest({
30+
root: 'fixtures/workspace/invalid-duplicate-configs',
31+
workspace: './fixtures/workspace/invalid-duplicate-configs/vitest.workspace.ts',
32+
}, [], 'test', {}, { fails: true })
33+
expect(stderr).toContain(
34+
'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.',
35+
)
36+
})
37+
38+
it('fails if project names are identical inside the inline config', async () => {
39+
const { stderr } = await runVitest({
40+
root: 'fixtures/workspace/invalid-duplicate-inline',
41+
workspace: './fixtures/workspace/invalid-duplicate-inline/vitest.workspace.ts',
42+
}, [], 'test', {}, { fails: true })
43+
expect(stderr).toContain(
44+
'Project name "test" is not unique. All projects in a workspace should have unique names. Make sure your configuration is correct.',
45+
)
46+
})
47+
48+
it('fails if referenced file doesnt exist', async () => {
49+
const { stderr } = await runVitest({
50+
root: 'fixtures/workspace/invalid-non-existing-config',
51+
workspace: './fixtures/workspace/invalid-non-existing-config/vitest.workspace.ts',
52+
}, [], 'test', {}, { fails: true })
53+
expect(stderr).toContain(
54+
`Workspace config file "vitest.workspace.ts" references a non-existing file or a directory: ${resolve('fixtures/workspace/invalid-non-existing-config/vitest.config.js')}`,
55+
)
56+
})

‎test/test-utils/cli.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export class Cli {
7676
}
7777

7878
const timeout = setTimeout(() => {
79-
error.message = `Timeout when waiting for error "${expected}".\nReceived:\n${this[source]}`
79+
error.message = `Timeout when waiting for error "${expected}".\nReceived:\nstdout: ${this.stdout}\nstderr: ${this.stderr}`
8080
reject(error)
8181
}, process.env.CI ? 20_000 : 4_000)
8282

‎test/test-utils/index.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Cli } from './cli'
1212

1313
interface VitestRunnerCLIOptions {
1414
std?: 'inherit'
15+
fails?: boolean
1516
}
1617

1718
export async function runVitest(
@@ -83,7 +84,9 @@ export async function runVitest(
8384
})
8485
}
8586
catch (e: any) {
86-
console.error(e)
87+
if (runnerOptions.fails !== true) {
88+
console.error(e)
89+
}
8790
cli.stderr += e.stack
8891
}
8992
finally {

‎test/workspaces/vitest.workspace.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import type { Plugin } from 'vite'
55

66
export default defineWorkspace([
77
'space_2',
8-
'./space_*/*.config.ts',
8+
'./space_*/vitest.config.ts',
9+
'./space_1/*.config.ts',
910
async () => ({
1011
test: {
1112
name: 'happy-dom',

0 commit comments

Comments
 (0)
Please sign in to comment.