From 6d675ed39e5d1acdbe11eb255196b5f385dc9c9f Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Mon, 14 Sep 2026 02:59:17 +0100 Subject: [PATCH 1/2] Run Firefox test files in parallel Firefox pages opened in the same browser share focus and mouse states: a mouse event raises the window of its page, blurring the focused element of other pages, and ends pointer captures in other pages. Launch a separate browser for each Firefox test session so test files can run in parallel, remove `fileParallelism: false`, and set `maxWorkers: 8`. Co-Authored-By: Claude Opus 5 (1M context) --- vite.config.ts | 74 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 547f96a261..a06c5cb1c3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,10 +1,15 @@ import { tanstackRouter } from '@tanstack/router-plugin/vite'; import react from '@vitejs/plugin-react'; -import { playwright, type PlaywrightProviderOptions } from '@vitest/browser-playwright'; +import { + playwright, + PlaywrightBrowserProvider, + type PlaywrightProviderOptions +} from '@vitest/browser-playwright'; import { ecij } from 'ecij/plugin'; import { Features } from 'lightningcss'; +import type { Browser } from 'playwright'; import { defineConfig, type ViteUserConfig } from 'vitest/config'; -import type { BrowserCommand } from 'vitest/node'; +import type { BrowserCommand, BrowserProviderOption, TestProject } from 'vitest/node'; const isCI = process.env.CI === 'true'; const isTest = process.env.VITEST === 'true'; @@ -50,6 +55,64 @@ const dragFill: BrowserCommand<[from: string, to: string]> = async ( await page.mouse.up(); }; +// Firefox pages opened in the same browser share focus and mouse states, +// so test files running in parallel interfere with each other: +// a mouse event raises the window of its page, blurring the focused element of other pages, +// and ends pointer captures in other pages. +// To isolate test files, every session after the first one gets its own browser. +class PlaywrightBrowserPerSessionProvider extends PlaywrightBrowserProvider { + readonly #project: TestProject; + readonly #options: PlaywrightProviderOptions; + readonly #browsers = new Set(); + #firstSessionId: string | undefined; + + constructor(project: TestProject, options: PlaywrightProviderOptions) { + super(project, options); + this.#project = project; + this.#options = options; + } + + override async openPage(sessionId: string, url: string, options: { parallel: boolean }) { + this.#firstSessionId ??= sessionId; + + // the base provider reuses the session's context if it exists, + // instead of creating one in its own browser + if (sessionId !== this.#firstSessionId && !this.contexts.has(sessionId)) { + const { launchOptions, contextOptions, actionTimeout } = this.#options; + const playwright = await import('playwright'); + const browser = await playwright[this.browserName].launch({ + ...launchOptions, + headless: this.#project.config.browser.headless + }); + this.#browsers.add(browser); + const context = await browser.newContext({ ...contextOptions, ignoreHTTPSErrors: true }); + if (actionTimeout !== undefined) { + context.setDefaultTimeout(actionTimeout); + } + this.contexts.set(sessionId, context); + } + + await super.openPage(sessionId, url, options); + } + + override async close() { + await super.close(); + await Promise.all(Array.from(this.#browsers, (browser) => browser.close())); + this.#browsers.clear(); + } +} + +function playwrightBrowserPerSession( + options: PlaywrightProviderOptions +): BrowserProviderOption { + return { + ...playwright(options), + providerFactory(project) { + return new PlaywrightBrowserPerSessionProvider(project, options); + } + }; +} + const actionTimeout = 2000; const viewport = { width: 1920, height: 1080 } as const; const playwrightOptions: PlaywrightProviderOptions = { @@ -116,6 +179,7 @@ export default defineConfig(({ isPreview }): ViteUserConfig => ({ globals: true, injectCjsGlobals: false, printConsoleTrace: true, + maxWorkers: 8, env: { // @ts-expect-error CI: isCI @@ -165,16 +229,14 @@ export default defineConfig(({ isPreview }): ViteUserConfig => ({ }, { browser: 'firefox', - provider: playwright({ + provider: playwrightBrowserPerSession({ ...playwrightOptions, launchOptions: { firefoxUserPrefs: { 'accessibility.force_disabled': 1 } } - }), - // TODO: remove when FF tests are stable - fileParallelism: false + }) }, { browser: 'webkit', From ab086f99ffd52119b50110053205a61d0b86bd44 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Mon, 14 Sep 2026 03:07:08 +0100 Subject: [PATCH 2/2] limit workers in CI --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index a06c5cb1c3..94101f5a2b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -179,7 +179,7 @@ export default defineConfig(({ isPreview }): ViteUserConfig => ({ globals: true, injectCjsGlobals: false, printConsoleTrace: true, - maxWorkers: 8, + maxWorkers: isCI ? 2 : 8, env: { // @ts-expect-error CI: isCI