dorfteich/apps/web/e2e/setup.spec.ts
Claude Fable 5 28aa04d5e4
All checks were successful
CI / Lint, typecheck, test (push) Successful in 3m6s
CI / Build container images (push) Has been skipped
CD / Build and push images (push) Successful in 3m13s
CD / Deploy to Test (push) Successful in 12s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 10s
CI / Auth e2e pack (push) Successful in 5m6s
CI / Import/export fidelity gate (push) Successful in 45s
Add the first-run setup wizard UI (#81)
The SPA now probes GET /setup on boot: while setup is pending it
renders only the wizard at /setup, the login page (to resume a started
wizard as the Site Admin), and a "setup pending" notice on every other
route — without the regular chrome, whose pond/search queries would
all 503. If the probe itself fails (offline reload), the app falls
through to the normal routes. Once completed, /setup just goes home.

The wizard walks six steps against the #80 api: welcome with language
choice (drives i18n immediately and pre-fills the admin/instance
locales), Site Admin account (signs in via the step-1 session cookie),
instance basics, SMTP with live-test-before-save plus an explicit skip,
registration mode, and a summary whose finish unlocks the app
logged-in-ready. Every step validates through the shared Zod schemas
before advancing; entered values live in the parent component, so Back
preserves them, and re-submitting a step on a second forward pass just
overwrites the same settings. A wizard someone else started shows a
sign-in hand-off instead of dead admin-gated steps. New `setup` i18n
namespace in de and en.

Two #80 touch-ups fell out of verifying this end to end: the SMTP
port's NaN case now maps to the translated required-message, and
GET /setup's smtpConfigured uses `||` instead of `??` so the empty
strings compose passes for unset vars fall through to the secret store.

The e2e pack (setup.spec.ts) needs an instance where setup is still
pending, so the CI job provisions a second api + static web against a
virgin database on their own ports and runs the full wizard journey
there, including the failing-relay path and both languages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
2026-07-11 16:05:01 +02:00

89 lines
4.6 KiB
TypeScript

import { expect, test } from '@playwright/test';
/**
* First-run setup wizard (issue #81). Runs only against the dedicated fresh
* stack (empty database, setup still pending) that CI provisions on its own
* ports — the regular e2e stack is seeded and long past setup. The tests
* advance one shared instance's state, so they run serially.
*/
test.skip(!process.env.E2E_SETUP, 'requires the fresh setup stack (E2E_SETUP=1)');
test.describe.configure({ mode: 'serial' });
const SMTP_HOST = process.env.E2E_SMTP_HOST ?? 'mailpit';
const SMTP_PORT = process.env.E2E_SMTP_PORT ?? '1025';
test('while setup is pending, every route shows the pending screen', async ({ page }) => {
await page.goto('/');
await page.getByRole('link', { name: /start setup|einrichtung starten/i }).click();
await expect(page).toHaveURL(/\/setup$/);
await expect(page.getByRole('heading', { name: /welcome|willkommen/i })).toBeVisible();
});
test('a full wizard run configures the instance and ends signed in', async ({ page }) => {
await page.goto('/setup');
// Language step — both languages must carry the wizard (AC): look at
// German first, come back, then continue in English.
await page.getByRole('button', { name: 'Deutsch' }).click();
await expect(page.getByRole('heading', { name: 'Site-Admin-Konto anlegen' })).toBeVisible();
await page.getByRole('button', { name: 'Zurück' }).click();
await page.getByRole('button', { name: 'English' }).click();
await expect(page.getByRole('heading', { name: 'Create the Site Admin account' })).toBeVisible();
// The admin step validates before advancing.
await page.getByRole('button', { name: 'Create account & continue' }).click();
await expect(page.getByRole('alert').first()).toBeVisible();
await expect(page.getByRole('heading', { name: 'Create the Site Admin account' })).toBeVisible();
await page.getByLabel('Username').fill('wizard-admin');
await page.getByLabel('E-mail address').fill('wizard-admin@dorfteich.test');
await page.getByLabel('Display name').fill('Wizard Admin');
await page.getByLabel(/^Password/).fill('ein sehr langes wizard passwort');
await page.getByRole('button', { name: 'Create account & continue' }).click();
// Instance step; Back must preserve what was typed (AC).
await expect(page.getByRole('heading', { name: 'Name your instance' })).toBeVisible();
await page.getByLabel('Instance name').fill('Wizard Pond');
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByRole('heading', { name: 'E-mail delivery' })).toBeVisible();
await page.getByRole('button', { name: 'Back' }).click();
await expect(page.getByLabel('Instance name')).toHaveValue('Wizard Pond');
await page.getByRole('button', { name: 'Continue' }).click();
// SMTP step: a dead relay blocks the step with the transport error…
await page.getByLabel('SMTP server').fill('127.0.0.1');
await page.getByLabel('Port').fill('2525');
await page.getByLabel('Sender address').fill('dorfteich@dorfteich.test');
await page.getByRole('button', { name: 'Send test mail & continue' }).click();
await expect(page.getByText(/SMTP test failed/i)).toBeVisible();
await expect(page.getByRole('heading', { name: 'E-mail delivery' })).toBeVisible();
// …the real one (Mailpit in CI) lets it advance.
await page.getByLabel('SMTP server').fill(SMTP_HOST);
await page.getByLabel('Port').fill(SMTP_PORT);
await page.getByRole('button', { name: 'Send test mail & continue' }).click();
// Registration step.
await expect(page.getByRole('heading', { name: 'Who may join?' })).toBeVisible();
await page.getByRole('radio', { name: /open registration/i }).check();
await page.getByRole('button', { name: 'Continue' }).click();
// The summary shows the collected values; finishing unlocks the app.
await expect(page.getByRole('heading', { name: 'Summary' })).toBeVisible();
await expect(page.getByText('Wizard Pond')).toBeVisible();
await expect(page.getByText('wizard-admin@dorfteich.test')).toBeVisible();
await page.getByRole('button', { name: 'Finish setup' }).click();
// Logged-in-ready (AC): the regular app loads with the admin's session.
await expect(page).toHaveURL(/\/$/);
await expect(page.getByRole('button', { name: 'Wizard Admin' })).toBeVisible();
});
test('the wizard url just goes home once setup completed', async ({ page }) => {
// A fresh anonymous context: the app is unlocked, /setup redirects home,
// and the regular login navigation is available again.
await page.goto('/setup');
await expect(page).toHaveURL(/\/$/);
await expect(page.getByRole('link', { name: /sign in|anmelden/i })).toBeVisible();
});