dorfteich/apps/web/e2e/README.md
Claude Fable 5 809e071f14
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 5m56s
CI / Build container images (pull_request) Successful in 1m27s
CI / Auth e2e pack (pull_request) Successful in 9m18s
CI / Import/export fidelity gate (pull_request) Successful in 1m6s
CI / Import/export fidelity gate (push) Blocked by required conditions
CD / Build and push images (push) Successful in 20s
CD / Deploy to Test (push) Successful in 11s
CD / Smoke tests against Test (push) Successful in 1m25s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m5s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Has been cancelled
#207: print stylesheet with the classification on every printed sheet
First @media print support at all: page size/margins, navigation and
interactive chrome suppressed, break behaviour for headings, tables,
code blocks, figures and plugin blocks. The VS-NfD marking runs as
header AND footer on every sheet via a real-table PrintFrame whose
thead/tfoot browsers repeat per page — @page margin boxes are
unimplemented and position:fixed places unreliably in both engines
(verified empirically); on screen the table chain renders as plain
blocks, so nothing changes visually. Verified as PDF-from-browser in
Chromium 140 and Firefox 153 (2 markings on every page of a multi-page
document); the repeatable procedure is documented in
apps/web/e2e/README.md. Unclassified pages print without a marking.

Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
2026-07-31 06:48:44 +02:00

198 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 14, 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
`<table class="print-frame">` whose `thead`/`tfoot` carry the banners —
the one construct Chromium and Gecko both repeat per page (`@page` margin
boxes are unimplemented; `position: fixed` places unreliably). The check,
repeatable against a running local stack (seeded, signed in as
`fixture-user`):
1. Classify a multi-page document:
`UPDATE pages SET classification='VS_NFD' WHERE slug='every-element';`
2. **Chromium**: print `/p/content-fixtures/every-element` to PDF
(browser print dialog, or Playwright `page.pdf({ preferCSSPageSize:
true })` after logging in).
3. **Gecko (Firefox)**: open the same page (or grant it a public reader
and use `/public/content-fixtures/every-element`), Cmd+P → save as
PDF. Headless equivalent: geckodriver + WebDriver `POST
/session/<id>/print`.
4. Every page of both PDFs must show `VS NUR FÜR DEN DIENSTGEBRAUCH`
once at the top and once at the bottom, with no navigation chrome and
no overlap with content (quick text check:
`pypdf``page.extract_text().count(...) == 2` per page).
Last verified 2026-07-31: Chromium 140 (Playwright) and Firefox 153,
2 markings on every page of a 2-page PDF each.
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 `<feature>.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.