Workstream 8: Detailseiten & Kontakt (Phase 5)

Drei serverseitige Lese-Detailseiten (Fahrzeug, Gerät, Wehr), default-deny:
- src/lib/detail/merkmale.ts: formatMerkmal (de-AT Tausenderpunkt + NBSP,
  Ja/Nein, enum-Label, „–"), toEckdaten. ICU-unabhängige Zahl-Formatierung
  (formatToParts -> Punkt/Komma), da de-AT je nach ICU-Build U+202F gruppiert.
- src/lib/detail/queries.ts: read-only, wehrübergreifend; loadMerkmalRows
  (Join merkmal_values↔merkmale↔merkmal_optionen via wert=value_text),
  getFahrzeugDetail (+Beladung, +WehrCard), getGeraetDetail (Fahrzeug-Link
  oder „im Gerätehaus"), getWehrDetail (Fuhrpark + Geräte im Haus),
  getBrigadeCard. UUID-IDs.
- Komponenten: detail/{DetailHeader,EckdatenGrid,BeladungListe,StatusBadge},
  kontakt/{KontaktButton (tel:/mailto:, Telefon ohne Leerzeichen, subject;
  Empty-State),WehrCard}.
- Seiten (app)/{fahrzeuge,geraete,wehren}/[id]/page.tsx mit requireSession()
  als erster Zeile (Default-deny in der Tiefe) + fahrzeuge/[id]/not-found.tsx.
- i18n-Keys (detail/kontakt/wehr) ergänzt; keine hartkodierten Strings.

Tests: merkmale.test.ts (11), queries.test.ts (3, gemockte DB für
„im Gerätehaus" + not-found). Playwright detail-auth.spec.ts geschrieben
(deferred: kein Server/DB in Sandbox); Detailrouten ins Gating-Manifest
aufgenommen.

Offline verifiziert: vitest src/lib/detail grün; tsc --noEmit ok; eslint
ok; next build erfolgreich (alle drei [id]-Routen vorhanden).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthias Hochmeister
2026-06-09 11:35:34 +02:00
parent 632ba2b081
commit 44050c7278
17 changed files with 1088 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
import { test, expect } from "@playwright/test";
/**
* Detailseiten-Auth & -Inhalt (Workstream 8, Phase 5).
*
* NICHT in der Sandbox ausführbar (kein Server/DB) — deferred. Wird über
* `npm run test:e2e` gegen einen laufenden Server mit Seed-Daten ausgeführt.
*
* Garantien:
* - Default-deny (Querschnittsstandard 1): anonyme Aufrufe der Detailseiten
* leiten auf /login um (das `(app)`-Layout-Gate + `requireSession()` je Seite).
* - Eingeloggt: Eckdaten, Beladung-Links (`/geraete/<id>`), Wehr-Kontakt
* (`tel:`/`mailto:`) sind sichtbar.
* - Ungültige IDs -> deutsche 404-Seite (not-found).
*
* Negativ-Probe (manuell/CI): Entfernen von `requireSession()` aus einer der
* Detailseiten ODER aus `(app)/layout.tsx` muss die Default-deny-Tests rot
* machen.
*
* Platzhalter-IDs: zur Laufzeit gegen echte Seed-UUIDs ersetzen
* (Env `E2E_FAHRZEUG_ID` etc.) oder per Suchseite ermitteln.
*/
const FAHRZEUG_ID = process.env.E2E_FAHRZEUG_ID ?? "00000000-0000-0000-0000-000000000001";
const GERAET_ID = process.env.E2E_GERAET_ID ?? "00000000-0000-0000-0000-000000000002";
const WEHR_ID = process.env.E2E_WEHR_ID ?? "00000000-0000-0000-0000-000000000003";
const UNGUELTIGE_ID = "ffffffff-ffff-ffff-ffff-ffffffffffff";
const DETAIL_PAGES = [
`/fahrzeuge/${FAHRZEUG_ID}`,
`/geraete/${GERAET_ID}`,
`/wehren/${WEHR_ID}`,
];
test.describe("Default-deny: Detailseiten (anonym)", () => {
for (const path of DETAIL_PAGES) {
test(`anonymer Aufruf von ${path} leitet auf /login um`, async ({ page }) => {
await page.goto(path);
await expect(page).toHaveURL(/\/login/);
});
}
});
test.describe("Eingeloggt: Detail-Inhalte", () => {
test.use({ storageState: "tests/e2e/.auth/wehr-read.json" });
test("Fahrzeug-Detail zeigt Eckdaten, Beladung-Links und Wehr-Kontakt", async ({
page,
}) => {
await page.goto(`/fahrzeuge/${FAHRZEUG_ID}`);
await expect(page.getByRole("heading", { name: "Eckdaten" })).toBeVisible();
// Beladung verlinkt auf Gerät-Detailseiten.
const beladungLink = page.locator('a[href^="/geraete/"]').first();
await expect(beladungLink).toBeVisible();
// Out-of-band Kontakt: tel:/mailto:-Link vorhanden.
const kontaktLink = page.locator('a[href^="tel:"], a[href^="mailto:"]').first();
await expect(kontaktLink).toBeVisible();
});
test("Gerät-Detail verlinkt Fahrzeug oder zeigt „im Gerätehaus“", async ({
page,
}) => {
await page.goto(`/geraete/${GERAET_ID}`);
const hatFahrzeug = await page.locator('a[href^="/fahrzeuge/"]').count();
if (hatFahrzeug === 0) {
await expect(page.getByText("im Gerätehaus")).toBeVisible();
} else {
await expect(page.locator('a[href^="/fahrzeuge/"]').first()).toBeVisible();
}
});
test("Wehr-Detail listet Fuhrpark und Kontakt", async ({ page }) => {
await page.goto(`/wehren/${WEHR_ID}`);
await expect(page.getByRole("heading", { name: "Fahrzeuge" })).toBeVisible();
const kontaktLink = page.locator('a[href^="tel:"], a[href^="mailto:"]').first();
await expect(kontaktLink).toBeVisible();
});
test("ungültige Fahrzeug-ID -> deutsche 404-Seite", async ({ page }) => {
await page.goto(`/fahrzeuge/${UNGUELTIGE_ID}`);
await expect(page.getByText("Nicht gefunden.")).toBeVisible();
});
});