#!/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:-}" # ── 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 \ -e OPENAPI_UNDESCRIBED_BUDGET="${OPENAPI_UNDESCRIBED_BUDGET:-}" \ "$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"