dorfteich/apps/web/e2e
Claude Opus 5 6377faf332
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 7m28s
CI / Build container images (pull_request) Successful in 2m7s
CI / Auth e2e pack (pull_request) Successful in 9m37s
CI / Import/export fidelity gate (pull_request) Successful in 1m7s
CD / Build and push images (push) Successful in 23s
CD / Deploy to Test (push) Successful in 12s
CD / Smoke tests against Test (push) Successful in 1m47s
CD / Promote to Int (push) Successful in 16s
CI / Lint, typecheck, test (push) Successful in 7m25s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 9m41s
CI / Import/export fidelity gate (push) Successful in 1m12s
#306: instance branding — logo and favicon, cropped in the browser
An instance had no way to look like itself: the top bar said "Dorfteich"
whatever the operator called their instance, `instance.name` was never
rendered in the running app at all, and there was no favicon anywhere —
`index.html` had no `<link rel="icon">` and `public/` held only fonts and
theme-init.js.

Where the line is drawn, and why:

- **The api never decodes an image.** Cropping, scaling and the conversion
  to PNG happen on a canvas in the browser; the api checks the PNG
  signature, reads the IHDR dimensions at their fixed offsets and enforces
  the caps. An image library would put a decoder in front of
  attacker-supplied bytes AND would have to be carried through the
  `--network none` offline build. Reading two big-endian integers is not
  decoding.
- **SVG is refused**, with its own error message rather than a generic
  "not a PNG": it can carry script, and serving it from our own origin
  would be a cross-site-scripting vector. An operator who tried one should
  learn that it is deliberate.
- **The crop is driven by number inputs, not by dragging.** A drag-only
  cropper excludes keyboard and switch users outright; a number input is
  arrow-key operable and screen-reader readable without any custom aria.
  The resulting pixel size is stated in text, not only drawn as a frame.
- **The variant is chosen by CSS, not JavaScript.** `theme-init.js` has
  already resolved `data-theme` before first paint, so the correct logo is
  the one painted rather than the one that appears after a flash. Without a
  dark variant the LIGHT logo carries both themes — the operator's own
  asset shown unchanged beats one they did not choose (the rule #307
  extends to ponds). The settings screen warns; it never blocks.
- **The favicon link is static, its resource dynamic.** index.html stays a
  static file and the api answers with the uploaded icon or a shipped
  default — that route must never 404, or the browser keeps its generic
  icon for good. The default is generated by a script from Node's own zlib
  (`gen-default-favicon.mjs`), for the same offline-build reason.
- Both favicon sizes are uploaded together: one source, one crop, so the
  tab icon and the home-screen icon can never disagree.
- Branding is served WITHOUT a session, because the login screen carries it
  and the browser fetches the favicon before anyone signs in. The admin
  screen says so — an operator may not expect their logo to be public.
- The metadata is not writable through the settings endpoint: it describes
  bytes on disk, and hand-writing it would claim an asset that is not
  there.

`./data/branding` follows the three-step rule #303 paid for: env default +
`data-dirs.ts` entry, compose volume (repo AND the stages on ONE), and the
`mkdir`/`chown` line in the api Dockerfile. `data-dirs.test.ts` is new and
closes the hole that made #303's variant invisible: the nightly archive
skips a missing directory WORDLESSLY, so the fence now demands that every
`*_DIR` the backup env declares actually travels in the archive. Verified
against the real defect — removing the line fails it by name.

Audit catalogue v1.7 (`branding.changed`), carrying `scope` from the start
so #307 is the same event with a different scope, not a second id.

