···6767Extend this interface when creating custom test artifacts. Vitest automatically manages the `attachments` array and injects the `location` property to indicate where the artifact was created in your test code.
68686969::: danger
7070-When running with [`api.allowWrite`](/config/api#api-allowwrite) or [`browser.api.allowWrite`](/config/browser/api#api-allowwrite) disabled, Vitest empties the `attachments` array on every artifact before reporting it.
7070+When running with [`api.allowWrite`](/config/api#api-allowwrite) disabled, Vitest empties the `attachments` array on every artifact before reporting it.
71717272If your custom artifact narrows the `attachments` type (e.g. to a tuple), include `| []` in the union so the type reflects what actually happens at runtime.
7373:::
+1-1
docs/api/advanced/test-module.md
···153153function logs(): ReadonlyArray<UserConsoleLog>
154154```
155155156156-Console logs recorded on top level of the module during test collection.For example:
156156+Console logs recorded on top level of the module during test collection. For example:
157157158158```ts
159159console.log('included') // [!code highlight]
+3-3
docs/api/browser/commands.md
···1818::: tip
1919The built-in file commands follow Vite's [`server.fs`](https://vitejs.dev/config/server-options.html#server-fs-allow) restrictions for security reasons.
20202121-`writeFile` and `removeFile` also require write access through [`browser.api.allowWrite`](/config/browser/api) and [`api.allowWrite`](/config/api#api-allowwrite).
2121+`writeFile` and `removeFile` also require write access through [`api.allowWrite`](/config/api#api-allowwrite).
2222:::
23232424```ts
···6060::: warning
6161CDP session works only with `playwright` provider and only when using `chromium` browser. You can read more about it in playwright's [`CDPSession`](https://playwright.dev/docs/api/class-cdpsession) documentation.
62626363-CDP is a privileged debugging API. It is available only when browser API write and exec operations are enabled through [`browser.api.allowWrite`](/config/browser/api#api-allowwrite), [`browser.api.allowExec`](/config/browser/api#api-allowexec), [`api.allowWrite`](/config/api#api-allowwrite), and [`api.allowExec`](/config/api#api-allowexec).
6363+CDP is a privileged debugging API. It is available only when browser API write and exec operations are enabled through [`api.allowWrite`](/config/api#api-allowwrite), and [`api.allowExec`](/config/api#api-allowexec).
6464:::
65656666## Custom Commands
···131131132132Vitest's built-in file commands validate paths against Vite's [`server.fs`](https://vite.dev/config/server-options#server-fs-allow) restrictions and separately check whether writes are allowed. Custom commands do not automatically inherit these protections. If a custom command accepts browser-provided input and uses it to read, write, delete, execute, or expose local resources, validate that input before using it.
133133134134-For file reads or fixture loading, use `isFileLoadingAllowed` from `vitest/node` or an explicit allowlist. For writes and deletes, also require an explicit mutation policy, such as [`browser.api.allowWrite`](/config/browser/api#api-allowwrite), [`api.allowWrite`](/config/api#api-allowwrite), and a command-specific allowed directory. For commands that execute code, shell commands, or project scripts, also check [`browser.api.allowExec`](/config/browser/api#api-allowexec) and [`api.allowExec`](/config/api#api-allowexec).
134134+For file reads or fixture loading, use `isFileLoadingAllowed` from `vitest/node` or an explicit allowlist. For writes and deletes, also require an explicit mutation policy, such as [`api.allowWrite`](/config/api#api-allowwrite), and a command-specific allowed directory. For commands that execute code, shell commands, or project scripts, also check [`api.allowExec`](/config/api#api-allowexec).
135135136136For example, if you create your own file-writing command instead of using Vitest's built-in `writeFile`, apply the same checks:
137137
+1-1
docs/api/browser/context.md
···212212::: warning
213213CDP session works only with `playwright` provider and only when using `chromium` browser. You can read more about it in playwright's [`CDPSession`](https://playwright.dev/docs/api/class-cdpsession) documentation.
214214215215-CDP is a privileged debugging API. It is available only when browser API write and exec operations are enabled through [`browser.api.allowWrite`](/config/browser/api#api-allowwrite), [`browser.api.allowExec`](/config/browser/api#api-allowexec), [`api.allowWrite`](/config/api#api-allowwrite), and [`api.allowExec`](/config/api#api-allowexec).
215215+CDP is a privileged debugging API. It is available only when browser API write and exec operations are enabled through [`api.allowWrite`](/config/api#api-allowwrite), and [`api.allowExec`](/config/api#api-allowexec).
216216:::
217217218218```ts
+5-2
docs/config/api.md
···99- **Default:** `false`
1010- **CLI:** `--api`, `--api.port`, `--api.host`, `--api.strictPort`
11111212-Listen to port and serve API for [the UI](/guide/ui) or [browser server](/guide/browser/). When set to `true`, the default port is `51204`.
1212+Listen to port and serve API for [the UI](/guide/ui) or [browser server](/guide/browser/). When set to `true`, the default port is `51204` or `63315` if running in Browser Mode.
13131414## api.allowWrite <Version>4.1.0</Version> {#api-allowwrite}
1515···1717- **Default:** `true` if not exposed to the network, `false` otherwise
18181919Vitest server can save test files or snapshot files via the API. This allows anyone who can connect to the API the ability to run any arbitrary code on your machine.
2020+2121+In Browser Mode Vitest saves [annotation attachments](/guide/test-annotations), [artifacts](/api/advanced/artifacts) and [snapshots](/guide/snapshot) by receiving a WebSocket connection from the browser. This allows anyone who can connect to the API write any arbitrary code on your machine within the root of your project (configured by [`fs.allow`](https://vite.dev/config/server-options#server-fs-allow)). This option also gates privileged browser APIs that can write files indirectly, such as raw Chrome DevTools Protocol access through [`cdp()`](/api/browser/context#cdp).
20222123::: danger SECURITY ADVICE
2224Vitest does not expose the API to the internet by default and only listens on `localhost`. However if `host` is manually exposed to the network, anyone who connects to it can run arbitrary code on your machine, unless `api.allowWrite` and `api.allowExec` are set to `false`.
···2931- **Type:** `boolean`
3032- **Default:** `true` if not exposed to the network, `false` otherwise
31333232-Allows running any test file via the API. See the security advice in [`api.allowWrite`](#api-allowwrite).
3434+Allows running any test file via the UI. This applies to the interactive elements (and the server code behind them) in the [UI](/guide/ui) that can run the code. This option also gates privileged browser APIs that can execute code indirectly, such as raw Chrome DevTools Protocol access through [`cdp()`](/api/browser/context#cdp).
3535+
-28
docs/config/browser/api.md
···11----
22-title: browser.api | Config
33-outline: deep
44----
55-66-# browser.api
77-88-- **Type:** `number | object`
99-- **Default:** `63315`
1010-- **CLI:** `--browser.api=63315`, `--browser.api.port=1234, --browser.api.host=example.com`
1111-1212-Configure options for Vite server that serves code in the browser. Does not affect [`test.api`](/config/api) option. By default, Vitest assigns port `63315` to avoid conflicts with the development server, allowing you to run both in parallel.
1313-1414-## api.allowWrite <Version>4.1.0</Version> {#api-allowwrite}
1515-1616-- **Type:** `boolean`
1717-- **Default:** `true` if not exposed to the network, `false` otherwise
1818-1919-Vitest saves [annotation attachments](/guide/test-annotations), [artifacts](/api/advanced/artifacts) and [snapshots](/guide/snapshot) by receiving a WebSocket connection from the browser. This allows anyone who can connect to the API write any arbitrary code on your machine within the root of your project (configured by [`fs.allow`](https://vite.dev/config/server-options#server-fs-allow)). This option also gates privileged browser APIs that can write files indirectly, such as raw Chrome DevTools Protocol access through [`cdp()`](/api/browser/context#cdp).
2020-2121-If browser server is not exposed to the internet (the host is `localhost`), this should not be a problem, so the default value in that case is `true`. If you override the host, Vitest will set `allowWrite` to `false` by default to prevent potentially harmful writes.
2222-2323-## api.allowExec <Version>4.1.0</Version> {#api-allowexec}
2424-2525-- **Type:** `boolean`
2626-- **Default:** `true` if not exposed to the network, `false` otherwise
2727-2828-Allows running any test file via the UI. This applies to the interactive elements (and the server code behind them) in the [UI](/guide/ui) that can run the code. This option also gates privileged browser APIs that can execute code indirectly, such as raw Chrome DevTools Protocol access through [`cdp()`](/api/browser/context#cdp). See [`api.allowExec`](/config/api#api-allowexec) for more information.
-16
docs/config/browser/isolate.md
···11----
22-title: browser.isolate | Config
33-outline: deep
44----
55-66-# browser.isolate <Deprecated />
77-88-- **Type:** `boolean`
99-- **Default:** the same as [`--isolate`](/config/isolate)
1010-- **CLI:** `--browser.isolate`, `--browser.isolate=false`
1111-1212-Run every test in a separate iframe.
1313-1414-::: danger DEPRECATED
1515-This option is deprecated. Use [`isolate`](/config/isolate) instead.
1616-:::
+59-15
docs/guide/advanced/index.md
···8585function resolveConfig(
8686 options: UserConfig = {},
8787 viteOverrides: ViteUserConfig = {},
8888-): Promise<{
8989- vitestConfig: ResolvedConfig
9090- viteConfig: ResolvedViteConfig
9191-}>
8888+ harness?: PluginHarness,
8989+): Promise<ResolvedViteConfig>
9290```
93919494-This method resolves the config with custom parameters. If no parameters are given, the `root` will be `process.cwd()`.
9292+This method resolves the config with custom parameters, without creating a Vite server. If no parameters are given, the `root` will be `process.cwd()`.
9393+9494+It returns the resolved Vite config. The fully resolved Vitest config, including every project, lives on its `test` property.
95959696```ts
9797import { resolveConfig } from 'vitest/node'
98989999-// vitestConfig only has resolved "test" properties
100100-const { vitestConfig, viteConfig } = await resolveConfig({
9999+const viteConfig = await resolveConfig({
101100 mode: 'custom',
102101 configFile: false,
103102 resolve: {
···108107 pool: 'threads',
109108 },
110109})
110110+111111+viteConfig.test.pool // 'threads'
111112```
112113113114::: info
114114-Due to how Vite's `createServer` works, Vitest has to resolve the config during the plugin's `configResolve` hook. Therefore, this method is not actually used internally and is exposed exclusively as a public API.
115115+This is the same method Vitest uses internally to resolve the config before creating the server. If you pass the options down to `startVitest` or `createVitest`, Vitest resolves them again.
115116116116-If you pass down the config to the `startVitest` or `createVitest` APIs, Vitest will still resolve the config again.
117117-:::
118118-119119-::: warning
120120-The `resolveConfig` doesn't resolve `projects`. To resolve projects configs, Vitest needs an established Vite server.
121121-122122-Also note that `viteConfig.test` will not be fully resolved. If you need Vitest config, use `vitestConfig` instead.
117117+You can pass a shared [`PluginHarness`](#pluginharness) as the third argument to reuse a logger and package installer across calls.
123118:::
124119125120## parseCLI
···147142result.filter
148143// ['./files.ts']
149144```
145145+146146+## createCLI
147147+148148+```ts
149149+function createCLI(options?: CliParseOptions): CAC
150150+```
151151+152152+Creates the Vitest command-line interface: a [`cac`](https://github.com/cacjs/cac) instance with all of Vitest's commands and options registered. [`parseCLI`](#parsecli) is built on top of it; use `createCLI` directly if you need the raw parser.
153153+154154+```ts
155155+import { createCLI } from 'vitest/node'
156156+157157+const cli = createCLI()
158158+```
159159+160160+## PluginHarness
161161+162162+```ts
163163+class PluginHarness {
164164+ vitest?: Vitest
165165+ version: string
166166+ logger: Logger
167167+ packageInstaller: VitestPackageInstaller
168168+ getVitest(): Vitest
169169+}
170170+```
171171+172172+A container that Vitest passes to its internal plugins while the config is being resolved, before a [`Vitest`](/api/advanced/vitest) instance exists. It holds the [`Logger`](#logger), the package installer and the resolved version, and exposes the `Vitest` instance via `getVitest()` once it has been created (calling it earlier throws).
173173+174174+This is an advanced, plugin-facing API. You rarely construct one directly, but you can pass a shared instance to [`resolveConfig`](#resolveconfig) to reuse a logger and package installer.
175175+176176+## Logger
177177+178178+```ts
179179+class Logger {
180180+ constructor(
181181+ outputStream?: Writable,
182182+ errorStream?: Writable,
183183+ )
184184+}
185185+```
186186+187187+Vitest's terminal logger, exposed as [`vitest.logger`](/api/advanced/vitest). It handles formatted output, the error summary, the run banner and screen clearing. Construct one with custom `stdout`/`stderr` streams to capture or redirect Vitest's output when running it programmatically.
188188+189189+```ts
190190+import { Logger } from 'vitest/node'
191191+192192+const logger = new Logger(process.stdout, process.stderr)
193193+```
+1-1
docs/guide/browser/index.md
···118118```
119119120120::: info
121121-Vitest assigns port `63315` to avoid conflicts with the development server, allowing you to run both in parallel. You can change that with the [`browser.api`](/config/browser/api) option.
121121+Vitest assigns port `63315` to avoid conflicts with the development server, allowing you to run both in parallel. You can change that with the [`api`](/config/api) option.
122122:::
123123124124If you have not used Vite before, make sure you have your framework's plugin installed and specified in the config. Some frameworks might require extra configuration to work - check their Vite related documentation to be sure.
-48
docs/guide/cli-generated.md
···353353354354Run the browser in headless mode (i.e. without opening the GUI (Graphical User Interface)). If you are running Vitest in CI, it will be enabled by default (default: `process.env.CI`)
355355356356-### browser.api.port
357357-358358-- **CLI:** `--browser.api.port [port]`
359359-- **Config:** [browser.api.port](/config/browser/api#api-port)
360360-361361-Specify server port. Note if the port is already being used, Vite will automatically try the next available port so this may not be the actual port the server ends up listening on. If true will be set to `63315`
362362-363363-### browser.api.host
364364-365365-- **CLI:** `--browser.api.host [host]`
366366-- **Config:** [browser.api.host](/config/browser/api#api-host)
367367-368368-Specify which IP addresses the server should listen on. Set this to `0.0.0.0` or `true` to listen on all addresses, including LAN and public addresses
369369-370370-### browser.api.strictPort
371371-372372-- **CLI:** `--browser.api.strictPort`
373373-- **Config:** [browser.api.strictPort](/config/browser/api#api-strictport)
374374-375375-Set to true to exit if port is already in use, instead of automatically trying the next available port
376376-377377-### browser.api.allowExec
378378-379379-- **CLI:** `--browser.api.allowExec`
380380-- **Config:** [browser.api.allowExec](/config/browser/api#api-allowexec)
381381-382382-Allow API to execute code. (Be careful when enabling this option in untrusted environments)
383383-384384-### browser.api.allowWrite
385385-386386-- **CLI:** `--browser.api.allowWrite`
387387-- **Config:** [browser.api.allowWrite](/config/browser/api#api-allowwrite)
388388-389389-Allow API to edit files. (Be careful when enabling this option in untrusted environments)
390390-391391-### browser.isolate
392392-393393-- **CLI:** `--browser.isolate`
394394-- **Config:** [browser.isolate](/config/browser/isolate)
395395-396396-Run every browser test file in isolation. To disable isolation, use `--browser.isolate=false` (default: `true`)
397397-398356### browser.ui
399357400358- **CLI:** `--browser.ui`
···408366- **Config:** [browser.detailsPanelPosition](/config/browser/detailspanelposition)
409367410368Default position for the details panel in browser mode. Either `right` (horizontal split) or `bottom` (vertical split) (default: `right`)
411411-412412-### browser.fileParallelism
413413-414414-- **CLI:** `--browser.fileParallelism`
415415-416416-Should browser test files run in parallel. Use `--browser.fileParallelism=false` to disable (default: `true`)
417369418370### browser.connectTimeout
419371
···8686 }
8787 }
88888989- if (config.browser.isolate === false) {
8989+ if (config.isolate === false) {
9090 await this.runNonIsolatedTests(container, options, startTime, orchestratorSpan.context)
9191 await endSpan()
9292 return
···119119120120 public async cleanupTesters(): Promise<void> {
121121 const config = getConfig()
122122- if (config.browser.isolate) {
122122+ if (config.isolate) {
123123 // isolated mode assigns filepaths as ids
124124 const files = Array.from(this.iframes.keys())
125125 // when the run is completed, show the last file in the UI
···11import type { Rollup } from 'vite'
22import type { Plugin } from 'vitest/config'
33-import type { BrowserProvider } from 'vitest/node'
33+import type { BrowserProvider, BrowserServerContribution } from 'vitest/node'
44import type { ParentBrowserProject } from '../projectParent'
55import { fileURLToPath } from 'node:url'
66import { slash } from '@vitest/utils/helpers'
···8899const VIRTUAL_ID_CONTEXT = '\0vitest/browser'
1010const ID_CONTEXT = 'vitest/browser'
1111-// for libraries that use an older import but are not type checked
1212-const DEPRECATED_ID_CONTEXT = '@vitest/browser/context'
1313-1414-const DEPRECATED_VIRTUAL_ID_UTILS = '\0@vitest/browser/utils'
1515-const DEPRECATED_ID_UTILS = '@vitest/browser/utils'
16111712const __dirname = dirname(fileURLToPath(import.meta.url))
18131919-export default function BrowserContext(globalServer: ParentBrowserProject): Plugin {
1414+export default function BrowserContext(contribution: BrowserServerContribution): Plugin {
2015 return {
2116 name: 'vitest:browser:virtual-module:context',
2217 enforce: 'pre',
2323- resolveId(id, importer) {
2424- if (id === ID_CONTEXT) {
2525- return VIRTUAL_ID_CONTEXT
2626- }
2727- if (id === DEPRECATED_ID_CONTEXT) {
2828- if (importer && !importer.includes('/node_modules/')) {
2929- globalServer.vitest.logger.deprecate(
3030- `${importer} tries to load a deprecated "${id}" module. `
3131- + `This import will stop working in the next major version. `
3232- + `Please, use "vitest/browser" instead.`,
3333- )
1818+ resolveId: {
1919+ order: 'pre',
2020+ handler(id) {
2121+ if (id === ID_CONTEXT) {
2222+ return VIRTUAL_ID_CONTEXT
3423 }
3535- return VIRTUAL_ID_CONTEXT
3636- }
3737- if (id === DEPRECATED_ID_UTILS) {
3838- return DEPRECATED_VIRTUAL_ID_UTILS
3939- }
2424+ },
4025 },
4126 load(id) {
2727+ const globalServer = contribution.parent as ParentBrowserProject
4228 if (id === VIRTUAL_ID_CONTEXT) {
4329 return generateContextFile.call(this, globalServer)
4430 }
4545- if (id === DEPRECATED_VIRTUAL_ID_UTILS) {
4646- return `
4747-import { utils } from 'vitest/browser'
4848-export const getElementLocatorSelectors = utils.getElementLocatorSelectors
4949-export const debug = utils.debug
5050-export const prettyDOM = utils.prettyDOM
5151-export const getElementError = utils.getElementError
5252- `
5353- }
5431 },
5532 }
5633}
···6037 globalServer: ParentBrowserProject,
6138) {
6239 const commands = Object.keys(globalServer.commands)
6363- const provider = [...globalServer.children][0].provider
6464- const providerName = provider?.name || 'preview'
4040+ // The provider instance is initialized lazily when a page opens, so reading
4141+ // `child.provider` here races and can be `undefined` before the first page is
4242+ // opened. The provider name is uniform across a project's instances and is
4343+ // already resolved on the config, so read it from there for this shared
4444+ // (cached) context module. The instance is still used below for the
4545+ // preview-only user-event import.
4646+ const child = [...globalServer.children][0]
4747+ const provider = child.provider
4848+ const providerName = child.config.browser.provider?.name
4949+ if (!providerName) {
5050+ throw new Error(
5151+ `Browser provider is not defined for the project "${child.project.name}". This is a bug in Vitest. Please, open a new issue with a reproduction.`,
5252+ )
5353+ }
65546655 const commandsCode = commands
6756 .filter(command => !command.startsWith('__vitest'))
···10695 throw new Error(`Failed to resolve user-event package from ${previewDistRoot}`)
10796 }
10897 return `\
109109-import { userEvent as __vitest_user_event__ } from '${slash(`/@fs/${resolved.id}`)}'
110110-const _userEventSetup = __vitest_user_event__
9898+import { userEvent as _userEventSetup } from '${slash(`/@fs/${resolved.id}`)}'
11199`
112100}
+7-7
packages/browser/src/node/projectParent.ts
···5151 private sourceMapCache = new Map<string, any>()
52525353 constructor(
5454- public project: TestProject,
5454+ ctx: { config: ResolvedConfig; vitest: Vitest },
5555 public base: string,
5656 ) {
5757- this.vitest = project.vitest
5858- this.config = project.config
5757+ this.vitest = ctx.vitest
5858+ this.config = ctx.config
5959 this.stackTraceOptions = {
6060- frameFilter: project.config.onStackTrace,
6060+ frameFilter: this.config.onStackTrace,
6161 getSourceMap: (id) => {
6262 if (this.sourceMapCache.has(id)) {
6363 return this.sourceMapCache.get(id)
···100100 }
101101102102 // validate names because they can't be used as identifiers
103103- for (const command in project.config.browser.commands) {
103103+ for (const command in this.config.browser.commands) {
104104 if (!/^[a-z_$][\w$]*$/i.test(command)) {
105105 throw new Error(
106106 `Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`,
107107 )
108108 }
109109- this.commands[command] = project.config.browser.commands[command]
109109+ this.commands[command] = this.config.browser.commands[command]
110110 }
111111112112 this.prefixTesterUrl = `${base || '/'}`
···119119 )
120120 })().then(manifest => (this.manifest = manifest))
121121122122- this.orchestratorHtml = (project.config.browser.ui
122122+ this.orchestratorHtml = (this.config.browser.ui
123123 ? readFile(resolve(uiClientRoot, 'index.html'), 'utf8')
124124 : readFile(resolve(distRoot, 'client/orchestrator.html'), 'utf8'))
125125 .then(html => (this.orchestratorHtml = html))
+9-14
packages/browser/src/node/rpc.ts
···11import type { MockerRegistry } from '@vitest/mocker'
22+import type { IncomingMessage } from 'node:http'
23import type { Duplex } from 'node:stream'
34import type { TestError } from 'vitest'
45import type { BrowserCommandContext, ResolveSnapshotPathHandlerContext, TestProject } from 'vitest/node'
···26272728 const wss = new WebSocketServer({ noServer: true })
28292929- vite.httpServer?.on('upgrade', (request, socket: Duplex, head: Buffer) => {
3030+ vite.httpServer?.on('upgrade', (request: IncomingMessage, socket: Duplex, head: Buffer) => {
3031 if (!request.url) {
3132 return
3233 }
···124125125126 function canWrite(project: TestProject) {
126127 return (
127127- project.config.browser.api.allowWrite
128128- && project.vitest.config.browser.api.allowWrite
129129- && project.config.api.allowWrite
128128+ project.config.api.allowWrite
130129 && project.vitest.config.api.allowWrite
131130 )
132131 }
···134133 function isCdpAllowed(project: TestProject) {
135134 return (
136135 project.config.api.allowExec
137137- && project.config.browser.api.allowExec
138136 && project.vitest.config.api.allowExec
139139- && project.vitest.config.browser.api.allowExec
140137 && project.config.api.allowWrite
141141- && project.config.browser.api.allowWrite
142138 && project.vitest.config.api.allowWrite
143143- && project.vitest.config.browser.api.allowWrite
144139 )
145140 }
146141147142 function assertCdpAllowed(project: TestProject) {
148143 if (!isCdpAllowed(project)) {
149144 throw new Error(
150150- `Cannot use CDP because browser API write or exec operations are disabled. See https://vitest.dev/config/browser/api.`,
145145+ `Cannot use CDP because browser API write or exec operations are disabled. See https://vitest.dev/config/api.`,
151146 )
152147 }
153148 }
···199194 if (artifact.type === 'internal:annotation' && artifact.annotation.attachment) {
200195 artifact.annotation.attachment = undefined
201196 vitest.logger.error(
202202- `[vitest] Cannot record annotation attachment because file writing is disabled. See https://vitest.dev/config/browser/api.`,
197197+ `[vitest] Cannot record annotation attachment because file writing is disabled. See https://vitest.dev/config/api.`,
203198 )
204199 }
205200 // remove attachments if cannot write
···207202 const attachments = artifact.attachments.map(n => n.path).filter(r => !!r).join('", "')
208203 artifact.attachments = []
209204 vitest.logger.error(
210210- `[vitest] Cannot record attachments ("${attachments}") because file writing is disabled, removing attachments from artifact "${artifact.type}". See https://vitest.dev/config/browser/api.`,
205205+ `[vitest] Cannot record attachments ("${attachments}") because file writing is disabled, removing attachments from artifact "${artifact.type}". See https://vitest.dev/config/api.`,
211206 )
212207 }
213208 }
···237232 async writeBenchmarkResult(relativePath, data) {
238233 if (!canWrite(project)) {
239234 vitest.logger.error(
240240- `[vitest] Cannot write benchmark artifact "${relativePath}" because file writing is disabled. See https://vitest.dev/config/browser/api.`,
235235+ `[vitest] Cannot write benchmark artifact "${relativePath}" because file writing is disabled. See https://vitest.dev/config/api.`,
241236 )
242237 return
243238 }
···285280 checkFileAccess(id)
286281 if (!canWrite(project)) {
287282 vitest.logger.error(
288288- `[vitest] Cannot save snapshot file "${id}". File writing is disabled because server is exposed to the internet, see https://vitest.dev/config/browser/api.`,
283283+ `[vitest] Cannot save snapshot file "${id}". File writing is disabled because server is exposed to the internet, see https://vitest.dev/config/api.`,
289284 )
290285 return
291286 }
···296291 checkFileAccess(id)
297292 if (!canWrite(project)) {
298293 vitest.logger.error(
299299- `[vitest] Cannot remove snapshot file "${id}". File writing is disabled because server is exposed to the internet, see https://vitest.dev/config/browser/api.`,
294294+ `[vitest] Cannot remove snapshot file "${id}". File writing is disabled because server is exposed to the internet, see https://vitest.dev/config/api.`,
300295 )
301296 return
302297 }
+1-1
packages/browser/src/node/utils.ts
···110110}
111111112112export function assertBrowserApiWrite(project: TestProject, path: string): void {
113113- if (!project.config.browser.api.allowWrite || !project.vitest.config.api.allowWrite) {
113113+ if (!project.config.api.allowWrite || !project.vitest.config.api.allowWrite) {
114114 throw new Error(
115115 `Cannot modify file "${path}". File writing is disabled because the server is exposed to the internet, see https://vitest.dev/config/browser/api.`,
116116 )
+2-6
packages/coverage-v8/src/provider.ts
···407407 project: TestProject = this.ctx.getRootProject(),
408408 environment: string,
409409 ): Promise<CoverageMap> {
410410- if (environment === '__browser__' && !project.browser) {
411411- throw new Error(`Cannot access browser module graph because it was torn down.`)
412412- }
413413-414410 const onTransform = async (filepath: string, isExtendedContext: ScriptCoverageWithOffset['isExtendedContext'] = false) => {
415411 const result = await this.transformFile(filepath, project, environment, !isExtendedContext)
416416- if (result && environment === '__browser__' && project.browser) {
412412+ if (result && project.isBrowserEnabled()) {
417413 return { ...result, code: `${result.code}// <inline-source-map>` }
418414 }
419415 return result
···422418 const scriptCoverages = []
423419424420 for (const result of coverage.result) {
425425- if (environment === '__browser__') {
421421+ if (environment === 'client' && project.isBrowserEnabled()) {
426422 if (result.url.startsWith('/@fs')) {
427423 result.url = `${FILE_PROTOCOL}${removeStartsWith(result.url, '/@fs')}`
428424 }
+11-8
packages/mocker/src/node/dynamicImportPlugin.ts
···1818 return {
1919 name: 'vitest:browser:esm-injector',
2020 enforce: 'post',
2121- transform(source, id) {
2121+ transform: {
2222+ order: 'post',
2323+ handler(source, id) {
2224 // TODO: test is not called for static imports
2323- if (!regexDynamicImport.test(source)) {
2424- return
2525- }
2626- if (options.filter && !options.filter(id, this.environment)) {
2727- return
2828- }
2929- return injectDynamicImport(source, id, this.parse, options)
2525+ if (!regexDynamicImport.test(source)) {
2626+ return
2727+ }
2828+ if (options.filter && !options.filter(id, this.environment)) {
2929+ return
3030+ }
3131+ return injectDynamicImport(source, id, this.parse, options)
3232+ },
3033 },
3134 }
3235}
···346346347347> The MIT License (MIT)
348348>
349349-> Copyright (c) 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Simon Lydell
349349+> Copyright (c) 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024 Simon Lydell
350350>
351351> Permission is hereby granted, free of charge, to any person obtaining a copy
352352> of this software and associated documentation files (the "Software"), to deal
···109109 },
110110 api: {
111111 argument: '[port]',
112112- description: `Specify server port. Note if the port is already being used, Vite will automatically try the next available port so this may not be the actual port the server ends up listening on. If true will be set to ${defaultPort}`,
112112+ description: `Specify server port. Note if the port is already being used, Vite will automatically try the next available port so this may not be the actual port the server ends up listening on. If true will be set to ${defaultPort} or ${defaultBrowserPort} in browser mode`,
113113 subcommands: apiConfig(defaultPort),
114114 transform(portOrOptions) {
115115 if (typeof portOrOptions === 'number') {
···138138 hideSkippedTests: {
139139 description: 'Hide logs for skipped tests',
140140 },
141141+ reporter: null,
141142 reporters: {
142143 alias: 'reporter',
143144 description: `Specify reporters (default, agent, minimal, blob, verbose, dot, json, tap, tap-flat, junit, tree, hanging-process, github-actions)`,
···380381 description:
381382 'Run the browser in headless mode (i.e. without opening the GUI (Graphical User Interface)). If you are running Vitest in CI, it will be enabled by default (default: `process.env.CI`)',
382383 },
383383- api: {
384384- description:
385385- 'Specify options for the browser API server. Does not affect the --api option',
386386- argument: '[port]',
387387- subcommands: apiConfig(defaultBrowserPort),
388388- },
389389- isolate: {
390390- description:
391391- 'Run every browser test file in isolation. To disable isolation, use `--browser.isolate=false` (default: `true`)',
392392- },
393384 ui: {
394385 description:
395386 'Show Vitest UI when running tests (default: `!process.env.CI`)',
···398389 description:
399390 'Default position for the details panel in browser mode. Either `right` (horizontal split) or `bottom` (vertical split) (default: `right`)',
400391 argument: '<position>',
401401- },
402402- fileParallelism: {
403403- description:
404404- 'Should browser test files run in parallel. Use `--browser.fileParallelism=false` to disable (default: `true`)',
405392 },
406393 connectTimeout: {
407394 description: 'If connection to the browser takes longer, the test suite will fail (default: `60_000`)',
+33
packages/vitest/src/node/config/pluginHarness.ts
···11+import type { Vitest } from '../core'
22+import { version } from '../../../package.json' with { type: 'json' }
33+import { defaultBrowserPort } from '../../constants'
44+import { Logger } from '../logger'
55+import { VitestPackageInstaller } from '../packageInstaller'
66+77+export class PluginHarness {
88+ public vitest?: Vitest
99+1010+ public version: string = version
1111+1212+ /**
1313+ * @internal
1414+ */
1515+ public _browserLastPort = defaultBrowserPort
1616+1717+ constructor(
1818+ public logger: Logger = new Logger(),
1919+ public packageInstaller: VitestPackageInstaller = new VitestPackageInstaller(),
2020+ ) {}
2121+2222+ setVitest(vitest: Vitest | undefined): this {
2323+ this.vitest = vitest
2424+ return this
2525+ }
2626+2727+ getVitest(): Vitest {
2828+ if (!this.vitest) {
2929+ throw new Error(`Don't have access to the "vitest" instance yet. This is a bug in Vitest.`)
3030+ }
3131+ return this.vitest
3232+ }
3333+}
+259-190
packages/vitest/src/node/config/resolveConfig.ts
···11-import type { ResolvedConfig as ResolvedViteConfig } from 'vite'
22-import type { Vitest } from '../core'
11+import type {
22+ InlineConfig,
33+ ResolvedConfig as ResolvedViteConfig,
44+ UserConfig as ViteUserConfig,
55+} from 'vite'
36import type { Logger } from '../logger'
44-import type { ResolvedBrowserOptions } from '../types/browser'
77+import type { BrowserContributionHolder } from '../plugins/browserLoader'
58import type {
69 ApiConfig,
710 ResolvedConfig,
···1013import type { CoverageOptions, CoverageReporterWithOptions } from '../types/coverage'
1114import { existsSync, statSync } from 'node:fs'
1215import { pathToFileURL } from 'node:url'
1313-import { slash, toArray } from '@vitest/utils/helpers'
1616+import { deepMerge, slash, toArray } from '@vitest/utils/helpers'
1417import { resolveModule } from 'local-pkg'
1518import { join, normalize, relative, resolve } from 'pathe'
1619import { isDynamicPattern } from 'tinyglobby'
1720import c from 'tinyrainbow'
1818-import { mergeConfig } from 'vite'
2121+import { mergeConfig, resolveConfig as viteResolveConfig } from 'vite'
1922import {
2023 configFiles,
2121- defaultBrowserPort,
2224 defaultInspectPort,
2323- defaultPort,
2425} from '../../constants'
2526import { benchmarkConfigDefaults, configDefaults } from '../../defaults'
2727+import { wildcardPatternToRegExp } from '../../utils/base'
2628import { isAgent, isCI, stdProvider } from '../../utils/env'
2729import { getWorkersCountByPercentage } from '../../utils/workers'
3030+import { BrowserLoaderPlugin } from '../plugins/browserLoader'
3131+import { CliOverride } from '../plugins/cliOverride'
3232+import { VitestConfig } from '../plugins/config'
3333+import { VitestCorePlugin } from '../plugins/index'
3434+import { VitestConfigServer } from '../plugins/server'
3535+import { resolveFsAllow } from '../plugins/utils'
3636+import { resolveProjectEntries } from '../projects/resolveProjects'
2837import { withLabel } from '../reporters/renderers/utils'
2938import { BaseSequencer } from '../sequencers/BaseSequencer'
3039import { RandomSequencer } from '../sequencers/RandomSequencer'
3131-import { resolveApiToken } from './apiToken'
4040+import { API_TOKEN_FILE, resolveApiToken } from './apiToken'
4141+import { PluginHarness } from './pluginHarness'
32423343function resolvePath(path: string, root: string) {
3444 // local-pkg (mlly)'s resolveModule("./file", { paths: ["/some/root"] }) tries
···7686 return { host, port: Number(port) || defaultInspectPort }
7787}
78887979-/**
8080- * @deprecated Internal function
8181- */
8282-export function resolveApiServerConfig<Options extends ApiConfig & Omit<UserConfig, 'expect'>>(
8383- options: Options,
8989+export function resolveApiServerConfig(
9090+ config: UserConfig,
8491 defaultPort: number,
8585- parentApi?: ApiConfig,
8686- logger?: Logger,
8787-): ApiConfig | undefined {
9292+ logger: Logger,
9393+): ApiConfig {
9494+ const isBrowserEnabled = !!config.browser?.enabled
9595+8896 let api: ApiConfig | undefined
89979090- if (options.ui && !options.api) {
9898+ if (config.ui && !config.api) {
9199 api = { port: defaultPort }
92100 }
9393- else if (options.api === true) {
101101+ else if (config.api === true) {
94102 api = { port: defaultPort }
95103 }
9696- else if (typeof options.api === 'number') {
9797- api = { port: options.api }
104104+ else if (typeof config.api === 'number') {
105105+ api = { port: config.api }
98106 }
99107100100- if (typeof options.api === 'object') {
108108+ if (typeof config.api === 'object') {
101109 if (api) {
102102- if (options.api.port) {
103103- api.port = options.api.port
110110+ if (config.api.port) {
111111+ api.port = config.api.port
104112 }
105105- if (options.api.strictPort) {
106106- api.strictPort = options.api.strictPort
113113+ if (config.api.strictPort) {
114114+ api.strictPort = config.api.strictPort
107115 }
108108- if (options.api.host) {
109109- api.host = options.api.host
116116+ if (config.api.host) {
117117+ api.host = config.api.host
110118 }
111119 }
112120 else {
113113- api = { ...options.api }
121121+ api = { ...config.api }
114122 }
115123 }
116124···123131 api = { middlewareMode: true }
124132 }
125133134134+ if (api && isBrowserEnabled) {
135135+ // Always force middlewareMode to false in browser mode
136136+ api.middlewareMode = false
137137+ // The browser server is standalone, so it always needs a port even when the
138138+ // user didn't configure `api`/`ui` (in which case `api` defaulted above)
139139+ if (!api.port) {
140140+ api.port = defaultPort
141141+ }
142142+ }
143143+126144 // if the API server is exposed to network, disable write operations by default
127145 if (!api.middlewareMode && api.host && api.host !== 'localhost' && api.host !== '127.0.0.1') {
128128- // assigned to browser
129129- if (parentApi) {
130130- if (api.allowWrite == null && api.allowExec == null) {
131131- logger?.error(
132132- c.yellow(
133133- `${c.yellowBright(' WARNING ')} API server is exposed to network, disabling write and exec operations by default for security reasons. This can cause some APIs to not work as expected. Set \`browser.api.allowExec\` manually to hide this warning. See https://vitest.dev/config/browser/api for more details.`,
134134- ),
135135- )
136136- }
146146+ if (api.allowWrite == null && api.allowExec == null) {
147147+ logger.error(
148148+ c.yellow(
149149+ `${c.bgYellow(' WARNING ')} API server is exposed to network, disabling write and exec operations by default for security reasons. This can cause some APIs to not work as expected. Set \`browser.api.allowExec\` manually to hide this warning. See https://vitest.dev/config/api for more details.`,
150150+ ),
151151+ )
137152 }
138138- api.allowWrite ??= parentApi?.allowWrite ?? false
139139- api.allowExec ??= parentApi?.allowExec ?? false
153153+ api.allowWrite ??= false
154154+ api.allowExec ??= false
140155 }
141156 else {
142142- api.allowWrite ??= parentApi?.allowWrite ?? true
143143- api.allowExec ??= parentApi?.allowExec ?? true
157157+ api.allowWrite ??= true
158158+ api.allowExec ??= true
144159 }
145160146161 return api
···159174// that's why it's on a module-level
160175let warnedTypeCheck = false
161176162162-export function resolveConfig(
163163- vitest: Vitest,
177177+/**
178178+ * Resolve Vitest's test config for a single Vite resolved config (root or a single project).
179179+ *
180180+ * This is the internal single-config resolver. The top-level `resolveConfig`
181181+ * orchestrates the full pipeline (root + projects + browser/benchmark expansion).
182182+ *
183183+ * `globalConfig` is the resolved root config, passed when resolving a project.
184184+ */
185185+export function resolveTestConfig(
186186+ logger: Logger,
164187 options: UserConfig,
165188 viteConfig: ResolvedViteConfig,
189189+ globalConfig?: ResolvedConfig,
166190): ResolvedConfig {
167167- const logger = vitest.logger
168191 if (options.dom) {
169192 if (
170193 viteConfig.test?.environment != null
171194 && viteConfig.test!.environment !== 'happy-dom'
172195 ) {
173173- logger.console.warn(
196196+ logger.warn(
174197 withLabel(
175198 'yellow',
176199 'Vitest',
···182205 options.environment = 'happy-dom'
183206 }
184207185185- const resolved = {
186186- ...configDefaults,
187187- ...options,
188188- root: viteConfig.root,
189189- } as any as ResolvedConfig
208208+ const resolved = deepMerge({}, configDefaults, options) as ResolvedConfig
209209+ resolved.root = viteConfig.root
210210+211211+ // Coverage is collected once for the whole run using the root config, so projects
212212+ // share its resolved coverage. Each project's setup/test/config files are then
213213+ // appended to the same exclude list below, keeping them out of the report.
214214+ if (globalConfig) {
215215+ resolved.coverage = globalConfig.coverage
216216+ }
190217191218 const rootStats = statSync(resolved.root, { throwIfNoEntry: false })
192219 if (!rootStats?.isDirectory()) {
···196223 resolved.mode ??= viteConfig.mode ?? 'test'
197224198225 if (resolved.retry && typeof resolved.retry === 'object' && typeof resolved.retry.condition === 'function') {
199199- logger.console.warn(
226226+ logger.warn(
200227 c.yellow('Warning: retry.condition function cannot be used inside a config file. '
201228 + 'Use a RegExp pattern instead, or define the function in your test file.'),
202229 )
···213240 }
214241215242 if ('poolOptions' in resolved) {
216216- logger.deprecate('`test.poolOptions` was removed in Vitest 4. All previous `poolOptions` are now top-level options. Please, refer to the migration guide: https://vitest.dev/guide/migration#pool-rework')
243243+ logger.deprecate('`test.poolOptions` was removed in Vitest 4. All previous `poolOptions` are now top-level options. Please, refer to the migration guide: https://v4.vitest.dev/guide/migration#pool-rework')
244244+ }
245245+246246+ if ('workspace' in resolved) {
247247+ throw new Error('The `test.workspace` option was removed in Vitest 4. Please, migrate to `test.projects` instead. See https://vitest.dev/guide/projects for examples.')
217248 }
218249219250 resolved.pool ??= 'forks'
···314345 resolved.maxWorkers = resolveInlineWorkerOption(resolved.maxWorkers)
315346 }
316347317317- const fileParallelism = options.fileParallelism ?? true
348348+ // `browser.fileParallelism` was replaced by the top-level `fileParallelism`. Map
349349+ // it (only when browser is enabled, since it was a browser-only option) so
350350+ // existing configs keep working instead of being silently ignored.
351351+ const browserOptions = options.browser as { enabled?: boolean; fileParallelism?: boolean } | undefined
352352+ const browserFileParallelism = browserOptions?.enabled ? browserOptions.fileParallelism : undefined
353353+ if (browserFileParallelism !== undefined) {
354354+ logger.deprecate('`browser.fileParallelism` is deprecated. Use the top-level `fileParallelism` option instead.')
355355+ }
356356+357357+ const fileParallelism = options.fileParallelism ?? browserFileParallelism ?? true
318358319359 if (!fileParallelism) {
320360 // ignore user config, parallelism cannot be implemented without limiting workers
···337377 }
338378 }
339379340340- // apply browser CLI options only if the config already has the browser config and not disabled manually
341341- if (
342342- vitest._cliOptions.browser
343343- && resolved.browser
344344- // if enabled is set to `false`, but CLI overrides it, then always override it
345345- && (resolved.browser.enabled !== false || vitest._cliOptions.browser.enabled)
346346- ) {
347347- resolved.browser = mergeConfig(
348348- resolved.browser,
349349- vitest._cliOptions.browser,
350350- ) as ResolvedBrowserOptions
351351- }
352352-353380 resolved.browser ??= {} as any
354381 const browser = resolved.browser
355382···359386 browser.instances = []
360387 }
361388389389+ // The whole project shares a single browser Vite server, so every instance
390390+ // must use the same provider. Validate that here and hoist the provider to
391391+ // the project level so the cluster resolves one even when it is only set per
392392+ // instance (e.g. connect mode). Because the provider is uniform, any
393393+ // instance's server factory represents the whole project.
394394+ const providerNames = new Set<string>()
395395+ // It's possible to provide the same name and sneak in a different factory
396396+ const providerFactories = new Set()
397397+ if (browser.provider?.name) {
398398+ providerNames.add(browser.provider.name)
399399+ providerFactories.add(browser.provider.serverFactory)
400400+ }
401401+ for (const instance of browser.instances) {
402402+ if (instance.provider?.name) {
403403+ providerNames.add(instance.provider.name)
404404+ providerFactories.add(instance.provider.serverFactory)
405405+ }
406406+ }
407407+ if (providerNames.size > 1 || providerFactories.size > 1) {
408408+ throw new Error(
409409+ `All browser instances within a project must use the same provider, but found: ${[...providerNames].join(', ')}. `
410410+ + `Use a single provider for the project, or move the instances into separate projects.`,
411411+ )
412412+ }
413413+362414 // use `chromium` by default when the preview provider is specified
363415 // for a smoother experience. if chromium is not available, it will
364416 // open the default browser anyway
···379431 ].join(''))
380432 }
381433 }
434434+435435+ browser.instances.forEach((instance) => {
436436+ instance.name ??= resolved.name
437437+ ? `${resolved.name} (${instance.browser})`
438438+ : instance.browser
439439+ })
382440 }
383441384442 if (resolved.coverage.enabled && resolved.coverage.provider === 'istanbul' && resolved.experimental?.viteModuleRunner === false) {
···389447 logger.console.warn(c.yellow('The option "detectAsyncLeaks" is not supported in browser mode and will be ignored.'))
390448 }
391449392392- const containsChromium = hasBrowserChromium(vitest, resolved)
393393- const hasOnlyChromium = hasOnlyBrowserChromium(vitest, resolved)
394394-395395- // Browser-mode "Chromium" only features:
396396- if (browser.enabled && (!containsChromium || !hasOnlyChromium)) {
397397- const browserConfig = `
398398-{
399399- browser: {
400400- provider: ${browser.provider?.name || 'preview'}(),
401401- instances: [
402402- ${(browser.instances || []).map(i => `{ browser: '${i.browser}' }`).join(',\n ')}
403403- ],
404404- },
405405-}
406406- `.trim()
407407-408408- const preferredProvider = (!browser.provider?.name || browser.provider.name === 'preview')
409409- ? 'playwright'
410410- : browser.provider.name
411411- const preferredBrowser = preferredProvider === 'playwright' ? 'chromium' : 'chrome'
412412- const correctExample = `
413413-{
414414- browser: {
415415- provider: ${preferredProvider}(),
416416- instances: [
417417- { browser: '${preferredBrowser}' }
418418- ],
419419- },
420420-}
421421- `.trim()
422422-423423- // requires all projects to be chromium
424424- if (!hasOnlyChromium && resolved.coverage.enabled && resolved.coverage.provider === 'v8') {
425425- const coverageExample = `
426426-{
427427- coverage: {
428428- provider: 'istanbul',
429429- },
430430-}
431431- `.trim()
432432-433433- throw new Error(
434434- `@vitest/coverage-v8 does not work with\n${browserConfig}\n`
435435- + `\nUse either:\n${correctExample}`
436436- + `\n\n...or change your coverage provider to:\n${coverageExample}\n`,
437437- )
438438- }
439439-440440- // ignores non-chromium browsers when there is at least one chromium project
441441- if (!containsChromium && (resolved.inspect || resolved.inspectBrk)) {
442442- const inspectOption = `--inspect${resolved.inspectBrk ? '-brk' : ''}`
443443-444444- throw new Error(
445445- `${inspectOption} does not work with\n${browserConfig}\n`
446446- + `\nUse either:\n${correctExample}`
447447- + `\n\n...or disable ${inspectOption}\n`,
448448- )
449449- }
450450- }
451451-452450 resolved.coverage.reporter = resolveCoverageReporters(resolved.coverage.reporter)
453451 if (isAgent) {
454452 // default to `skipFull` and add `text-summary` reporter when `text` reporter is used on agents
···648646 resolved.isolate = false
649647 }
650648649649+ // `browser.isolate` was replaced by the top-level `isolate` option. Map it
650650+ // (only when browser is enabled, since it was a browser-only option) so
651651+ // existing configs keep working instead of silently falling back to the
652652+ // isolated default (which is much slower).
653653+ const browserIsolate = browser.enabled ? (browser as { isolate?: boolean }).isolate : undefined
654654+ if (browserIsolate !== undefined) {
655655+ logger.deprecate('`browser.isolate` is deprecated. Use the top-level `isolate` option instead.')
656656+ if (options.isolate === undefined) {
657657+ resolved.isolate = browserIsolate
658658+ }
659659+ }
660660+651661 if (process.env.VITEST_MAX_WORKERS) {
652662 resolved.maxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS)
653663 }
···657667 resolved.forceRerunTriggers.push(resolved.diff)
658668 }
659669660660- // the server has been created, we don't need to override vite.server options
661661- const api = resolveApiServerConfig(options, defaultPort)
662662- const { token, tokenCreated } = resolveApiToken(resolved.root)
663663- resolved.api = { ...api, token, tokenCreated }
664664-665670 if (options.related) {
666671 resolved.related = toArray(options.related).map(file =>
667672 resolve(resolved.root, file),
···708713 }
709714 }
710715 }
716716+ }
717717+ else {
718718+ resolved.reporters = []
711719 }
712720713713- // @ts-expect-error "reporter" is from CLI, should be absolute to the running directory
714721 // it is passed down as "vitest --reporter ../reporter.js"
715715- const reportersFromCLI = resolved.reporter
722722+ const reportersFromCLI = options.reporter
716723717724 const cliReporters = toArray(reportersFromCLI || []).map(
718725 (reporter: string) => {
···757764 resolved.passWithNoTests ??= true
758765 }
759766767767+ if (resolved.browser.enabled) {
768768+ // browser mode renders real CSS in the page
769769+ resolved.css = true
770770+ }
760771 resolved.css ??= {}
761772 if (typeof resolved.css === 'object') {
762773 resolved.css.modules ??= {}
···765776766777 if (resolved.cache !== false) {
767778 if (resolved.cache && typeof resolved.cache.dir === 'string') {
768768- vitest.logger.deprecate(
779779+ logger.deprecate(
769780 `"cache.dir" is deprecated, use Vite's "cacheDir" instead if you want to change the cache director. Note caches will be written to "cacheDir\/vitest"`,
770781 )
771782 }
···811822812823 resolved.browser.enabled ??= false
813824 resolved.browser.headless ??= isCI
814814- if (resolved.browser.isolate) {
815815- logger.console.warn(
816816- c.yellow('`browser.isolate` is deprecated. Use top-level `isolate` instead.'),
817817- )
818818- }
819819- resolved.browser.isolate ??= resolved.isolate ?? true
820820- resolved.browser.fileParallelism
821821- ??= options.fileParallelism ?? true
822825 // disable in headless mode by default, and if CI is detected
823826 resolved.browser.ui ??= resolved.browser.headless === true ? false : !isCI
824827 resolved.browser.commands ??= {}
···873876 resolved.browser.provider.options = {}
874877 }
875878876876- resolved.browser.api = resolveApiServerConfig(
877877- resolved.browser,
878878- defaultBrowserPort,
879879- resolved.api,
880880- logger,
881881- ) || {
882882- port: defaultBrowserPort,
879879+ if ('api' in resolved.browser) {
880880+ logger.deprecate('`test.browser.api` was deprecated in Vitest 5. Use `test.api` instead.')
883881 }
884882885883 // enable includeTaskLocation by default in UI mode
···1000998 return resolved
1001999}
1002100010031003-export function isBrowserEnabled(config: ResolvedConfig): boolean {
10041004- return Boolean(config.browser?.enabled)
10011001+function resolveConfigPath(root: string, options: UserConfig) {
10021002+ if (options.config === false) {
10031003+ return false
10041004+ }
10051005+ if (options.config) {
10061006+ return resolveModule(options.config, { paths: [root] }) ?? resolve(root, options.config)
10071007+ }
10081008+ return findConfigFile(root)
10091009+}
10101010+10111011+export async function resolveConfig(
10121012+ options: UserConfig = {},
10131013+ viteOverrides: ViteUserConfig = {},
10141014+ pluginsHarness: PluginHarness = new PluginHarness(),
10151015+): Promise<ResolvedViteConfig> {
10161016+ // We clone CLI Options and Vite overrides to reuse when a watch mode is triggered.
10171017+ const cliOptionsCopy = deepMerge({}, options)
10181018+ const viteOverridesCopy = deepMerge({}, viteOverrides)
10191019+ const root = resolve(options.root || process.cwd())
10201020+ const configPath = resolveConfigPath(root, options)
10211021+ options.config = configPath
10221022+ options.root = root
10231023+10241024+ const rootBrowserHolder: BrowserContributionHolder = {}
10251025+ const inlineConfig: InlineConfig = mergeConfig(
10261026+ {
10271027+ configFile: configPath,
10281028+ configLoader: options.configLoader,
10291029+ mode: options.mode || 'test',
10301030+ plugins: [
10311031+ CliOverride(cliOptionsCopy),
10321032+ VitestConfigServer(pluginsHarness),
10331033+ ...VitestConfig(pluginsHarness),
10341034+ ...VitestCorePlugin(pluginsHarness, options),
10351035+ ...BrowserLoaderPlugin(rootBrowserHolder, pluginsHarness),
10361036+ ],
10371037+ } satisfies InlineConfig,
10381038+ mergeConfig(viteOverrides, { root }),
10391039+ )
10401040+10411041+ const rootViteConfig = await viteResolveConfig(inlineConfig, 'serve')
10421042+10431043+ const rootConfig = resolveTestConfig(
10441044+ pluginsHarness.logger,
10451045+ (rootViteConfig.test as UserConfig | undefined) || {},
10461046+ rootViteConfig,
10471047+ )
10481048+ rootViteConfig.test = rootConfig
10491049+10501050+ rootViteConfig.server.fs.allow.push(
10511051+ ...resolveFsAllow(rootViteConfig.root, rootViteConfig.configFile),
10521052+ )
10531053+ rootViteConfig.server.fs.deny.push(API_TOKEN_FILE)
10541054+10551055+ // the server has been created, we don't need to override vite.server options
10561056+ const { token, tokenCreated } = resolveApiToken(rootViteConfig.root)
10571057+ rootConfig.api.token = token
10581058+ rootConfig.api.tokenCreated = tokenCreated
10591059+10601060+ if (rootConfig.ui && rootConfig.open) {
10611061+ // Note: `tokenCreated` is only an approximation of "the browser is not
10621062+ // authenticated yet". If the user clears cookies while the token file
10631063+ // persists, the clean URL will block until they re-open the `?token=`
10641064+ // URL printed in the terminal.
10651065+ if (rootConfig.api.tokenCreated) {
10661066+ // First run that generated the token: no browser holds the auth
10671067+ // cookie yet, so open the authenticated URL to set it. A new tab
10681068+ // here is fine since no clean-URL tab exists to reuse.
10691069+ const url = new URL(rootConfig.uiBase, 'http://localhost')
10701070+ url.searchParams.set('token', rootConfig.api.token)
10711071+ rootViteConfig.server.open = `${url.pathname}${url.search}`
10721072+ }
10731073+ else {
10741074+ // Subsequent runs: open the clean UI base URL (without `?token=`)
10751075+ // rather than the authenticated URL printed by the logger. On macOS,
10761076+ // `openBrowser` reuses an existing tab whose URL matches via substring
10771077+ // and reloads it (Vite's `bin/openChrome.js`). Since the 302 redirect
10781078+ // strips the token, an already-authenticated tab lives at the clean
10791079+ // URL, so opening the clean URL matches and reloads it; opening the
10801080+ // token URL would never match and would spawn a new tab on every
10811081+ // restart.
10821082+ rootViteConfig.server.open = rootConfig.uiBase
10831083+ }
10841084+ }
10851085+10861086+ rootConfig.cliOptions = cliOptionsCopy
10871087+ rootConfig.viteOverrides = viteOverridesCopy
10881088+ rootConfig._browserContribution = rootBrowserHolder.contribution
10891089+10901090+ rootConfig.resolvedProjects = await resolveProjectEntries(
10911091+ pluginsHarness,
10921092+ rootViteConfig,
10931093+ rootConfig,
10941094+ rootConfig.projects,
10951095+ )
10961096+10971097+ return rootViteConfig
10051098}
1006109910071100export function resolveCoverageReporters(configReporters: NonNullable<CoverageOptions['reporter']>): CoverageReporterWithOptions[] {
···10261119 return resolvedReporters
10271120}
1028112110291029-function isChromiumName(provider: string, name: string) {
10301030- if (provider === 'playwright') {
10311031- return name === 'chromium'
10321032- }
10331033- return name === 'chrome' || name === 'edge'
10341034-}
10351035-10361036-function hasBrowserChromium(vitest: Vitest, config: ResolvedConfig) {
10371037- const browser = config.browser
10381038- if (!browser || !browser.provider || browser.provider.name === 'preview' || !browser.enabled) {
10391039- return false
10401040- }
10411041- if (browser.name) {
10421042- return isChromiumName(browser.provider.name, browser.name)
10431043- }
10441044- if (!browser.instances) {
10451045- return false
11221122+export function matchesProjectFilter(projects: string[], name: string): boolean {
11231123+ // no filters applied, any project can be included
11241124+ if (!projects.length) {
11251125+ return true
10461126 }
10471047- return browser.instances.some((instance) => {
10481048- const name = instance.name || (config.name ? `${config.name} (${instance.browser})` : instance.browser)
10491049- // browser config is filtered out
10501050- if (!vitest.matchesProjectFilter(name)) {
10511051- return false
10521052- }
10531053- return isChromiumName(browser.provider!.name, instance.browser)
11271127+ return projects.some((project) => {
11281128+ const regexp = wildcardPatternToRegExp(project)
11291129+ return regexp.test(name)
10541130 })
10551131}
1056113210571057-function hasOnlyBrowserChromium(vitest: Vitest, config: ResolvedConfig) {
10581058- const browser = config.browser
10591059- if (!browser || !browser.provider || browser.provider.name === 'preview' || !browser.enabled) {
10601060- return false
10611061- }
10621062- if (browser.name) {
10631063- return isChromiumName(browser.provider.name, browser.name)
10641064- }
10651065- if (!browser.instances) {
11331133+export function isExcludedByProjectFilter(projects: string[], name: string): boolean {
11341134+ if (!projects.length) {
10661135 return false
10671136 }
10681068- return browser.instances.every((instance) => {
10691069- const name = instance.name || (config.name ? `${config.name} (${instance.browser})` : instance.browser)
10701070- // browser config is filtered out
10711071- if (!vitest.matchesProjectFilter(name)) {
10721072- return true // ignore this project
11371137+11381138+ return projects.some((project) => {
11391139+ if (!project.startsWith('!')) {
11401140+ return false
10731141 }
10741074- return isChromiumName(browser.provider!.name, instance.browser)
11421142+ const positivePattern = project.slice(1)
11431143+ return wildcardPatternToRegExp(positivePattern).test(name)
10751144 })
10761145}
···11import type {
22- InlineConfig as ViteInlineConfig,
32 UserConfig as ViteUserConfig,
43} from 'vite'
54import type { CliOptions } from './cli/cli-api'
65import type { VitestOptions } from './core'
76import type { VitestRunMode } from './types/config'
88-import { resolve } from 'node:path'
99-import { deepClone, slash } from '@vitest/utils/helpers'
1010-import { resolveModule } from 'local-pkg'
1111-import { mergeConfig } from 'vite'
1212-import { findConfigFile } from './config/resolveConfig'
77+import { PluginHarness } from './config/pluginHarness'
88+import { resolveConfig } from './config/resolveConfig'
139import { Vitest } from './core'
1414-import { VitestPlugin } from './plugins'
1515-import { createViteServer } from './vite'
1010+import { Logger } from './logger'
1111+import { VitestPackageInstaller } from './packageInstaller'
16121713export async function createVitest(
1814 options: CliOptions,
···4743 viteOverrides = optionsOrViteOverrides as ViteUserConfig
4844 vitestOptions = viteOverridesOrVitestOptions as VitestOptions
4945 }
5050- const ctx = new Vitest(deepClone(options), vitestOptions)
5151- const root = slash(resolve(options.root || process.cwd()))
5252-5353- const configPath
5454- = options.config === false
5555- ? false
5656- : options.config
5757- ? (resolveModule(options.config, { paths: [root] }) ?? resolve(root, options.config))
5858- : findConfigFile(root)
59466060- options.config = configPath
4747+ const logger = new Logger(vitestOptions.stdout, vitestOptions.stderr)
4848+ const packageInstaller = vitestOptions.packageInstaller ?? new VitestPackageInstaller()
4949+ const pluginHarness = new PluginHarness(logger, packageInstaller)
61506262- const { browser: _removeBrowser, ...restOptions } = options
5151+ const config = await resolveConfig(
5252+ options,
5353+ viteOverrides,
5454+ pluginHarness,
5555+ )
63566464- const config: ViteInlineConfig = {
6565- configFile: configPath,
6666- configLoader: options.configLoader,
6767- mode: options.mode || 'test',
6868- plugins: await VitestPlugin(restOptions, ctx),
6969- }
5757+ const vitest = new Vitest(
5858+ pluginHarness,
5959+ config,
6060+ )
70617162 try {
7272- const server = await createViteServer(
7373- mergeConfig(config, mergeConfig(viteOverrides, { root: options.root })),
7474- )
6363+ await vitest._start(config)
75647676- if (ctx.config.api?.port) {
7777- await server.listen()
7878- if (ctx.config.ui && ctx.config.open) {
7979- // Note: `tokenCreated` is only an approximation of "the browser is not
8080- // authenticated yet". If the user clears cookies while the token file
8181- // persists, the clean URL will block until they re-open the `?token=`
8282- // URL printed in the terminal.
8383- if (ctx.config.api.tokenCreated) {
8484- // First run that generated the token: no browser holds the auth
8585- // cookie yet, so open the authenticated URL to set it. A new tab
8686- // here is fine since no clean-URL tab exists to reuse.
8787- const url = new URL(ctx.config.uiBase, 'http://localhost')
8888- url.searchParams.set('token', ctx.config.api.token)
8989- server.config.server.open = `${url.pathname}${url.search}`
9090- }
9191- else {
9292- // Subsequent runs: open the clean UI base URL (without `?token=`)
9393- // rather than the authenticated URL printed by the logger. On macOS,
9494- // `openBrowser` reuses an existing tab whose URL matches via substring
9595- // and reloads it (Vite's `bin/openChrome.js`). Since the 302 redirect
9696- // strips the token, an already-authenticated tab lives at the clean
9797- // URL, so opening the clean URL matches and reloads it; opening the
9898- // token URL would never match and would spawn a new tab on every
9999- // restart.
100100- server.config.server.open = ctx.config.uiBase
101101- }
102102- server.openBrowser()
103103- }
6565+ if (vitest.config.api.port && vitest.config.ui && vitest.config.open) {
6666+ vitest.vite.openBrowser()
10467 }
10568106106- return ctx
6969+ return vitest
10770 }
108108- // Vitest can fail at any point inside "setServer" or inside a custom plugin
109109- // Then we need to make sure everything was properly closed (like the logger)
7171+ // Vitest can fail at any point during setup or inside a custom plugin.
7272+ // Make sure everything is properly closed (like the logger).
11073 catch (error) {
111111- await ctx.close()
7474+ await vitest.close()
11275 throw error
11376 }
11477}
···11-import type { DevEnvironment } from 'vite'
11+import type { DevEnvironment, ViteDevServer } from 'vite'
22import type { ResolvedConfig } from '../types/config'
33import type { VitestFetchFunction } from './fetchModule'
44import { readFile } from 'node:fs/promises'
55import { VitestModuleEvaluator } from '#module-evaluator'
66+import { isRunnableDevEnvironment } from 'vite'
67import { ModuleRunner } from 'vite/module-runner'
78import { normalizeResolvedIdToUrl } from './normalizeUrl'
89···5960 return super.import(url)
6061 }
6162}
6363+6464+// Override the SSR environment's runner so user `configureServer` hooks that
6565+// call `server.environments.ssr.runner.import(...)` get Vitest's module runner.
6666+export function installSsrModuleRunner(
6767+ server: ViteDevServer,
6868+ fetcher: VitestFetchFunction,
6969+ config: ResolvedConfig,
7070+): void {
7171+ const ssrEnvironment = server.environments.ssr
7272+ if (!isRunnableDevEnvironment(ssrEnvironment)) {
7373+ return
7474+ }
7575+ const ssrRunner = new ServerModuleRunner(ssrEnvironment, fetcher, config)
7676+ Object.defineProperty(ssrEnvironment, 'runner', {
7777+ value: ssrRunner,
7878+ writable: true,
7979+ configurable: true,
8080+ })
8181+}
+8-3
packages/vitest/src/node/logger.ts
···3636 private _highlights = new Map<string, string>()
3737 private cleanupListeners: Listener[] = []
3838 public console: Console
3939+ private ctx!: Vitest
39404041 constructor(
4141- public ctx: Vitest,
4242 public outputStream: NodeJS.WriteStream | Writable = process.stdout,
4343 public errorStream: NodeJS.WriteStream | Writable = process.stderr,
4444 ) {
4545 this.console = new Console({ stdout: outputStream, stderr: errorStream })
4646 this._highlights.clear()
4747- this.addCleanupListeners()
4848- this.registerUnhandledRejection()
49475048 if ((this.outputStream as typeof process.stdout).isTTY) {
5149 (this.outputStream as Writable).write(HIDE_CURSOR)
5250 }
5151+ }
5252+5353+ setVitest(vitest: Vitest): this {
5454+ this.ctx = vitest
5555+ this.addCleanupListeners()
5656+ this.registerUnhandledRejection()
5757+ return this
5358 }
54595560 log(...args: any[]): void {
+132
packages/vitest/src/node/plugins/browserLoader.ts
···11+import type {
22+ ResolvedConfig as ResolvedViteConfig,
33+ ViteDevServer,
44+ Plugin as VitePlugin,
55+} from 'vite'
66+import type { PluginHarness } from '../config/pluginHarness'
77+import type { Vitest } from '../core'
88+import type {
99+ BrowserServerContribution,
1010+ ParentProjectBrowser,
1111+} from '../types/browser'
1212+import type { ResolvedConfig } from '../types/config'
1313+import { createViteServer } from '../vite'
1414+1515+export interface BrowserContributionHolder {
1616+ contribution?: BrowserServerContribution
1717+}
1818+1919+function sortPluginsByEnforce(plugins: VitePlugin[]): VitePlugin[] {
2020+ const pre: VitePlugin[] = []
2121+ const normal: VitePlugin[] = []
2222+ const post: VitePlugin[] = []
2323+ for (const plugin of plugins) {
2424+ if (plugin.enforce === 'pre') {
2525+ pre.push(plugin)
2626+ }
2727+ else if (plugin.enforce === 'post') {
2828+ post.push(plugin)
2929+ }
3030+ else {
3131+ normal.push(plugin)
3232+ }
3333+ }
3434+ return [...pre, ...normal, ...post]
3535+}
3636+3737+export function BrowserLoaderPlugin(
3838+ holder: BrowserContributionHolder,
3939+ harness: PluginHarness,
4040+): VitePlugin[] {
4141+ return [
4242+ {
4343+ name: 'vitest:browser:loader',
4444+ // `pre` so the browser plugins injected via `applyToEnvironment` land before
4545+ // Vite's internal resolver in the `client` environment (so e.g. the
4646+ // `vitest/browser` virtual module wins over the node stub resolution).
4747+ enforce: 'pre',
4848+ async config(viteConfig) {
4949+ const browser = viteConfig.test?.browser
5050+ if (!browser?.enabled) {
5151+ return
5252+ }
5353+ // The provider can be configured at the project level or per instance
5454+ // (e.g. connect mode). All instances in a project share one provider
5555+ // (validated in `resolveTestConfig`), so any instance's server factory
5656+ // builds the shared server.
5757+ const provider = browser.provider
5858+ ?? browser.instances?.find(instance => instance.provider)?.provider
5959+ if (!provider || typeof provider.serverFactory !== 'function') {
6060+ throw new Error(`Browser Mode was enabled, but provider was not specified anywhere. See https://vitest.dev/guide/browser/#configuration`)
6161+ }
6262+ const contribution = await provider.serverFactory()
6363+ holder.contribution = contribution
6464+ const browserConfig = await contribution.config(viteConfig, harness)
6565+ return browserConfig
6666+ },
6767+ applyToEnvironment(environment) {
6868+ const contribution = holder.contribution
6969+ if (contribution && environment.name === 'client') {
7070+ // `post` browser plugins are injected by `vitest:browser:loader:post`
7171+ // instead, so they run after the `post` plugins of the main pipeline
7272+ // rather than at this `pre` position. For example, the mocker's
7373+ // `vitest:browser:esm-injector` must run after `vitest:mocks`, which is
7474+ // added by the main pipeline and is not part of `contribution.plugins`.
7575+ return sortPluginsByEnforce(
7676+ contribution.plugins.filter(plugin => plugin.enforce !== 'post'),
7777+ )
7878+ }
7979+ return false
8080+ },
8181+ configureServer: {
8282+ order: 'pre',
8383+ async handler(server) {
8484+ await holder.contribution?.configureServer(server)
8585+ },
8686+ },
8787+ transformIndexHtml: {
8888+ order: 'pre',
8989+ async handler(html, ctx) {
9090+ return holder.contribution?.transformIndexHtml(ctx)
9191+ },
9292+ },
9393+ },
9494+ {
9595+ name: 'vitest:browser:loader:post',
9696+ enforce: 'post',
9797+ applyToEnvironment(environment) {
9898+ const contribution = holder.contribution
9999+ if (contribution && environment.name === 'client') {
100100+ return sortPluginsByEnforce(
101101+ contribution.plugins.filter(plugin => plugin.enforce === 'post'),
102102+ )
103103+ }
104104+ return false
105105+ },
106106+ },
107107+ ]
108108+}
109109+110110+export async function createClusterServer(
111111+ vitest: Vitest,
112112+ viteConfig: ResolvedViteConfig,
113113+ config: ResolvedConfig,
114114+): Promise<{ server: ViteDevServer; parent?: ParentProjectBrowser }> {
115115+ const contribution = config._browserContribution
116116+117117+ if (!contribution) {
118118+ const server = await createViteServer(viteConfig)
119119+ if (config.api.port) {
120120+ await server.listen(config.api.port)
121121+ }
122122+ return { server }
123123+ }
124124+125125+ const parent = contribution.createParent({ config, vitest })
126126+ contribution.parent = parent
127127+128128+ const server = await createViteServer(viteConfig)
129129+ await server.listen(config.api.port)
130130+ contribution.setupRpc(parent)
131131+ return { server, parent }
132132+}
+33
packages/vitest/src/node/plugins/cliOverride.ts
···11+import type { Plugin } from 'vite'
22+import type { ResolvedBrowserOptions } from '../types/browser'
33+import type { UserConfig } from '../types/config'
44+import { deepMerge } from '@vitest/utils/helpers'
55+import { mergeConfig } from 'vite'
66+77+export function CliOverride(cliOptions: UserConfig): Plugin {
88+ return {
99+ // The CLI plugin overwrites config values with CLI options, making them
1010+ // available in the next plugin. We have to do this via plugins because of watch mode.
1111+ name: 'vitest:config:cli',
1212+ enforce: 'pre',
1313+ config: {
1414+ order: 'pre',
1515+ handler(config) {
1616+ const { browser, ...options } = cliOptions
1717+1818+ config.test ??= {}
1919+ // We don't want to use Vite's merge because we want to OVERRIDE options
2020+ // By default, Vite extends arrays, for example, but CLI options should have the priority
2121+ config.test = deepMerge({}, config.test, options)
2222+2323+ // apply browser CLI options only if the config already has the browser config and not disabled manually
2424+ if (config.test.browser && browser && (config.test.browser.enabled !== false || browser.enabled)) {
2525+ config.test.browser = mergeConfig(
2626+ config.test.browser,
2727+ browser,
2828+ ) as ResolvedBrowserOptions
2929+ }
3030+ },
3131+ },
3232+ }
3333+}
···11import type { Plugin } from 'vite'
22-import type { Vitest } from '../core'
22+import type { PluginHarness } from '../config/pluginHarness'
33import { join, resolve } from 'pathe'
44import { distDir } from '../../paths'
5566-export function VitestProjectResolver(ctx: Vitest): Plugin {
66+export function VitestProjectResolver(harness: PluginHarness): Plugin {
77 const plugin: Plugin = {
88 name: 'vitest:resolve-root',
99 enforce: 'pre',
···1919 if (id === 'vitest' || id.startsWith('@vitest/') || id.startsWith('vitest/')) {
2020 // always redirect the request to the root vitest plugin since
2121 // it will be the one used to run Vitest
2222- const resolved = await ctx.vite.pluginContainer.resolveId(id, undefined, {
2222+ const resolved = await harness.getVitest().vite.pluginContainer.resolveId(id, undefined, {
2323 skip: new Set([plugin]),
2424 ssr,
2525 })
···3030 return plugin
3131}
32323333-export function VitestCoreResolver(ctx: Vitest): Plugin {
3333+export function VitestCoreResolver(): Plugin {
3434+ let root: string
3435 return {
3536 name: 'vitest:resolve-core',
3637 enforce: 'pre',
···4243 }
4344 },
4445 },
4646+ configResolved(config) {
4747+ root = config.root
4848+ },
4549 async resolveId(id) {
4650 if (id === 'vitest') {
4751 return resolve(distDir, 'index.js')
4852 }
4953 if (id.startsWith('@vitest/') || id.startsWith('vitest/')) {
5054 // ignore actual importer, we want it to be resolved relative to the root
5151- return this.resolve(id, join(ctx.config.root, 'index.html'), {
5555+ return this.resolve(id, join(root, 'index.html'), {
5256 skipSelf: true,
5357 })
5458 }
+44-232
packages/vitest/src/node/plugins/workspace.ts
···11+import type * as vite from 'vite'
12import type { UserConfig as ViteConfig, Plugin as VitePlugin } from 'vite'
22-import type { TestProject } from '../project'
33-import type { BrowserConfigOptions, ResolvedConfig, TestProjectInlineConfiguration, UserConfig } from '../types/config'
44-import { existsSync, readFileSync } from 'node:fs'
55-import { deepMerge } from '@vitest/utils/helpers'
66-import { basename, dirname, relative, resolve } from 'pathe'
77-import * as vite from 'vite'
88-import { configDefaults } from '../../defaults'
99-import { generateScopedClassName } from '../../integrations/css/css-modules'
33+import type { PluginHarness } from '../config/pluginHarness'
44+import type { ResolvedConfig, TestProjectInlineConfiguration } from '../types/config'
105import { API_TOKEN_FILE } from '../config/apiToken'
1111-import { VitestFilteredOutProjectError } from '../errors'
1212-import { createViteLogger, silenceImportViteIgnoreWarning } from '../viteLogger'
66+import { VitestConfig } from './config'
137import { CoverageTransform } from './coverageTransform'
148import { CSSEnablerPlugin } from './cssEnabler'
159import { MetaEnvReplacerPlugin } from './metaEnvReplacer'
1610import { MocksPlugins } from './mocks'
1711import { NormalizeURLPlugin } from './normalizeURL'
1818-import { VitestOptimizer } from './optimizer'
1919-import { ModuleRunnerTransform } from './runnerTransform'
2020-import {
2121- deleteDefineConfig,
2222- getDefaultResolveOptions,
2323- resolveFsAllow,
2424-} from './utils'
1212+import { VitestConfigServer } from './server'
1313+import { SsrRunnerFixerPlugin } from './ssrRunnerFixer'
2514import { VitestProjectResolver } from './vitestResolver'
26152716interface WorkspaceOptions extends TestProjectInlineConfiguration {
2817 root?: string
2929- workspacePath: string | number
3018}
31193220export function WorkspaceVitestPlugin(
3333- project: TestProject,
2121+ harness: PluginHarness,
2222+ globalViteConfig: vite.ResolvedConfig,
2323+ globalConfig: ResolvedConfig,
3424 options: WorkspaceOptions,
3535-) {
3636- return <VitePlugin[]>[
3737- {
3838- name: 'vitest:project:name',
3939- enforce: 'post',
4040- config(viteConfig) {
4141- viteConfig.test ??= {}
4242-4343- const testConfig = viteConfig.test
4444-4545- let { label: name, color } = typeof testConfig.name === 'string'
4646- ? { label: testConfig.name }
4747- : { label: '', ...testConfig.name }
4848-4949- if (!name) {
5050- if (typeof options.workspacePath === 'string') {
5151- // if there is a package.json, read the name from it
5252- const dir = options.workspacePath.endsWith('/')
5353- ? options.workspacePath.slice(0, -1)
5454- : dirname(options.workspacePath)
5555- const pkgJsonPath = resolve(dir, 'package.json')
5656- if (existsSync(pkgJsonPath)) {
5757- name = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')).name
5858- }
5959- if (typeof name !== 'string' || !name) {
6060- name = basename(dir)
6161- }
6262- }
6363- else {
6464- name = options.workspacePath.toString()
6565- }
6666- }
6767-6868- if (project.vitest._cliOptions.benchmarkOnly) {
6969- viteConfig.test.benchmark ??= {}
7070- viteConfig.test.benchmark.enabled = true
7171- }
7272-7373- const isUserBrowserEnabled = viteConfig.test?.browser?.enabled
7474- const isBrowserEnabled = isUserBrowserEnabled ?? (viteConfig.test?.browser && project.vitest._cliOptions.browser?.enabled)
7575- // keep project names to potentially filter it out
7676- const workspaceNames = [name]
7777- const browser = (viteConfig.test!.browser || {}) as BrowserConfigOptions
7878- if (isBrowserEnabled && browser.name && !browser.instances?.length) {
7979- // vitest injects `instances` in this case later on
8080- workspaceNames.push(name ? `${name} (${browser.name})` : browser.name)
8181- }
8282-8383- viteConfig.test?.browser?.instances?.forEach((instance) => {
8484- // every instance is a potential project
8585- instance.name ??= name ? `${name} (${instance.browser})` : instance.browser
8686- if (isBrowserEnabled) {
8787- workspaceNames.push(instance.name)
8888- }
8989- })
9090- if (viteConfig.test?.benchmark?.enabled) {
9191- workspaceNames.push(name ? `${name} (bench)` : 'bench')
9292- }
9393-9494- const filters = project.vitest.config.project
9595- // if there is `--project=...` filter, check if any of the potential projects match
9696- // if projects don't match, we ignore the test project altogether
9797- // if some of them match, they will later be filtered again by `resolveWorkspace`
9898- if (filters.length) {
9999- const hasProject = workspaceNames.some((name) => {
100100- return project.vitest.matchesProjectFilter(name)
101101- })
102102- if (!hasProject) {
103103- throw new VitestFilteredOutProjectError()
104104- }
105105- }
106106-107107- const vitestConfig: UserConfig = {
108108- name: { label: name, color },
109109- }
110110-111111- vitestConfig.experimental ??= {}
112112-113113- // always inherit the global `fsModuleCache` value even without `extends: true`
114114- if (testConfig.experimental?.fsModuleCache == null && project.vitest.config.experimental?.fsModuleCache != null) {
115115- vitestConfig.experimental.fsModuleCache = project.vitest.config.experimental.fsModuleCache
116116- }
117117- if (testConfig.experimental?.fsModuleCachePath == null && project.vitest.config.experimental?.fsModuleCachePath != null) {
118118- vitestConfig.experimental.fsModuleCachePath = project.vitest.config.experimental.fsModuleCachePath
119119- }
120120- if (testConfig.experimental?.viteModuleRunner == null && project.vitest.config.experimental?.viteModuleRunner != null) {
121121- vitestConfig.experimental.viteModuleRunner = project.vitest.config.experimental.viteModuleRunner
122122- }
123123- if (testConfig.experimental?.nodeLoader == null && project.vitest.config.experimental?.nodeLoader != null) {
124124- vitestConfig.experimental.nodeLoader = project.vitest.config.experimental.nodeLoader
125125- }
126126- if (testConfig.experimental?.importDurations == null && project.vitest.config.experimental?.importDurations != null) {
127127- vitestConfig.experimental.importDurations = project.vitest.config.experimental.importDurations
128128- }
129129-130130- return {
131131- base: '/',
132132- environments: {
133133- __vitest__: {
134134- dev: {},
135135- },
136136- },
137137- test: vitestConfig,
138138- }
139139- },
140140- },
2525+): VitePlugin[] {
2626+ return [
14127 {
14228 name: 'vitest:project',
143143- enforce: 'pre',
2929+ enforce: 'post',
14430 options() {
14531 this.meta.watchMode = false
14632 },
14733 config(viteConfig) {
148148- const originalDefine = { ...viteConfig.define } // stash original defines for browser mode
149149- const defines: Record<string, any> = deleteDefineConfig(viteConfig)
150150-15134 const testConfig = viteConfig.test || {}
152152- const root = testConfig.root || viteConfig.root || options.root
3535+ const root = options.root || testConfig.root || viteConfig.root
15336154154- const resolveOptions = getDefaultResolveOptions()
155155- let config: ViteConfig = {
3737+ const config: ViteConfig = {
3838+ base: '/',
15639 root,
157157- define: {
158158- // disable replacing `process.env.NODE_ENV` with static string by vite:client-inject
159159- 'process.env.NODE_ENV': 'process.env.NODE_ENV',
160160- },
161161- resolve: {
162162- ...resolveOptions,
163163- alias: testConfig.alias,
164164- },
16540 server: {
166166- // disable watch mode in workspaces,
167167- // because it is handled by the top-level watcher
168168- watch: null,
16941 open: false,
170170- hmr: false,
171171- ws: false,
172172- preTransformRequests: false,
173173- middlewareMode: true,
17442 fs: {
175175- allow: resolveFsAllow(
176176- project.vitest.config.root,
177177- project.vitest.vite.config.configFile,
178178- ),
4343+ allow: globalViteConfig.server.fs.allow,
17944 deny: [API_TOKEN_FILE],
18045 },
18146 },
182182- // eslint-disable-next-line ts/ban-ts-comment
183183- // @ts-ignore Vite 6 compat
184184- environments: {
185185- ssr: {
186186- resolve: resolveOptions,
187187- },
188188- },
189189- test: {},
19047 }
19148192192- if ('rolldownVersion' in vite) {
193193- config = {
194194- ...config,
195195- // eslint-disable-next-line ts/ban-ts-comment
196196- // @ts-ignore rolldown-vite only
197197- oxc: viteConfig.oxc === false
198198- ? false
199199- : {
200200- // eslint-disable-next-line ts/ban-ts-comment
201201- // @ts-ignore rolldown-vite only
202202- target: viteConfig.oxc?.target || 'node18',
203203- },
204204- }
4949+ // TODO: remove this after "extends: false" is flipped
5050+ testConfig.experimental ??= {}
5151+5252+ // always inherit the global `fsModuleCache` value even without `extends: true`
5353+ if (testConfig.experimental?.fsModuleCache == null && globalConfig.experimental?.fsModuleCache != null) {
5454+ testConfig.experimental.fsModuleCache = globalConfig.experimental.fsModuleCache
20555 }
206206- else {
207207- config = {
208208- ...config,
209209- esbuild: viteConfig.esbuild === false
210210- ? false
211211- : {
212212- // Lowest target Vitest supports is Node18
213213- target: viteConfig.esbuild?.target || 'node18',
214214- sourcemap: 'external',
215215- // Enables using ignore hint for coverage providers with @preserve keyword
216216- legalComments: 'inline',
217217- },
218218- }
5656+ if (testConfig.experimental?.fsModuleCachePath == null && globalConfig.experimental?.fsModuleCachePath != null) {
5757+ testConfig.experimental.fsModuleCachePath = globalConfig.experimental.fsModuleCachePath
21958 }
220220-221221- ;(config.test as ResolvedConfig).defines = defines
222222- ;(config.test as ResolvedConfig).viteDefine = originalDefine
223223-224224- const classNameStrategy
225225- = (typeof testConfig.css !== 'boolean'
226226- && testConfig.css?.modules?.classNameStrategy)
227227- || 'stable'
228228-229229- if (classNameStrategy !== 'scoped') {
230230- config.css ??= {}
231231- config.css.modules ??= {}
232232- if (config.css.modules) {
233233- config.css.modules.generateScopedName = (
234234- name: string,
235235- filename: string,
236236- ) => {
237237- const root = project.config.root
238238- return generateScopedClassName(
239239- classNameStrategy,
240240- name,
241241- relative(root, filename),
242242- )!
243243- }
244244- }
5959+ if (testConfig.experimental?.viteModuleRunner == null && globalConfig.experimental?.viteModuleRunner != null) {
6060+ testConfig.experimental.viteModuleRunner = globalConfig.experimental.viteModuleRunner
6161+ }
6262+ if (testConfig.experimental?.nodeLoader == null && globalConfig.experimental?.nodeLoader != null) {
6363+ testConfig.experimental.nodeLoader = globalConfig.experimental.nodeLoader
6464+ }
6565+ if (testConfig.experimental?.importDurations == null && globalConfig.experimental?.importDurations != null) {
6666+ testConfig.experimental.importDurations = globalConfig.experimental.importDurations
24567 }
246246- config.customLogger = createViteLogger(
247247- project.vitest.logger,
248248- viteConfig.logLevel || 'warn',
249249- {
250250- allowClearScreen: false,
251251- },
252252- )
253253- config.customLogger = silenceImportViteIgnoreWarning(config.customLogger)
2546825569 return config
25670 },
257257- },
258258- {
259259- name: 'vitest:project:server',
260260- enforce: 'pre',
261261- async configureServer(server) {
262262- const options = deepMerge({}, configDefaults, server.config.test || {})
263263- await project._configureServer(options, server)
264264-265265- await server.watcher.close()
7171+ configResolved(config) {
7272+ // Projects always inherit non-project config options
7373+ config.test.coverage = globalConfig.coverage
7474+ config.test.attachmentsDir = globalConfig.attachmentsDir
7575+ // project servers never watch; the top-level server owns the watcher
7676+ config.server.watch = null
26677 },
26778 },
7979+ VitestConfigServer(harness, globalConfig),
8080+ SsrRunnerFixerPlugin(harness),
26881 MetaEnvReplacerPlugin(),
269269- ...CSSEnablerPlugin(project),
270270- CoverageTransform(project.vitest),
8282+ ...CSSEnablerPlugin(),
8383+ CoverageTransform(harness),
8484+ ...VitestConfig(harness),
27185 ...MocksPlugins(),
272272- VitestProjectResolver(project.vitest),
273273- VitestOptimizer(),
8686+ VitestProjectResolver(harness),
27487 NormalizeURLPlugin(),
275275- ModuleRunnerTransform(),
27688 ]
27789}
+1-2
packages/vitest/src/node/pools/browser.ts
···148148 const config = project.config.browser
149149 if (
150150 !config.headless
151151- || !config.fileParallelism
152151 || !project.browser!.provider.supportsParallelism
153152 ) {
154153 return 1
···350349351350 if (!file) {
352351 debug?.('[%s] no more tests to run', sessionId)
353353- const isolate = this.project.config.browser.isolate
352352+ const isolate = this.project.config.isolate
354353 // we don't need to cleanup testers if isolation is enabled,
355354 // because cleanup is done at the end of every test
356355 if (isolate) {
+1-4
packages/vitest/src/node/printError.ts
···149149 : stacks.find((stack) => {
150150 // we are checking that this module was processed by us at one point
151151 try {
152152- const environments = [
153153- ...Object.values(project._vite?.environments || {}),
154154- ...Object.values(project.browser?.vite.environments || {}),
155155- ]
152152+ const environments = Object.values(project.vite.environments || {})
156153 const hasResult = environments.some((environment) => {
157154 const modules = environment.moduleGraph.getModulesByFile(stack.file)
158155 return [...modules?.values() || []].some(module => !!module.transformResult)
+80-263
packages/vitest/src/node/project.ts
···11-import type { GlobOptions } from 'tinyglobby'
22-import type { DevEnvironment, ViteDevServer, InlineConfig as ViteInlineConfig } from 'vite'
11+import type { DevEnvironment, ResolvedConfig as ResolvedViteConfig, ViteDevServer } from 'vite'
32import type { ModuleRunner } from 'vite/module-runner'
43import type { Typechecker } from '../typecheck/typechecker'
54import type { ProvidedContext } from '../types/general'
···1211 ProjectName,
1312 ResolvedConfig,
1413 SerializedConfig,
1515- TestProjectInlineConfiguration,
1616- UserConfig,
1714} from './types/config'
1815import crypto from 'node:crypto'
1919-import { promises as fs, readFileSync } from 'node:fs'
1616+import { readFileSync } from 'node:fs'
2017import { rm } from 'node:fs/promises'
2118import { tmpdir } from 'node:os'
2222-import path from 'node:path'
2319import { deepMerge, nanoid, slash } from '@vitest/utils/helpers'
2420import { isAbsolute, join, relative } from 'pathe'
2521import pm from 'picomatch'
2626-import { glob } from 'tinyglobby'
2727-import { isRunnableDevEnvironment } from 'vite'
2828-import { setup } from '../api/setup'
2922import { createDefinesScript } from '../utils/config-helpers'
3023import { NativeModuleRunner } from '../utils/nativeModuleRunner'
3124import { BenchmarkManager } from './benchmark'
3232-import { isBrowserEnabled, resolveConfig } from './config/resolveConfig'
3325import { serializeConfig } from './config/serializeConfig'
3426import { createFetchModuleFunction } from './environments/fetchModule'
3527import { ServerModuleRunner } from './environments/serverRunner'
3628import { loadGlobalSetupFiles } from './globalSetup'
3737-import { CoverageTransform } from './plugins/coverageTransform'
3838-import { MetaEnvReplacerPlugin } from './plugins/metaEnvReplacer'
3939-import { MocksPlugins } from './plugins/mocks'
4040-import { WorkspaceVitestPlugin } from './plugins/workspace'
4129import { getFilePoolName } from './pool'
3030+import { globProjectFiles, globProjectTestFiles, isInSourceTestCode } from './projects/globProjectFiles'
4231import { VitestResolver } from './resolver'
4332import { TestSpecification } from './test-specification'
4444-import { createViteServer } from './vite'
45334634export class TestProject {
4735 /**
···66546755 public readonly benchmark: BenchmarkManager = new BenchmarkManager(this)
68565757+ public config: ResolvedConfig
5858+ public viteConfig: ResolvedViteConfig
5959+ public vite: ViteDevServer
6060+ public hash: string
6161+6962 /** @internal */ typechecker?: Typechecker
7070- /** @internal */ _config?: ResolvedConfig
7171- /** @internal */ _vite?: ViteDevServer
7272- /** @internal */ _hash?: string
7363 /** @internal */ _resolver!: VitestResolver
7464 /** @internal */ _fetcher!: VitestFetchFunction
7565 /** @internal */ _serializedDefines?: string
···87778878 constructor(
8979 vitest: Vitest,
9090- public options?: InitializeProjectOptions | undefined,
9191- tmpDir?: string,
8080+ server: ViteDevServer,
8181+ viteConfig: ResolvedViteConfig,
8282+ projectConfig: ResolvedConfig,
9283 ) {
9384 this.vitest = vitest
9485 this.globalConfig = vitest.config
9595- this.tmpDir = tmpDir || join(tmpdir(), nanoid())
8686+ this.tmpDir = join(tmpdir(), nanoid())
8787+ this.vite = server
8888+ this.viteConfig = viteConfig
8989+ this.config = projectConfig
9090+ this.hash = generateHash(
9191+ this.config.root + this.config.name,
9292+ )
9393+ this._provideObject(projectConfig.provide)
9694 }
97959898- /**
9999- * The unique hash of this project. This value is consistent between the reruns.
100100- *
101101- * It is based on the root of the project (not consistent between OS) and its name.
102102- */
103103- public get hash(): string {
104104- if (!this._hash) {
105105- throw new Error('The server was not set. It means that `project.hash` was called before the Vite server was established.')
106106- }
107107- return this._hash
9696+ /** @internal */
9797+ _initializeRunners(server: ViteDevServer) {
9898+ this._serializedDefines = createDefinesScript(server.config.define)
9999+ this._resolver = new VitestResolver(server.config.cacheDir, this.config)
100100+ this._fetcher = createFetchModuleFunction(
101101+ this._resolver,
102102+ this.config,
103103+ this.vitest._fsCache,
104104+ this.vitest._traces,
105105+ this.tmpDir,
106106+ )
107107+108108+ const environment = server.environments.__vitest__
109109+ this.runner = this.config.experimental.viteModuleRunner === false
110110+ ? new NativeModuleRunner(this.config.root)
111111+ : new ServerModuleRunner(
112112+ environment,
113113+ this._fetcher,
114114+ this.config,
115115+ )
108116 }
109117110118 // "provide" is a property, not a method to keep the context when destructed in the global setup,
···176184 }
177185178186 /**
179179- * Vite's dev server instance. Every workspace project has its own server.
180180- */
181181- public get vite(): ViteDevServer {
182182- if (!this._vite) {
183183- throw new Error('The server was not set. It means that `project.vite` was called before the Vite server was established.')
184184- }
185185- // checking it once should be enough
186186- Object.defineProperty(this, 'vite', {
187187- configurable: true,
188188- writable: true,
189189- value: this._vite,
190190- })
191191- return this._vite
192192- }
193193-194194- /**
195195- * Resolved project configuration.
196196- */
197197- public get config(): ResolvedConfig {
198198- if (!this._config) {
199199- throw new Error('The config was not set. It means that `project.config` was called before the Vite server was established.')
200200- }
201201- // checking it once should be enough
202202- // Object.defineProperty(this, 'config', {
203203- // configurable: true,
204204- // writable: true,
205205- // value: this._config,
206206- // })
207207- return this._config
208208- }
209209-210210- /**
211187 * The name of the project or an empty string if not set.
212188 */
213189 public get name(): string {
···337313 return this.testFilesList
338314 }
339315340340- const testFiles = await this.globFiles(include, exclude, cwd)
341341-342342- if (includeSource?.length) {
343343- const files = await this.globFiles(includeSource, exclude, cwd)
344344-345345- await Promise.all(
346346- files.map(async (file) => {
347347- try {
348348- const code = await fs.readFile(file, 'utf-8')
349349- if (this.isInSourceTestCode(code)) {
350350- testFiles.push(file)
351351- }
352352- }
353353- catch {
354354- return null
355355- }
356356- }),
357357- )
358358- }
316316+ const testFiles = await globProjectTestFiles(include, exclude, includeSource, cwd)
359317360318 this.testFilesList = testFiles
361319···363321 }
364322365323 isBrowserEnabled(): boolean {
366366- return isBrowserEnabled(this.config)
324324+ return !!this.config.browser?.enabled
367325 }
368326369327 private markTestFile(testPath: string): void {
···394352 }
395353396354 /** @internal */
397397- async globFiles(include: string[], exclude: string[], cwd: string) {
398398- const globOptions: GlobOptions = {
399399- dot: true,
400400- cwd,
401401- ignore: exclude,
402402- expandDirectories: false,
403403- }
404404-405405- const files = await glob(include, globOptions)
406406- // keep the slashes consistent with Vite
407407- // we are not using the pathe here because it normalizes the drive letter on Windows
408408- // and we want to keep it the same as working dir
409409- return files.map(file => slash(path.resolve(cwd, file)))
355355+ globFiles(include: string[], exclude: string[], cwd: string): Promise<string[]> {
356356+ return globProjectFiles(include, exclude, cwd)
410357 }
411358412359 /**
···429376 && pm.isMatch(relativeId, this.config.includeSource)
430377 ) {
431378 const code = source?.() || readFileSync(moduleId, 'utf-8')
432432- if (this.isInSourceTestCode(code)) {
379379+ if (isInSourceTestCode(code)) {
433380 this.markTestFile(moduleId)
434381 return true
435382 }
436383 }
437384 return false
438438- }
439439-440440- private isInSourceTestCode(code: string): boolean {
441441- return code.includes('import.meta.vitest')
442385 }
443386444387 private filterFiles(testFiles: string[], filters: string[], dir: string): string[] {
···469412 return testFiles
470413 }
471414472472- private _parentBrowser?: ParentProjectBrowser
415415+ /**
416416+ * The parent browser project that owns this cluster's single Vite server.
417417+ * Set on the primary (the hidden parent of a browser cluster) at server
418418+ * creation; instance siblings get their own `ProjectBrowser` view via
419419+ * `_parentBrowser.spawn`.
420420+ * @internal
421421+ */
422422+ _parentBrowser?: ParentProjectBrowser
473423 /** @internal */
474424 public _parent?: TestProject
475475- /** @internal */
476476- _initParentBrowser = deduped(async (childProject: TestProject) => {
477477- if (!this.isBrowserEnabled() || this._parentBrowser) {
478478- return
479479- }
480480- const provider = this.config.browser.provider || childProject.config.browser.provider
481481- if (provider == null) {
482482- throw new Error(`Provider was not specified in the "browser.provider" setting. Please, pass down playwright(), webdriverio() or preview() from "@vitest/browser-playwright", "@vitest/browser-webdriverio" or "@vitest/browser-preview" package respectively.`)
483483- }
484484- if (typeof provider.serverFactory !== 'function') {
485485- throw new TypeError(`The browser provider options do not return a "serverFactory" function. Are you using the latest "@vitest/browser-${provider.name}" package?`)
486486- }
487487- const browser = await provider.serverFactory({
488488- project: this,
489489- mocksPlugins: options => MocksPlugins(options),
490490- metaEnvReplacer: () => MetaEnvReplacerPlugin(),
491491- coveragePlugin: () => CoverageTransform(this.vitest),
492492- })
493493- this._parentBrowser = browser
494494- if (this.config.browser.ui) {
495495- setup(this.vitest, browser.vite)
496496- }
497497- })
498498-499499- /** @internal */
500500- _initBrowserServer = deduped(async () => {
501501- await this._parent?._initParentBrowser(this)
502502-503503- if (!this.browser && this._parent?._parentBrowser) {
504504- this.browser = this._parent._parentBrowser.spawn(this)
505505- await this.vitest.report('onBrowserInit', this)
506506- }
507507- })
508425509426 /**
510427 * Closes the project and all associated resources. This can only be called once; the closing promise is cached until the server restarts.
···514431 if (!this.closingPromise) {
515432 this.closingPromise = Promise.all(
516433 [
517517- this.vite?.close(),
434434+ this.vite.close(),
518435 this.typechecker?.stop(),
519519- // browser might not be set if it threw an error during initialization
520520- (this.browser || this._parent?._parentBrowser?.vite)?.close(),
521436 this.clearTmpDir(),
522437 ].filter(Boolean),
523438 ).then(() => {
···526441 }
527442 }).then(() => {
528443 this._provided = {} as any
529529- this._vite = undefined
530444 })
531445 }
532446 return this.closingPromise
···540454 return this.runner.import(moduleId)
541455 }
542456543543- private _setHash() {
544544- this._hash = generateHash(
545545- this._config!.root + this._config!.name,
546546- )
547547- }
548548-549549- /** @internal */
550550- async _configureServer(options: UserConfig, server: ViteDevServer): Promise<void> {
551551- this._config = resolveConfig(
552552- this.vitest,
553553- {
554554- ...options,
555555- // root-only configs
556556- coverage: this.vitest.config.coverage,
557557- attachmentsDir: this.vitest.config.attachmentsDir,
558558- },
559559- server.config,
560560- )
561561- this._config.api.token = this.vitest.config.api.token
562562- this._config.mergeReportsLabel = this.vitest.config.mergeReportsLabel
563563- this._setHash()
564564- for (const _providedKey in this.config.provide) {
565565- const providedKey = _providedKey as keyof ProvidedContext
566566- // type is very strict here, so we cast it to any
567567- (this.provide as (key: string, value: unknown) => void)(
568568- providedKey,
569569- this.config.provide[providedKey],
570570- )
571571- }
572572-573573- this.closingPromise = undefined
574574-575575- this._resolver = new VitestResolver(server.config.cacheDir, this._config)
576576- this._vite = server
577577- this._serializedDefines = createDefinesScript(server.config.define)
578578- this._fetcher = createFetchModuleFunction(
579579- this._resolver,
580580- this._config,
581581- this.vitest._fsCache,
582582- this.vitest._traces,
583583- this.tmpDir,
584584- )
585585-586586- const environment = server.environments.__vitest__
587587- this.runner = this._config.experimental.viteModuleRunner === false
588588- ? new NativeModuleRunner(this._config.root)
589589- : new ServerModuleRunner(
590590- environment,
591591- this._fetcher,
592592- this._config,
593593- )
594594-595595- const ssrEnvironment = server.environments.ssr
596596- if (isRunnableDevEnvironment(ssrEnvironment)) {
597597- const ssrRunner = new ServerModuleRunner(
598598- ssrEnvironment,
599599- this._fetcher,
600600- this._config,
601601- )
602602- Object.defineProperty(ssrEnvironment, 'runner', {
603603- value: ssrRunner,
604604- writable: true,
605605- configurable: true,
606606- })
607607- }
608608- }
609609-610457 /** @internal */
611458 public _getViteEnvironments(): DevEnvironment[] {
612612- return [
613613- ...Object.values(this.browser?.vite.environments || {}),
614614- ...Object.values(this.vite.environments || {}),
615615- ]
459459+ return Object.values(this.vite.environments || {})
616460 }
617461618462 /** @internal */
···697541 if (!this.isBrowserEnabled() || this.browser?.provider) {
698542 return
699543 }
700700- if (!this.browser) {
701701- await this._initBrowserServer()
544544+ // The browser server is created eagerly with the project, so `this.browser`
545545+ // is already set here; we only need to initialize the provider.
546546+ if (this.browser) {
547547+ await this.vitest.report('onBrowserInit', this)
702548 }
703549 await this.browser?.initBrowserProvider(this)
704550 })
705551706706- /** @internal */
707707- public _provideObject(context: Partial<ProvidedContext>): void {
552552+ private _provideObject(context: Partial<ProvidedContext>): void {
708553 for (const _providedKey in context) {
709554 const providedKey = _providedKey as keyof ProvidedContext
710555 // type is very strict here, so we cast it to any
···719564 static _createBasicProject(vitest: Vitest): TestProject {
720565 const project = new TestProject(
721566 vitest,
722722- undefined,
723723- vitest._tmpDir,
567567+ vitest.vite,
568568+ vitest.viteConfig,
569569+ vitest.config,
724570 )
725571 project.runner = vitest.runner
726726- project._vite = vitest.vite
727727- project._config = vitest.config
728572 project._resolver = vitest._resolver
729573 project._fetcher = vitest._fetcher
730574 project._serializedDefines = createDefinesScript(vitest.vite.config.define)
731731- project._setHash()
732732- project._provideObject(vitest.config.provide)
733575 return project
734576 }
735577736736- /** @internal */
737737- static _cloneTestProject(parent: TestProject, config: ResolvedConfig): TestProject {
738738- const clone = new TestProject(parent.vitest, undefined, parent.tmpDir)
739739- clone.runner = parent.runner
740740- clone._vite = parent._vite
741741- clone._resolver = parent._resolver
742742- clone._fetcher = parent._fetcher
743743- clone._config = config
744744- clone._setHash()
745745- clone._parent = parent
746746- clone._serializedDefines = parent._serializedDefines
747747- clone._provideObject(config.provide)
748748- return clone
578578+ /**
579579+ * Create a sibling project that shares server-derived resources (Vite server,
580580+ * runner, resolver, fetcher) with a primary project. The sibling has its own
581581+ * distinct `projectConfig`, but the same `viteConfig` reference as the primary.
582582+ *
583583+ * Used for browser-instance and benchmark variants whose entries share a
584584+ * `viteConfig` reference with a primary project entry.
585585+ *
586586+ * @internal
587587+ */
588588+ static _spawnSibling(parent: TestProject, config: ResolvedConfig): TestProject {
589589+ const sibling = new TestProject(parent.vitest, parent.vite, parent.viteConfig, config)
590590+ sibling.runner = parent.runner
591591+ sibling._resolver = parent._resolver
592592+ sibling._fetcher = parent._fetcher
593593+ sibling._parent = parent
594594+ sibling._serializedDefines = parent._serializedDefines
595595+ return sibling
749596 }
750597}
751598···765612 name: string
766613 serializedConfig: SerializedConfig
767614 context: ProvidedContext
768768-}
769769-770770-interface InitializeProjectOptions extends TestProjectInlineConfiguration {
771771- configFile: string | false
772772-}
773773-774774-export async function initializeProject(
775775- workspacePath: string | number,
776776- ctx: Vitest,
777777- options: InitializeProjectOptions,
778778-): Promise<TestProject> {
779779- const project = new TestProject(ctx, options)
780780-781781- const { configFile, ...restOptions } = options
782782-783783- const config: ViteInlineConfig = {
784784- ...restOptions,
785785- configFile,
786786- configLoader: ctx.vite.config.inlineConfig.configLoader,
787787- // this will make "mode": "test" | "benchmark" inside defineConfig
788788- mode: options.test?.mode || options.mode || ctx.config.mode,
789789- plugins: [
790790- ...(options.plugins || []),
791791- WorkspaceVitestPlugin(project, { ...options, workspacePath }),
792792- ],
793793- }
794794-795795- await createViteServer(config)
796796-797797- return project
798615}
799616800617function generateHash(str: string): string {
···44import type { MergedBlobs } from './reporters/blob'
55import type { OnUnhandledErrorCallback } from './types/config'
66import { relative } from 'pathe'
77-import { defaultBrowserPort } from '../constants'
87import { createFileTask, generateFileHash } from '../utils/tasks'
98import { TestCase, TestModule, TestSuite } from './reporters/reported-tasks'
109···42414342 /** @internal */
4443 _data = {
4545- browserLastPort: defaultBrowserPort,
4644 timeoutIncreased: false,
4745 }
4846
+48-37
packages/vitest/src/node/types/browser.ts
···11import type { MockedModule } from '@vitest/mocker'
22import type { Awaitable, ParsedStack, TestError } from '@vitest/utils'
33import type { StackTraceParserOptions } from '@vitest/utils/source-map'
44-import type { Plugin, ViteDevServer } from 'vite'
44+import type { IndexHtmlTransformContext, IndexHtmlTransformResult, Plugin, ViteDevServer, UserConfig as ViteUserConfig } from 'vite'
55import type { BrowserCommands, CDPSession, MarkOptions } from 'vitest/browser'
66import type { BrowserTraceViewMode } from '../../runtime/config'
77import type { CancelReason } from '../../runtime/runner/types'
88import type { BrowserTesterOptions } from '../../types/browser'
99import type { OTELCarrier } from '../../utils/traces'
1010+import type { PluginHarness } from '../config/pluginHarness'
1111+import type { Vitest } from '../core'
1012import type { TestProject } from '../project'
1111-import type { ApiConfig, ProjectConfig } from './config'
1313+import type { ProjectConfig, ResolvedConfig } from './config'
12141315export type { CDPSession }
1416···2628 serverFactory: BrowserServerFactory
2729}
28302929-export interface BrowserServerOptions {
3030- project: TestProject
3131- coveragePlugin: () => Plugin
3232- mocksPlugins: (options: { filter: (id: string) => boolean }) => Plugin[]
3333- metaEnvReplacer: () => Plugin
3434-}
3535-3631export interface BrowserServerFactory {
3737- (options: BrowserServerOptions): Promise<ParentProjectBrowser>
3232+ (): Promise<BrowserServerContribution>
3833}
39344035export interface BrowserProvider {
···170165 * @default process.env.CI
171166 */
172167 headless?: boolean
173173-174174- /**
175175- * Serve API options.
176176- *
177177- * The default port is 63315.
178178- */
179179- api?: ApiConfig | number
180180-181181- /**
182182- * Isolate test environment after each test
183183- *
184184- * @default true
185185- * @deprecated use top-level `isolate` instead
186186- */
187187- isolate?: boolean
188188-189189- /**
190190- * Run test files in parallel if provider supports this option
191191- * This option only has effect in headless mode (enabled in CI by default)
192192- *
193193- * @default // Same as "test.fileParallelism"
194194- * @deprecated use top-level `fileParallelism` instead
195195- */
196196- fileParallelism?: boolean
197168198169 /**
199170 * Show Vitest UI
···395366export interface ParentProjectBrowser {
396367 spawn: (project: TestProject) => ProjectBrowser
397368 vite: ViteDevServer
369369+ vitest: Vitest
370370+ config: ResolvedConfig
371371+}
372372+373373+export interface BrowserServerContribution {
374374+ transformIndexHtml: (ctx: IndexHtmlTransformContext) => Awaitable<IndexHtmlTransformResult | undefined>
375375+ configureServer: (server: ViteDevServer) => Awaitable<void>
376376+ /**
377377+ * Browser-specific Vite config (`resolve.alias`, `define`, esbuild). Applied
378378+ * by the core loader plugin's `config` hook during the single project
379379+ * resolution, so other plugins observe it (e.g. alias must be baked at
380380+ * resolution time). The loader always forces `server.middlewareMode = false`
381381+ * on top. `harness` provides the package installer's `isPackageExists` (no
382382+ * `Vitest` instance is available during resolution).
383383+ */
384384+ config: (config: ViteUserConfig, harness: PluginHarness) => Awaitable<ViteUserConfig>
385385+ /**
386386+ * Browser `optimizeDeps`, aggregated across every project that shares the
387387+ * single browser Vite server (instance and benchmark variants). Called by core
388388+ * after all projects are resolved and before the server is created; the result
389389+ * is merged into the resolved Vite config's `client` environment
390390+ * `optimizeDeps`. `testFiles` is the aggregated, already-globbed set of test
391391+ * files for the server (globbing lives in the core package).
392392+ */
393393+ resolveOptimizeDeps: (projectConfigs: ResolvedConfig[], testFiles: string[], harness: PluginHarness) => Awaitable<NonNullable<ViteUserConfig['optimizeDeps']>>
394394+ /**
395395+ * Runtime plugins. Injected into the browser (`client`) environment by the
396396+ * loader's `applyToEnvironment`; their `configureServer`/`transformIndexHtml`
397397+ * are run by the loader. MUST NOT define `config`/`configResolved` hooks.
398398+ */
399399+ plugins: Plugin[]
400400+ /**
401401+ * Constructs the `ParentBrowserProject`. Called by core at server creation,
402402+ * when the `Vitest` instance exists.
403403+ */
404404+ createParent: (ctx: { config: ResolvedConfig; vitest: Vitest }) => ParentProjectBrowser
405405+ /** Called by core after `server.listen()` to wire up the browser RPC. */
406406+ setupRpc: (parent: ParentProjectBrowser) => void
407407+ /**
408408+ * Mutable. Filled by core at server creation; the pushed `BrowserPlugin`
409409+ * closes over this same object and reads `.parent` in `configureServer`.
410410+ */
411411+ parent?: ParentProjectBrowser
398412}
399413400414export interface ProjectBrowser {
···456470 name: string
457471 enabled: boolean
458472 headless: boolean
459459- isolate: boolean
460460- fileParallelism: boolean
461461- api: ApiConfig
462473 ui: boolean
463474 viewport: {
464475 width: number
+50-10
packages/vitest/src/node/types/config.ts
···33import type { SnapshotStateOptions } from '@vitest/snapshot'
44import type { Arrayable } from '@vitest/utils'
55import type { SerializedDiffOptions } from '@vitest/utils/diff'
66-import type { AliasOptions, ConfigEnv, DepOptimizationConfig, ServerOptions, UserConfig as ViteUserConfig } from 'vite'
66+import type { AliasOptions, ConfigEnv, DepOptimizationConfig, ResolvedConfig as ResolvedViteConfig, ServerOptions, UserConfig as ViteUserConfig } from 'vite'
77import type { ChaiConfig } from '../../integrations/chai/config'
88import type { SerializedConfig } from '../../runtime/config'
99import type { SequenceHooks, SequenceSetupFiles, SerializableRetry, TestTagDefinition } from '../../runtime/runner/types'
1010import type { LabelColor, ParsedStack, ProvidedContext, TestError } from '../../types/general'
1111import type { HappyDOMOptions } from '../../types/happy-dom-options'
1212import type { JSDOMOptions } from '../../types/jsdom-options'
1313+import type { CliOptions } from '../cli/cli-api'
1314import type { PoolRunnerInitializer } from '../pools/types'
1415import type {
1516 BuiltinReporterOptions,
···2021import type { VCSProvider } from '../vcs/vcs'
2122import type { WatcherTriggerPattern } from '../watcher'
2223import type { BenchmarkUserOptions } from './benchmark'
2323-import type { BrowserConfigOptions, ResolvedBrowserOptions } from './browser'
2424+import type { BrowserConfigOptions, BrowserServerContribution, ResolvedBrowserOptions } from './browser'
2425import type { CoverageOptions, ResolvedCoverageOptions } from './coverage'
2526import type { Reporter } from './reporter'
2627···6162 */
6263 allowExec?: boolean
6364}
6565+6666+export type ResolvedApiConfig = ApiConfig & { token: string; tokenCreated: boolean }
64676568export interface EnvironmentOptions {
6669 /**
···683686 sequence?: SequenceOptions
684687685688 /**
689689+ * Overrides Vite mode
690690+ * @default 'test'
691691+ */
692692+ mode?: string
693693+694694+ /**
686695 * Specifies an `Object`, or an `Array` of `Object`,
687696 * which defines aliases used to replace values in `import` or `require` statements.
688697 * Will be merged with the default aliases inside `resolve.alias`.
···10751084 related?: string[] | string
1076108510771086 /**
10781078- * Overrides Vite mode
10791079- * @default 'test'
10801080- */
10811081- mode?: string
10821082-10831083- /**
10841087 * Test suite shard to execute in a format of <index>/<count>.
10851088 * Will divide tests into a `count` numbers, and run only the `indexed` part.
10861089 * Cannot be used with enabled watch.
···11251128 * Log all available tags instead of running tests.
11261129 */
11271130 listTags?: boolean | 'json'
11311131+11321132+ configLoader?: 'bundle' | 'runner' | 'native'
11331133+11341134+ /**
11351135+ * The `--reporter` argument from the CLI
11361136+ */
11371137+ reporter?: string | string[]
11281138}
1129113911301140export type OnUnhandledErrorCallback = (error: (TestError | Error) & { type: string }) => boolean | void
···11581168 | 'vmMemoryLimit'
11591169 | 'fileParallelism'
11601170 | 'tagsFilter'
11711171+ | 'reporter'
11611172 > {
11621173 name: ProjectName['label']
11631174 color?: ProjectName['color']
···11831194 reporters: (InlineReporter | ReporterWithOptions)[]
1184119511851196 defines: Record<string, any>
11861186- viteDefine: Record<string, any>
1187119711881188- api: ApiConfig & { token: string; tokenCreated: boolean }
11981198+ api: ResolvedApiConfig
11891199 cliExclude?: string[]
1190120011911201 project: string[]
···12371247 }
12381248 }
12391249 }
12501250+12511251+ cliOptions: CliOptions
12521252+ viteOverrides: ViteUserConfig
12531253+ resolvedProjects: ResolvedProjectEntry[]
12541254+ /**
12551255+ * Browser server contribution captured by the `vitest:browser:loader` plugin
12561256+ * during this config's resolution (set only when `browser.enabled`). Used by
12571257+ * server creation to build the single Vite server shared by `project.vite` and
12581258+ * `project.browser.vite`.
12591259+ *
12601260+ * @internal
12611261+ */
12621262+ _browserContribution?: BrowserServerContribution
12631263+}
12641264+12651265+/**
12661266+ * A resolved project entry. `viteConfig` may be shared by reference across multiple
12671267+ * entries (e.g. browser instances or benchmark variants of the same parent), while
12681268+ * `projectConfig` is always a distinct object per entry.
12691269+ */
12701270+export interface ResolvedProjectEntry {
12711271+ viteConfig: ResolvedViteConfig
12721272+ projectConfig: ResolvedConfig
12731273+ /**
12741274+ * When set, this entry exists only so browser-instance siblings can attach
12751275+ * to a parent that owns the Vite server and (later) the browser provider.
12761276+ * The resulting `TestProject` is created and kept alive (so siblings can
12771277+ * reference it via `_parent`) but is NOT pushed to `vitest.projects`.
12781278+ */
12791279+ hidden?: boolean
12401280}
1241128112421282type NonProjectOptions
+9-1
packages/vitest/src/node/types/vite.ts
···11/* eslint-disable unused-imports/no-unused-vars */
2233import type { HookHandler } from 'vite'
44-import type { InlineConfig } from './config'
44+import type { InlineConfig, ResolvedConfig } from './config'
55import type { VitestPluginContext } from './plugin'
6677type VitestInlineConfig = InlineConfig
88+type VitestResolvedConfig = ResolvedConfig
89910declare module 'vite' {
1011 interface UserConfig {
···1213 * Options for Vitest
1314 */
1415 test?: VitestInlineConfig
1616+ }
1717+1818+ interface ResolvedConfig {
1919+ /**
2020+ * Options for Vitest
2121+ */
2222+ test: VitestResolvedConfig
1523 }
16241725 interface Plugin<A = any> {
+1-1
packages/vitest/src/node/vite.ts
···22import { cleanUrl } from '@vitest/utils/helpers'
33import { createServer, isFileLoadingAllowed, normalizePath } from 'vite'
4455-export async function createViteServer(inlineConfig: InlineConfig): Promise<ViteDevServer> {
55+export async function createViteServer(inlineConfig: InlineConfig | ResolvedConfig): Promise<ViteDevServer> {
66 // Vite prints an error (https://github.com/vitejs/vite/issues/14328)
77 // But Vitest works correctly either way
88 const error = console.error
+1
packages/vitest/src/public/browser.ts
···99 loadDiffConfig,
1010 loadSnapshotSerializers,
1111 setupCommonEnv,
1212+ setupEnv,
1213} from '../runtime/setup-common'
1314export * as SpyModule from '@vitest/spy'
1415export type { ParsedStack, StringifyOptions } from '@vitest/utils'
+8-7
packages/vitest/src/public/node.ts
···66export { isValidApiRequest } from '../api/check'
77export { escapeTestName } from '../node/ast-collect'
88export type { CacheKeyIdGenerator, CacheKeyIdGeneratorContext } from '../node/cache/fsModuleCache'
99-export { parseCLI } from '../node/cli/cac'
99+export { createCLI, parseCLI } from '../node/cli/cac'
1010export type { CliParseOptions } from '../node/cli/cac'
1111export type { CliOptions } from '../node/cli/cli-api'
1212export { startVitest } from '../node/cli/cli-api'
1313+export { PluginHarness } from '../node/config/pluginHarness'
1314export { resolveApiServerConfig } from '../node/config/resolveConfig'
1515+export { resolveConfig } from '../node/config/resolveConfig'
1416export type {
1517 OnServerRestartHandler,
1618 OnTestsRerunHandler,
···2022export { BaseCoverageProvider } from '../node/coverage'
2123export { createVitest } from '../node/create'
2224export { GitNotFoundError, FilesNotFoundError as TestsNotFoundError } from '../node/errors'
2525+export { Logger } from '../node/logger'
2326export { VitestPackageInstaller } from '../node/packageInstaller'
2424-export { VitestPlugin } from '../node/plugins'
2525-export { resolveConfig } from '../node/plugins/publicConfig'
2627export { resolveFsAllow } from '../node/plugins/utils'
2728export type { ProcessPool } from '../node/pool'
2829export { getFilePoolName } from '../node/pool'
···4041export { TypecheckPoolWorker } from '../node/pools/workers/typecheckWorker'
4142export { VmForksPoolWorker } from '../node/pools/workers/vmForksWorker'
4243export { VmThreadsPoolWorker } from '../node/pools/workers/vmThreadsWorker'
4444+4345export type { SerializedTestProject, TestProject } from '../node/project'
4444-4546export {
4647 AgentReporter,
4748 DefaultReporter,
···6970} from '../node/reporters'
7071export type { HTMLOptions } from '../node/reporters/html'
7172export type { JsonOptions } from '../node/reporters/json'
7272-export type { JUnitOptions } from '../node/reporters/junit'
73737474+export type { JUnitOptions } from '../node/reporters/junit'
7475export type { Report } from '../node/reporters/report'
7576export type {
7677 ModuleDiagnostic,
···9091 TestSuiteState,
9192} from '../node/reporters/reported-tasks'
9293export { experimental_getRunnerTask } from '../node/reporters/reported-tasks'
9494+9395export { BaseSequencer } from '../node/sequencers/BaseSequencer'
9494-9596export type {
9697 TestSequencer,
9798 TestSequencerConstructor,
···112113 BrowserProvider,
113114 BrowserProviderOption,
114115 BrowserScript,
116116+ BrowserServerContribution,
115117 BrowserServerFactory,
116116- BrowserServerOptions,
117118 BrowserServerState,
118119 BrowserServerStateSession,
119120 CDPSession,
···14021402 *
14031403 * Extend this interface when creating custom test artifacts. Vitest automatically manages the `attachments` array and injects the `location` property to indicate where the artifact was created in your test code.
14041404 *
14051405- * **Important**: when running with [`api.allowWrite`](https://vitest.dev/config/api#api-allowwrite) or [`browser.api.allowWrite`](https://vitest.dev/config/browser/api#api-allowwrite) disabled, Vitest empties the `attachments` array on every artifact before reporting it.
14051405+ * **Important**: when running with [`api.allowWrite`](https://vitest.dev/config/api#api-allowwrite) disabled, Vitest empties the `attachments` array on every artifact before reporting it.
14061406 */
14071407export interface TestArtifactBase {
14081408 /** File or data attachments associated with this artifact */
+3-10
packages/vitest/src/runtime/setup-common.ts
···55import type { PublicModuleRunner } from './moduleRunner/types'
66import { addSerializer } from '@vitest/snapshot'
77import { setSafeTimers } from '@vitest/utils/timers'
88-import { getWorkerState } from './utils'
98109let globalSetup = false
1110export async function setupCommonEnv(config: SerializedConfig): Promise<void> {
1211 setupDefines(config)
1313- setupEnv(config.env)
14121513 if (globalSetup) {
1614 return
···3028 }
3129}
32303333-function setupEnv(env: Record<string, any>) {
3434- const state = getWorkerState()
3535- // same boolean-to-string assignment as VitestPlugin.configResolved
3636- const { PROD, DEV, ...restEnvs } = env
3737- state.metaEnv.PROD = PROD
3838- state.metaEnv.DEV = DEV
3939- for (const key in restEnvs) {
4040- state.metaEnv[key] = env[key]
3131+export function setupEnv(env: Record<string, any>, metaEnv: Record<string, any>): void {
3232+ for (const key in env) {
3333+ metaEnv[key] = env[key]
4134 }
4235}
4336
···1515import { createNodeImportMeta } from '../moduleRunner/moduleRunner'
1616import { startVitestModuleRunner } from '../moduleRunner/startVitestModuleRunner'
1717import { run } from '../runBaseTests'
1818+import { setupEnv } from '../setup-common'
1819import { getSafeWorkerState, provideWorkerState } from '../utils'
19202021let _moduleRunner: TestModuleRunner
···8182 rpc,
8283 config,
8384 } = context
8585+8686+ setupEnv(config.env, context.metaEnv)
84878588 // we could load @vite/env, but it would take ~8ms, while this takes ~0,02ms
8689 if (context.config.serializedDefines) {
+31-1
packages/vitest/src/runtime/workers/init.ts
···11import type { WorkerRequest, WorkerResponse } from '../../node/pools/types'
22-import type { WorkerSetupContext } from '../../types/worker'
22+import type { MetaEnv, WorkerSetupContext } from '../../types/worker'
33import type { FileSpecification } from '../runner/types'
44import type { VitestWorker } from './types'
55import { serializeError } from '@vitest/utils/error'
···88import * as listeners from '../listeners'
99import { createRuntimeRpc } from '../rpc'
1010import * as entrypoint from '../worker'
1111+1212+function createImportMetaEnvProxy(): MetaEnv {
1313+ const booleanKeys = ['DEV', 'PROD', 'SSR']
1414+ return new Proxy(process.env, {
1515+ get(_, key) {
1616+ if (typeof key !== 'string') {
1717+ return undefined
1818+ }
1919+ if (booleanKeys.includes(key)) {
2020+ return !!process.env[key]
2121+ }
2222+ return process.env[key]
2323+ },
2424+ set(_, key, value) {
2525+ if (typeof key !== 'string') {
2626+ return true
2727+ }
2828+ if (booleanKeys.includes(key)) {
2929+ process.env[key] = value ? '1' : ''
3030+ }
3131+ else {
3232+ process.env[key] = value
3333+ }
3434+ return true
3535+ },
3636+ }) as MetaEnv
3737+}
3838+3939+const importMetaEnvProxy = createImportMetaEnvProxy()
11401241interface Options extends VitestWorker {
1342 teardown?: () => void
···74103 config,
75104 pool,
76105 rpc,
106106+ metaEnv: importMetaEnvProxy,
77107 projectName: config.name || '',
78108 traces,
79109 }
+3
packages/vitest/src/runtime/workers/vm.ts
···1313import { getDefaultRequestStubs } from '../moduleRunner/moduleEvaluator'
1414import { createNodeImportMeta } from '../moduleRunner/moduleRunner'
1515import { startVitestModuleRunner, VITEST_VM_CONTEXT_SYMBOL } from '../moduleRunner/startVitestModuleRunner'
1616+import { setupEnv } from '../setup-common'
1617import { provideWorkerState } from '../utils'
1718import { FileMap } from '../vm/file-map'
1819···118119 writable: false,
119120 })
120121 context.__vitest_mocker__ = moduleRunner.mocker
122122+123123+ setupEnv(ctx.config.env, state.metaEnv)
121124122125 if (ctx.config.serializedDefines) {
123126 try {
···6666 test('tests are actually running', () => {
6767 expect(stderr).toBe('')
68686969- const testFiles = browserResultJson.testResults.map(t => t.name)
6969+ const testFiles = Array.from(new Set(browserResultJson.testResults.map(t => t.name)))
70707171 vitest.projects.forEach((project) => {
7272 // the order is non-deterministic
···75757676 // test files are optimized automatically (type-check-only files are excluded)
7777 const runtimeTestFiles = testFiles.filter(f => !f.endsWith('.test-d.ts'))
7878- expect(vitest.projects.map(p => p.browser?.vite.config.optimizeDeps.entries))
7878+ expect(vitest.projects.map(p => p.vite.config.optimizeDeps.entries))
7979 .toEqual(vitest.projects.map(() => expect.arrayContaining(runtimeTestFiles)))
80808181 const testFilesCount = readdirSync('./test')
+4-4
test/browser/specs/server-url.test.ts
···1010 root: './fixtures/server-url',
1111 watch: true, // otherwise the browser is closed before we can get the url
1212 })
1313- const url = ctx?.projects[0].browser?.vite.resolvedUrls?.local[0]
1313+ const url = ctx?.projects[0].vite.resolvedUrls?.local[0]
1414 expect(stderr).toBe('')
1515- expect(url).toBeDefined()
1515+ expect.assert(url)
1616 expect(new URL(url).port).toBe('51133')
1717})
1818···2323 watch: true, // otherwise the browser is closed before we can get the url
2424 })
2525 expect(stderr).toBe('')
2626- const url = ctx?.projects[0].browser?.vite.resolvedUrls?.local[0]
2727- expect(url).toBeDefined()
2626+ const url = ctx?.projects[0].vite.resolvedUrls?.local[0]
2727+ expect.assert(url)
2828 expect(new URL(url).port).toBe('51122')
2929 expect(stdout).toReportSummaryTestFiles({ passed: instances.length })
3030})
+6-6
test/browser/test/browser.bench.ts
···1111}
12121313test('perProject registrations flow through the browser RPC (onTestBenchmark)', async ({ bench }) => {
1414- await bench('1 + 1', { perProject: true, ...fastBenchOptions }, () => {
1414+ await bench('1 + 1', { perProject: true }, () => {
1515 const result = 1 + 1
1616 expect.assert(result === 2)
1717- }).run()
1818- await bench('1 + 2', { perProject: true, ...fastBenchOptions }, () => {
1717+ }).run(fastBenchOptions)
1818+ await bench('1 + 2', { perProject: true }, () => {
1919 const result = 1 + 2
2020 expect.assert(result === 3)
2121- }).run()
2121+ }).run(fastBenchOptions)
2222})
23232424test('bench.compare resolves a BenchStorage in the browser', async ({ bench }) => {
···3636 // The browser worker forwards writeResult through the WebSocket RPC to the
3737 // node side. We don't assert on the file contents here (the spec layer can
3838 // do that), just that the round-trip completes without throwing.
3939- const result = await bench('with-write', { writeResult: './out/with-write.json', ...fastBenchOptions }, () => {
3939+ const result = await bench('with-write', { writeResult: './dist/with-write.json' }, () => {
4040 const _ = 1 + 1
4141- }).run()
4141+ }).run(fastBenchOptions)
4242 expect.assert(typeof result.latency.mean === 'number')
4343})
+1-1
test/browser/test/viewport.test.ts
···55 // preview cannot control viewport
66 server.provider === 'preview'
77 // other tests affect the viewport if they run in a different order
88- || server.config.browser.isolate === false,
88+ || server.config.isolate === false,
99)('viewport window has been properly initialized', () => {
1010 it.skipIf(!server.config.browser.headless)('viewport has proper size', () => {
1111 const { width, height } = server.config.browser.viewport
···121121 vi.stubEnv(name, 'string')
122122 })
123123124124+ it.each(['PROD', 'DEV', 'SSR'] as const)('stubbing boolean env.%s to undefined removes it', (name) => {
125125+ vi.stubEnv(name, undefined)
126126+ // removed entirely, not set to an empty string like a `false` boolean stub
127127+ expect(name in import.meta.env).toBe(false)
128128+ expect(process.env[name]).toBeUndefined()
129129+ vi.unstubAllEnvs()
130130+ })
131131+124132 it('setting boolean casts the value to string', () => {
125133 // @ts-expect-error value should be a string
126134 vi.stubEnv('MY_TEST_ENV', true)
+4-4
test/workspaces/space_1/test/env-injected.spec.ts
···1010})
11111212test('env variable is assigned', () => {
1313- // we override it with "local" in .env.local, but dotenv prefers the root .env
1414- // this is consistent with how Vite works
1515- expect(import.meta.env.VITE_MY_TEST_VARIABLE).toBe('core')
1616- expect(process.env.VITE_MY_TEST_VARIABLE).toBe('core')
1313+ // the project's own .env.local wins over the inherited root value, like every
1414+ // other option a project can override
1515+ expect(import.meta.env.VITE_MY_TEST_VARIABLE).toBe('local')
1616+ expect(process.env.VITE_MY_TEST_VARIABLE).toBe('local')
1717 expect(import.meta.env.CUSTOM_MY_TEST_VARIABLE).toBe('custom')
1818 expect(process.env.CUSTOM_MY_TEST_VARIABLE).toBe('custom')
1919