pediatric-ai-scribe-v3/e2e/playwright.config.js
Daniel 05dcd1146d test(e2e): repair the harness, taking the browser suite from 96 failures to 11
Three separate reasons tests were failing, none of them a defect in the app.

The calculators. e2e-harness.html loaded calculators.js and drugs-loader.js with
`defer` after they were split into ES modules; index.html was updated at the
time and this page was not. A module parsed as a classic script throws "Cannot
use import statement outside a module" before a line runs, so no click handler
was ever attached: the pills rendered from static HTML and did nothing. Only the
first calculator appeared to pass, because it carries `active` in the markup and
needs no click. That was 52 failures.

Settings and FAQ. Both moved from the tab rail into the account-card menu; the
helper still clicked button.tab-btn[data-tab=…] and timed out. Ten more.

The AI mocks, which had stopped intercepting for two independent reasons and so
were calling the real model on every run — spending credits and comparing
genuine output against strings like "MOCK HPI from dictation". A '**/api/x' glob
matches no URL on Playwright 1.50, and page.route fails silently when nothing
matches; measured against a real URL, that glob and '*/**/api/x' both matched
zero times where a regex matched. Fixing that alone was not enough: the app
registers a service worker that answers every /api/ request with its own
fetch(), and a request made inside a service worker never reaches page.route.
Blocking registration in the config puts them back in the page. The mocked
dictation test now finishes in 1.6s rather than 7.5s, which is what a real model
call costs.

Whole suite: 204 passed / 96 failed in 15.8 minutes, now 289 passed / 11 failed
in 6.8. The remaining eleven are spread across nine specs with no shared cause
and are not touched here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-11 18:54:03 +02:00

47 lines
2.1 KiB
JavaScript

// Playwright config — runs smoke tests against the already-running PedScribe
// container (no dev server spin-up). Expects BASE_URL, which scripts/e2e.sh
// supplies.
const { defineConfig, devices } = require('@playwright/test');
// BASE_URL must be a loopback origin (127.0.0.1 / localhost), not a container
// hostname. The app is a secure context in production and is written on that
// assumption: AccountBoundary mints a session generation with
// crypto.randomUUID() on every sign-in. Over plain http on a hostname that is
// not loopback the browser provides no crypto.randomUUID at all, so that call
// throws, the boot handler's catch swallows it, and every test lands on the
// login screen no matter how valid its session is — which is exactly what the
// whole browser suite was doing.
//
// Chrome's --unsafely-treat-insecure-origin-as-secure was tried first and does
// not work here: Playwright rejects the --user-data-dir it has to be paired
// with, and the flag alone leaves isSecureContext false. Loopback needs no
// flags, so scripts/e2e.sh runs the browser on the host network and reaches the
// app through its published port instead.
module.exports = defineConfig({
testDir: './tests',
timeout: 30_000,
expect: { timeout: 5_000 },
fullyParallel: false,
retries: 0,
workers: 1,
reporter: [['list']],
use: {
baseURL: process.env.BASE_URL || 'http://127.0.0.1:3553',
// The app registers a service worker that answers every /api/ request with
// its own fetch(). A request made inside a service worker never reaches
// page.route, so mockAI could not intercept anything while one was running
// and the tests called the real model. Blocking registration puts the
// requests back in the page, where the mocks can see them.
serviceWorkers: 'block',
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
actionTimeout: 5_000,
navigationTimeout: 15_000,
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
// Mobile pass — catches layout regressions at ~375 px (iPhone SE)
{ name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
],
});