diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/README.md b/crates/vp_cli_snapshots/tests/cli_snapshots/README.md index fc3547a9f2..6ce4e4ea36 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/README.md +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/README.md @@ -306,3 +306,14 @@ Fixture trees are excluded from repo-wide fmt, lint, typecheck, and vitest (`vite.config.ts`, `tsconfig.json`); recorded snapshots and `snapshots.toml` are runner metadata and never appear inside the staged workspace a test runs in. + +Test servers must request port `0` so the OS selects an available port at bind +time. Fixed ports can be reserved on Windows, where `EACCES` does not trigger +Vite's port-in-use fallback. Vitest 5.0.4 replaces a plain `test.api.port: 0` +with its default, including when it comes from `--api.port=0`. Set the port in +a post-configuration hook instead; see +[`test_v5_browser_defines/config.mjs`](fixtures/test_v5_browser_defines/config.mjs) +for independent, inherited, referenced, and injected projects, and +[`test_v5_optional_packages/verify-ui.mjs`](fixtures/test_v5_optional_packages/verify-ui.mjs) +for the UI server. Assert that each resolved API port is `0` so regressions fail +on every platform, even when the default port is available. diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_browser_defines/config.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_browser_defines/config.mjs index 1eada66183..33ed4c03de 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_browser_defines/config.mjs +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_browser_defines/config.mjs @@ -10,6 +10,18 @@ export const executablePath = existsSync('chromium-path.json') export function project(name, browser = true) { return { + plugins: [{ + name: 'browser-server-port', + // Each project needs its own hook, even with extends: true. + // Vitest replaces a plain api.port: 0 with its default. Override it last. + enforce: 'post', + config: { + order: 'post', + handler(config) { + config.test.api.port = 0; + }, + }, + }], define: { __VP_STRING_DEFINE__: JSON.stringify('/messages?locale="en"'), __VP_FALSE_DEFINE__: 'false', @@ -43,7 +55,7 @@ export function configFor(name) { switch (name) { case 'injected-hook': { const config = project('root'); - config.plugins = [{ + config.plugins.push({ name: 'inject-browser-project', configureVitest: { order: 'pre', @@ -56,7 +68,7 @@ export function configFor(name) { await injectTestProjects({ extends: false, ...injected }); }, }, - }]; + }); return config; } case 'injected': return project('root'); @@ -78,15 +90,18 @@ export function configFor(name) { case 'inherited': { const config = project('root'); config.test.projects = [ - { test: project('shared').test }, - { extends: true, test: project('inherited').test }, + { plugins: config.plugins, test: project('shared').test }, + { extends: true, plugins: config.plugins, test: project('inherited').test }, ]; return config; } case 'separate': { const config = project('root'); config.test.sharedViteServer = false; - config.test.projects = [{ test: project('first').test }, { test: project('second').test }]; + config.test.projects = [ + { plugins: config.plugins, test: project('first').test }, + { plugins: config.plugins, test: project('second').test }, + ]; return config; } case 'independent': return { diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_browser_defines/verify.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_browser_defines/verify.mjs index 7035c9983e..a588657cfd 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_browser_defines/verify.mjs +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_browser_defines/verify.mjs @@ -39,6 +39,11 @@ try { ]); } assert.equal(runner.projects.length, count, name); + for (const project of runner.projects) { + if (project.config.browser.enabled) { + assert.equal(project.config.api.port, 0, `${name}: ${project.name} must use an OS-assigned port`); + } + } const servers = new Set(runner.projects.map((project) => project.vite)); if (name === 'shared' || name === 'shared-node') assert.equal(servers.size, 1); if (name === 'separate') assert.equal(servers.size, 2); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_optional_packages/verify-ui.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_optional_packages/verify-ui.mjs index 27ac337d1e..0040653f63 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_optional_packages/verify-ui.mjs +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_optional_packages/verify-ui.mjs @@ -4,9 +4,22 @@ import { createVitest } from 'vite-plus/test/node'; const runner = await createVitest({ config: false, watch: false, reporters: [], ui: true, open: false, - api: { host: '127.0.0.1', port: 0 }, -}, defineConfig({})); + api: { host: '127.0.0.1' }, +}, defineConfig({ + plugins: [{ + name: 'ui-server-port', + // Vitest replaces a plain api.port: 0 with its default. Override it last. + enforce: 'post', + config: { + order: 'post', + handler(config) { + config.test.api.port = 0; + }, + }, + }], +})); try { + assert.equal(runner.config.api.port, 0); if (!runner.vite.httpServer.listening) await runner.vite.listen(0); const base = `http://127.0.0.1:${runner.vite.httpServer.address().port}`; const ui = new URL(runner.config.uiBase, base); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_preview/verify.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_preview/verify.mjs index 54108af36c..5abb89f12f 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_preview/verify.mjs +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_preview/verify.mjs @@ -44,6 +44,7 @@ try { browser: { enabled: true, provider, headless: false, instances: [{ browser: 'chromium' }] }, }, })); + assert.equal(runner.projects[0].config.api.port, 0); const result = await runner.start(); assert.equal(result.unhandledErrors.length, 0, JSON.stringify(result.unhandledErrors)); assert.equal(runner.state.getFiles().length, 1); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_screenshots/verify.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_screenshots/verify.mjs index 37307f3255..6bdf19f276 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_screenshots/verify.mjs +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_screenshots/verify.mjs @@ -38,6 +38,7 @@ for (const phase of ['record', 'compare', 'failure']) { }, })); try { + assert.equal(runner.projects[0].config.api.port, 0); const result = await runner.start(); assert.equal(result.unhandledErrors.length, 0, JSON.stringify(result.unhandledErrors)); assert.equal(runner.state.getFiles().length, 1); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_webdriverio/verify.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_webdriverio/verify.mjs index 29c77f8283..918aab4085 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_webdriverio/verify.mjs +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/test_v5_webdriverio/verify.mjs @@ -47,18 +47,32 @@ provider.providerFactory = (project) => { }; return instance; }; -const runner = await createVitest({ config: false, watch: false, reporters: [] }, defineConfig({ test: { - include: ['browser.test.js'], - browser: { - enabled: true, headless: true, provider, instances: [{ browser: 'chrome' }], - viewport: { width: 800, height: 600 }, - commands: { inspectLocator(context, locator) { - assert.equal(typeof locator, 'object'); - return { ...locator, session: typeof context.sessionId === 'string' && context.sessionId.length > 0 }; - } }, +const runner = await createVitest({ config: false, watch: false, reporters: [] }, defineConfig({ + plugins: [{ + name: 'browser-server-port', + // Vitest replaces a plain api.port: 0 with its default. Override it last. + enforce: 'post', + config: { + order: 'post', + handler(config) { + config.test.api.port = 0; + }, + }, + }], + test: { + include: ['browser.test.js'], + browser: { + enabled: true, headless: true, provider, instances: [{ browser: 'chrome' }], + viewport: { width: 800, height: 600 }, + commands: { inspectLocator(context, locator) { + assert.equal(typeof locator, 'object'); + return { ...locator, session: typeof context.sessionId === 'string' && context.sessionId.length > 0 }; + } }, + }, }, -} })); +})); try { + assert.equal(runner.projects[0].config.api.port, 0); const result = await runner.start(); assert.equal(result.unhandledErrors.length, 0, inspect(result.unhandledErrors)); assert.equal(runner.state.getCountOfFailedTests(), 0, JSON.stringify(runner.state.getFiles().flatMap((file) => [file.result, ...file.tasks.map((task) => task.result)]))); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vitest_browser_mode/vitest.config.ts b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vitest_browser_mode/vitest.config.ts index 00dd92e6b7..5a73fc804c 100644 --- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vitest_browser_mode/vitest.config.ts +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/vitest_browser_mode/vitest.config.ts @@ -1,21 +1,29 @@ +import assert from 'node:assert/strict'; import { defineConfig } from 'vite-plus'; import { playwright } from 'vite-plus/test/browser-playwright'; export default defineConfig({ plugins: [ + { + name: 'browser-server-port', + // Vitest replaces a plain api.port: 0 with its default. Override it last. + enforce: 'post', + config: { + order: 'post', + handler(config) { + config.test.api.port = 0; + }, + }, + }, { name: 'vitest-browser-mode-suppress-known-vite-logs', configResolved(config) { - // "is in use, trying another one": port fallback depends on other - // concurrently running servers. "[optimizer]" progress lines fire on - // a 1s timer, so they only appear on slow cold-cache runs. - const nonDeterministicLogs = [ - 'is in use, trying another one', - '[optimizer]', - ]; + assert.equal(config.test.api.port, 0); + // "[optimizer]" progress lines fire on a 1s timer, so they only + // appear on slow cold-cache runs. const info = config.logger.info; config.logger.info = (message, options) => { - if (!nonDeterministicLogs.some((log) => message.includes(log))) { + if (!message.includes('[optimizer]')) { info(message, options); } };