statusBadgeText(email) after deactivating a user hung for the full test
timeout: ManageUsersContent.handleEditUser() refetches the list with the
current statusFilter ('active' by default) after every save, so the
just-deactivated row silently drops out of view — the locator then waits
forever for a row that filter will never show again (no actionTimeout is
configured, so only the outer test timeout eventually kills it).
Add ManageUsersPanel.showAllUsers() and switch to it before reading the
badge. Also gave the test more headroom (60s -> 120s) since it drives two
full UI logins plus create/edit/verify against a shared-CI DEV LXC.
Verified: full local suite green (37 passed, 6 skipped) against DEV.
PunimTag e2e (Playwright + @levkin/playkit)
Smoke tests against the public DEV URL so we catch LAN-redirect bugs
(e.g. NEXTAUTH_URL=http://10.0.10.121:3001 → Kolby #57).
Quick start
cd e2e
cp .env.example .env # edit credentials
npm ci
npx playwright install chromium
npm test
Private Gitea installs need auth once: set a local git insteadOf with a token
before npm ci, then unset it. CI does this in .gitea/workflows/ci.yml.
Never commit lockfile URLs that embed tokens.
Environment
| Variable | Required | Purpose |
|---|---|---|
PLAYKIT_BASE_URL |
yes (default in env-defaults.json) |
Public viewer host |
PLAYKIT_API_BASE_URL |
no | LAN API (see env-defaults.json) |
E2E_ADMIN_EMAIL |
for auth specs | Prefer e2e@levkine.ca — not admin@admin.com |
E2E_ADMIN_PASSWORD |
for auth specs | Dedicated e2e password (Vaultwarden / Infisical) |
E2E_API_USERNAME / E2E_API_PASSWORD |
optional | FastAPI bearer login, admin role (separate DB from NextAuth; mirrors e2e@levkine.ca) |
E2E_API_VIEWER_USERNAME / E2E_API_VIEWER_PASSWORD |
optional | FastAPI bearer login, viewer role — role-permission gate specs only |
E2E_VIEWER_EMAIL / E2E_VIEWER_PASSWORD |
optional | NextAuth auth-DB viewer, hasWriteAccess=false (third, independent store) — viewer.write-gates.spec.ts only |
PLAYKIT_MAIL_PROVIDER |
for mail specs | mailpit (homelab) |
MAILPIT_BASE_URL |
for mail specs | http://10.0.10.45:8025 |
MAILPIT_USER / MAILPIT_PASSWORD |
for mail specs | Mailpit basic auth |
Secrets: Infisical LevkinOps → dev → /playkit/punimtag + Gitea Actions.
See ansible docs/hardening/SECRETS.md.
Playkit pin: @levkin/playkit@v0.3.1 (git tag). Zod schema option, storageState
helpers, playkitFailureArtifacts, Mailpit, and network interception
(interceptNetworkCall, startNetworkErrorMonitor — see playkit docs/NETWORK.md)
are all in this pin — see network-errors.spec.ts for the silent-4xx/5xx monitors on
gallery/search/upload/manage-users. Timing → Pushgateway is supported by the kit but
not enabled in this CI job yet.
What we assert
- Login page loads on the public hostname
- Sign-out stays on that hostname (never
10.x) - FastAPI
/health→{ "status": "ok" } - Forgot-password / reset-password email in Mailpit; links use public host
- Register → unverified block → verify email via Mailpit → login
- Idle logout (
?e2e_idle_ms=) stays on public host - Upload: submit a tiny fixture via
#file-upload(uses storageState) - Manage Users open/close then sign-out (Kolby #57 overlay path)
- API
/api/v1/auth/meand/api/v1/photosreturn 401 without token - API catalog: people/tags lists, login 422/401, users/roles 401, missing photo 404|401
- Viewer
/api/auth/sessionreturns email when storageState is loaded - FastAPI role-permission write gates: viewer 403 vs admin 200 on users, role-permissions, bulk-delete
- Public gallery search (
/api/search): tag filter, person filter, and combined-filter narrowing return correct subsets; UI tag-filter interaction updates URL + result count - Logged-in-only filters: Favorites checkbox toggles
favoritesOnlyURL param; People filter UI selection updates URL + result count (uses storageState) - Silent-failure network monitor (
startNetworkErrorMonitor): gallery home, search+filter, upload, and Manage Users overlay all stay free of background 4xx/5xx while the UI loads/interacts interceptNetworkCallspies on the real upload POST (status +{ message, photos }shape) and the filtered/api/search?tags=GET (status + Zod-validated shape) instead of brittlewaitForResponse/text scraping- Zod-validated API responses: FastAPI login
TokenResponse+/auth/meUserResponse(api.fastapi-login.spec.ts), gallery searchSearchResponse(gallery.search-filters.spec.ts) - NextAuth (browser-session) write gates: viewer (
hasWriteAccess=false) gets 403, admin gets past the gate, onPOST /api/faces/{id}/identify(viewer.write-gates.spec.ts) — distinct from the FastAPI role-permission gates in #12 - PROD smoke (
prod.smoke.spec.ts, opt-in viaPROD_BASE_URL):/api/health+ login page load on the public PROD host — dormant until the PROD LXC exists (see ROADMAP) - Manage Users real CRUD + causal cross-session effect (
admin.manage-users-actions.spec.ts): admin creates/edits/deletes a user through the actual panel (not just open/close), and deactivating a user via the UI immediately revokes that user's already open NextAuth session (jwt callback re-checksisActive, Kolby #57 fix) — every run creates a disposablee2e-manage-test-*account and deletes it in afinally, never touching the shared punimtagdev/MirrorMatch DB's real accounts
See repo root ROADMAP.md for gaps and next steps.
Defaults for base URLs live in env-defaults.json (imported by env-defaults.ts,
fixtures.ts, playwright.config.ts, and the Gitea e2e job).