From 6d77fe9da4b69aa5c0c87ebf65cec983290664a5 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 14 Oct 2026 13:16:39 +0200 Subject: [PATCH 1/2] Never reuse an existing Playwright fixture server Locally, reuseExistingServer adopted any process answering 200-403 on the fixture port without checking what it serves. When two worktrees run Playwright at the same time, the second run tests the first worktree's rendered tests/docs/playwright HTML: a spec passed with this checkout's fixture deleted. An unrelated app answering 200 made every spec fail on missing elements instead, which reads like a feature regression. Nothing starts the fixture or multiplex server ahead of a run, so reuse only ever applied to that collision case. A busy port now fails at once with Playwright's "is already used" error. The cost is starting the server on every invocation (about 1.4 s on Windows). A process answering 404 is still not detected as busy: Playwright starts its own http.server, which on Windows binds over the occupied port via SO_REUSEADDR, and the run times out after 120 s. That case never runs tests against the wrong content, so it is left as is and documented. --- .claude/rules/testing/playwright-tests.md | 6 ++++-- tests/integration/playwright/playwright.config.ts | 7 +++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.claude/rules/testing/playwright-tests.md b/.claude/rules/testing/playwright-tests.md index 2520787ad3..180f57bab9 100644 --- a/.claude/rules/testing/playwright-tests.md +++ b/.claude/rules/testing/playwright-tests.md @@ -119,8 +119,10 @@ uv run python -m http.server 8765 # Serves from tests/docs/playwright/ ``` -Locally (`reuseExistingServer: !isCI`), Playwright reuses whatever already listens on that port, even another app. -Every spec then fails with element-not-found errors; set `QUARTO_PLAYWRIGHT_PORT` to a free port. +The server is never reused (`reuseExistingServer: false`): if another process answers on the port, the run stops with `... is already used`. +Set `QUARTO_PLAYWRIGHT_PORT` to a free port, e.g. when two worktrees run Playwright at the same time. +The multiplex server port 1948 is fixed in `tests/docs/playwright/revealjs/multiplex.qmd`, so concurrent runs collide there too. +A process answering 404 on the port is not detected as busy; the run then times out after 120 s. ## Best Practices diff --git a/tests/integration/playwright/playwright.config.ts b/tests/integration/playwright/playwright.config.ts index c715b56090..d77f1f3dc5 100644 --- a/tests/integration/playwright/playwright.config.ts +++ b/tests/integration/playwright/playwright.config.ts @@ -95,12 +95,15 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ /* We use python for this but we could also try using another tool */ + /* Never reuse a server already on the port: it may be another app, or another + checkout serving its own tests/docs/playwright, so tests would run against + the wrong HTML. A busy port fails the run instead. */ webServer: [ { // HTTP server for rendered HTML files command: `uv run python -m http.server ${fixtureServerPort} --bind 127.0.0.1`, url: fixtureServerUrl, - reuseExistingServer: !isCI, + reuseExistingServer: false, cwd: '../../docs/playwright', // http.server does a reverse DNS lookup before listening, slow on some hosts timeout: 120000, @@ -110,7 +113,7 @@ export default defineConfig({ // Socket.IO multiplex server for RevealJS command: 'npm start', url: 'http://127.0.0.1:1948', - reuseExistingServer: !isCI, + reuseExistingServer: false, cwd: './multiplex-server', timeout: 10000, stderr: 'ignore', // Suppress verbose logs From 03459f6d55e2e930305ce66f8b2e2ca09025bc7e Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Wed, 14 Oct 2026 13:31:46 +0200 Subject: [PATCH 2/2] Keep reusing the multiplex server locally The multiplex webServer starts on every Playwright run, whatever specs are selected, and its port 1948 is fixed in revealjs/multiplex.qmd. With reuse off, a second concurrent run (another worktree) failed at once with "http://127.0.0.1:1948 is already used", even on its own QUARTO_PLAYWRIGHT_PORT, so concurrent runs became impossible. The multiplex server only relays socket messages and serves no test fixtures, so sharing another checkout's instance cannot make a run test the wrong HTML. Verified: with 1948 held by a separate multiplex server, revealjs-multiplex.spec.ts passes and a run on its own fixture port passes alongside it. The fixture server stays never-reused. --- .claude/rules/testing/playwright-tests.md | 9 +++++---- tests/integration/playwright/playwright.config.ts | 11 +++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.claude/rules/testing/playwright-tests.md b/.claude/rules/testing/playwright-tests.md index 180f57bab9..6b7370f866 100644 --- a/.claude/rules/testing/playwright-tests.md +++ b/.claude/rules/testing/playwright-tests.md @@ -119,10 +119,11 @@ uv run python -m http.server 8765 # Serves from tests/docs/playwright/ ``` -The server is never reused (`reuseExistingServer: false`): if another process answers on the port, the run stops with `... is already used`. -Set `QUARTO_PLAYWRIGHT_PORT` to a free port, e.g. when two worktrees run Playwright at the same time. -The multiplex server port 1948 is fixed in `tests/docs/playwright/revealjs/multiplex.qmd`, so concurrent runs collide there too. -A process answering 404 on the port is not detected as busy; the run then times out after 120 s. +The fixture server is never reused (`reuseExistingServer: false`): if another process answers on the port, the run stops with `http://127.0.0.1: is already used`. +Each concurrent run (another worktree, a second terminal) needs its own free `QUARTO_PLAYWRIGHT_PORT`; the default 8765 suits only one run at a time. +A process answering 404 on the port is not detected as busy; the run then times out after 120 s (`Timed out waiting 120000ms from config.webServer`), so pick another port. + +The revealjs multiplex server has a fixed port, 1948 (set in `tests/docs/playwright/revealjs/multiplex.qmd`), and is reused locally: concurrent runs share the first run's instance, which serves no test fixtures. ## Best Practices diff --git a/tests/integration/playwright/playwright.config.ts b/tests/integration/playwright/playwright.config.ts index d77f1f3dc5..689814dc28 100644 --- a/tests/integration/playwright/playwright.config.ts +++ b/tests/integration/playwright/playwright.config.ts @@ -95,14 +95,14 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ /* We use python for this but we could also try using another tool */ - /* Never reuse a server already on the port: it may be another app, or another - checkout serving its own tests/docs/playwright, so tests would run against - the wrong HTML. A busy port fails the run instead. */ webServer: [ { // HTTP server for rendered HTML files command: `uv run python -m http.server ${fixtureServerPort} --bind 127.0.0.1`, url: fixtureServerUrl, + // Never reuse: a server already on the port may be another app, or another + // checkout serving its own tests/docs/playwright, so tests would run + // against the wrong HTML. A busy port fails the run instead. reuseExistingServer: false, cwd: '../../docs/playwright', // http.server does a reverse DNS lookup before listening, slow on some hosts @@ -113,7 +113,10 @@ export default defineConfig({ // Socket.IO multiplex server for RevealJS command: 'npm start', url: 'http://127.0.0.1:1948', - reuseExistingServer: false, + // Reused locally: it relays socket messages and serves no test fixtures, + // and its port is fixed in revealjs/multiplex.qmd, so concurrent runs + // from several checkouts share one instance. + reuseExistingServer: !isCI, cwd: './multiplex-server', timeout: 10000, stderr: 'ignore', // Suppress verbose logs