From dee6e55af731e666548ee249be09015029703855 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Sun, 25 Oct 2026 01:22:20 +0000 Subject: [PATCH 1/6] Run Firefox tests in parallel Firefox sends the mouse events of all its pages with the same pointer ID, so a page that captures the pointer receives the other pages' mouse events. The two test files that drag a resize or drag-fill handle now run one at a time in a browser of their own, and the other Firefox tests run in parallel, once the other browsers are done. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 1 + vite.config.ts | 111 +++++++++++++++++++++++++++++++++---------------- 2 files changed, 76 insertions(+), 36 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6c710d1639..9704f2b4c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,6 +45,7 @@ website/ # demo site (Vite + TanStack Router) - Browser tests use `vite-plus/test/browser` + Playwright. `test/setupBrowser.ts` configures `page.render()` via `vitest-browser-react` and registers custom locators via `locators.extend()` — prefer `page.getGrid()`, `page.getCell({ name })`, `page.getRow()`, `page.getHeaderCell()`, `page.getActiveCell()`, etc. over raw `page.getByRole()`. - Test helpers in `test/browser/utils.tsx`: `setup()`, `getRowWithCell()`, `getCellsAtRowIndex()`, `validateCellPosition()`, `scrollGrid()`, `safeTab()`, `testCount()`, `testRowCount()`. - `test/failOnConsole.ts` fails tests on unexpected console warnings/errors. +- Firefox runs test files in parallel, after the other browsers. Firefox sends the mouse events of all its pages with the same pointer ID, so tests that drag a resize or drag-fill handle, which captures the pointer, run one at a time in a separate `browser (firefox, pointer capture)` instance: add such test files to `firefoxPointerCaptureTests` in `vite.config.ts`. Use `--project "browser (firefox*)"` to run both Firefox instances. - **Never run visual regression tests** — screenshots are environment-dependent, so visual regression tests must run in CI only. ## Tooling diff --git a/vite.config.ts b/vite.config.ts index 4fe6cec15b..786d22b802 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -66,6 +66,28 @@ const playwrightOptions: PlaywrightProviderOptions = { viewport } }; +const firefoxProvider = playwright({ + ...playwrightOptions, + launchOptions: { + firefoxUserPrefs: { + 'accessibility.force_disabled': 1 + } + } +}); +// Firefox sends the mouse events of all its pages with the same pointer ID, +// so a page that captures the pointer also receives the other pages' mouse events. +// These tests drag a resize or drag-fill handle, which captures the pointer, +// so they run one at a time, in a browser of their own. +const firefoxPointerCaptureTests = [ + 'browser/dragFill.test.tsx', + 'browser/column/resizable.test.tsx' +]; +const browserTests = ['browser/**/*.test.*', ...(isCI ? ['visual/*.test.*'] : [])]; +const browserSetupFiles = [ + 'test/browser/styles.css', + 'test/setupBrowser.ts', + 'test/failOnConsole.ts' +]; export default defineConfig({ staged: { @@ -273,47 +295,64 @@ export default defineConfig({ toMatchScreenshot: { screenshotDirectory: 'screenshots' } - }, - instances: [ - { - browser: 'chromium', - provider: playwright({ - ...playwrightOptions, - launchOptions: { - channel: 'chromium', - args: [ - '--disable-renderer-accessibility', - '--disable-platform-accessibility-integration' - ] - } - }) - }, - { - browser: 'firefox', - provider: playwright({ - ...playwrightOptions, - launchOptions: { - firefoxUserPrefs: { - 'accessibility.force_disabled': 1 - } - } - }), - // TODO: remove when FF tests are stable - fileParallelism: false - }, - { - browser: 'webkit', - provider: playwright(playwrightOptions) - } - ] + } }, projects: [ { test: { name: 'browser', - include: ['browser/**/*.test.*', ...(isCI ? ['visual/*.test.*'] : [])], - browser: { enabled: true }, - setupFiles: ['test/browser/styles.css', 'test/setupBrowser.ts', 'test/failOnConsole.ts'] + include: browserTests, + browser: { + enabled: true, + instances: [ + { + browser: 'chromium', + provider: playwright({ + ...playwrightOptions, + launchOptions: { + channel: 'chromium', + args: [ + '--disable-renderer-accessibility', + '--disable-platform-accessibility-integration' + ] + } + }) + }, + { + browser: 'webkit', + provider: playwright(playwrightOptions) + }, + { + browser: 'firefox', + name: 'browser (firefox, pointer capture)', + provider: firefoxProvider, + include: firefoxPointerCaptureTests, + fileParallelism: false + } + ] + }, + setupFiles: browserSetupFiles + } + }, + { + test: { + name: 'firefox', + include: browserTests, + exclude: firefoxPointerCaptureTests, + browser: { + enabled: true, + instances: [ + { + browser: 'firefox', + name: 'browser (firefox)', + provider: firefoxProvider + } + ] + }, + setupFiles: browserSetupFiles, + // Firefox's pages wait for each other's mouse events, which time out when they compete + // with the other browsers for the CPU, so Firefox runs once they're done + sequence: { groupOrder: 1 } } }, { From 3b7b64714523deb6c5a5e22abfca228438731c3d Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Sun, 25 Oct 2026 01:43:28 +0000 Subject: [PATCH 2/6] Link the Playwright issue about Firefox's shared pointer ID Co-Authored-By: Claude Opus 5.5 --- vite.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/vite.config.ts b/vite.config.ts index 786d22b802..978a99bba6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -78,6 +78,7 @@ const firefoxProvider = playwright({ // so a page that captures the pointer also receives the other pages' mouse events. // These tests drag a resize or drag-fill handle, which captures the pointer, // so they run one at a time, in a browser of their own. +// https://github.com/microsoft/playwright/issues/43631 const firefoxPointerCaptureTests = [ 'browser/dragFill.test.tsx', 'browser/column/resizable.test.tsx' From 5f8b8c4216a56043142862dd01bf1120ff7d1f82 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Sun, 25 Oct 2026 03:06:31 +0000 Subject: [PATCH 3/6] Run each Firefox pointer-capture test file in a browser of its own The test files that capture the pointer each get a Firefox instance of their own, which runs alongside Chromium and WebKit, instead of one serial instance that ran after all the other tests. The other Firefox tests still run in parallel once Chromium and WebKit are done, as their mouse events time out when they compete with them for the CPU. The `dragFill` command now waits for the grid to render the dragged-over cells before releasing the mouse, instead of waiting 20ms in WebKit, which wasn't always enough. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- vite.config.ts | 35 +++++++++++++++-------------------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9704f2b4c9..84a38678c7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,7 +45,7 @@ website/ # demo site (Vite + TanStack Router) - Browser tests use `vite-plus/test/browser` + Playwright. `test/setupBrowser.ts` configures `page.render()` via `vitest-browser-react` and registers custom locators via `locators.extend()` — prefer `page.getGrid()`, `page.getCell({ name })`, `page.getRow()`, `page.getHeaderCell()`, `page.getActiveCell()`, etc. over raw `page.getByRole()`. - Test helpers in `test/browser/utils.tsx`: `setup()`, `getRowWithCell()`, `getCellsAtRowIndex()`, `validateCellPosition()`, `scrollGrid()`, `safeTab()`, `testCount()`, `testRowCount()`. - `test/failOnConsole.ts` fails tests on unexpected console warnings/errors. -- Firefox runs test files in parallel, after the other browsers. Firefox sends the mouse events of all its pages with the same pointer ID, so tests that drag a resize or drag-fill handle, which captures the pointer, run one at a time in a separate `browser (firefox, pointer capture)` instance: add such test files to `firefoxPointerCaptureTests` in `vite.config.ts`. Use `--project "browser (firefox*)"` to run both Firefox instances. +- Firefox runs test files in parallel, after the other browsers. Firefox sends the mouse events of all its pages with the same pointer ID, so each test file that drags a resize or drag-fill handle, which captures the pointer, runs in a separate Firefox instance with a browser of its own: add such test files to `firefoxPointerCaptureTests` in `vite.config.ts`. Use `--project "*firefox*"` to run all the Firefox instances. - **Never run visual regression tests** — screenshots are environment-dependent, so visual regression tests must run in CI only. ## Tooling diff --git a/vite.config.ts b/vite.config.ts index 978a99bba6..29d89980b4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,9 +1,10 @@ +import { basename } from 'node:path'; import react from '@vitejs/plugin-react'; import { ecij } from 'ecij/plugin'; import { Features } from 'lightningcss'; import { defineConfig, lazyPlugins } from 'vite-plus'; import { playwright, type PlaywrightProviderOptions } from 'vite-plus/test/browser-playwright'; -import type { BrowserCommandContext } from 'vite-plus/test/node'; +import type { BrowserCommandContext, BrowserInstanceOption } from 'vite-plus/test/node'; import pkg from './package.json' with { type: 'json' }; @@ -34,21 +35,14 @@ async function resizeColumn( } // TODO: remove when `userEvent.pointer` is supported -async function dragFill( - { page, iframe, project }: BrowserCommandContext, - from: string, - to: string -) { +async function dragFill({ page, iframe }: BrowserCommandContext, from: string, to: string) { await iframe.getByRole('gridcell', { name: from, exact: true }).click(); await iframe.locator('.rdg-cell-drag-handle').hover(); await page.mouse.down(); - await iframe.getByRole('gridcell', { name: to, exact: true }).hover(); - if (project.name.includes('webkit')) { - // let React re-render after handleDragHandlePointerMove calls setDraggedOverRowIdx() - await new Promise((resolve) => { - setTimeout(resolve, 20); - }); - } + const target = iframe.getByRole('gridcell', { name: to, exact: true }); + await target.hover(); + // the grid fills the cells it rendered as dragged over, so wait for it to render the drag + await target.and(iframe.locator('.rdg-cell-dragged-over')).waitFor(); await page.mouse.up(); } @@ -77,7 +71,7 @@ const firefoxProvider = playwright({ // Firefox sends the mouse events of all its pages with the same pointer ID, // so a page that captures the pointer also receives the other pages' mouse events. // These tests drag a resize or drag-fill handle, which captures the pointer, -// so they run one at a time, in a browser of their own. +// so each of them runs in a browser of its own. // https://github.com/microsoft/playwright/issues/43631 const firefoxPointerCaptureTests = [ 'browser/dragFill.test.tsx', @@ -323,13 +317,14 @@ export default defineConfig({ browser: 'webkit', provider: playwright(playwrightOptions) }, - { + // the names sort after `browser (webkit)`, so with few workers, as in CI, + // WebKit's tests start before these + ...firefoxPointerCaptureTests.map((test): BrowserInstanceOption => ({ + name: `firefox (${basename(test).split('.')[0]})`, browser: 'firefox', - name: 'browser (firefox, pointer capture)', - provider: firefoxProvider, - include: firefoxPointerCaptureTests, - fileParallelism: false - } + include: [test], + provider: firefoxProvider + })) ] }, setupFiles: browserSetupFiles From 24d378b64a6baddca62e9bc05e755d4f38af0dad Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Sun, 25 Oct 2026 04:18:09 +0000 Subject: [PATCH 4/6] Run Firefox's tests alongside the other browsers The main Firefox instance no longer waits for Chromium and WebKit to finish in a project of its own, as the pointer-capture tests already run in browsers of their own. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- vite.config.ts | 96 +++++++++++++++++++------------------------------- 2 files changed, 37 insertions(+), 61 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 84a38678c7..3f2a0fa850 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,7 +45,7 @@ website/ # demo site (Vite + TanStack Router) - Browser tests use `vite-plus/test/browser` + Playwright. `test/setupBrowser.ts` configures `page.render()` via `vitest-browser-react` and registers custom locators via `locators.extend()` — prefer `page.getGrid()`, `page.getCell({ name })`, `page.getRow()`, `page.getHeaderCell()`, `page.getActiveCell()`, etc. over raw `page.getByRole()`. - Test helpers in `test/browser/utils.tsx`: `setup()`, `getRowWithCell()`, `getCellsAtRowIndex()`, `validateCellPosition()`, `scrollGrid()`, `safeTab()`, `testCount()`, `testRowCount()`. - `test/failOnConsole.ts` fails tests on unexpected console warnings/errors. -- Firefox runs test files in parallel, after the other browsers. Firefox sends the mouse events of all its pages with the same pointer ID, so each test file that drags a resize or drag-fill handle, which captures the pointer, runs in a separate Firefox instance with a browser of its own: add such test files to `firefoxPointerCaptureTests` in `vite.config.ts`. Use `--project "*firefox*"` to run all the Firefox instances. +- Firefox runs test files in parallel. Firefox sends the mouse events of all its pages with the same pointer ID, so each test file that drags a resize or drag-fill handle, which captures the pointer, runs in a separate Firefox instance with a browser of its own: add such test files to `firefoxPointerCaptureTests` in `vite.config.ts`. Use `--project "*firefox*"` to run all the Firefox instances. - **Never run visual regression tests** — screenshots are environment-dependent, so visual regression tests must run in CI only. ## Tooling diff --git a/vite.config.ts b/vite.config.ts index 29d89980b4..8e84cafc33 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -77,12 +77,6 @@ const firefoxPointerCaptureTests = [ 'browser/dragFill.test.tsx', 'browser/column/resizable.test.tsx' ]; -const browserTests = ['browser/**/*.test.*', ...(isCI ? ['visual/*.test.*'] : [])]; -const browserSetupFiles = [ - 'test/browser/styles.css', - 'test/setupBrowser.ts', - 'test/failOnConsole.ts' -]; export default defineConfig({ staged: { @@ -290,65 +284,47 @@ export default defineConfig({ toMatchScreenshot: { screenshotDirectory: 'screenshots' } - } + }, + instances: [ + { + browser: 'chromium', + provider: playwright({ + ...playwrightOptions, + launchOptions: { + channel: 'chromium', + args: [ + '--disable-renderer-accessibility', + '--disable-platform-accessibility-integration' + ] + } + }) + }, + { + browser: 'firefox', + exclude: firefoxPointerCaptureTests, + provider: firefoxProvider + }, + // the names sort after `browser (webkit)`, so with few workers, as in CI, + // WebKit's tests start before these + ...firefoxPointerCaptureTests.map((test): BrowserInstanceOption => ({ + name: `firefox (${basename(test).split('.')[0]})`, + browser: 'firefox', + include: [test], + provider: firefoxProvider + })), + { + browser: 'webkit', + provider: playwright(playwrightOptions) + } + ] }, projects: [ { test: { name: 'browser', - include: browserTests, - browser: { - enabled: true, - instances: [ - { - browser: 'chromium', - provider: playwright({ - ...playwrightOptions, - launchOptions: { - channel: 'chromium', - args: [ - '--disable-renderer-accessibility', - '--disable-platform-accessibility-integration' - ] - } - }) - }, - { - browser: 'webkit', - provider: playwright(playwrightOptions) - }, - // the names sort after `browser (webkit)`, so with few workers, as in CI, - // WebKit's tests start before these - ...firefoxPointerCaptureTests.map((test): BrowserInstanceOption => ({ - name: `firefox (${basename(test).split('.')[0]})`, - browser: 'firefox', - include: [test], - provider: firefoxProvider - })) - ] - }, - setupFiles: browserSetupFiles - } - }, - { - test: { - name: 'firefox', - include: browserTests, - exclude: firefoxPointerCaptureTests, - browser: { - enabled: true, - instances: [ - { - browser: 'firefox', - name: 'browser (firefox)', - provider: firefoxProvider - } - ] - }, - setupFiles: browserSetupFiles, - // Firefox's pages wait for each other's mouse events, which time out when they compete - // with the other browsers for the CPU, so Firefox runs once they're done - sequence: { groupOrder: 1 } + include: ['browser/**/*.test.*', ...(isCI ? ['visual/*.test.*'] : [])], + browser: { enabled: true }, + setupFiles: ['test/browser/styles.css', 'test/setupBrowser.ts', 'test/failOnConsole.ts'] } }, { From 8ac64fd8753990d35a3ae47c37146e419d14d9a2 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Sun, 25 Oct 2026 12:39:20 +0000 Subject: [PATCH 5/6] Use half the cores as workers on Windows On Windows, WebKit makes threads in other processes wait on win32k's session-wide lock, which slows down the mouse events of Playwright's Firefox. Firefox dispatches the mouse events of all its pages one after the other, so with all cores but one as workers, its clicks time out. In a terminal, the browser instances already get half the cores, as in watch mode, since they don't inherit the `watch: false` of `vitest run`. Runs with a captured stdin, like those of agents or cached `vp run` tasks, got all cores but one and failed most of the time. Co-Authored-By: Claude Opus 5.5 --- vite.config.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/vite.config.ts b/vite.config.ts index 8e84cafc33..d957ec51b0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,6 +12,7 @@ const isCI = process.env.CI === 'true'; const isCIWorkflow = process.env.GITHUB_WORKFLOW === 'CI'; const isTest = process.env.VITEST === 'true'; const isAgent = process.env.AI_AGENT !== undefined; +const isWindows = process.platform === 'win32'; // TODO: remove when `userEvent.pointer` is supported async function resizeColumn( @@ -275,6 +276,12 @@ export default defineConfig({ } }, slowTestThreshold: 1000, + // On Windows, WebKit slows down the mouse events of Playwright's Firefox, + // which dispatches those of all its pages one after the other, + // so with all cores but one as workers, Firefox's clicks time out. + // In a terminal, the browser instances get half the cores anyway, as in watch mode, + // since they don't inherit the `watch: false` of `vitest run`. + ...(isWindows && { maxWorkers: '50%' }), browser: { headless: true, ui: false, From fa6aed8862bf27c4f5150db452e7d4a5b109164c Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Sun, 25 Oct 2026 13:39:41 +0000 Subject: [PATCH 6/6] Link the Vitest issue about projects not inheriting `watch: false` Co-Authored-By: Claude Opus 5.5 --- vite.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/vite.config.ts b/vite.config.ts index d957ec51b0..415f693cc5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -281,6 +281,7 @@ export default defineConfig({ // so with all cores but one as workers, Firefox's clicks time out. // In a terminal, the browser instances get half the cores anyway, as in watch mode, // since they don't inherit the `watch: false` of `vitest run`. + // https://github.com/vitest-dev/vitest/issues/11721 ...(isWindows && { maxWorkers: '50%' }), browser: { headless: true,