dorfteich/apps/web/e2e/README.md
Claude Opus 4.8 699c003d04
All checks were successful
CD / Build and push images (push) Successful in 3m57s
CI / Lint, typecheck, test (push) Successful in 3m7s
CI / Auth e2e pack (push) Successful in 3m58s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 11s
Add pond ZIP + per-page docx/odt export (#65)
Two export paths, both permission-aware (permissions.md):

- `GET /ponds/:id/export/markdown` streams a ZIP of the pond's readable
  pages as Markdown (one `<slug>.md` per page, a `media/` directory,
  wikilinks rewritten to relative `[text](slug.md)` links, image sources to
  `media/<id>.<ext>`). The `reader` guard is "may see the pond"; the service
  filters to the pages the requester may actually read, so a label-restricted
  reader gets only their slice. Media is appended as read streams and pages as
  small strings, so memory stays bounded for a large pond (500-page test).
- `POST /pages/:id/export {format: docx|odt}` enqueues a `markdown → pandoc →
  file` conversion job (the #62 queue): embedded images are inlined as data
  URIs so the sidecar embeds them, wikilinks flatten to text. The client polls
  `GET /jobs/:id` and downloads `GET /jobs/:id/result`.

Frontend: office-export buttons in the page menu (`.docx`/`.odt` run the job
and download the result; PDF is a disabled placeholder for Gotenberg, #67) and
a "Download pond as ZIP" link in pond settings. New `export` i18n namespace
(de+en). Markdown copy/download stay as-is (#30).

Robustness: the pond ZIP skips an attachment whose bytes are missing on disk
(data drift) rather than letting an unhandled read-stream error crash the api;
`FileStorageService.exists` gates inclusion, with a defensive stream error
handler. The per-page export drops an unreadable image the same way.

- shared: EXPORT_FORMATS + pageExportInputSchema; export-markdown transform
  helpers (image/wikilink rewrites, MIME→extension).
- deps: archiver (streaming ZIP; v7 for CommonJS compat), fflate (dev, reads
  ZIPs in tests).
- tests: export-markdown unit + export.service.db (ZIP contents & relative
  links, label-restricted omission, docx job with inlined images, 500-page
  streaming, missing-media skip); e2e export pack (ZIP download; `.docx`
  self-skips without a pandoc sidecar, as in the import pack, #64).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
2026-07-10 10:31:19 +02:00

149 lines
8.3 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.
## 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).
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.