Skip to content

E2E Testing

Ist-Stand (2026-04-24)

Durchstich stehtyarn 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) → JSON

bin/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:

  • globalSetup lä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-required wird derzeit geskipped wenn das Sample-Layout keine required-Condition pro Field deklariert. Das Framework propagiert entity.fields[x].required nicht automatisch in den form-controller-field-state — computeFieldStates liest nur layout.fields[i].required (eine FieldCondition). Der Generator liest korrekt aus der Entity-Def und emittiert spec.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 (DefaultInput hat keinen case "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 Registry
Custom Screens (Dashboard, Kanban) → Dev schreibt Tests manuell

Test-Stack

WasToolWarum
Unit + IntegrationVitest (haben wir)Schnell, Bun-kompatibel
E2E WebPlaywrightStabil, schnell, guter Expo-Support nicht noetig
E2E MobileMaestroEinfacher 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:

TestPrueft
List laedtScreen oeffnen, keine Crashes, Spalten sichtbar
CreatePflichtfelder ausfuellen → Save → Eintrag in Liste
DetailEintrag oeffnen → Felder zeigen korrekte Werte
UpdateFeld aendern → Save → Aenderung sichtbar
DeleteEintrag loeschen → nicht mehr in Liste
ValidationPflichtfeld leer lassen → Save → Fehlermeldung
AccessUser ohne Rolle → Screen nicht sichtbar / kein Zugriff
Conditional FieldsFeld-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:

features/dashboard/__tests__/dashboard.e2e.ts
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

Terminal window
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:

Terminal window
yarn kumiko dev # startet API + Web Dev Server
yarn kumiko test e2e web # Playwright testet gegen localhost

Playwright Config zeigt auf http://localhost:3000 (oder den konfigurierten Port).

Maestro (Mobile)

Laeuft gegen den laufenden Expo Dev Server oder einen Build:

Terminal window
yarn kumiko dev # startet API + Expo Dev Server
yarn kumiko test e2e mobile # Maestro testet gegen Simulator

Maestro braucht einen laufenden iOS Simulator oder Android Emulator.

CI

# GitHub Actions
e2e-web:
- yarn kumiko dev --headless
- yarn kumiko test e2e web
e2e-mobile:
# Nur wenn Mobile-Aenderungen (Pfad-Filter)
- yarn kumiko test e2e mobile --ci

Mobile 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 Flow

Was 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