From b67ae68d514854915199dc8f20c48b72b4e39cde Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sat, 1 Aug 2026 21:23:00 +0300 Subject: [PATCH] Try to fix E2E test port stall --- .claude/skills/e2e/SKILL.md | 2 +- e2e/global-setup.ts | 16 +++++++++------- e2e/helpers/playwright.ts | 21 ++++++++++++++++++++- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/.claude/skills/e2e/SKILL.md b/.claude/skills/e2e/SKILL.md index 88afdf070..2967690ee 100644 --- a/.claude/skills/e2e/SKILL.md +++ b/.claude/skills/e2e/SKILL.md @@ -10,7 +10,7 @@ description: Run, debug, and manage Playwright e2e tests. Use when running e2e t - Tests live in `e2e/*.spec.ts`, config in `playwright.config.ts` - Page objects live in `e2e/pages//` — every spec uses them; conventions in `docs/dev/e2e-page-objects.md`, gotchas in `docs/dev/e2e-page-objects-migration.md` - Global setup (`e2e/global-setup.ts`) builds the app (skipped when no build input changed since the last e2e build — tracked via `.e2e-build-marker`), creates/migrates per-worker databases (via `scripts/ensure-test-db.ts`: pending migrations are applied, drifted databases are rebuilt), and starts one server per worker -- Port calculation: `E2E_BASE_PORT = PORT (from .env) + 500`. Default PORT is typically 4001, so base port = 4501. Worker N uses port base+N +- Port calculation: `E2E_BASE_PORT = PORT (from .env) + 500`. Worker N uses port base+N, except ports on the WHATWG fetch bad port list (e.g. 6679) are skipped — see `e2eWorkerPort` in `e2e/helpers/playwright.ts` - Worker count: `E2E_WORKERS` env, defaulting to `min(8, max(4, cores - 2))` - Worker databases: `db-test-e2e-.sqlite3` in the project root; every test starts from a wiped database holding only the admin (Sendou) and N-ZAP users, and builds its own data with the `factories` fixture - MinIO (S3-compatible storage) is started via Docker Compose if not already running diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts index bc7db0c6f..8ac434fcd 100644 --- a/e2e/global-setup.ts +++ b/e2e/global-setup.ts @@ -2,7 +2,7 @@ import { type ChildProcess, execSync, spawn } from "node:child_process"; import fs from "node:fs"; import type { FullConfig } from "@playwright/test"; import { ensureMigratedDb } from "../scripts/ensure-test-db"; -import { E2E_BASE_PORT } from "./helpers/playwright"; +import { E2E_BASE_PORT, e2eWorkerPort } from "./helpers/playwright"; const DEBUG = process.env.E2E_DEBUG === "true"; const SERVER_PROCESSES: ChildProcess[] = []; @@ -182,12 +182,13 @@ async function globalSetup(config: FullConfig) { // Prepare databases and start servers for each worker const serverPromises: Promise[] = []; - // Kill any existing processes on our ports before starting + // Kill any existing processes on our ports before starting; sweep beyond the + // current worker count so leftovers from a run with more workers also die // biome-ignore lint/suspicious/noConsole: CLI script output console.log("Cleaning up any existing processes on e2e ports..."); const killedSomething = killProcessesOnPorts( E2E_BASE_PORT, - E2E_BASE_PORT + workerCount - 1, + e2eWorkerPort(Math.max(workerCount, 8) - 1), ); if (killedSomething) { // Wait briefly for ports to be released @@ -195,7 +196,7 @@ async function globalSetup(config: FullConfig) { } for (let i = 0; i < workerCount; i++) { - const port = E2E_BASE_PORT + i; + const port = e2eWorkerPort(i); const dbPath = `db-test-e2e-${i}.sqlite3`; ensureMigratedDb(dbPath); @@ -255,12 +256,13 @@ async function globalSetup(config: FullConfig) { ); } + // Store server processes globally for teardown before awaiting readiness so + // a failed startup still gets every already-spawned server cleaned up + global.__E2E_SERVERS__ = SERVER_PROCESSES; + // Wait for all servers to be ready await Promise.all(serverPromises); - // Store server processes globally for teardown - global.__E2E_SERVERS__ = SERVER_PROCESSES; - // biome-ignore lint/suspicious/noConsole: CLI script output console.log("\nAll servers started successfully!\n"); } diff --git a/e2e/helpers/playwright.ts b/e2e/helpers/playwright.ts index a270efbe7..4449c4fd9 100644 --- a/e2e/helpers/playwright.ts +++ b/e2e/helpers/playwright.ts @@ -24,6 +24,25 @@ export const E2E_BASE_PORT = Number(process.env.PORT || 5173) + 500; export const MOBILE_VIEWPORT = { width: 375, height: 667 }; export const TABLET_VIEWPORT = { width: 768, height: 1024 }; +/** Registered (>=1024) ports on the WHATWG fetch bad port list: Node's fetch + * fails on them with "bad port" and Chromium with ERR_UNSAFE_PORT, so no worker + * server may listen on one (e.g. base port 6673 would put worker 6 on 6679). */ +const UNSAFE_PORTS = new Set([ + 1719, 1720, 1723, 2049, 3659, 4045, 4190, 5060, 5061, 5432, 5500, 5938, 6000, + 6566, 6665, 6666, 6667, 6668, 6669, 6679, 6697, 10080, +]); + +/** The port of the given worker's server: base port + index, skipping unsafe ports. */ +export function e2eWorkerPort(workerIndex: number) { + let port = E2E_BASE_PORT - 1; + for (let i = 0; i <= workerIndex; i++) { + do { + port++; + } while (UNSAFE_PORTS.has(port)); + } + return port; +} + type WorkerFixtures = { workerPort: number; workerBaseURL: string; @@ -49,7 +68,7 @@ export const test = base.extend({ workerPort: [ // biome-ignore lint/correctness/noEmptyPattern: Playwright requires object destructuring async ({}, use, workerInfo) => { - const port = E2E_BASE_PORT + workerInfo.parallelIndex; + const port = e2eWorkerPort(workerInfo.parallelIndex); await use(port); }, { scope: "worker" },