The e2e stack shared production's Postgres — same server, same database, same table. Seeded robots sat in `users` beside real clinicians, and anything a test wrote, or a migration under test changed, landed on real data. Nothing about "run the tests" should be able to reach an account belonging to a person. Now it has a Postgres and a Redis of its own, both on tmpfs: created empty on every run, held in RAM, gone on teardown. scripts/e2e.sh is one command that recreates the stack, seeds it, runs the browser and leaves the app up at 127.0.0.1:3553 so it can be clicked around in, with the report served at :3554. Two bugs fell out of it immediately, both of which only a database that did not already exist could have found: The schema could not be built from nothing. The entrypoint migrated before the app created its baseline tables, so the first migration failed on saved_encounters not existing. It never showed because every database this has ever run against already had the baseline. Then, one layer down, 1777800000000_generated-images creates a table with a foreign key to learning_content — which the baseline stopped creating when Learning Hub was removed. Restoring into a brand-new database could not have booted. The entrypoint now stands aside when the database is empty and lets the app do it in the order it already gets right, and the foreign key is only created where its target is. All 20 migrations replay from empty, producing the same 23 tables production has. Configuration lives in the database, so a throwaway one starts at defaults — 14 settings against production's 49. That is why every model picker was empty: models.custom did not exist. The tests were right and the environment was incomplete, so the seed now states what the suite depends on, with fictional model ids: a test should not pass because of a setting somebody changed on the live system last week. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
52 lines
2.5 KiB
JavaScript
52 lines
2.5 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,
|
|
// list for the terminal, html for afterwards. The html report is a
|
|
// self-contained directory with the trace and screenshot of every failure in
|
|
// it; docker-compose.e2e.yml serves it at 127.0.0.1:3554 so it is a link
|
|
// rather than a path. open:'never' because this runs in a container that has
|
|
// no browser to open it with.
|
|
reporter: [['list'], ['html', { outputFolder: 'playwright-report', open: 'never' }]],
|
|
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'] } },
|
|
],
|
|
});
|