# End-to-end tests Playwright suites, most local-only — three run in CI/CD: | Suite | Target | Where it runs | | ----------------- | --------------------------------- | --------------------------------------------------------------------------- | | `smoke.spec.ts` | any deployed stage | CD pipeline against `https://test.dorfteich.cloud` after every deploy | | `auth.spec.ts` | full local stack **with Mailpit** | CI job `auth-e2e` on every PR/push; locally against the dev stack | | `content.spec.ts` | full local stack | same CI job `auth-e2e` (a second step), right after the auth pack | | everything else | full local stack | locally only — `editor`/`sidebar`/`image`/`link`/`markdown`/`trash`.spec.ts | `content.spec.ts` is the M2 content regression pack (issue #32): page lifecycle, editor basics, image paste, Markdown round-trip, and trash — enough to catch a regression across the whole content model without re-running every edge case the feature-specific packs above already cover. Its Markdown round-trip test is a real regression pin, not just a smoke check: it compares the seeded "Every Element" fixture page's exported Markdown byte-for-byte against the checked-in `content-page.md` (see "Content fixtures" below) — any schema/serializer change that alters how a node round-trips fails it, once the seed has re-run against the changed code (build → migrate → seed → test, exactly CI's order). ## Running locally ```sh # 1. Stack: database + Mailpit, api (3001), web dev server (5173) docker compose -f deploy/compose/docker-compose.yml -f deploy/compose/compose.dev.yml up -d db mailpit DATABASE_URL=postgresql://dorfteich:dorfteich@localhost:5434/dorfteich pnpm --filter @dorfteich/api db:seed DATABASE_URL=postgresql://dorfteich:dorfteich@localhost:5434/dorfteich PORT=3001 pnpm --filter @dorfteich/api start:dev & pnpm --filter @dorfteich/web dev & # 2. Tests E2E_BASE_URL=http://localhost:5173 E2E_MAILPIT_URL=http://localhost:8025 pnpm --filter @dorfteich/web e2e ``` `auth.spec.ts` skips itself when `E2E_MAILPIT_URL` is unset, so the CD smoke run never trips over it. ## Fixture matrix Seeded by `pnpm --filter @dorfteich/api db:seed` (idempotent — re-running never duplicates). Shared password: `fixture passwort 123`. Fixtures exist only on dev machines and disposable CI/Test databases. | Username | State | Purpose | | ------------------ | ------------------- | ---------------------------------------------------------------------------------------- | | `fixture-admin` | active, Site Admin | admin UI/permissions cases | | `fixture-user` | active | regular journeys, settings, sessions | | `fixture-editor` | active | second regular account for the collab-permissions pack (reader/editor of another's pond) | | `fixture-viewer` | active | signed-in non-member for `authenticated`/`public` access-rule cases (issue #55) | | `fixture-outsider` | active | the "foreign user" of the permission matrix — member of nothing (issue #60) | | `fixture-pending` | e-mail not verified | unverified-login cases | ## Permission matrix (`permission-matrix.spec.ts`, issue #60) The cross-feature permission hardening pack pins the security-relevant **subject × surface** combinations against regressions. It is API-level (the UI adds nothing over the resolved status code) and enforces the 404-vs-403 policy: an unauthorized _read_ is 404 (existence hidden), an unauthorized _write_ on something readable is 403. - **Subjects:** site admin (`fixture-admin`), pond admin / owner (`fixture-user`), editor (`fixture-editor`), the same editor _label-restricted_ by a `secret`-label deny, reader (`fixture-viewer`), public (anonymous), and the foreign user (`fixture-outsider`, a member of nothing). - **Surfaces:** page read, edit (collab-token `rw`/`ro`), sidebar list, search, versions (history = write), media, and the public HTML endpoint. - **Extending it:** a new permission-touching feature adds a surface here (one `expect` row per subject) rather than a bespoke test, so the matrix stays the one place the policy is pinned. A weakened guard is caught here — verified by temporarily loosening a route decorator and watching the pack go red. ## Attachments (`attachments.spec.ts`, issue #61) Non-image attachments: a page's attachments section uploads an allowlisted file, lists it, and inserts it into the document as a download link (verified to serve with `Content-Disposition: attachment` + `nosniff`, never inline); a disallowed extension is rejected with the localized allowlist error; the Pond Admin file manager reports storage usage and flags an orphan (a pond-level upload with no embedding page). The SVG sanitize/reject policy is covered at the api level in `files.e2e.db.test.ts`. ## Import (`import.spec.ts`, issue #64) The sidebar "import document" action: pick a file, upload, watch progress, open the new page. `.md` imports directly (the response is already `succeeded`); `.docx`/`.odt` poll a conversion job. Cases: a `.docx` corpus fixture (#63) opens the converted page, a `.md` opens directly, an unsupported `.txt` shows the localized error and creates no page, and two concurrent `.md` imports both complete. The **`.docx` case self-skips unless `E2E_PANDOC` is set** — CI's e2e stack has no reachable pandoc sidecar (jobs are container-networked; same reason the api's real-pandoc fixtures test skips in CI, #63), so it runs locally / on a stage. Run it locally with a sidecar reachable at the api's `PANDOC_URL`: ```sh docker run -d -p 3030:3030 pandoc/core:3.6 server # api PANDOC_URL → this E2E_PANDOC=1 E2E_BASE_URL=http://localhost:5990 \ pnpm --filter @dorfteich/web exec playwright test e2e/import.spec.ts ``` ## Content fixtures `db:seed` also creates a **shared** pond `content-fixtures` (owned by `fixture-user`) with two pages, for the content regression pack and manual QA: - **Every Element** (`every-element`) — every editor schema node and mark (issue #24: headings 1–4, all list types, table, blockquote, code block, horizontal rule, hard break, and all five marks). Loaded from the checked-in `apps/api/prisma/fixtures/content-page.yjs`, a Yjs snapshot generated from the human-readable `content-page.md` next to it — `content-page.md` is the thing to read or edit; the `.yjs` file is a build artifact of it, not source. - **Fixture Image** (`fixture-image`) — one real, servable uploaded image (the placeholder `fileId` inside the Markdown fixture above is not a real attachment; this page's image is). Regenerating after editing `content-page.md`: ```sh pnpm --filter @dorfteich/api fixtures:regenerate ``` This is deterministic — re-running without editing the Markdown produces a byte-identical `.yjs` file (the script pins the Yjs document's `clientID`, which is otherwise randomized per `Y.Doc` instance) — and it refuses to write a snapshot that isn't a fixed point of the Markdown round-trip (`docToMarkdown(markdownToDoc(x)) === x`), so a stale fixture can't get checked in silently. ## Conventions - New feature packs get their own `.spec.ts` next to these and extend the fixture matrix here (permission matrix arrives with M5, issue #60). - Use `contextForUser()` from `helpers.ts` for signed-in tests — it logs in through the api and hands you a browser context with the session cookie, no UI login repetition. - Flaky tests are defects (ADR 0014): fix or quarantine immediately.