···283283 */
284284 async _attachRootServer(): Promise<void> {
285285 const resolved = this.config
286286+ const children = resolved.resolvedProjects
287287+ .filter(entry => entry.viteConfig === this.viteConfig)
286288 // For a root-level browser config (no `projects`) this builds the single
287289 // browser server; otherwise it just creates the Vite server.
288288- const { server, parent } = await createClusterServer(this, this.viteConfig, resolved)
290290+ const { server, parent } = await createClusterServer(this, this.viteConfig, resolved, children)
289291 this.vite = server
290292 this._rootBrowserParent = parent
291293
+1
packages/vitest/src/node/config/resolveConfig.ts
···416416 + `Use a single provider for the project, or move the instances into separate projects.`,
417417 )
418418 }
419419+ browser.provider ??= browser.instances.find(instance => instance.provider)?.provider
419420420421 // use `chromium` by default when the preview provider is specified
421422 // for a smoother experience. if chromium is not available, it will
+19-1
packages/vitest/src/node/plugins/browserLoader.ts
···99 BrowserServerContribution,
1010 ParentProjectBrowser,
1111} from '../types/browser'
1212-import type { ResolvedConfig } from '../types/config'
1212+import type { ResolvedConfig, ResolvedProjectEntry } from '../types/config'
1313import { createViteServer } from '../vite'
14141515export interface BrowserContributionHolder {
···111111 vitest: Vitest,
112112 viteConfig: ResolvedViteConfig,
113113 config: ResolvedConfig,
114114+ children: readonly ResolvedProjectEntry[],
114115): Promise<{ server: ViteDevServer; parent?: ParentProjectBrowser }> {
115116 const contribution = config._browserContribution
116117···124125125126 const parent = contribution.createParent({ config, vitest })
126127 contribution.parent = parent
128128+129129+ // Start browser launches now so their latency overlaps Vite server creation.
130130+ // Entries that cannot run browser tests are skipped because they will never
131131+ // initialize a provider that could adopt and close the prepared browser.
132132+ for (const child of children) {
133133+ if (
134134+ child.hidden
135135+ || child.hasTestFiles === false
136136+ || (child.projectConfig.typecheck.enabled && child.projectConfig.typecheck.only)
137137+ ) {
138138+ continue
139139+ }
140140+ // The Vite server is shared, but each child carries its own resolved
141141+ // provider and browser options, so it must be prewarmed independently.
142142+ const projectConfig = child.projectConfig
143143+ projectConfig.browser.provider?.prewarm?.({ config: projectConfig, vitest })
144144+ }
127145128146 const server = await createViteServer(viteConfig)
129147 await server.listen(config.api.port)
···202202 harness: PluginHarness,
203203 entries: ResolvedProjectEntry[],
204204): Promise<void> {
205205- const groups = new Map<ResolvedViteConfig, ResolvedConfig[]>()
206206- for (const { viteConfig, projectConfig } of entries) {
205205+ const groups = new Map<ResolvedViteConfig, ResolvedProjectEntry[]>()
206206+ for (const entry of entries) {
207207+ const { viteConfig } = entry
207208 let group = groups.get(viteConfig)
208209 if (!group) {
209210 group = []
210211 groups.set(viteConfig, group)
211212 }
212212- group.push(projectConfig)
213213+ group.push(entry)
213214 }
214215215216 // Most projects in a group share identical glob inputs (the `dir`/`root` is
···228229 }
229230230231 await Promise.all(
231231- Array.from(groups, async ([viteConfig, projectConfigs]) => {
232232+ Array.from(groups, async ([viteConfig, projectEntries]) => {
233233+ const projectConfigs = projectEntries.map(entry => entry.projectConfig)
232234 const contribution = projectConfigs.find(config => config._browserContribution)?._browserContribution
233235 if (!contribution) {
234236 return
235237 }
236238 const fileLists = await Promise.all(projectConfigs.map(globTestFiles))
239239+ projectEntries.forEach((entry, index) => {
240240+ entry.hasTestFiles = fileLists[index].length > 0
241241+ })
237242 const testFiles = [...new Set(fileLists.flat())]
238243 const optimizeDeps = await contribution.resolveOptimizeDeps(projectConfigs, testFiles, harness)
239244 // the browser runs in the `client` environment, but Vite's dep scanner
···453458454459 for (const entry of browserEntries) {
455460 const { projectConfig, viteConfig } = entry
456456- const instances = projectConfig.browser.instances ?? []
457461 const parentName = projectConfig.name
458462463463+ const instances = projectConfig.browser.instances ?? []
459464 if (instances.length === 0 || isExcludedByProjectFilter(globalConfig.project, parentName)) {
460465 continue
461466 }
···916921 // provider). Siblings (browser instance variants, benchmark variants) share
917922 // these resources by linking to the primary via `_parent`.
918923 const primaryByViteConfig = new Map<ResolvedViteConfig, TestProject>()
924924+ const childrenByViteConfig = new Map<ResolvedViteConfig, ResolvedProjectEntry[]>()
925925+ for (const entry of entries) {
926926+ const children = childrenByViteConfig.get(entry.viteConfig) ?? []
927927+ children.push(entry)
928928+ childrenByViteConfig.set(entry.viteConfig, children)
929929+ }
919930920931 // The root Vite config can also serve as a project's `viteConfig` — either
921932 // the default no-`projects` case or browser/benchmark variants of it.
···963974 // Workspace project with its own `viteConfig`: own a fresh Vite server. For
964975 // a browser cluster this is the single server shared by `project.vite` and
965976 // `project.browser.vite`.
966966- const { server, parent } = await createClusterServer(vitest, viteConfig, projectConfig)
977977+ const children = childrenByViteConfig.get(viteConfig) ?? []
978978+ const { server, parent } = await createClusterServer(vitest, viteConfig, projectConfig, children)
967979 const project = new TestProject(vitest, server, viteConfig, projectConfig)
968980 project._initializeRunners(server)
969981 if (parent) {
+10
packages/vitest/src/node/types/browser.ts
···2424 name: string
2525 supportedBrowser?: ReadonlyArray<string>
2626 options: Options
2727+ /**
2828+ * Called once for every resolved browser project right before its shared
2929+ * Vite server is created, so the provider can start preparing the browser
3030+ * (e.g. launching it) concurrently. Optional, fire-and-forget: errors must
3131+ * surface through the normal provider flow.
3232+ */
3333+ prewarm?: (ctx: {
3434+ config: ResolvedConfig
3535+ vitest: Vitest
3636+ }) => void
2737 providerFactory: (project: TestProject) => BrowserProvider
2838 serverFactory: BrowserServerFactory
2939}
+8
packages/vitest/src/node/types/config.ts
···12991299 viteConfig: ResolvedViteConfig
13001300 projectConfig: ResolvedConfig
13011301 /**
13021302+ * Whether test files were found while resolving browser dependencies. This
13031303+ * early result is used only to decide whether prewarming is useful; runtime
13041304+ * discovery still globs after plugins have configured the server.
13051305+ *
13061306+ * @internal
13071307+ */
13081308+ hasTestFiles?: boolean
13091309+ /**
13021310 * When set, this entry exists only so browser-instance siblings can attach
13031311 * to a parent that owns the Vite server and (later) the browser provider.
13041312 * The resulting `TestProject` is created and kept alive (so siblings can