E2E Testing
Ist-Stand (2026-04-24)
Durchstich steht — yarn kumiko test e2e läuft im Sample ui-walkthrough grün: drei handgeschriebene Specs (smoke, create-flow, update-flow) + vier programmatisch aus der Registry abgeleitete Specs (list-renders, list-has-fixture-row, edit-save-persists, edit-validates-required). Chromium-only, Bun dev-server-Fixture, ephemeral Test-DB pro Run.
Struktur:
samples/ui-walkthrough/ playwright.config.ts ← webServer-Fixture PORT 4174 + globalSetup e2e/ smoke.spec.ts ← handgeschrieben (Stack-Durchstich) create-flow.spec.ts ← handgeschrieben update-flow.spec.ts ← handgeschrieben generated.spec.ts ← programmatisch: liest JSON, iteriert Kind-Handler global-setup.ts ← spawnt bun-Script vor allen Tests .e2e-data.json ← von globalSetup erzeugt (gitignored) scripts/ emit-e2e-data.ts ← bun-Script: generateE2ESpec(registry) → JSONbin/kumiko.ts findet jedes Package/Sample mit einer playwright.config.ts (packages zuerst, dann samples) und startet dessen Playwright-Run sequentiell. Opt-in — nicht Teil von yarn kumiko check.
Warum der JSON-Umweg
Der naive Ansatz wäre: generated.spec.ts importiert direkt generateE2ESpec aus dem Framework und iteriert. Scheitert, weil @kumiko/framework/engine transitiv Module lädt die mit Playwrights expect-Implementierung kollidieren (Object.prototype-Symbol $$jest-matchers-object, non-configurable, zweite Definition wirft). Der Fix wäre ein framework-weiter Packaging-Refactor (peer-deps, isolierte Sub-Exports) — teuer, out-of-scope.
Die JSON-Pipeline umgeht das sauber:
globalSetupläuft als Node-Hook VOR den Test-Workers- Spawnt einen bun-Subprozess (
emit-e2e-data.ts), der die volle framework-runtime lädt und TestSpecs als JSON schreibt - Playwright-Worker lädt nur
generated.spec.ts+ liest.e2e-data.json— kein framework-Import im Worker, keine Kollision
Vorteil gegenüber File-Template (die ältere Idee): Kind-Handler leben im Spec-File, nicht im Framework-Text-Template. Wenn der Renderer sein testId-Schema ändert, passt man genau einen Handler in generated.spec.ts an — kein Framework-Touch, keine Drift zwischen emitted Text und runtime-Behavior.
Bekannte Gaps
edit-validates-requiredwird derzeit geskipped wenn das Sample-Layout keinerequired-Condition pro Field deklariert. Das Framework propagiertentity.fields[x].requirednicht automatisch in den form-controller-field-state —computeFieldStatesliest nurlayout.fields[i].required(eineFieldCondition). Der Generator liest korrekt aus der Entity-Def und emittiertspec.requiredFields, aber DefaultField zeigt keinen Marker, weil der Layout-Override fehlt. Followup: entweder Entity→Layout-Propagation im form-controller, oder ui-walkthrough-Layout ergänzt die Condition.- Select-Primitives sind im Renderer noch nicht implementiert (
DefaultInputhat keinencase "select"). Der Kind-Handler überspringt select-Fills stillschweigend — wer select testen will, ergänzt ein Custom-Primitive.
Grundprinzip
Das Framework kennt alle Screens, Felder und Actions aus der Registry. Daraus werden E2E Tests automatisch generiert fuer Config-driven Screens. Custom Screens testet der Dev manuell.
Config-driven (entityList, entityEdit) → Tests generiert aus RegistryCustom Screens (Dashboard, Kanban) → Dev schreibt Tests manuellTest-Stack
| Was | Tool | Warum |
|---|---|---|
| Unit + Integration | Vitest (haben wir) | Schnell, Bun-kompatibel |
| E2E Web | Playwright | Stabil, schnell, guter Expo-Support nicht noetig |
| E2E Mobile | Maestro | Einfacher als Detox, YAML-basiert, Expo-kompatibel |
Warum Maestro statt Detox
- Detox: Nativer Build noetig, frickiges Setup, bricht bei Expo-Updates, C++ Bridge-Code
- Maestro: Laeuft gegen laufende App (dev oder build), YAML-Tests, kein nativer Build-Schritt, Expo-freundlich
Maestro-Tests sehen so aus:
appId: com.kumiko.app---- launchApp- tapOn: "Aufträge"- assertVisible: "Auftragsliste"- tapOn: "Neu"- inputText: id: "customer" text: "Firma ABC"- tapOn: "Speichern"- assertVisible: "Firma ABC"Falls Maestro nicht reicht (komplexe Gesten, Performance-Tests), kann Detox spaeter ergaenzt werden.
Automatisch generierte Tests
Was generiert wird
Fuer jeden entityList + entityEdit Screen erzeugt das Framework:
| Test | Prueft |
|---|---|
| List laedt | Screen oeffnen, keine Crashes, Spalten sichtbar |
| Create | Pflichtfelder ausfuellen → Save → Eintrag in Liste |
| Detail | Eintrag oeffnen → Felder zeigen korrekte Werte |
| Update | Feld aendern → Save → Aenderung sichtbar |
| Delete | Eintrag loeschen → nicht mehr in Liste |
| Validation | Pflichtfeld leer lassen → Save → Fehlermeldung |
| Access | User ohne Rolle → Screen nicht sichtbar / kein Zugriff |
| Conditional Fields | Feld-Sichtbarkeit basierend auf Werten |
Wie es funktioniert
// Framework liest die Registry:const screens = registry.getAllScreens();
for (const screen of screens) { if (screen.type === "entityList" || screen.type === "entityEdit") { generateCrudTests(screen, { entity: registry.getEntity(screen.entity), fields: screen.layout.sections.flatMap(s => s.fields), access: screen.access, }); }}Der Generator erzeugt:
- Playwright Tests (Web) → TypeScript, laufen headless
- Maestro Flows (Mobile) → YAML, laufen gegen Simulator/Emulator
Test-Daten
Generierte Tests brauchen Testdaten. Das Framework generiert Fixtures aus den Zod Schemas:
// Zod Schema: z.object({ title: z.string().min(1), status: z.string() })// → generierte Fixture: { title: "Test-Title-1", status: "open" }Fuer Relations und Select-Felder nutzt der Generator die Entity-Definition (Options, Foreign Keys).
Manuell geschriebene Tests
Fuer Custom Screens und Business-Logik schreibt der Dev Tests selbst:
import { test, expect } from "@playwright/test";
test("dashboard shows revenue chart", async ({ page }) => { await page.goto("/t/test-tenant/dashboard"); await expect(page.locator("[data-testid=revenue-chart]")).toBeVisible();});
test("filter changes chart data", async ({ page }) => { await page.goto("/t/test-tenant/dashboard"); await page.click("text=Monat"); await expect(page.locator("[data-testid=period-label]")).toHaveText("April 2026");});CLI
yarn kumiko test # Unit Tests (Vitest)yarn kumiko test integration # Integration Tests (Vitest, Docker noetig)yarn kumiko test e2e # Alle Playwright-Samples (iteriert samples/*/playwright.config.ts)yarn kumiko test all # Unit + Integration (NICHT e2e — opt-in)Mobile-E2E (Maestro) ist Roadmap, nicht verdrahtet.
Runner-Setup
Playwright (Web)
Laeuft gegen den laufenden Dev-Server:
yarn kumiko dev # startet API + Web Dev Serveryarn kumiko test e2e web # Playwright testet gegen localhostPlaywright Config zeigt auf http://localhost:3000 (oder den konfigurierten Port).
Maestro (Mobile)
Laeuft gegen den laufenden Expo Dev Server oder einen Build:
yarn kumiko dev # startet API + Expo Dev Serveryarn kumiko test e2e mobile # Maestro testet gegen SimulatorMaestro braucht einen laufenden iOS Simulator oder Android Emulator.
CI
# GitHub Actionse2e-web: - yarn kumiko dev --headless - yarn kumiko test e2e web
e2e-mobile: # Nur wenn Mobile-Aenderungen (Pfad-Filter) - yarn kumiko test e2e mobile --ciMobile E2E in CI ist teuer (Emulator starten). Empfehlung: nur bei Aenderungen an renderer-expo/ oder app/mobile/ ausfuehren.
Verzeichnisstruktur
packages/ testing/ e2e/ generator.ts ← liest Registry, erzeugt Tests fixtures.ts ← generiert Testdaten aus Zod Schemas playwright/ config.ts helpers.ts ← Login, Tenant-Setup, etc. maestro/ config.yaml helpers/
features/dashboard/ __tests__/ dashboard.e2e.ts ← manueller Playwright Test dashboard.maestro.yaml ← manueller Maestro FlowWas NICHT automatisch getestet wird
- Performance — manuell, kein generierter Last-Test
- Pixel-perfekte Darstellung — kein Visual Regression (kann spaeter mit Playwright Screenshots ergaenzt werden)
- Offline — zu komplex fuer generierte Tests, Dev testet manuell
- Cross-Feature Workflows — (Order anlegen → Notification → Email) manuell