meta/design/dev-server-browser-tests.md
The dev server browser tests verify HMR and lazy compilation using real Playwright browser instances against running dev servers. All browser tests live in a single file (browser.spec.ts) with shared setup — this is a deliberate choice to avoid Windows CI flakiness caused by Vitest's forks pool.
packages/test-dev-server/tests/
browser.spec.ts # All browser tests (HMR + lazy compilation)
vitest-setup-browser.ts # Shared setup: servers, browser, pages
vitest.config.browser.mts # Vitest config
test-utils.ts # Helpers (editFile, waitForBuildStable, etc.)
playground/
hmr-full-bundle-mode/ # HMR test fixture (copied to tmp at runtime)
lazy-compilation/ # Lazy compilation test fixture
The setup file (vitest-setup-browser.ts) runs as Vitest setupFiles (same process as tests):
tmp-playground/ from playground/ (always fresh copy)pnpm serve) — HMR on 3636, lazy on 3637global.__page and global.__lazyPageThe lazy-compilation test navigates its page inside the test itself, not in beforeAll. This is intentional: main.js triggers import('./lazy-module.js') after 1 second. If the page were navigated during setup, the dynamic import would fire during the HMR tests, warming the server's lazy-compilation state before the lazy test runs. Navigating in the test ensures we exercise the cold /@vite/lazy compilation path.
Teardown closes the browser and kills dev servers via killPort().
Vitest's forks pool creates a separate worker process per test file. On Windows, splitting tests across multiple files causes flaky failures:
killPort() to clean up orphaned serverskillPort (using taskkill) doesn't always finish cleanup before Worker B needs the portsWith a single file: one fork, one setup, one teardown, no cross-process coordination.
Ports are defined in two places that must stay in sync:
| Server | src/config.ts | playground/*/dev.config.mjs |
|---|---|---|
| HMR | CONFIG.ports.hmrFullBundleMode = 3636 | hmr-full-bundle-mode/dev.config.mjs |
| Lazy | CONFIG.ports.lazyCompilation = 3637 | lazy-compilation/dev.config.mjs |
Note: waitForBuildStable(port) takes port as its first argument. Passing the wrong port causes silent 30-second timeouts (it polls a non-existent server until the timeout fires).
playground/ with dev.config.mjs, package.json, and source filessrc/config.ts and dev.config.mjsvitest-setup-browser.tsbrowser.spec.ts in a new describe block