End-to-End mit Playwright-MCP — Tests die Claude selbst schreibt
Artikel 8 · Serie: Agentic Coding mit Claude Code
Die Komponenten-Tests aus v0.7 laufen durch, der Build ist grün — auf dem Papier sieht alles gut aus. Was die Tests aber nicht zeigen: wie die App sich in einem echten Browser verhält. Vitest läuft in jsdom, einem simulierten Browser, der sich in einem entscheidenden Punkt vom echten unterscheidet: er rendert nichts. Ob ein Tab-Klick tatsächlich die richtige Visualisierung zeigt, ob die SVG-Charts im Chromium sauber zeichnen, ob die Breadcrumb-Navigation so reagiert wie erwartet — das bleibt ungeprüft. End-to-End-Tests in einem echten Browser sind die Antwort darauf. Wir schreiben sie nicht von Hand, sondern lassen Claude sie generieren: aus einer Markdown-Datei, die den User-Flow beschreibt.
Playwright-Library und Playwright-MCP: zwei verschiedene Dinge
Playwright gibt es in zwei Formen, die verschiedene Rollen spielen.
Die Playwright-Library (@playwright/test) ist ein Test-Runner. Man schreibt TypeScript-Code, ruft page.goto("/") auf, macht Assertions mit expect() — und Playwright führt das in einem echten Browser aus. Der Code liegt im Repo, der Mensch schreibt ihn.
Der Playwright-MCP-Server ist etwas anderes. Er lässt Claude Code einen echten Browser steuern, während eine Konversation läuft. Claude kann eine URL öffnen, auf Elemente klicken, den DOM-Snapshot lesen, Screenshots machen — alles zur Laufzeit, ohne vorher Tests zu schreiben. Das Ergebnis ist nicht Test-Execution, sondern Test-Generierung: Claude exploriert live, findet robuste Selektoren, und schreibt danach den Playwright-Test.
Die Kombination macht den Unterschied: der Skill liest eine Markdown-Spec, öffnet den Browser per MCP, geht den User-Flow Schritt für Schritt nach, und schreibt am Ende eine .spec.ts-Datei. Kein Raten, kein Selektor auf Verdacht.
Plan-File für Art 8
Task 1 (sequenziell): Playwright installieren und konfigurieren.
Task 2 (nach Task 1): drei Markdown-Specs schreiben.
Sunburst, Sankey, Treemap — je eine Spec die den User-Flow beschreibt.
Task 3 (parallel zu Task 2): e2e-spec-Skill schreiben.
Workflow für den Skill: Spec lesen, Browser per MCP explorieren,
Selektoren finden, Test schreiben, Screenshot speichern.
Task 4 (nach Task 2 + 3): erste Generierung und Baseline.
Skill in Aktion, generierte Tests laufen, Baseline-Screenshots gespeichert.
Task 1: Playwright installieren
Installiere @playwright/test als Dev-Dependency in web/.
Lass Chromium via npx playwright install chromium laden.
Erstelle playwright.config.ts: testDir auf tests/e2e/generated,
baseURL http://localhost:5173, Playwright startet den Dev-Server automatisch.
Ergänze package.json um test:e2e und test:e2e:update-baseline.
Der wichtigste Teil der Konfiguration ist der webServer-Block. Er startet den Vite-Dev-Server automatisch bevor ein Test läuft — kein manuelles npm run dev in einem separaten Terminal nötig:
webServer: {
command: "npm run dev -- --port 5173",
url: "http://localhost:5173",
reuseExistingServer: !process.env.CI,
timeout: 60_000,
}
Das reuseExistingServer-Flag macht lokal den Unterschied: wenn der Dev-Server bereits läuft, hängt Playwright sich einfach ein. Im CI dagegen, wo kein laufender Server vorausgesetzt werden kann, startet er einen frischen.
Task 2: Markdown-Specs schreiben
Die Specs beschreiben User-Flows in einer Sprache, die Menschen lesen und die Claude in Tests übersetzen kann. Wer eine Spec schreibt, muss weder Playwright kennen noch wissen, wie Selektoren funktionieren — es genügt, den gewünschten Ablauf zu beschreiben: Ausgangszustand, Aktion, erwartetes Ergebnis. Jede Spec folgt demselben Aufbau:
# Spec: <Vis-Name>
## Voraussetzung
- Dev-Server läuft auf http://localhost:5173
- Daten sind via `npm run dev` bereitgestellt
## Schritt 1: <Name>
**Aktion:** ...
**Erwartet:** ...
## Screenshot
**Ablageort:** `screenshots/baseline/<vis>.png`
**Zeitpunkt:** nach Schritt N
Wie das konkret aussieht, zeigt die Sunburst-Spec:
# Spec: Sunburst-Visualisierung
## Schritt 1: Sunburst ist Default
**Aktion:** Seite öffnen.
**Erwartet:**
- Header zeigt „Bayerischer Haushalt 2026"
- Tab „Sunburst" ist aktiv (aria-selected)
- SVG mit data-testid="sunburst" ist sichtbar
## Schritt 2: Halbkreis-Layout
**Erwartet:**
- Überschrift „Einnahmen" oberhalb des SVG
- Überschrift „Ausgaben" unterhalb des SVG
## Screenshot
**Ablageort:** `screenshots/baseline/sunburst.png`
**Zeitpunkt:** nach Schritt 2
Diese Spec ist kein Code und kein Test — sie ist eine Absichtserklärung. Wer sie liest, versteht sofort, was geprüft wird. Und wer sie schreibt, denkt in User-Flows, nicht in Selektoren.
Task 3: e2e-spec-Skill
Der Skill in .claude/skills/e2e-spec/SKILL.md beschreibt, wie aus einer Markdown-Spec ein Playwright-Test wird. Er liest die Spec, prüft ob die Daten verfügbar sind, öffnet dann per Playwright-MCP einen echten Browser und geht den User-Flow Schritt für Schritt durch. Was er dabei sucht: stabile Selektoren. Was er daraus macht: eine .spec.ts-Datei mit Assertions die direkt auf Spec-Schritten basieren, und einen Screenshot an der dafür vorgesehenen Stelle.
Besonders wichtig ist die Selektor-Strategie. Der Skill legt fest:
Selektor-Priorität:
1. data-testid (in den Komponenten gesetzt, am stabilsten)
2. getByRole mit accessible name (getByRole("tab", { name: /Sunburst/ }))
3. getByText für eindeutigen Text-Inhalt
4. KEINE CSS-Selektoren mit nth-child, IDs aus Frameworks, Klassen-Ketten
Wer einen Test schreibt, der .btn-primary > span:nth-child(3) als Selektor nutzt, hat einen Test, der beim nächsten Refactor bricht. getByTestId("switch-sunburst") überlebt Layout-Änderungen.
Task 4: Erste Generierung
Den Skill anzuwerfen ist unkompliziert:
Generiere den E2E-Test aus web/tests/e2e/specs/sunburst.spec.md.
Was der Skill dabei macht, ist nicht blind Code schreiben. Er öffnet den Browser per Playwright-MCP, navigiert zur App, liest den DOM-Snapshot um zu verstehen was vorhanden ist, klickt durch den User-Flow und notiert dabei, welche Selektoren stabil genug sind. Erst danach schreibt er den Test. Was am Ende in tests/e2e/generated/sunburst.spec.ts landet:
test("Spec aus tests/e2e/specs/sunburst.spec.md", async ({ page }) => {
// Schritt 1: Sunburst ist Default
await page.goto("/");
await expect(
page.getByRole("heading", { name: /Bayerischer Haushalt 2026/ })
).toBeVisible();
await expect(page.getByTestId("switch-sunburst"))
.toHaveAttribute("aria-selected", "true");
await expect(page.getByTestId("sunburst")).toBeVisible();
// Schritt 2: Halbkreis-Layout
await expect(page.locator("h3", { hasText: /^Einnahmen$/ })).toBeVisible();
await expect(page.locator("h3", { hasText: /^Ausgaben$/ })).toBeVisible();
// Screenshot: Default-Halbkreis-Layout
await page.getByTestId("sunburst").screenshot({
path: "tests/e2e/screenshots/baseline/sunburst.png",
});
Die Kommentare im Test-Code beziehen sich auf die Spec-Schritte. Wer den Test liest, kann ihn zur Spec zurückverfolgen.
Visual-Regression-Baseline
Neben den drei generierten Tests speichert der Skill beim ersten Lauf auch je einen Screenshot pro Visualisierung unter web/tests/e2e/screenshots/baseline/. Was diese Screenshots leisten sollen, ist bewusst begrenzt: kein pixel-genauer Diff, sondern ein Sanity-Check.
Pixel-genaue Bildvergleiche sind bei dynamischen Charts ein Albtraum. Anti-Aliasing, Font-Rendering und GPU-Unterschiede zwischen Entwicklungs-Maschine und CI-Server erzeugen Diffs in Tests, die eigentlich grün sein sollten. Wer den Screenshot öffnet, soll stattdessen auf einen Blick erkennen, ob die Visualisierung den richtigen Charakter hat: das Sankey mit drei Spalten, der Sunburst als Halbkreis, die Treemap mit zwei großen Kacheln. Das genügt als Baseline.
Tests laufen
cd web && npm run test:e2e
Output:
Running 3 tests using 1 worker
✓ sankey.spec.ts Spec aus tests/e2e/specs/sankey.spec.md (760ms)
✓ sunburst.spec.ts Spec aus tests/e2e/specs/sunburst.spec.md (456ms)
✓ treemap.spec.ts Spec aus tests/e2e/specs/treemap.spec.md (490ms)
3 passed (3.1s)
Vitest bleibt für Komponenten-Tests zuständig und läuft in Sekunden. Playwright braucht einen echten Browser-Start und entsprechend etwas mehr Zeit — deshalb laufen beide über getrennte Scripts. npm run test für die schnellen Komponenten-Tests, npm run test:e2e wenn man wissen will, was im echten Browser passiert.
Stand am Ende dieses Artikels
git clone https://codeberg.org/rotecodefraktion/byhaushalt.git
cd byhaushalt
git checkout v0.8
cd parser && uv run python -m parser.normalize
cd ../web && npm install && npx playwright install chromium
npm run test:e2e
Vollständiger Stand unter byhaushalt @ v0.8.
v0.8 enthält die vollständige E2E-Infrastruktur: playwright.config.ts, drei Markdown-Specs in web/tests/e2e/specs/, drei daraus generierte Playwright-Tests in web/tests/e2e/generated/, drei Baseline-Screenshots und den .claude/skills/e2e-spec/SKILL.md. Die Gesamt-Testbilanz: 3 Vitest, 3 Playwright und 19 pytest grün, 3 pytest-xfail dokumentiert.
Wie es weitergeht
Artikel 9 behandelt Hooks — automatische Aktionen, die an Ereignisse in Claude Code gebunden sind: ein Hook der vor einem Commit die Tests lauft, einer der nach einer Session den Kontext-Stand protokolliert, einer der bei bestimmten Datei-Änderungen den Skill triggert. Die Grundlage ist fertig: Tests laufen, Baseline ist gesetzt. Jetzt kommt die Automatisierung.
Wie MCP-Server konfiguriert und Context7 für Library-Doku genutzt wurde, zeigt Artikel 7. Worktrees und parallele Visualisierungen beschreibt Artikel 6.