# 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. ## Plugin sandbox (`plugins.spec.ts`, issue #73) The sandbox security pack drives the Site-Admin plugin preview page (`/admin/plugins/:id/preview`) — the exact sandbox runtime pages embed — with three fixture plugins built in `plugin-fixtures.ts` and installed through the admin API: - **well-behaved**: answers `render` and resizes its own frame via the declared `ui` capability; - **malicious** (the permanent security regression asset, ADR 0008): probes the parent DOM, cookies, `localStorage`, same-origin and external `fetch`, and an undeclared capability — every probe must report `blocked`. A `LEAKED` verdict is a sandbox escape and fails the build; - **hung**: never answers, so the host's 5 s deadline must collapse it to the failure placeholder while the surrounding page stays responsive. Runs in the `auth-e2e` CI job (needs the api's writable `PLUGINS_DIR`, satisfied by the default `./data/plugins`). ## 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 ``` ## Export (`export.spec.ts`, issue #65) The pond-settings "Download pond as ZIP" link (a Markdown ZIP of the readable pages) and the page-menu office export. The ZIP download needs no sidecar; the `.docx` export runs a conversion job and, like the import `.docx` case, **self-skips unless `E2E_PANDOC` is set**. The permission-filtered ZIP contents, the docx pandoc output, and the 500-page streaming path are covered at the api level in `export.service.db.test.ts` (+ `export-markdown.test.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). - **Classified Note** (`classified-note`) — a VS-NfD-marked page (issue #206, ADR 0022) so the a11y pack can assert the classification banner (top and bottom, both themes). ## Print marking check (`print.css`, issue #207) The VS-NfD marking must appear at the top AND bottom of **every printed sheet** (ADR 0022). Mechanism: the page content sits in a real `