Skip to content

fix: apply browser CLI options only if the project has the browser set in the config already #7984

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 19, 2025
4 changes: 0 additions & 4 deletions packages/vitest/src/node/cli/cac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,10 +283,6 @@ function normalizeCliOptions(cliFilters: string[], argv: CliOptions): CliOptions
argv.includeTaskLocation ??= true
}

// running "vitest --browser.headless"
if (typeof argv.browser === 'object' && !('enabled' in argv.browser)) {
argv.browser.enabled = true
}
if (typeof argv.typecheck?.only === 'boolean') {
argv.typecheck.enabled ??= true
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/node/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ export const cliOptionsConfig: VitestCLIOptions = {
return { enabled: browser === 'yes' }
}
if (typeof browser === 'string') {
return { enabled: true, name: browser }
return { name: browser }
}
return browser
},
Expand Down
22 changes: 17 additions & 5 deletions packages/vitest/src/node/config/resolveConfig.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ResolvedConfig as ResolvedViteConfig } from 'vite'
import type { Vitest } from '../core'
import type { BenchmarkBuiltinReporters } from '../reporters'
import type { ResolvedBrowserOptions } from '../types/browser'
import type {
ApiConfig,
ResolvedConfig,
Expand All @@ -13,6 +14,7 @@ import { toArray } from '@vitest/utils'
import { resolveModule } from 'local-pkg'
import { normalize, relative, resolve } from 'pathe'
import c from 'tinyrainbow'
import { mergeConfig } from 'vite'
import {
defaultBrowserPort,
defaultInspectPort,
Expand Down Expand Up @@ -205,8 +207,6 @@ export function resolveConfig(
resolved.minWorkers = resolveInlineWorkerOption(resolved.minWorkers)
}

resolved.browser ??= {} as any

// run benchmark sequentially by default
resolved.fileParallelism ??= mode !== 'benchmark'

Expand Down Expand Up @@ -238,10 +238,23 @@ export function resolveConfig(
}
}

// apply browser CLI options only if the config already has the browser config and not disabled manually
if (
vitest._cliOptions.browser
&& resolved.browser
// if enabled is set to `false`, but CLI overrides it, then always override it
&& (resolved.browser.enabled !== false || vitest._cliOptions.browser.enabled)
) {
resolved.browser = mergeConfig(
resolved.browser,
vitest._cliOptions.browser,
) as ResolvedBrowserOptions
}

resolved.browser ??= {} as any
const browser = resolved.browser

// if browser was enabled via CLI and it's configured by the user, then validate the input
if (browser.enabled && viteConfig.test?.browser) {
if (browser.enabled) {
if (!browser.name && !browser.instances) {
throw new Error(`Vitest Browser Mode requires "browser.name" (deprecated) or "browser.instances" options, none were set.`)
}
Expand Down Expand Up @@ -806,7 +819,6 @@ export function resolveConfig(
)
}

resolved.browser ??= {} as any
resolved.browser.enabled ??= false
resolved.browser.headless ??= isCI
resolved.browser.isolate ??= true
Expand Down
20 changes: 11 additions & 9 deletions packages/vitest/src/node/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { ViteDevServer } from 'vite'
import type { defineWorkspace } from 'vitest/config'
import type { SerializedCoverageConfig } from '../runtime/config'
import type { ArgumentsType, ProvidedContext, UserConsoleLog } from '../types/general'
import type { CliOptions } from './cli/cli-api'
import type { ProcessPool, WorkspaceSpec } from './pool'
import type { TestSpecification } from './spec'
import type { ResolvedConfig, TestProjectConfiguration, UserConfig, VitestRunMode } from './types/config'
Expand Down Expand Up @@ -97,7 +98,7 @@ export class Vitest {
resolvedProjects: TestProject[] = []
/** @internal */ _browserLastPort = defaultBrowserPort
/** @internal */ _browserSessions = new BrowserSessions()
/** @internal */ _options: UserConfig = {}
/** @internal */ _cliOptions: CliOptions = {}
/** @internal */ reporters: Reporter[] = []
/** @internal */ vitenode: ViteNodeServer = undefined!
/** @internal */ runner: ViteNodeRunner = undefined!
Expand All @@ -118,8 +119,10 @@ export class Vitest {

constructor(
public readonly mode: VitestRunMode,
cliOptions: UserConfig,
options: VitestOptions = {},
) {
this._cliOptions = cliOptions
this.logger = new Logger(this, options.stdout, options.stderr)
this.packageInstaller = options.packageInstaller || new VitestPackageInstaller()
this.specifications = new VitestSpecifications(this)
Expand Down Expand Up @@ -192,13 +195,12 @@ export class Vitest {
}

/** @deprecated internal */
setServer(options: UserConfig, server: ViteDevServer, cliOptions: UserConfig): Promise<void> {
return this._setServer(options, server, cliOptions)
setServer(options: UserConfig, server: ViteDevServer): Promise<void> {
return this._setServer(options, server)
}

/** @internal */
async _setServer(options: UserConfig, server: ViteDevServer, cliOptions: UserConfig) {
this._options = options
async _setServer(options: UserConfig, server: ViteDevServer) {
this.watcher.unregisterWatcher()
clearTimeout(this._rerunTimer)
this.restartsCount += 1
Expand Down Expand Up @@ -274,7 +276,7 @@ export class Vitest {
}
catch { }

const projects = await this.resolveProjects(cliOptions)
const projects = await this.resolveProjects(this._cliOptions)
this.resolvedProjects = projects
this.projects = projects

Expand All @@ -287,7 +289,7 @@ export class Vitest {
}))
}))

if (options.browser?.enabled) {
if (this._cliOptions.browser?.enabled) {
const browserProjects = this.projects.filter(p => p.config.browser.enabled)
if (!browserProjects.length) {
throw new Error(`Vitest received --browser flag, but no project had a browser configuration.`)
Expand Down Expand Up @@ -327,7 +329,7 @@ export class Vitest {
const currentNames = new Set(this.projects.map(p => p.name))
const projects = await resolveProjects(
this,
this._options,
this._cliOptions,
undefined,
Array.isArray(config) ? config : [config],
currentNames,
Expand Down Expand Up @@ -1335,7 +1337,7 @@ export class Vitest {
* Check if the project with a given name should be included.
*/
matchesProjectFilter(name: string): boolean {
const projects = this._config?.project || this._options?.project
const projects = this._config?.project || this._cliOptions?.project
// no filters applied, any project can be included
if (!projects || !projects.length) {
return true
Expand Down
8 changes: 5 additions & 3 deletions packages/vitest/src/node/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { CliOptions } from './cli/cli-api'
import type { VitestOptions } from './core'
import type { VitestRunMode } from './types/config'
import { resolve } from 'node:path'
import { slash } from '@vitest/utils'
import { deepClone, slash } from '@vitest/utils'
import { findUp } from 'find-up'
import { mergeConfig } from 'vite'
import { configFiles } from '../constants'
Expand All @@ -20,7 +20,7 @@ export async function createVitest(
viteOverrides: ViteUserConfig = {},
vitestOptions: VitestOptions = {},
): Promise<Vitest> {
const ctx = new Vitest(mode, vitestOptions)
const ctx = new Vitest(mode, deepClone(options), vitestOptions)
const root = slash(resolve(options.root || process.cwd()))

const configPath
Expand All @@ -32,12 +32,14 @@ export async function createVitest(

options.config = configPath

const { browser: _removeBrowser, ...restOptions } = options

const config: ViteInlineConfig = {
configFile: configPath,
configLoader: options.configLoader,
// this will make "mode": "test" | "benchmark" inside defineConfig
mode: options.mode || mode,
plugins: await VitestPlugin(options, ctx),
plugins: await VitestPlugin(restOptions, ctx),
}

const server = await createViteServer(
Expand Down
27 changes: 14 additions & 13 deletions packages/vitest/src/node/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { UserConfig as ViteConfig, Plugin as VitePlugin } from 'vite'
import type { ResolvedConfig, UserConfig } from '../types/config'
import {
deepClone,
deepMerge,
notNullish,
toArray,
Expand All @@ -27,13 +28,13 @@ import { VitestCoreResolver } from './vitestResolver'

export async function VitestPlugin(
options: UserConfig = {},
ctx: Vitest = new Vitest('test'),
vitest: Vitest = new Vitest('test', deepClone(options)),
): Promise<VitePlugin[]> {
const userConfig = deepMerge({}, options) as UserConfig

async function UIPlugin() {
await ctx.packageInstaller.ensureInstalled('@vitest/ui', options.root || process.cwd(), ctx.version)
return (await import('@vitest/ui')).default(ctx)
await vitest.packageInstaller.ensureInstalled('@vitest/ui', options.root || process.cwd(), vitest.version)
return (await import('@vitest/ui')).default(vitest)
}

return [
Expand Down Expand Up @@ -143,13 +144,13 @@ export async function VitestPlugin(
},
}

if (ctx.configOverride.project) {
if (vitest.configOverride.project) {
// project filter was set by the user, so we need to filter the project
options.project = ctx.configOverride.project
options.project = vitest.configOverride.project
}

config.customLogger = createViteLogger(
ctx.logger,
vitest.logger,
viteConfig.logLevel || 'warn',
{
allowClearScreen: false,
Expand Down Expand Up @@ -207,7 +208,7 @@ export async function VitestPlugin(
name: string,
filename: string,
) => {
const root = ctx.config.root || options.root || process.cwd()
const root = vitest.config.root || options.root || process.cwd()
return generateScopedClassName(
classNameStrategy,
name,
Expand Down Expand Up @@ -258,7 +259,7 @@ export async function VitestPlugin(
})

const originalName = options.name
if (options.browser?.enabled && options.browser?.instances) {
if (options.browser?.instances) {
options.browser.instances.forEach((instance) => {
instance.name ??= originalName ? `${originalName} (${instance.browser})` : instance.browser
})
Expand All @@ -274,9 +275,9 @@ export async function VitestPlugin(
console.log('[debug] watcher is ready')
})
}
await ctx._setServer(options, server, userConfig)
await vitest._setServer(options, server)
if (options.api && options.watch) {
(await import('../../api/setup')).setup(ctx)
(await import('../../api/setup')).setup(vitest)
}

// #415, in run mode we don't need the watcher, close it would improve the performance
Expand All @@ -287,9 +288,9 @@ export async function VitestPlugin(
},
},
SsrReplacerPlugin(),
...CSSEnablerPlugin(ctx),
CoverageTransform(ctx),
VitestCoreResolver(ctx),
...CSSEnablerPlugin(vitest),
CoverageTransform(vitest),
VitestCoreResolver(vitest),
options.ui ? await UIPlugin() : null,
...MocksPlugins(),
VitestOptimizer(),
Expand Down
4 changes: 2 additions & 2 deletions packages/vitest/src/node/plugins/publicConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {
UserConfig as ViteUserConfig,
} from 'vite'
import type { ResolvedConfig, UserConfig } from '../types/config'
import { slash } from '@vitest/utils'
import { deepClone, slash } from '@vitest/utils'
import { findUp } from 'find-up'
import { resolve } from 'pathe'
import { mergeConfig, resolveConfig as resolveViteConfig } from 'vite'
Expand All @@ -27,7 +27,7 @@ export async function resolveConfig(
: await findUp(configFiles, { cwd: root } as any)
options.config = configPath

const vitest = new Vitest('test')
const vitest = new Vitest('test', deepClone(options))
const config = await resolveViteConfig(
mergeConfig(
{
Expand Down
36 changes: 14 additions & 22 deletions packages/vitest/src/node/plugins/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type { ResolvedConfig, TestProjectInlineConfiguration } from '../types/co
import { existsSync, readFileSync } from 'node:fs'
import { deepMerge } from '@vitest/utils'
import { basename, dirname, relative, resolve } from 'pathe'
import { mergeConfig } from 'vite'
import { configDefaults } from '../../defaults'
import { generateScopedClassName } from '../../integrations/css/css-modules'
import { VitestFilteredOutProjectError } from '../errors'
Expand Down Expand Up @@ -116,32 +115,25 @@ export function WorkspaceVitestPlugin(
},
}

// if this project defines a browser configuration, respect --browser flag
// otherwise if we always override the configuration, every project will run in browser mode
if (project.vitest._options.browser && viteConfig.test?.browser) {
viteConfig.test.browser = mergeConfig(
viteConfig.test.browser,
project.vitest._options.browser,
)
}

(config.test as ResolvedConfig).defines = defines
;(config.test as ResolvedConfig).defines = defines

const isUserBrowserEnabled = viteConfig.test?.browser?.enabled
const isBrowserEnabled = isUserBrowserEnabled ?? (viteConfig.test?.browser && project.vitest._cliOptions.browser?.enabled)
// keep project names to potentially filter it out
const workspaceNames = [name]
if (viteConfig.test?.browser?.enabled) {
if (viteConfig.test.browser.name && !viteConfig.test.browser.instances?.length) {
const browser = viteConfig.test.browser.name
// vitest injects `instances` in this case later on
workspaceNames.push(name ? `${name} (${browser})` : browser)
}
const browser = viteConfig.test!.browser || {}
if (isBrowserEnabled && browser.name && !browser.instances?.length) {
// vitest injects `instances` in this case later on
workspaceNames.push(name ? `${name} (${browser.name})` : browser.name)
}

viteConfig.test.browser.instances?.forEach((instance) => {
// every instance is a potential project
instance.name ??= name ? `${name} (${instance.browser})` : instance.browser
viteConfig.test?.browser?.instances?.forEach((instance) => {
// every instance is a potential project
instance.name ??= name ? `${name} (${instance.browser})` : instance.browser
if (isBrowserEnabled) {
workspaceNames.push(instance.name)
})
}
}
})

const filters = project.vitest.config.project
// if there is `--project=...` filter, check if any of the potential projects match
Expand Down
12 changes: 11 additions & 1 deletion test/cli/fixtures/public-api/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
export default {}
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
browser: {
provider: 'playwright',
instances: [{ browser: 'chromium' }],
headless: true,
},
},
})
3 changes: 0 additions & 3 deletions test/cli/test/public-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ it.each([
name: 'running in the browser',
browser: {
enabled: true,
provider: 'playwright',
instances: [{ browser: 'chromium' }],
headless: true,
},
},
] as UserConfig[])('passes down metadata when $name', { timeout: 60_000, retry: 1 }, async (config) => {
Expand Down
4 changes: 0 additions & 4 deletions test/config/fixtures/bail/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,6 @@ export default defineConfig({
},
browser: {
headless: true,
provider: 'webdriverio',
instances: [
{ browser: 'chrome' },
],
},
},
})
2 changes: 1 addition & 1 deletion test/config/fixtures/browser-no-config/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
browser: {
enabled: false,
headless: true,
},
},
})
Loading
Loading