diff --git a/docs/api/advanced/plugin.md b/docs/api/advanced/plugin.md
index dbdc1111f..a176f8303 100644
--- a/docs/api/advanced/plugin.md
+++ b/docs/api/advanced/plugin.md
@@ -117,11 +117,11 @@ Note that this will only affect projects injected with [`injectTestProjects`](#i
:::
::: tip Referencing the Current Config
-If you want to keep the user configuration, you can specify the `extends` property. All other properties will be merged with the user defined config.
+Inline configurations inherit the root config by default. If you want to inherit a specific configuration file instead, set the `extends` property to its path. All other properties will be merged with the user defined config.
The project's `configFile` can be accessed in Vite's config: `project.vite.config.configFile`.
-Note that this will also inherit the `name` - Vitest doesn't allow multiple projects with the same name, so this will throw an error. Make sure you specified a different name. You can access the current name via the `project.name` property and all used names are available in the `vitest.projects` array.
+Note that the `name` is never inherited because Vitest doesn't allow multiple projects with the same name. Make sure every project has a unique name. You can access the current name via the `project.name` property and all used names are available in the `vitest.projects` array.
:::
### defineCacheKeyGenerator 5.0.0 {#definecachekeygenerator}
diff --git a/docs/guide/advanced/index.md b/docs/guide/advanced/index.md
index ac83e0b53..1b7d3cc5a 100644
--- a/docs/guide/advanced/index.md
+++ b/docs/guide/advanced/index.md
@@ -130,17 +130,20 @@ The root configuration is resolved from three inputs, in ascending priority:
Every project then resolves its own Vite config independently:
- A project referenced as a config file or a directory resolves only its own file. It does not inherit any options from the root configuration.
-- An inline project with the [`extends`](/guide/projects#configuration) option re-executes the extended config file and merges the project's own options on top. Only the config **file** participates in this: `viteOverrides` and CLI options are not part of the file, so `extends` does not carry them into projects.
+- An inline project inherits the root configuration by default (see [`extends`](/guide/projects#configuration)): the root config file is re-executed for the project, `viteOverrides` are merged on top of it, and the project's own options are merged last. Inheritance works even when there is no root config file, because `viteOverrides` are part of the effective root configuration.
+- With `extends: false`, an inline project resolves only its own options. With `extends: './path'`, the referenced file is re-executed instead of the root config file, and `viteOverrides` are not merged.
-Independently of `extends`, several groups of options reach every project:
+A few options are excluded from inheritance:
-- A fixed subset of CLI options that configure how tests run (`--testTimeout`, `--retry`, `--pool`, and similar) is applied to every project at the highest priority, mirroring the root resolution.
-- Run-level options only make sense for the test run as a whole: every project receives the root's resolved `coverage`, `attachmentsDir`, and `mergeReportsLabel` values.
-- The root's `fsModuleCache`, `fsModuleCachePath`, `experimental.viteModuleRunner`, `experimental.nodeLoader`, and `experimental.importDurations` values are applied as defaults to every project that doesn't define them.
+- `plugins` from `viteOverrides` are never inherited. A config file is re-executed for every project, which creates fresh plugin instances, but plugin instances passed in `viteOverrides` belong to the root Vite server and cannot be shared with project servers.
+- `test.browser` and `test.tagsFilter` from `viteOverrides` are never inherited: `browser` describes the instances of a single project, and `tagsFilter` applies to the whole run.
+- `name` and `projects` are never inherited; the root `globalSetup` is not inherited because it already runs once per test run.
+- The project's own `tags` always replace the `tags` array merged from an extended config instead of being concatenated with it, so the same tag names can be redefined.
-The project's own `tags` always replace the `tags` array merged from an extended config instead of being concatenated with it, so the same tag names can be redefined.
+Independently of `extends`, two groups of options reach every project:
-Note that `plugins` reach a project only through a config file: the file is re-executed for every project, which creates fresh plugin instances. Plugin instances passed in `viteOverrides` belong to the root Vite server and are never shared with project servers.
+- A fixed subset of CLI options that configure how tests run (`--testTimeout`, `--retry`, `--pool`, and similar) is applied to every project at the highest priority, mirroring the root resolution.
+- Run-level options only make sense for the test run as a whole: every project receives the root's resolved `coverage`, `attachmentsDir`, and `mergeReportsLabel` values.
## parseCLI
diff --git a/docs/guide/advanced/pool.md b/docs/guide/advanced/pool.md
index cebe4aea9..5ae745c19 100644
--- a/docs/guide/advanced/pool.md
+++ b/docs/guide/advanced/pool.md
@@ -43,13 +43,11 @@ export default defineConfig({
test: {
projects: [
{
- extends: true,
test: {
pool: 'threads',
},
},
{
- extends: true,
test: {
pool: customPool({
customProperty: true,
diff --git a/docs/guide/browser/visual-regression-testing.md b/docs/guide/browser/visual-regression-testing.md
index a2242f584..2156e42cf 100644
--- a/docs/guide/browser/visual-regression-testing.md
+++ b/docs/guide/browser/visual-regression-testing.md
@@ -82,14 +82,12 @@ export default defineConfig({
// ...other configurations
projects: [
{
- extends: true,
test: {
name: 'unit',
exclude: [vrtPattern, ...defaultExclude],
},
},
{
- extends: true,
test: {
name: 'vrt',
browser: {
@@ -675,7 +673,6 @@ export default defineConfig({
// ...other configurations
projects: [
{
- extends: true,
test: {
name: 'vrt',
browser: {
diff --git a/docs/guide/migration.md b/docs/guide/migration.md
index 972aa5927..015d29fd0 100644
--- a/docs/guide/migration.md
+++ b/docs/guide/migration.md
@@ -56,6 +56,60 @@ export default defineConfig({
})
```
+### Inline Projects Inherit the Root Config by Default
+
+The [`extends`](/guide/projects#configuration) option now defaults to `true`: every project defined as an inline configuration in [`test.projects`](/guide/projects) inherits all options from the root configuration, including Vite options like `plugins` or `resolve.alias`. The options are merged with the same rules that applied to an explicit `extends: true` in Vitest 4:
+
+```ts [vitest.config.ts]
+import { defineConfig } from 'vitest/config'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ projects: [
+ {
+ // v4: this project didn't apply the react plugin
+ // v5: the plugin is inherited from the root config
+ test: {
+ name: 'unit',
+ include: ['**/*.unit.test.ts'],
+ },
+ },
+ ],
+ },
+})
+```
+
+A few options are excluded because they are always scoped to a single project or to the whole test run:
+
+- `name` and `projects` are never inherited.
+- `globalSetup` is not inherited from the root config: the root-level `globalSetup` already runs once per test run, so inheriting it would run the same files again for every project. It is still inherited when extending a non-root config file.
+- The project's own `tags` replace the inherited array instead of being merged with it.
+
+Projects referenced as config files or directories are not affected; they still don't inherit any options from the root config.
+
+Keep in mind that arrays are merged, not overridden. For example, if the root config defines `setupFiles`, the project's own `setupFiles` are appended to the inherited ones. If you need the previous behavior, set `extends: false` in the project configuration:
+
+```ts [vitest.config.ts]
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ setupFiles: ['./setup.global.ts'],
+ projects: [
+ {
+ extends: false, // [!code ++]
+ test: {
+ name: 'unit',
+ setupFiles: ['./setup.unit.ts'],
+ },
+ },
+ ],
+ },
+})
+```
+
### Hoisted Mocking Calls Must Be at the Top Level
[`vi.mock`](/api/vi#vi-mock), [`vi.unmock`](/api/vi#vi-unmock), and [`vi.hoisted`](/api/vi#vi-hoisted) are hoisted to the top of the file and run before any surrounding code. Calling them inside a function, block, or `describe`/`test` callback previously only logged a warning. Vitest 5.0 now throws, because the call does not execute where it is written:
diff --git a/docs/guide/projects.md b/docs/guide/projects.md
index 794a41ddd..e2c22b160 100644
--- a/docs/guide/projects.md
+++ b/docs/guide/projects.md
@@ -126,8 +126,8 @@ export default defineConfig({
// matches every folder and file inside the `packages` folder
'packages/*',
{
- // add "extends: true" to inherit the options from the root config
- extends: true,
+ // inline projects inherit the options
+ // from this config file by default
test: {
include: ['tests/**/*.{browser}.test.{ts,js}'],
// it is recommended to define a name when using inline configs
@@ -136,6 +136,9 @@ export default defineConfig({
}
},
{
+ // add "extends: false" to ignore
+ // the options defined in this config file
+ extends: false,
test: {
include: ['tests/**/*.{node}.test.{ts,js}'],
// color of the name label can be changed
@@ -234,23 +237,7 @@ bun run test --project e2e --project unit
## Configuration
-None of the configuration options are inherited from the root-level config file. You can create a shared config file and merge it with the project config yourself:
-
-```ts [packages/a/vitest.config.ts]
-import { defineProject, mergeConfig } from 'vitest/config'
-import configShared from '../vitest.shared.js'
-
-export default mergeConfig(
- configShared,
- defineProject({
- test: {
- environment: 'jsdom',
- }
- })
-)
-```
-
-Additionally, you can use the `extends` option to inherit from your root-level configuration. All options will be merged.
+Projects defined with an inline configuration inherit all options from the root-level configuration. This is controlled by the `extends` option, which is enabled by default since Vitest 5.0:
```ts [vitest.config.ts]
import { defineConfig } from 'vitest/config'
@@ -262,8 +249,8 @@ export default defineConfig({
pool: 'threads',
projects: [
{
- // will inherit options from this config like plugins and pool
- extends: true,
+ // inherits options from this config like plugins and pool
+ // (`extends: true` is the default)
test: {
name: 'unit',
include: ['**/*.unit.test.ts'],
@@ -271,7 +258,6 @@ export default defineConfig({
},
{
// won't inherit any options from this config
- // this is the default behaviour
extends: false,
test: {
name: 'integration',
@@ -283,6 +269,50 @@ export default defineConfig({
})
```
+The `extends` option also accepts a path to another config file if you want to inherit options from a config file other than the root config:
+
+```ts [vitest.config.ts]
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ projects: [
+ {
+ extends: './vitest.shared.ts',
+ test: {
+ name: 'unit',
+ include: ['**/*.unit.test.ts'],
+ },
+ },
+ ],
+ },
+})
+```
+
+All options from the extended config are merged with the project's own options. Note that arrays like `setupFiles` are concatenated, not overridden. A few options are treated specially:
+
+- `name` and `projects` are never inherited.
+- `globalSetup` is not inherited from the root config: the root-level `globalSetup` already runs once per test run, so inheriting it would run the same files again for every project. It is still inherited when extending a non-root config file.
+- The project's own `tags` replace the inherited array instead of being merged with it.
+
+If you run Vitest through the [advanced API](/guide/advanced/), see [Project Configuration Resolution](/guide/advanced/#project-configuration-resolution) for how the programmatic configuration participates in inheritance.
+
+Projects referenced as config files or directories do not inherit any options from the root config. You can create a shared config file and merge it with the project config yourself:
+
+```ts [packages/a/vitest.config.ts]
+import { defineProject, mergeConfig } from 'vitest/config'
+import configShared from '../vitest.shared.js'
+
+export default mergeConfig(
+ configShared,
+ defineProject({
+ test: {
+ environment: 'jsdom',
+ }
+ })
+)
+```
+
::: danger Unsupported Options
Some of the configuration options are not allowed in a project config. Most notably:
diff --git a/packages/vitest/src/node/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts
index 8ff92afd1..0949aaef4 100644
--- a/packages/vitest/src/node/plugins/workspace.ts
+++ b/packages/vitest/src/node/plugins/workspace.ts
@@ -46,27 +46,6 @@ export function WorkspaceVitestPlugin(
},
}
- // always inherit the global `fsModuleCache` values even without `extends: true`
- if (testConfig.fsModuleCache == null && globalConfig.fsModuleCache != null) {
- testConfig.fsModuleCache = globalConfig.fsModuleCache
- }
- if (testConfig.fsModuleCachePath == null && globalConfig.fsModuleCachePath != null) {
- testConfig.fsModuleCachePath = globalConfig.fsModuleCachePath
- }
-
- // TODO: remove this after "extends: false" is flipped
- testConfig.experimental ??= {}
-
- if (testConfig.experimental?.viteModuleRunner == null && globalConfig.experimental?.viteModuleRunner != null) {
- testConfig.experimental.viteModuleRunner = globalConfig.experimental.viteModuleRunner
- }
- if (testConfig.experimental?.nodeLoader == null && globalConfig.experimental?.nodeLoader != null) {
- testConfig.experimental.nodeLoader = globalConfig.experimental.nodeLoader
- }
- if (testConfig.experimental?.importDurations == null && globalConfig.experimental?.importDurations != null) {
- testConfig.experimental.importDurations = globalConfig.experimental.importDurations
- }
-
return config
},
configResolved(config) {
diff --git a/packages/vitest/src/node/projects/resolveProjects.ts b/packages/vitest/src/node/projects/resolveProjects.ts
index 262de3eaa..77fde1a0a 100644
--- a/packages/vitest/src/node/projects/resolveProjects.ts
+++ b/packages/vitest/src/node/projects/resolveProjects.ts
@@ -2,6 +2,7 @@ import type { GlobOptions } from 'tinyglobby'
import type {
ResolvedConfig as ResolvedViteConfig,
InlineConfig as ViteInlineConfig,
+ Plugin as VitePlugin,
} from 'vite'
import type { PluginHarness } from '../config/pluginHarness'
import type { Vitest } from '../core'
@@ -62,6 +63,9 @@ const PROJECT_CLI_OVERRIDES = [
'fileParallelism',
'tagsFilter',
'browser',
+ 'experimental',
+ 'fsModuleCache',
+ 'fsModuleCachePath',
] as const
/**
@@ -118,17 +122,19 @@ export async function resolveProjectEntries(
}
const existing = seenNames.get(name)
if (existing) {
- const entryFile = entry.viteConfig.configFile
+ // inline entries carry the configFile they extend, which doesn't say
+ // where the project is declared, so they are reported without a file
+ const entryFile = !entry.inline && entry.viteConfig.configFile
? relative(globalConfig.root, entry.viteConfig.configFile)
: ''
- const existingFile = existing.viteConfig.configFile
+ const existingFile = !existing.inline && existing.viteConfig.configFile
? relative(globalConfig.root, existing.viteConfig.configFile)
: ''
const filesError = baseEntries.length > 1 && (entryFile || existingFile)
? [
'\n\nYour config matched these files:\n',
baseEntries
- .filter(e => e.viteConfig.configFile)
+ .filter(e => !e.inline && e.viteConfig.configFile)
.map(e => ` - ${relative(globalConfig.root, e.viteConfig.configFile as string)}`)
.join('\n'),
'\n\n',
@@ -282,7 +288,7 @@ async function resolveDeclaredProjectEntries(
// if extends a config file, resolve the file path
const configFile = typeof options.extends === 'string'
? resolve(configRoot, options.extends)
- : options.extends === true
+ : options.extends !== false
? (globalViteConfig.configFile || false)
: false
// if `root` is configured, resolve it relative to vite root (like other options)
@@ -345,11 +351,77 @@ async function resolveDeclaredProjectEntries(
return entries
}
+// `name` must stay unique per project, `projects` would redefine the whole workspace
+const NON_INHERITED_OPTIONS = ['name', 'projects'] as const
+
+// the root `globalSetup` already runs once per test run; a non-root
+// config keeps it because nothing else runs it
+const NON_INHERITED_ROOT_OPTIONS = [...NON_INHERITED_OPTIONS, 'globalSetup'] as const
+
+function ProjectInheritancePlugin(options: ViteInlineConfig, extendsRootConfig: boolean): VitePlugin {
+ const nonInheritedOptions = extendsRootConfig
+ ? NON_INHERITED_ROOT_OPTIONS
+ : NON_INHERITED_OPTIONS
+ return {
+ name: 'vitest:project-inheritance',
+ enforce: 'pre',
+ config: {
+ // run before other `config` hooks so only the values merged from the
+ // extended config file are removed, not the values set by plugins
+ order: 'pre',
+ handler(config) {
+ config.test ??= {}
+ // the project's own `tags` replace the inherited array so tags can be overridden
+ if (options.test?.tags) {
+ config.test.tags = options.test.tags
+ }
+ for (const key of nonInheritedOptions) {
+ if (options.test?.[key] !== undefined) {
+ (config.test as any)[key] = options.test[key]
+ }
+ else {
+ delete config.test[key]
+ }
+ }
+ },
+ },
+ api: {
+ vitest: {
+ ignoreFsModuleCache: true,
+ },
+ },
+ }
+}
+
+/**
+ * Merges the programmatic config passed to `createVitest` into an extending
+ * project's options. The programmatic config is part of the effective root
+ * config, so a project inherits it even when the root config file doesn't
+ * exist. Some options never transfer to a project:
+ * - `plugins` are live instances owned by the root server
+ * - `tagsFilter` is CLI-only; `PROJECT_CLI_OVERRIDES` applies it per project
+ * - `browser` describes the instances of a single project; inheriting it
+ * would create duplicate instance names (the `--browser` flags have the
+ * same guard in `CliOverride`)
+ */
+function inheritRootViteOverrides(
+ globalConfig: ResolvedConfig,
+ options: ViteInlineConfig,
+): ViteInlineConfig {
+ const { plugins: _plugins, ...rootViteOverrides } = globalConfig.viteOverrides
+ // cloned so plugins that mutate inherited arrays in place don't share
+ // them between the root and every project
+ const inherited = deepClone(rootViteOverrides)
+ delete (inherited.test as UserConfig | undefined)?.tagsFilter
+ delete (inherited.test as UserConfig | undefined)?.browser
+ return mergeConfig(inherited, options)
+}
+
async function resolveSingleProjectEntry(
harness: PluginHarness,
globalViteConfig: ResolvedViteConfig,
globalConfig: ResolvedConfig,
- options: ViteInlineConfig,
+ options: ViteInlineConfig & { extends?: string | boolean },
workspacePath: string | number,
cliOverrides: UserConfig,
): Promise {
@@ -357,8 +429,22 @@ async function resolveSingleProjectEntry(
const browserHolder: BrowserContributionHolder = {}
+ // only inline entries (keyed by their index) extend another config;
+ // file-based projects own all of their values
+ const isInlineEntry = typeof workspacePath === 'number'
+ const inheritsRootConfig = isInlineEntry
+ && options.extends !== false
+ && typeof options.extends !== 'string'
+ // `extends: './path'` can still point back to the root config file
+ const extendsRootConfig = inheritsRootConfig
+ || (!!configFile && configFile === globalViteConfig.configFile)
+
+ const inlineOptions = inheritsRootConfig
+ ? inheritRootViteOverrides(globalConfig, restOptions)
+ : restOptions
+
const projectInline: ViteInlineConfig = {
- ...restOptions,
+ ...inlineOptions,
configFile,
configLoader: globalViteConfig.inlineConfig.configLoader,
// this will make "mode": "test" inside defineConfig
@@ -373,23 +459,9 @@ async function resolveSingleProjectEntry(
options,
),
...BrowserLoaderPlugin(browserHolder, harness),
- {
- name: 'vitest:tags',
- config(config) {
- // We need to keep the `tags` array untouched if `extends` is `true`,
- // Otherwise it gets merged with the top level tags and we don't want that because tags could be overridden
- // Setting it to `options.test?.tags` overrides the merged value
- if (options.test?.tags) {
- config.test ??= {}
- config.test.tags = options.test?.tags
- }
- },
- api: {
- vitest: {
- ignoreFsModuleCache: true,
- },
- },
- },
+ ...(isInlineEntry
+ ? [ProjectInheritancePlugin(options, extendsRootConfig)]
+ : []),
],
}
@@ -423,6 +495,7 @@ async function resolveSingleProjectEntry(
return {
viteConfig: projectViteConfig,
projectConfig,
+ inline: isInlineEntry,
}
}
@@ -732,7 +805,7 @@ async function resolveTestProjectConfigs(
projectsDefinition: TestProjectConfiguration[],
) {
// project configurations that were specified directly
- const projectsOptions: (UserWorkspaceConfig & { extends?: true | string })[] = []
+ const projectsOptions: (UserWorkspaceConfig & { extends?: boolean | string })[] = []
// custom config files that were specified directly or resolved from a directory
const projectsConfigFiles: string[] = []
diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts
index 5d87d7e4f..2090fe6ee 100644
--- a/packages/vitest/src/node/types/config.ts
+++ b/packages/vitest/src/node/types/config.ts
@@ -1313,6 +1313,12 @@ export interface ResolvedProjectEntry {
* reference it via `_parent`) but is NOT pushed to `vitest.projects`.
*/
hidden?: boolean
+ /**
+ * The project was declared as an inline configuration. Its
+ * `viteConfig.configFile` is the config it extends (the root config file
+ * by default), not a file of its own.
+ */
+ inline?: boolean
}
type NonProjectOptions
@@ -1407,9 +1413,11 @@ export type TestProjectInlineConfiguration = (UserWorkspaceConfig & {
/**
* Relative path to the extendable config. All other options will be merged with this config.
* If `true`, the project will inherit all options from the root config.
+ * Set to `false` to keep the project configuration completely separate from the root config.
+ * @default true
* @example '../vite.config.ts'
*/
- extends?: string | true
+ extends?: string | boolean
})
export type TestProjectConfiguration
diff --git a/test/e2e/test/projects.test.ts b/test/e2e/test/projects.test.ts
index 65d554e4a..1b39ff4fa 100644
--- a/test/e2e/test/projects.test.ts
+++ b/test/e2e/test/projects.test.ts
@@ -1,4 +1,4 @@
-import { runInlineTests, runVitest } from '#test-utils'
+import { runInlineTests, runVitest, ts } from '#test-utils'
import { resolve } from 'pathe'
import { describe, expect, it } from 'vitest'
@@ -82,7 +82,6 @@ it('can define inline workspace config programmatically', async () => {
},
projects: [
{
- extends: true,
test: {
name: 'project-1',
},
@@ -118,6 +117,124 @@ it('correctly inherits the root config', async () => {
expect(stdout).toContain('repro.test.js > importing a virtual module')
})
+describe('the root config inheritance', () => {
+ const basicTest = ts`
+ import { test } from 'vitest'
+ test('runs', () => {})
+ `
+
+ it('inline projects inherit options from the root config by default', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'vitest.config.js': {
+ test: {
+ testTimeout: 1234,
+ projects: [
+ { test: { name: 'inherited' } },
+ { extends: false, test: { name: 'isolated' } },
+ ],
+ },
+ },
+ 'basic.test.js': basicTest,
+ })
+ expect(stderr).toBe('')
+ const timeouts = Object.fromEntries(
+ ctx!.projects.map(project => [project.name, project.config.testTimeout]),
+ )
+ expect(timeouts).toEqual({
+ inherited: 1234,
+ isolated: 5000,
+ })
+ expect(ctx!.projects.map(project => project.config.projects)).toEqual([
+ undefined,
+ undefined,
+ ])
+ })
+
+ it('the root name and globalSetup are not inherited by the projects', async () => {
+ const { stderr, ctx, fs } = await runInlineTests({
+ 'globalSetup.js': ts`
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
+ import { resolve } from 'node:path'
+
+ export default function setup(project) {
+ const file = resolve(project.config.root, 'setup-runs.txt')
+ const runs = existsSync(file) ? Number(readFileSync(file, 'utf-8')) : 0
+ writeFileSync(file, String(runs + 1))
+ }
+ `,
+ 'vitest.config.js': {
+ test: {
+ name: 'root',
+ globalSetup: './globalSetup.js',
+ projects: [
+ { test: {} },
+ { test: {} },
+ ],
+ },
+ },
+ 'basic.test.js': basicTest,
+ })
+ expect(stderr).toBe('')
+ expect(ctx!.projects.map(project => project.name)).toEqual(['0', '1'])
+ expect(fs.readFile('setup-runs.txt')).toBe('1')
+ })
+
+ it('globalSetup from an extended non-root config runs for every project', async () => {
+ const { stderr, fs } = await runInlineTests({
+ 'globalSetup.js': ts`
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
+ import { resolve } from 'node:path'
+
+ export default function setup(project) {
+ const file = resolve(project.config.root, 'setup-runs.txt')
+ const runs = existsSync(file) ? Number(readFileSync(file, 'utf-8')) : 0
+ writeFileSync(file, String(runs + 1))
+ }
+ `,
+ 'vitest.shared.js': { test: { globalSetup: './globalSetup.js' } },
+ 'vitest.config.js': {
+ test: {
+ projects: [
+ { extends: './vitest.shared.js', test: { name: 'a' } },
+ { extends: './vitest.shared.js', test: { name: 'b' } },
+ ],
+ },
+ },
+ 'basic.test.js': basicTest,
+ })
+ expect(stderr).toBe('')
+ expect(fs.readFile('setup-runs.txt')).toBe('2')
+ })
+
+ it('the project tags replace the inherited tags', async () => {
+ const { stderr, ctx } = await runInlineTests({
+ 'vitest.config.js': {
+ test: {
+ tags: [{ name: 'shared', retry: 2 }],
+ projects: [
+ // the same tag name would be a duplicate tag error
+ // if the arrays were merged instead of replaced
+ { test: { name: 'own-tags', tags: [{ name: 'shared', retry: 5 }] } },
+ { test: { name: 'inherited-tags' } },
+ ],
+ },
+ },
+ 'basic.test.js': basicTest,
+ })
+ expect(stderr).toBe('')
+ const retries = Object.fromEntries(
+ ctx!.projects.map(project => [
+ project.name,
+ project.config.tags.find(tag => tag.name === 'shared')!.retry,
+ ]),
+ )
+ expect(retries).toEqual({
+ 'own-tags': 5,
+ 'inherited-tags': 2,
+ })
+ })
+})
+
it('fails if workspace is empty', async () => {
const { stderr } = await runVitest({
config: false,