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
100 lines
4.8 KiB
Bash
Executable file
100 lines
4.8 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# End-to-end tests: a real browser, against a real copy of the whole app, on a
|
|
# database that did not exist a minute ago.
|
|
#
|
|
# scripts/e2e.sh # fresh stack, run every spec, leave it up
|
|
# scripts/e2e.sh auth-screen # only specs matching a pattern
|
|
# scripts/e2e.sh --down # tear the stack down and stop
|
|
# scripts/e2e.sh --no-reset # reuse the running stack and its data
|
|
#
|
|
# Every run recreates the database from empty, so nothing carries over between
|
|
# runs and the migrations are proved from nothing each time. The stack is left
|
|
# running afterwards on purpose: http://127.0.0.1:3553 is then a working copy
|
|
# of the app you can click around in, and http://127.0.0.1:3554 is the report.
|
|
set -euo pipefail
|
|
|
|
cd "$(dirname "$0")/.."
|
|
|
|
COMPOSE=(docker compose -f docker-compose.yml -f docker-compose.e2e.yml)
|
|
SERVICES=(postgres-e2e redis-e2e pediatric-scribe-e2e)
|
|
PLAYWRIGHT_IMAGE="mcr.microsoft.com/playwright:v1.50.0-noble"
|
|
APP_URL="http://127.0.0.1:3553"
|
|
REPORT_URL="http://127.0.0.1:3554"
|
|
|
|
RESET=true
|
|
GREP=""
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--down)
|
|
echo "==> Tearing down the e2e stack"
|
|
"${COMPOSE[@]}" rm -sfv "${SERVICES[@]}" e2e-report >/dev/null 2>&1 || true
|
|
echo " gone (its database was in RAM, so nothing is left on disk)"
|
|
exit 0 ;;
|
|
--no-reset) RESET=false ;;
|
|
-*) echo "unknown option: $arg" >&2; exit 2 ;;
|
|
*) GREP="$arg" ;;
|
|
esac
|
|
done
|
|
|
|
# ── Static reference lint ─────────────────────────────────────────────
|
|
# Catches the class of bug where a JS file reaches for an id that no HTML
|
|
# element (or dynamic id assignment anywhere in the repo) ever produces — the
|
|
# lightbox and adminMilestones dead-code bugs were both this shape and both
|
|
# went undetected until someone tripped over them in the real app. Cheap, so
|
|
# it runs first and fails before anything is built.
|
|
echo "==> Static reference lint"
|
|
docker run --rm -v "$PWD:/work" -w /work node:20-alpine node scripts/lint-references.js
|
|
|
|
# ── A stack with nothing in it ────────────────────────────────────────
|
|
# rm -sfv, not `down`: `down` on a merged compose file would take production's
|
|
# services with it. This names only the e2e ones. Their database and Redis are
|
|
# tmpfs, so removing the containers is what makes the data ephemeral.
|
|
if [ "$RESET" = true ]; then
|
|
echo "==> Recreating the e2e stack (fresh database)"
|
|
"${COMPOSE[@]}" rm -sfv "${SERVICES[@]}" >/dev/null 2>&1 || true
|
|
GIT_REVISION="$(git rev-parse HEAD 2>/dev/null || echo unknown)" \
|
|
"${COMPOSE[@]}" up -d --build --wait "${SERVICES[@]}"
|
|
else
|
|
echo "==> Reusing the running e2e stack"
|
|
GIT_REVISION="$(git rev-parse HEAD 2>/dev/null || echo unknown)" \
|
|
"${COMPOSE[@]}" up -d --wait "${SERVICES[@]}"
|
|
fi
|
|
|
|
# What is actually being tested. A stale image here would make a green run
|
|
# meaningless, which is the failure mode worth naming out loud.
|
|
RUNNING="$(curl -fsS --max-time 10 "$APP_URL/api/build" | sed -n 's/.*"buildId":"\([^"]*\)".*/\1/p' || true)"
|
|
echo " testing revision ${RUNNING:-<unknown>}"
|
|
|
|
# ── Seed ──────────────────────────────────────────────────────────────
|
|
# The accounts the fixtures log in as. The database is empty every run, so
|
|
# unlike before this is not optional and a failure here is fatal: tests that
|
|
# cannot log in fail in a way that looks like the app is broken.
|
|
echo "==> Seeding e2e accounts"
|
|
"${COMPOSE[@]}" exec -T pediatric-scribe-e2e node e2e/seed.js
|
|
|
|
# ── The browser ───────────────────────────────────────────────────────
|
|
# Host network and a loopback URL, because the browser only treats loopback as
|
|
# a secure context over plain http, and the app cannot sign in without one.
|
|
echo "==> Playwright"
|
|
set +e
|
|
docker run --rm --ipc=host \
|
|
--network=host \
|
|
-v "$PWD/e2e":/work \
|
|
-w /work \
|
|
-e BASE_URL="$APP_URL" \
|
|
-e CI=true \
|
|
"$PLAYWRIGHT_IMAGE" \
|
|
sh -c "npm install --no-audit --no-fund --silent && npx playwright test ${GREP:+--grep \"$GREP\"}"
|
|
STATUS=$?
|
|
set -e
|
|
|
|
# The report is worth serving whether the run passed or failed — a pass is
|
|
# where you check that a spec did what you thought it did.
|
|
"${COMPOSE[@]}" up -d e2e-report >/dev/null 2>&1 || true
|
|
|
|
echo
|
|
if [ "$STATUS" -eq 0 ]; then echo "==> ✅ e2e passed"; else echo "==> ❌ e2e failed (exit $STATUS)"; fi
|
|
echo " app $APP_URL (a working copy, throwaway data)"
|
|
echo " report $REPORT_URL (traces and screenshots of any failure)"
|
|
echo " stop scripts/e2e.sh --down"
|
|
exit "$STATUS"
|