diff --git a/playwright.ui.config.ts b/playwright.ui.config.ts index d2ab7c67..dfab4155 100644 --- a/playwright.ui.config.ts +++ b/playwright.ui.config.ts @@ -1,8 +1,24 @@ +import { randomInt } from "node:crypto"; import { defineConfig } from "@playwright/test"; -const port = Number(process.env.UI_TEST_PORT || 3107); -if (!Number.isInteger(port) || port < 1024 || port > 65535) - throw Error("Invalid UI_TEST_PORT"); +// The fixture server binds a loopback port. The CI runner packs several jobs +// onto the shared host network (capacity > 1), so a fixed port makes concurrent +// runs fight over the bind: one wins, the other's browser then gets +// "connection refused". Pick one random high port for the whole run and pass it +// to every worker through the environment, unless a port is pinned explicitly +// (e.g. a developer reusing a manually started fixture server). +function fixturePort() { + const requested = Number(process.env.UI_TEST_PORT); + if (Number.isInteger(requested) && requested >= 1024 && requested <= 65535) + return requested; + const shared = Number(process.env.PW_UI_TEST_PORT); + if (Number.isInteger(shared) && shared >= 1024 && shared <= 65535) + return shared; + const port = randomInt(30000, 50000); + process.env.PW_UI_TEST_PORT = String(port); + return port; +} +const port = fixturePort(); const baseURL = `http://127.0.0.1:${port}`; export default defineConfig({ testDir: "./e2e/ui", @@ -65,7 +81,7 @@ export default defineConfig({ command: "node e2e/ui/fixture-server.mjs", url: `${baseURL}/health`, timeout: 120000, - reuseExistingServer: true, + reuseExistingServer: !process.env.CI, env: { UI_TEST_PORT: String(port) }, }, });