Verified: api suite 103 files green (a lone `public-api` ECONNRESET under
local parallel load, green in isolation — the documented local flake);
branding suite 12 tests against a real directory; crop arithmetic unit
tests; a11y pack 11/11 in both schemes; /admin measured at 320px with the
new section (overflow 0); and the whole flow walked in the browser: upload
→ crop 780×180 → stored as 512×118 → logo in the sidebar linking home with
the instance name as its accessible name → topbar wordmark following
`instance.name` → light logo still shown under `data-theme="dark"`.
2026-08-01 19:30:52 +02:00
..
a11y.spec.ts #306: instance branding — logo and favicon, cropped in the browser 2026-08-01 19:30:52 +02:00
access-rules.spec.ts Add effective-permissions inspector (#57) 2026-07-10 00:00:27 +02:00
admin-quotas.spec.ts Add Site-Admin quota override management UI (#58) 2026-07-10 00:14:28 +02:00
admin-users.spec.ts Add Site-Admin user management (#59) 2026-07-10 00:31:41 +02:00
attachments.spec.ts #213: warn on uploads to classified pages; instance policy can block 2026-07-31 07:33:34 +02:00
auth.spec.ts Bind the CI dev server to all interfaces and surface login errors 2026-07-05 06:07:49 +02:00
backlinks.spec.ts Add backlinks panel and phantom-pages view (#48) 2026-07-09 13:05:32 +02:00
collab-permissions.spec.ts Wire real permissions into collab tokens and revocation (#53) 2026-07-09 18:48:42 +02:00
collab.spec.ts Public REST API v1: personal access tokens, instance switch + per-pond opt-in (#104) 2026-07-12 11:17:03 +02:00
comments.spec.ts #133 Kommentare fest inline im Lesemodus (Slide-in-Panel ablösen) 2026-07-19 01:22:22 +02:00
content.spec.ts #169: Nicht-Text-Inhalte — Task-Checkboxen, Wissensgraph 2026-07-21 14:35:16 +02:00
create-missing-page.spec.ts Offer creating the page on the not-found screen (#115) 2026-07-14 16:45:27 +02:00
editor.spec.ts Modernize stale editor e2e pack to live-collab semantics 2026-07-09 17:01:41 +02:00
export.spec.ts Move page actions into the TopBar as self-hosted icon buttons (#101) 2026-07-12 04:39:42 +02:00
favorites.spec.ts Favorites: personal page stars, golden icons, sidebar filter (#132) 2026-07-16 12:03:55 +02:00
fonts.spec.ts Add per-pond fonts: catalog, build, application, and admin UI (#66) 2026-07-10 11:16:29 +02:00
graph.spec.ts Pond graph: size the canvas to the viewport, not the width ratio (#131) 2026-07-16 12:02:56 +02:00
helpers.ts #189: make the CSRF origin check fail closed 2026-07-30 09:34:44 +02:00
image.spec.ts Add image paste and insert in the editor (#28) 2026-07-08 11:30:25 +02:00
import-vault.spec.ts #302: the vault import test reaches its page through the sidebar 2026-08-01 11:29:54 +02:00
import.spec.ts Add document import UI in the sidebar (#64) 2026-07-10 09:38:31 +02:00
labels.spec.ts Add label UI: tree management, page assignment, and sidebar filter (#44) 2026-07-09 11:41:42 +02:00
legal.spec.ts #304: scope the legal spec's status locator to its own form 2026-08-01 19:05:18 +02:00
link.spec.ts Add link UX: edit URL and open in new tab (#29) 2026-07-08 11:50:34 +02:00
markdown.spec.ts Move pond settings to a TopBar gear, pin the trash link to the sidebar bottom (M10 follow-up) 2026-07-12 06:07:15 +02:00
members.spec.ts Pond lifecycle in the UI: create shared ponds, delete from settings 2026-07-12 19:15:34 +02:00
mermaid.spec.ts Add the Mermaid reference plugin (#78) 2026-07-11 13:51:15 +02:00
offline.spec.ts Harden the offline e2e for slower CI timing (#38) 2026-07-08 22:24:29 +02:00
page-tools.spec.ts Add pageTool plugins with toc and page-index references (#77) 2026-07-11 13:27:30 +02:00
page-tree.spec.ts QA: page-tree and graph e2e packs in CI, manuals updated (#114) 2026-07-14 11:10:14 +02:00
permission-matrix.spec.ts #302: the permission matrix counts the start page 2026-08-01 08:25:51 +02:00
plugin-admin.spec.ts Add plugin administration UI: instance modes and pond activation (#72) 2026-07-11 09:32:16 +02:00
plugin-blocks.spec.ts #160: Plugin-Block — Bearbeiten-Knopf nach Moduswechsel wieder da 2026-07-20 21:54:50 +02:00
plugin-fixtures.ts Add block plugins: plugin_block node with sandboxed rendering and editing (#76) 2026-07-11 12:45:14 +02:00
plugins.spec.ts Add the sandbox host runtime for plugin iframes (#73) 2026-07-11 09:16:30 +02:00
pond-theme.spec.ts #186: pond accent theming — scoped derivation, cascade pond > user > default 2026-07-29 09:09:36 +02:00
public.spec.ts Move live presence into the TopBar, signed-in only (#102) 2026-07-12 04:42:59 +02:00
README.md #245: mode hidden — hide profile-violating options, mark the hiding 2026-07-31 19:27:17 +02:00
reorder.spec.ts Add manual page ordering with drag-and-drop (#45) 2026-07-09 12:18:44 +02:00
search.spec.ts search.spec: Fehl-Klick entfernt — Reload IST der Weg zurück in den Lesemodus 2026-07-19 21:31:53 +02:00
section-styles.spec.ts e2e-Fixes nach CI: section-styles-Selektor eindeutig, settings-nav-Timing 2026-07-20 01:54:29 +02:00
settings-nav.spec.ts #180: settings jump-nav fence — nine sections since the Appearance section 2026-07-28 21:58:43 +02:00
setup.spec.ts Add the first-run setup wizard UI (#81) 2026-07-11 16:05:01 +02:00
sidebar.spec.ts #169: Nicht-Text-Inhalte — Task-Checkboxen, Wissensgraph 2026-07-21 14:35:16 +02:00
smoke.spec.ts Add collaboration server skeleton (Hocuspocus) with health, container, and CI/CD (#33) 2026-07-08 14:53:44 +02:00
social.spec.ts Format the social pack (#96) 2026-07-12 00:02:12 +02:00
system.spec.ts #224: read-trail storage — partitioning, retention, admin query path 2026-07-31 12:21:45 +02:00
tasks.spec.ts #154: Aufgabenübersicht als Kern-Block (Seite + Unterseiten) 2026-07-20 01:20:30 +02:00
theme-accent.spec.ts #184: user accent theming — presets and free color as one mechanism 2026-07-29 08:59:27 +02:00
theme.spec.ts #184: user accent theming — presets and free color as one mechanism 2026-07-29 08:59:27 +02:00
trash.spec.ts Trash: checkbox multi-select with bulk restore and purge (#128) 2026-07-16 12:03:35 +02:00
vs-nfd-marking.spec.ts #245: mode hidden — hide profile-violating options, mark the hiding 2026-07-31 19:27:17 +02:00
wikilink.spec.ts Add wikilink node with autocomplete (#46) 2026-07-09 12:42:01 +02:00

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

# 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.

vs-nfd-marking.spec.ts (issues #244/#245) has three parts selected by E2E_VS_NFD_MODE (marked, hidden, unset = off); each needs an api started with the matching VS_NFD_MODE. CI runs the marked half against the main e2e stack and the hidden half against a second api (port 3006) on the same database; the off assertions run in local default stacks.

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:

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: pypdfpage.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:

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.