From 210ba00cbb399bbc15791ab43e7826990df86aa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Mon, 17 Aug 2026 12:32:27 +0300 Subject: [PATCH] fix: preserve correct root during resolve config (#10909) Co-authored-by: Vladimir Sheremet --- .../vitest/src/node/config/resolveConfig.ts | 17 +- packages/vitest/src/node/plugins/config.ts | 2 - packages/vitest/src/node/plugins/index.ts | 7 +- packages/vitest/src/node/plugins/optimizer.ts | 27 -- .../vitest/src/node/plugins/testConfig.ts | 5 +- packages/vitest/src/node/plugins/utils.ts | 19 +- packages/vitest/src/node/plugins/workspace.ts | 16 +- .../src/node/projects/resolveProjects.ts | 22 +- .../global-setup-root/nested/example.test.ts | 5 + .../global-setup-root/nested/global-setup.ts | 3 + .../global-setup-root/vitest.config.ts | 9 + test/e2e/test/config/root.test.ts | 392 ++++++++++++++++++ test/e2e/test/global-setup.test.ts | 7 + test/test-utils/index.ts | 13 +- 14 files changed, 480 insertions(+), 64 deletions(-) delete mode 100644 packages/vitest/src/node/plugins/optimizer.ts create mode 100644 test/e2e/fixtures/global-setup-root/nested/example.test.ts create mode 100644 test/e2e/fixtures/global-setup-root/nested/global-setup.ts create mode 100644 test/e2e/fixtures/global-setup-root/vitest.config.ts create mode 100644 test/e2e/test/config/root.test.ts diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts index 317c09ae3..fe21cd4c1 100644 --- a/packages/vitest/src/node/config/resolveConfig.ts +++ b/packages/vitest/src/node/config/resolveConfig.ts @@ -56,13 +56,16 @@ function resolvePath(path: string, root: string) { ) } -export function findConfigFile(root: string): string | undefined { +export function findConfigFile(root: string): string | false { for (const configFile of configFiles) { const configPath = resolve(root, configFile) if (existsSync(configPath)) { return configPath } } + // if not found, then there is no config to find. + // `false` will stop vite from trying to find it again + return false } function parseInspector(inspect: string | undefined | boolean | number) { @@ -1118,10 +1121,12 @@ export async function resolveConfig( // We clone CLI Options and Vite overrides to reuse when a watch mode is triggered. const cliOptionsCopy = deepMerge({}, options) as UserConfig const viteOverridesCopy = deepMerge({}, viteOverrides) as ViteUserConfig - const root = resolve(options.root || process.cwd()) - const configPath = resolveConfigPath(root, options) + const configPath = resolveConfigPath( + // try to find the config relative to `--root` or process.cwd() + resolve(options.root || process.cwd()), + options, + ) options.config = configPath - options.root = root const captures: ConfigResolutionCaptures = {} const inlineConfig: InlineConfig = mergeConfig( @@ -1135,11 +1140,11 @@ export async function resolveConfig( CaptureRawTestConfig(captures, cliOptionsCopy.sharedViteServer), ...TestConfigPlugin(pluginsHarness, captures, cliOptionsCopy), ...ViteConfigPlugin(pluginsHarness), - ...VitestCorePlugin(pluginsHarness, options), + ...VitestCorePlugin(pluginsHarness), ...BrowserLoaderPlugin(captures, pluginsHarness), ], } satisfies InlineConfig, - mergeConfig(viteOverrides, { root }), + viteOverrides, ) const rootViteConfig = await viteResolveConfig(inlineConfig, 'serve') diff --git a/packages/vitest/src/node/plugins/config.ts b/packages/vitest/src/node/plugins/config.ts index db2e98d2b..c83a7a982 100644 --- a/packages/vitest/src/node/plugins/config.ts +++ b/packages/vitest/src/node/plugins/config.ts @@ -5,7 +5,6 @@ import { relative } from 'pathe' import * as vite from 'vite' import { generateScopedClassName } from '../../integrations/css/css-modules' import { createViteLogger, silenceImportViteIgnoreWarning } from '../viteLogger' -import { VitestOptimizer } from './optimizer' import { ModuleRunnerTransform } from './runnerTransform' import { getDefaultResolveOptions } from './utils' @@ -163,7 +162,6 @@ export function ViteConfigPlugin(harness: PluginHarness): Plugin[] { }, }, }, - VitestOptimizer(), ModuleRunnerTransform(), ] } diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index 3fd5a5f64..04a1cbee1 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -1,5 +1,4 @@ import type { Plugin as VitePlugin } from 'vite' -import type { CliOptions } from '../cli/cli-api' import type { PluginHarness } from '../config/pluginHarness' import { resolve } from 'pathe' import { configDefaults } from '../../defaults' @@ -9,9 +8,10 @@ import { MetaEnvReplacerPlugin } from './metaEnvReplacer' import { MocksPlugins } from './mocks' import { NormalizeURLPlugin } from './normalizeURL' import { SsrRunnerFixerPlugin } from './ssrRunnerFixer' +import { resolveTestCacheDir } from './utils' import { VitestCoreResolver } from './vitestResolver' -export function VitestCorePlugin(harness: PluginHarness, options: CliOptions = {}): VitePlugin[] { +export function VitestCorePlugin(harness: PluginHarness): VitePlugin[] { return [ { name: 'vitest:config:append', @@ -22,11 +22,12 @@ export function VitestCorePlugin(harness: PluginHarness, options: CliOptions = { config: { order: 'post', handler(viteConfig) { - const root = resolve(options.root || viteConfig.test?.root || viteConfig.root || process.cwd()) + const root = resolve(viteConfig.test?.root || viteConfig.root || process.cwd()) return { base: '/', root, + cacheDir: resolveTestCacheDir(root, viteConfig.test || {}, viteConfig.cacheDir), build: { // Vitest doesn't use outputDir, but this value affects what folders are watched // https://github.com/vitejs/vite/pull/16453 diff --git a/packages/vitest/src/node/plugins/optimizer.ts b/packages/vitest/src/node/plugins/optimizer.ts deleted file mode 100644 index aa9052047..000000000 --- a/packages/vitest/src/node/plugins/optimizer.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { Plugin } from 'vite' -import { resolve } from 'pathe' -import { VitestCache } from '../cache' - -export function VitestOptimizer(): Plugin { - return { - name: 'vitest:normalize-optimizer', - config: { - order: 'post', - handler(viteConfig) { - const testConfig = viteConfig.test || {} - - const root = resolve(viteConfig.root || process.cwd()) - const name = viteConfig.test?.name - const label = typeof name === 'string' ? name : (name?.label || '') - - viteConfig.cacheDir = VitestCache.resolveCacheDir( - root, - testConfig.cache != null && testConfig.cache !== false - ? testConfig.cache.dir - : viteConfig.cacheDir, - label, - ) - }, - }, - } -} diff --git a/packages/vitest/src/node/plugins/testConfig.ts b/packages/vitest/src/node/plugins/testConfig.ts index 0f868680e..e117a1952 100644 --- a/packages/vitest/src/node/plugins/testConfig.ts +++ b/packages/vitest/src/node/plugins/testConfig.ts @@ -17,8 +17,9 @@ import { escapeRegExp } from '../../utils/base' import { resolveApiServerConfig } from '../config/resolveConfig' import { deleteDefineConfig } from './utils' -// `name` must stay unique per project, `projects` would redefine the whole workspace -const NON_INHERITED_OPTIONS = ['name', 'projects'] as const +// `name` must stay unique per project, `projects` would redefine the whole +// workspace, and `root` would re-root the project onto the declaring config +const NON_INHERITED_OPTIONS = ['name', 'projects', 'root'] as const // the root `globalSetup` already runs once per test run; a non-root // config (a shared config or a container) keeps it because nothing else runs it diff --git a/packages/vitest/src/node/plugins/utils.ts b/packages/vitest/src/node/plugins/utils.ts index 703cd2e83..a5aa52546 100644 --- a/packages/vitest/src/node/plugins/utils.ts +++ b/packages/vitest/src/node/plugins/utils.ts @@ -2,11 +2,28 @@ import type { DepOptimizationOptions, UserConfig as ViteConfig, } from 'vite' -import type { DepsOptimizationOptions } from '../types/config' +import type { DepsOptimizationOptions, UserConfig } from '../types/config' import { dirname } from 'pathe' import { searchForWorkspaceRoot, version as viteVersion } from 'vite' import * as vite from 'vite' import { rootDir } from '../../paths' +import { VitestCache } from '../cache' + +export function resolveTestCacheDir( + root: string, + testConfig: UserConfig, + viteCacheDir: string | undefined, +): string { + const name = testConfig.name + const label = typeof name === 'string' ? name : (name?.label || '') + return VitestCache.resolveCacheDir( + root, + testConfig.cache != null && testConfig.cache !== false + ? testConfig.cache.dir + : viteCacheDir, + label, + ) +} export function resolveOptimizerConfig( testOptions_: DepsOptimizationOptions | undefined, diff --git a/packages/vitest/src/node/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts index 76f73a6cf..9b4075f02 100644 --- a/packages/vitest/src/node/plugins/workspace.ts +++ b/packages/vitest/src/node/plugins/workspace.ts @@ -1,7 +1,7 @@ import type * as vite from 'vite' import type { UserConfig as ViteConfig, Plugin as VitePlugin } from 'vite' import type { PluginHarness } from '../config/pluginHarness' -import type { ResolvedConfig, TestProjectInlineConfiguration } from '../types/config' +import { resolve } from 'pathe' import { API_TOKEN_FILE } from '../config/apiToken' import { ViteConfigPlugin } from './config' import { CoverageTransform } from './coverageTransform' @@ -10,17 +10,12 @@ import { MetaEnvReplacerPlugin } from './metaEnvReplacer' import { MocksPlugins } from './mocks' import { NormalizeURLPlugin } from './normalizeURL' import { SsrRunnerFixerPlugin } from './ssrRunnerFixer' +import { resolveTestCacheDir } from './utils' import { VitestProjectResolver } from './vitestResolver' -interface WorkspaceOptions extends TestProjectInlineConfiguration { - root?: string -} - export function WorkspaceVitestPlugin( harness: PluginHarness, globalViteConfig: vite.ResolvedConfig, - globalConfig: ResolvedConfig, - options: WorkspaceOptions, ): VitePlugin[] { return [ { @@ -31,11 +26,16 @@ export function WorkspaceVitestPlugin( }, config(viteConfig) { const testConfig = viteConfig.test || {} - const root = options.root || testConfig.root || viteConfig.root + const root = testConfig.root || viteConfig.root const config: ViteConfig = { base: '/', root, + cacheDir: resolveTestCacheDir( + resolve(root || process.cwd()), + testConfig, + viteConfig.cacheDir, + ), server: { open: false, fs: { diff --git a/packages/vitest/src/node/projects/resolveProjects.ts b/packages/vitest/src/node/projects/resolveProjects.ts index 7d4d4010d..e14e5ae64 100644 --- a/packages/vitest/src/node/projects/resolveProjects.ts +++ b/packages/vitest/src/node/projects/resolveProjects.ts @@ -329,16 +329,17 @@ async function resolveDeclaredProjectEntries( : options.extends !== false ? (parentViteConfig.configFile || false) : false - // if `root` is configured, resolve it relative to the declaring config's - // root (like other options); if `root` is not specified, inline configs - // use the same root as the declaring config - const rawRoot = options.test?.root ?? options.root - const root = rawRoot - ? resolve(configRoot, rawRoot) - : configRoot + // `test.root` overrides the top level `root`, so the entry carries a + // single resolved root; both are resolved relative to the declaring + // config's root (like other options), and inline configs without a + // root use the same root as the declaring config + const { root: testRoot, ...test } = options.test ?? {} + const customRoot = testRoot ?? options.root + const root = customRoot ? resolve(configRoot, customRoot) : configRoot promises.push(concurrent(() => resolveSingleProjectEntry(context, { ...options, + test, root, configFile, }, index))) @@ -733,12 +734,7 @@ async function resolveSingleProjectEntry( isInlineEntry ? { options, extendsTrueRootConfig } : undefined, ), ...(options.plugins || []), - ...WorkspaceVitestPlugin( - harness, - parentViteConfig, - rootConfig, - options, - ), + ...WorkspaceVitestPlugin(harness, parentViteConfig), ...BrowserLoaderPlugin(captures, harness), ], } diff --git a/test/e2e/fixtures/global-setup-root/nested/example.test.ts b/test/e2e/fixtures/global-setup-root/nested/example.test.ts new file mode 100644 index 000000000..f71db9781 --- /dev/null +++ b/test/e2e/fixtures/global-setup-root/nested/example.test.ts @@ -0,0 +1,5 @@ +import { expect, test } from 'vitest' + +test('example test', () => { + expect(1 + 1).toBe(2) +}) diff --git a/test/e2e/fixtures/global-setup-root/nested/global-setup.ts b/test/e2e/fixtures/global-setup-root/nested/global-setup.ts new file mode 100644 index 000000000..3e85587f6 --- /dev/null +++ b/test/e2e/fixtures/global-setup-root/nested/global-setup.ts @@ -0,0 +1,3 @@ +export function setup() { + // File should load without errors +} diff --git a/test/e2e/fixtures/global-setup-root/vitest.config.ts b/test/e2e/fixtures/global-setup-root/vitest.config.ts new file mode 100644 index 000000000..a37f47d0f --- /dev/null +++ b/test/e2e/fixtures/global-setup-root/vitest.config.ts @@ -0,0 +1,9 @@ +import { resolve } from "node:path"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + root: resolve(import.meta.dirname, './nested'), + globalSetup: './global-setup.ts' + } +}) diff --git a/test/e2e/test/config/root.test.ts b/test/e2e/test/config/root.test.ts new file mode 100644 index 000000000..93d4b5344 --- /dev/null +++ b/test/e2e/test/config/root.test.ts @@ -0,0 +1,392 @@ +import { createHash } from 'node:crypto' +import { relative, resolve } from 'pathe' +import { expect, test } from 'vitest' +import { resolveConfig } from 'vitest/node' +import { runVitest, ts, useFS, useTmpFS } from '#test-utils' + +const exampleTest = ts` + import { expect, test } from 'vitest' + + test('example', () => { + expect(1).toBe(1) + }) +` + +const testRootConfig = ts` + import { resolve } from 'node:path' + import { defineConfig } from 'vitest/config' + + export default defineConfig({ + test: { + root: resolve(import.meta.dirname, './nested'), + }, + }) +` + +test('tests are collected from `test.root` of the config file when `--root` is not passed', async () => { + const fs = useTmpFS({ + './vitest.config.ts': testRootConfig, + './nested/example.test.ts': exampleTest, + }) + + const { ctx, stderr } = await runVitest({ + config: fs.resolveFile('./vitest.config.ts'), + }) + + expect(stderr).toBe('') + expect(ctx?.config.root).toBe(resolve(fs.root, 'nested')) + expect(ctx?.state.getTestModules()).toHaveLength(1) +}) + +test('watch mode re-resolves `test.root` when the config changes', async () => { + const fs = useTmpFS({ + './vitest.config.ts': testRootConfig, + './nested/example.test.ts': exampleTest, + './nested2/example.test.ts': exampleTest, + }) + + const { ctx, vitest, stderr } = await runVitest({ + config: fs.resolveFile('./vitest.config.ts'), + watch: true, + }) + + expect(stderr).toBe('') + await vitest.waitForStdout('Waiting for file changes') + expect(ctx?.config.root).toBe(resolve(fs.root, 'nested')) + + fs.editFile('./vitest.config.ts', content => content.replace(`'./nested'`, `'./nested2'`)) + + await vitest.waitForStdout('Restarting due to config changes') + await expect.poll(() => ctx?.config.root, { timeout: 5000 }).toBe(resolve(fs.root, 'nested2')) +}) + +test('`--root` overrides `test.root` from the config file', async () => { + const fs = useTmpFS({ + './vitest.config.ts': testRootConfig, + }) + + const config = await resolveConfig({ root: fs.root }) + + expect(config.test.root).toBe(fs.root) +}) + +test('the Vite `root` from the config file applies when `--root` is not passed', async () => { + const fs = useTmpFS({ + './vitest.config.ts': ts` + import { resolve } from 'node:path' + import { defineConfig } from 'vitest/config' + + export default defineConfig({ + root: resolve(import.meta.dirname, './nested'), + }) + `, + './nested/.keep': '', + }) + + const config = await resolveConfig({ config: fs.resolveFile('./vitest.config.ts') }) + + expect(config.test.root).toBe(resolve(fs.root, 'nested')) +}) + +test('`test.root` overrides the Vite `root` of the same config file', async () => { + const fs = useTmpFS({ + './vitest.config.ts': ts` + import { resolve } from 'node:path' + import { defineConfig } from 'vitest/config' + + export default defineConfig({ + root: resolve(import.meta.dirname, './vite-root'), + test: { + root: resolve(import.meta.dirname, './nested'), + }, + }) + `, + './nested/.keep': '', + }) + + const config = await resolveConfig({ config: fs.resolveFile('./vitest.config.ts') }) + + expect(config.test.root).toBe(resolve(fs.root, 'nested')) +}) + +test('`--root` overrides the `root` from Vite overrides', async () => { + const fs = useTmpFS({ + './vitest.config.ts': ts` + import { defineConfig } from 'vitest/config' + + export default defineConfig({}) + `, + }) + + const config = await resolveConfig( + { root: fs.root, config: fs.resolveFile('./vitest.config.ts') }, + { root: resolve(fs.root, 'nested') }, + ) + + expect(config.test.root).toBe(fs.root) +}) + +test('the `root` from Vite overrides applies when `--root` is not passed', async () => { + const fs = useTmpFS({ + './vitest.config.ts': ts` + import { defineConfig } from 'vitest/config' + + export default defineConfig({}) + `, + './nested/.keep': '', + }) + + const config = await resolveConfig( + { config: fs.resolveFile('./vitest.config.ts') }, + { root: resolve(fs.root, 'nested') }, + ) + + expect(config.test.root).toBe(resolve(fs.root, 'nested')) +}) + +test('`test.root` from the config file overrides the `root` from Vite overrides', async () => { + const fs = useTmpFS({ + './vitest.config.ts': testRootConfig, + './nested/.keep': '', + }) + + const config = await resolveConfig( + { config: fs.resolveFile('./vitest.config.ts') }, + { root: resolve(fs.root, 'other') }, + ) + + expect(config.test.root).toBe(resolve(fs.root, 'nested')) +}) + +test('the `root` from Vite overrides wins over the Vite `root` of the config file', async () => { + const fs = useTmpFS({ + './vitest.config.ts': ts` + import { resolve } from 'node:path' + import { defineConfig } from 'vitest/config' + + export default defineConfig({ + root: resolve(import.meta.dirname, './vite-root'), + }) + `, + './nested/.keep': '', + }) + + const config = await resolveConfig( + { config: fs.resolveFile('./vitest.config.ts') }, + { root: resolve(fs.root, 'nested') }, + ) + + expect(config.test.root).toBe(resolve(fs.root, 'nested')) +}) + +test('a relative `--root` is resolved against the current working directory', async () => { + const fs = useTmpFS({ + './vitest.config.ts': ts` + import { defineConfig } from 'vitest/config' + + export default defineConfig({}) + `, + }) + + const config = await resolveConfig({ root: relative(process.cwd(), fs.root) }) + + expect(config.test.root).toBe(fs.root) +}) + +test('a relative `test.root` is resolved against the current working directory', async () => { + const dirName = `vitest-test-${crypto.randomUUID()}` + const fs = useFS(resolve(process.cwd(), dirName), { + './vitest.config.ts': ts` + import { defineConfig } from 'vitest/config' + + export default defineConfig({ + test: { + root: '${dirName}/nested', + }, + }) + `, + './nested/.keep': '', + }) + + const config = await resolveConfig({ config: fs.resolveFile('./vitest.config.ts') }) + + expect(config.test.root).toBe(resolve(fs.root, 'nested')) +}) + +test('does not load a config from the current working directory when the root has none', async () => { + const fs = useTmpFS({ + './.keep': '', + }, false) + + const config = await resolveConfig({ root: fs.root }) + + expect(config.configFile).toBe(undefined) + expect(config.test.root).toBe(fs.root) +}) + +test('`config: false` ignores the config file inside the root', async () => { + const fs = useTmpFS({ + './vitest.config.ts': testRootConfig, + }) + + const config = await resolveConfig({ root: fs.root, config: false }) + + expect(config.configFile).toBe(undefined) + expect(config.test.root).toBe(fs.root) +}) + +test('a relative `--config` is resolved against `--root`', async () => { + const fs = useTmpFS({ + './conf/vitest.config.ts': ts` + import { defineConfig } from 'vitest/config' + + export default defineConfig({}) + `, + }) + + const config = await resolveConfig({ root: fs.root, config: 'conf/vitest.config.ts' }) + + expect(config.configFile).toBe(fs.resolveFile('./conf/vitest.config.ts')) + expect(config.test.root).toBe(fs.root) +}) + +test('the cache directory is resolved against `test.root`', async () => { + const fs = useTmpFS({ + './vitest.config.ts': testRootConfig, + './nested/.keep': '', + }) + + const config = await resolveConfig({ config: fs.resolveFile('./vitest.config.ts') }) + + expect(config.cacheDir).toBe( + resolve(fs.root, 'nested/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709'), + ) +}) + +test('`test.root` from Vite overrides wins over the config file and loses to `--root`', async () => { + const fs = useTmpFS({ + './vitest.config.ts': testRootConfig, + './override/.keep': '', + }) + + const fromOverrides = await resolveConfig( + { config: fs.resolveFile('./vitest.config.ts') }, + { test: { root: resolve(fs.root, 'override') } }, + ) + expect(fromOverrides.test.root).toBe(resolve(fs.root, 'override')) + + const fromCli = await resolveConfig( + { root: fs.root, config: fs.resolveFile('./vitest.config.ts') }, + { test: { root: resolve(fs.root, 'override') } }, + ) + expect(fromCli.test.root).toBe(fs.root) +}) + +const projectsConfig = ts` + import { resolve } from 'node:path' + import { defineConfig } from 'vitest/config' + + export default defineConfig({ + test: { + projects: [ + { root: resolve(import.meta.dirname, './a'), test: { name: 'a' } }, + { test: { name: 'b', root: resolve(import.meta.dirname, './b') } }, + { + root: resolve(import.meta.dirname, './a'), + test: { name: 'both', root: resolve(import.meta.dirname, './b') }, + }, + ], + }, + }) +` + +function getProjectRoots(config: Awaited>) { + return Object.fromEntries( + config.test.resolvedProjects.map(p => [p.projectConfig.name, p.projectConfig.root]), + ) +} + +test('an inline project resolves its `root` and `test.root`, preferring `test.root`', async () => { + const fs = useTmpFS({ + './vitest.config.ts': projectsConfig, + './a/.keep': '', + './b/.keep': '', + }) + + const config = await resolveConfig({ config: fs.resolveFile('./vitest.config.ts') }) + + expect(getProjectRoots(config)).toEqual({ + a: resolve(fs.root, 'a'), + b: resolve(fs.root, 'b'), + both: resolve(fs.root, 'b'), + }) +}) + +test('a file-based project uses its config file directory as `root`', async () => { + const fs = useTmpFS({ + './vitest.config.ts': ts` + import { defineConfig } from 'vitest/config' + + export default defineConfig({ + test: { + projects: ['./packages/*'], + }, + }) + `, + './packages/one/vitest.config.ts': ts` + import { defineConfig } from 'vitest/config' + + export default defineConfig({}) + `, + './packages/two/vitest.config.ts': ts` + import { resolve } from 'node:path' + import { defineConfig } from 'vitest/config' + + export default defineConfig({ + test: { + root: resolve(import.meta.dirname, '..'), + }, + }) + `, + }) + + const config = await resolveConfig({ root: fs.root }) + + expect(getProjectRoots(config)).toEqual({ + one: resolve(fs.root, 'packages/one'), + two: resolve(fs.root, 'packages'), + }) +}) + +test('`--root` does not override project roots', async () => { + const fs = useTmpFS({ + './vitest.config.ts': projectsConfig, + './a/.keep': '', + './b/.keep': '', + }) + + const config = await resolveConfig({ root: fs.root }) + + expect(config.test.root).toBe(fs.root) + expect(getProjectRoots(config)).toEqual({ + a: resolve(fs.root, 'a'), + b: resolve(fs.root, 'b'), + both: resolve(fs.root, 'b'), + }) +}) + +test('the project cache directory is resolved against the project root', async () => { + const fs = useTmpFS({ + './vitest.config.ts': projectsConfig, + './a/.keep': '', + './b/.keep': '', + }) + + const config = await resolveConfig({ config: fs.resolveFile('./vitest.config.ts') }) + + const projectA = config.test.resolvedProjects.find(p => p.projectConfig.name === 'a')! + const nameHash = createHash('sha1').update('a').digest('hex') + expect(projectA.viteConfig.cacheDir).toBe( + resolve(fs.root, 'a/node_modules/.vite/vitest', nameHash), + ) +}) diff --git a/test/e2e/test/global-setup.test.ts b/test/e2e/test/global-setup.test.ts index 5964f6c36..f5fbbaf13 100644 --- a/test/e2e/test/global-setup.test.ts +++ b/test/e2e/test/global-setup.test.ts @@ -69,3 +69,10 @@ it('runs global setup/teardown', async () => { } `) }) + +it('respects root', async () => { + const config = resolve(import.meta.dirname, '../fixtures/global-setup-root/vitest.config.ts') + const { stderr } = await runVitest({ config }) + + expect(stderr).toBe('') +}) diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts index 5a9fa658a..f16907f84 100644 --- a/test/test-utils/index.ts +++ b/test/test-utils/index.ts @@ -187,7 +187,7 @@ export async function runVitest( ;(viteConfig as any).test = rest try { - ctx = await startVitest(cliFilters, { + ctx = await startVitest(cliFilters, removeUndefinedValues({ root, config: configFile, standalone, @@ -224,7 +224,7 @@ export async function runVitest( diagnostics: rest.experimental?.diagnostics ?? false, ...cliOptions?.experimental, }, - }, { + }), { ...viteConfig, plugins: [ ...(viteConfig.plugins ?? []), @@ -910,3 +910,12 @@ export function buildErrorProjectTree(testModules: TestModule[], options?: Build return projectTree } + +function removeUndefinedValues>(obj: T): T { + for (const key of Object.keys(obj)) { + if (obj[key] === undefined) { + delete obj[key] + } + } + return obj +} -- 2.51.2