Adds the last deferred #299/#363 follow-up: the flows that CANNOT be expressed
as curl calls. Scope rule (the durable part) — assert only what the curl
harness structurally cannot reach:
1. client-side form validation (the Setup confirm-password gate is pure React
state and makes no request, so there is no HTTP contract to assert)
2. AuthGate's RENDERED states (Setup vs Login vs app)
3. the session cookie authenticating the SPA's OWN /api XHRs — curl proves the
cookie works for curl, not that the app sends it
4. sign-out through the UserMenu back to the login gate
New: web/e2e/boot-gate.spec.ts, web/playwright.config.ts, scripts/e2e-ui.sh
(owns the whole lifecycle: fresh config dir -> boot -> specs -> always kill).
Runs as a second step of the EXISTING advisory `functional-e2e` job rather than
a new job: the dominant cost there is `npm ci` + the Release build, both already
done, so this adds ~5s instead of duplicating a heavy job. It boots its own
fresh instance on port 8410 because the first spec asserts the one-shot Setup
gate that the curl step has already claimed on its config dir.
Determinism (the issue asked for it explicitly): `serial`, `workers: 1`,
`retries: 0` even in CI — a retry would let a flaky flow merge looking green.
Measured 5 consecutive clean runs, ~2s each.
Pins all five `container:` jobs to the toolchain image built by the preceding
commit, which bakes `chromium-headless-shell`.
Non-obvious coupling fixed: vitest's default include glob would have collected
web/e2e/*.spec.ts and run it under jsdom. Excluded `e2e/**` by spreading
`configDefaults.exclude` rather than narrowing `include` to `src/**`, because
web/scripts/ holds a real vitest test an src-only include would silently stop
running.
`RebuildSearchIndexHandler` logs one of two mutually-exclusive lines just before
`SystemStartup.SearchIndexIsReady()`:
fresh config -> "Done migrating search index in {Duration}"
reused config -> "Search index is already version {Version}"
The probe watched only the first, so a reused dir waited out the full 120s
timeout and then killed a perfectly healthy server. Widened to a `grep -Eq`
alternation; the handler's if/else is exhaustive, so the pair covers every path
to readiness.
Verified with a negative control: on a reused dir the server is ready in 2s via
the "already version" line, and the OLD probe string is genuinely ABSENT from
that run's log — so the old code would have hung, i.e. the fix is load-bearing
rather than incidentally passing.
The "prefer a fresh config dir" guidance stays: that guards state bleed, which
is a separate concern from the probe hanging.
- `wait "$PID"` in the cleanup trap was a NO-OP: the server is a grandchild
(launched in e2e-local.sh's subshell, which then exits), so `wait` fails
instantly and was swallowed by `|| true` — cleanup did not actually ensure the
port was released, exactly what its comment claimed. Replaced with a bounded
`kill -0` poll, then SIGKILL.
- Added a port pre-flight check: previously an occupied port surfaced as a 120s
readiness timeout that reads like a broken build. Now fails in 0s naming the
PIDs, and warns against blanket-killing `dotnet ErsatzTV.dll` (that reaps
other sessions' servers).
- UI-E2E: 5x clean (3 specs, ~2s); back-to-back runs pass with no manual cleanup
- curl harness unaffected by the boot-script change: 45/45 PASS
- web: 983 tests / 105 files green; typecheck + lint clean
- vitest collection verified: excludes web/e2e, still collects web/scripts
- Dockerfile sequence + browser launch validated verbatim in a container on the
real amd64 base before committing; chromium launches as root with NO sandbox
opt-out needed
- decisions validator green; catalog regenerated
- docs/decisions.md TOC repaired: it had drifted to 69 of 97 records and held a
dangling anchor to the #72 record that #415 superseded into archive/.
Regenerated with a generator validated against the 68 existing anchors (0
mismatches) -> 97/97, no dangling, no duplicates.
Docs: docs/e2e-local.md (new "UI-E2E harness" section), docs/ci-cd.md (toolchain
image + UI-E2E step), docs/testing.md, docs/README.md, docs/decisions.md
(new `ci.ui-e2e-harness` record; `ci.functional-e2e-harness` amended — its Rule
said "curl-only", now accurate).
Refs #445 #533
42 lines
2.3 KiB
TypeScript
42 lines
2.3 KiB
TypeScript
import process from 'node:process';
|
|
import { defineConfig, devices } from '@playwright/test';
|
|
|
|
// UI-E2E config (ersatztv#445). These specs drive a REAL headless Chromium against a REAL running
|
|
// instance — the handful of contracts that `scripts/e2e-functional.sh`'s curl harness cannot express
|
|
// (see docs/e2e-local.md → "UI-E2E harness").
|
|
//
|
|
// Deliberate choices:
|
|
// - NO `webServer`. The instance is booted by `scripts/e2e-ui.sh` (via `scripts/e2e-local.sh`), which
|
|
// owns the wwwroot copy + readiness probe + a FRESH config dir. Letting Playwright boot the app
|
|
// would duplicate that logic and lose the fresh-config guarantee the setup-claim flow depends on.
|
|
// - `retries: 0` everywhere, including CI. #445 asks for *deterministic* flows; a retry would let a
|
|
// genuinely flaky flow merge looking green. A failure here should be read as a real defect.
|
|
// - Chromium only. This is a boot-gate/session smoke, not a cross-browser matrix; the browser is
|
|
// baked into the CI toolchain image (docker/ci/Dockerfile) and one browser keeps that image lean.
|
|
// - `workers: 1`. Every spec shares ONE server instance whose auth state is global and partly
|
|
// one-shot (the setup-claim). Parallel workers would race on that shared state.
|
|
export default defineConfig({
|
|
testDir: './e2e',
|
|
// The SPA is served under /app/ on the same port as the API (no dev server / proxy in this flow).
|
|
// ETV_BASE_URL is set by scripts/e2e-ui.sh from the port e2e-local.sh actually bound.
|
|
use: {
|
|
baseURL: process.env.ETV_BASE_URL ?? 'http://localhost:8409',
|
|
trace: 'retain-on-failure',
|
|
screenshot: 'only-on-failure'
|
|
},
|
|
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
|
|
fullyParallel: false,
|
|
workers: 1,
|
|
retries: 0,
|
|
// Fail fast in CI rather than burning the job on a hung flow; each of these flows is seconds.
|
|
timeout: 30_000,
|
|
expect: { timeout: 10_000 },
|
|
forbidOnly: !!process.env.CI,
|
|
// `list` only, in CI too. No workflow in this repo uploads artifacts, so an `html` report would be
|
|
// written and then discarded with the runner; the list reporter's inline failure output (assertion +
|
|
// call log + the accessibility snapshot Playwright prints) is what actually lands in the CI log.
|
|
// `trace`/`screenshot` above still write into outputDir, which is what a local re-run wants.
|
|
reporter: [['list']],
|
|
outputDir: './e2e/.output'
|
|
});
|