From 8f0c096c154a3ace31766d2558b6aee695616a8f Mon Sep 17 00:00:00 2001 From: Contentrain Date: Fri, 15 May 2026 13:27:25 +0300 Subject: [PATCH] fix(ci): wait for e2e port to free between suite spawns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `scripts/run-e2e-suite.mjs` runs each `*.e2e.test.ts` file as its own `vitest run` child process, but didn't wait for the previous Nuxt server to release port 4327 before spawning the next suite. On CI this intermittently surfaces as `listen EADDRINUSE: address already in use 127.0.0.1:4327` followed by `GetPortError: Timeout waiting for port 4327 after 20 retries with 500ms interval`, skipping the affected suite's tests (most recently observed on PR #47, run 25912452254). Every e2e file hard-codes `setupBrowserE2E(4327)`, so polling the single shared port between suites is the minimal fix — no test-file changes and no change to how Nuxt is booted. The check uses a short-lived listener on the same port as a probe and bails after 15s with a clear error. --- scripts/run-e2e-suite.mjs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/scripts/run-e2e-suite.mjs b/scripts/run-e2e-suite.mjs index 8d5660ae..0faaafcf 100644 --- a/scripts/run-e2e-suite.mjs +++ b/scripts/run-e2e-suite.mjs @@ -1,6 +1,7 @@ import { readdir } from 'node:fs/promises' import { join } from 'node:path' import { spawn } from 'node:child_process' +import { createServer } from 'node:net' const root = new URL('..', import.meta.url) const e2eDir = new URL('../tests/e2e', import.meta.url) @@ -8,6 +9,35 @@ const e2eDir = new URL('../tests/e2e', import.meta.url) const extraArgs = process.argv.slice(2) const pnpmBin = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm' +// All e2e suites bind Nuxt to this port (see tests/e2e/*.e2e.test.ts). +// Each suite is run as its own `vitest run` process; we must wait for the +// previous suite's Nuxt server to release the socket before spawning the +// next one, otherwise the next boot fails with EADDRINUSE. +const E2E_PORT = 4327 +const E2E_HOST = '127.0.0.1' +const PORT_FREE_TIMEOUT_MS = 15_000 +const PORT_FREE_POLL_MS = 200 + +async function isPortFree(port, host) { + return new Promise((resolve) => { + const tester = createServer() + tester.once('error', () => resolve(false)) + tester.once('listening', () => { + tester.close(() => resolve(true)) + }) + tester.listen(port, host) + }) +} + +async function waitForPortFree(port, host, timeoutMs) { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + if (await isPortFree(port, host)) return + await new Promise(r => setTimeout(r, PORT_FREE_POLL_MS)) + } + throw new Error(`Port ${port} on ${host} still in use after ${timeoutMs}ms`) +} + const files = (await readdir(e2eDir)) .filter(name => name.endsWith('.e2e.test.ts')) .sort() @@ -34,4 +64,6 @@ for (const file of files) { if (exitCode !== 0) { process.exit(exitCode) } + + await waitForPortFree(E2E_PORT, E2E_HOST, PORT_FREE_TIMEOUT_MS) }