End-to-end tests that drive the real SvelteKit SPA against a real
mailbrus-server backed by a real notmuch index — one freshly cloned,
freshly indexed mailbox and its own server per test, with guaranteed
teardown. No production code is exercised through mocks.
e2e/
playwright.config.ts # projects, workers; webServer disabled (harness owns servers)
tsconfig.json
fixtures/
maildir/ # PRISTINE, committed, READ-ONLY corpus (raw .eml, no index)
manifest.ts # typed SINGLE SOURCE OF TRUTH: accounts, folders, messages, states
generate.ts # materializes maildir/ from manifest.ts
harness/
paths.ts # absolute repo/corpus/build/binary locations
clone.ts # copy corpus -> /tmp/mailbrus-e2e-*; recursive cleanup
notmuch.ts # scoped notmuch config + `notmuch new` + hermeticity guard
server.ts # free port -> spawn server -> health-poll -> stop()
fixtures.ts # test.extend({ app }) wiring clone+index+server+teardown
global-setup.ts # ensure notmuch / build/ / server binary once per run
pages/ # page objects: AccountsPage, MailboxPage, MessagePage
specs/ # the tests; assert against the manifest only
The maildir tree and manifest.ts are the contract: specs assert against
the manifest, never against hard-coded literals. Per-test isolation is achieved
purely via NOTMUCH_CONFIG (the server resolves its DB through it), so no
production code changes are required.
Use the Nix devShell so notmuch + the Playwright browsers are on hand:
nix develop
deno install # hydrate node_modules (incl. @playwright/test, pinned to nixpkgs' driver)
deno task test:e2e # == npx playwright test --config=e2e/playwright.config.tsglobal-setup.ts builds build/ (SPA) and target/release/mailbrus-server
on demand if they are missing, and fails fast with a clear message if notmuch
is unavailable. Browsers come from PLAYWRIGHT_BROWSERS_PATH
(set by the devShell to nixpkgs' playwright-driver.browsers).
Outside Nix you must provide a matching browser yourself
(npx playwright install chromium) and keep @playwright/test in
package.json aligned with whatever driver supplies the browser.
- Add a
ManifestMessageto the right account/folder infixtures/manifest.ts(uniqueslugand uniquemessageId— notmuch dedups on Message-ID). State is encoded inbox(new= unread) andflags(F/R/S/…). - Regenerate the corpus:
deno task e2e:generate. specs/consistency.spec.tsverifies disk and manifest stay in lockstep.
Never hand-edit the generated .eml files; edit the manifest and regenerate.
Import test/expect from ../harness/fixtures.ts (gives you app.baseURL
and a per-test server via page), drive the UI through the page objects in
pages/, and derive every expected value from fixtures/manifest.ts. Specs
must contain no inline setup and no hard-coded DOM selectors.
specs/screenshots.spec.ts is a dedicated Playwright project that captures
five canonical documentation PNGs into docs/screenshots/. It is not part
of the default test:e2e run and is never invoked in CI.
deno task screenshots # generates docs/screenshots/*.pngThe five captures are: message-list.png, reader.png, accounts.png,
compose.png, and about-over-list.png. Re-run to regenerate after UI
changes; commit the updated PNGs alongside any visual change.
The spec reuses the same harness fixtures as functional tests (one cloned
mailbox + live server per test) and pins viewport, colour scheme, locale, and
device scale via the screenshots Playwright project in playwright.config.ts.
These document real product limitations; the corpus + backend already support them, so each test enables the moment the SPA does:
- Attachments are never shown in the reader. The list API omits attachments
and
+page.sveltediscardsfetchMessage().attachments. The backend serves them correctly (asserted inattachments.spec.ts). - No distinct broken-signature state. The SPA only distinguishes signed vs
unsigned (presence of a
--line); a tampered signature renders as unsigned.