# Technisches Konzept: UI Test Automation – Effigenix ERP **Stand:** 2026-03-27 **Branch:** `experiment/test-automation` --- ## 1. Ausgangslage & Ziel ### Ist-Zustand Das Projekt besteht aus drei Frontend-Targets: | App | Technologie | Status | |---|---|---| | `apps/cli` | Ink + React (TUI) | Primäre operative UI – vollständig ausgebaut | | `apps/web` | React + Vite + Tailwind v4 | Frühstadium – aktuell nur Component-Showcase | | `apps/scanner` | Tauri v2 (mobil) | Prototype | Die manuellen Testfälle (GitHub Issues mit Label `manual-testing`: #62–#70) decken alle implementierten Bounded Contexts ab: - `TC-CAT` – Produktkategorien - `TC-SUP` – Lieferanten - `TC-CUS` – Kunden - `TC-B2B` – Rahmenverträge - `TC-AUTH` – Autorisierung Masterdata - `TC-ART` – Artikel *(offen)* - `TC-CROSS` – Übergreifend Masterdata/Inventory/Production *(deferred)* Weiterhin enthalten alle Feature-Issues (US-Pxx, Story x.x) strukturierte **Akzeptanzkriterien**. ### Ziel Automatisierte UI/E2E-Tests, die: 1. Aus den GitHub-Issues (Akzeptanzkriterien + manuelle Testfälle) abgeleitet werden 2. Repeatable und CI-fähig sind 3. Per Docker reproduzierbar laufen 4. Mit wachsender Web-UI mitwachsen können --- ## 2. Framework-Entscheidung: Playwright ### Bewertungsmatrix | Kriterium | Playwright | Robot Framework | |---|---|---| | TypeScript-native | ✅ Erstklassig | ⚠️ via robotframework-browser (Python wrapper) | | Monorepo-Integration (pnpm) | ✅ Als pnpm Package | ⚠️ Eigenes Python-Ökosystem | | React/Vite-Support | ✅ Exzellent | ⚠️ Indirekt via Browser-Automation | | API-Testing (REST) | ✅ `request` Context eingebaut | ✅ Eigene HTTP-Library | | Lernkurve im Team | ✅ Gering (TypeScript-Kenntnisse vorhanden) | ⚠️ Keyword-DSL + Python | | Docker-Image Größe | ~600 MB | ~800 MB+ | | Parallelisierung | ✅ Built-in | ⚠️ Konfigurationsaufwand | | Community / Ökosystem | ✅ Stark (Microsoft-backed) | ✅ Stabil (älteres Ökosystem) | **Entscheidung: Playwright** Begründung: Das Team arbeitet bereits in TypeScript. Playwright lässt sich als Workspace-Package in den bestehenden pnpm-Monorepo integrieren. Die API-Testing-Fähigkeit erlaubt es, Backend-Akzeptanzkriterien ohne Browser zu testen – ideal für den aktuellen Stand, wo die Web-UI noch im Aufbau ist. --- ## 3. Architektur ### 3.1 Monorepo-Integration ``` frontend/ └── apps/ └── e2e/ # Neues Workspace-Package ├── package.json ├── playwright.config.ts ├── tests/ │ ├── api/ # API-E2E Tests (kein Browser nötig) │ │ ├── masterdata/ │ │ │ ├── categories.spec.ts # aus TC-CAT │ │ │ ├── suppliers.spec.ts # aus TC-SUP │ │ │ ├── customers.spec.ts # aus TC-CUS │ │ │ └── articles.spec.ts # aus TC-ART │ │ ├── inventory/ │ │ │ ├── stock.spec.ts │ │ │ ├── movements.spec.ts │ │ │ └── reservations.spec.ts │ │ ├── production/ │ │ │ ├── recipes.spec.ts │ │ │ ├── batches.spec.ts │ │ │ └── orders.spec.ts │ │ └── auth/ │ │ └── authorization.spec.ts # aus TC-AUTH │ └── web/ # Browser-UI Tests (Web App) │ └── .gitkeep # Platzhalter – wächst mit der Web-App ├── fixtures/ │ ├── auth.fixture.ts # Login-Helper (JWT) │ └── seed.fixture.ts # Testdaten-Setup └── helpers/ └── api-client.ts # Typisierter API-Wrapper ``` ### 3.2 Test-Ebenen ``` ┌─────────────────────────────────────────────────────┐ │ Phase 2: Web UI Tests (Browser) │ │ Playwright + React – sobald Web-App ausgebaut │ ├─────────────────────────────────────────────────────┤ │ Phase 1 (jetzt): API E2E Tests │ │ Playwright request context → Spring Boot REST API │ │ Direkte Ableitung aus Akzeptanzkriterien in Issues │ ├─────────────────────────────────────────────────────┤ │ Bestehend: Unit + Integration Tests (Maven/vitest) │ └─────────────────────────────────────────────────────┘ ``` **Phase 1** startet mit API-Level-Tests, da die Web-UI noch nicht ausgebaut ist. Diese Tests validieren denselben Scope wie die manuellen Testfälle, ohne einen Browser zu benötigen. --- ## 4. Ticket → Test Mapping ### 4.1 Struktur der Issues Die GitHub-Issues enthalten zwei Test-relevante Formate: **Format A – Feature Stories (US-Pxx / Story x.x):** ```markdown ## Akzeptanzkriterien - [ ] POST `/api/production-orders` → 201, Status PLANNED - [ ] PlannedDate gestern → 400 (PlannedDateInPast) - [ ] PlannedQuantity 0 → 400 ``` → Direkt in API-Tests übersetzbar (HTTP-Verb, Endpoint, Expected Status Code) **Format B – Manuelle Testfälle (TC-xxx):** ```markdown ### TC-SUP-01: Lieferant erstellen – Pflichtfelder 1. Name: `Frisch AG`, Telefon: `+49 30 12345` → Enter - [x] Erwartung: Lieferant erscheint in Liste, Status AKTIV ``` → Schritte + Erwartungen → Playwright `test()` Blöcke ### 4.2 Mapping-Tabelle: Issues → Spec-Dateien | GitHub Issue | Label | Spec-Datei | Priorität | |---|---|---|---| | #62 TC-CAT | manual-testing | `api/masterdata/categories.spec.ts` | Hoch | | #63 TC-SUP | manual-testing | `api/masterdata/suppliers.spec.ts` | Hoch | | #65 TC-CUS | manual-testing | `api/masterdata/customers.spec.ts` | Hoch | | #66 TC-B2B | manual-testing | `api/masterdata/contracts.spec.ts` | Mittel | | #67 TC-AUTH | manual-testing | `api/auth/authorization.spec.ts` | Hoch | | #64 TC-ART | manual-testing (offen) | `api/masterdata/articles.spec.ts` | Hoch | | #38–#42 US-P13–P17 | epic:production-order | `api/production/orders.spec.ts` | Mittel | | #33–#36 US-P09–P12 | epic:batch | `api/production/batches.spec.ts` | Mittel | | #26–#32 US-P02–P08 | epic:recipe | `api/production/recipes.spec.ts` | Mittel | | #43–#44 US-P18–P19 | epic:traceability | `api/production/traceability.spec.ts` | Mittel | | #4–#20 Story 2.x–6.x | epic:stock/inventory | `api/inventory/*.spec.ts` | Mittel | ### 4.3 Beispiel-Übersetzung **Issue #62, TC-CAT-01 → Playwright:** ```typescript // tests/api/masterdata/categories.spec.ts import { test, expect } from '@playwright/test'; test.describe('TC-CAT: Produktkategorien', () => { test('TC-CAT-01: Kategorie erstellen – Happy Path', async ({ request }) => { const res = await request.post('/api/product-categories', { data: { name: 'Obst & Gemüse', description: 'Frische Produkte' }, }); expect(res.status()).toBe(201); const body = await res.json(); expect(body.name).toBe('Obst & Gemüse'); expect(body.description).toBe('Frische Produkte'); }); test('TC-CAT-02: Kategorie erstellen – ohne Beschreibung', async ({ request }) => { const res = await request.post('/api/product-categories', { data: { name: 'Milchprodukte' }, }); expect(res.status()).toBe(201); }); test('TC-CAT-04: Doppelter Name wird abgelehnt', async ({ request }) => { await request.post('/api/product-categories', { data: { name: 'Duplikat-Test' } }); const res = await request.post('/api/product-categories', { data: { name: 'Duplikat-Test' } }); expect(res.status()).toBe(409); // oder 422 je nach Backend-Impl. }); test('TC-CAT-06: Leerer Name wird abgelehnt', async ({ request }) => { const res = await request.post('/api/product-categories', { data: { name: '' } }); expect(res.status()).toBe(400); }); }); ``` --- ## 5. Docker-Setup ### 5.1 Image-Strategie Zwei Images: ``` docker-compose.e2e.yml ├── db – postgres:15-alpine (gleiche Config wie dev) ├── backend – effigenix-backend:test (mit test-Profile + Seed-Daten) └── e2e-runner – effigenix-e2e:latest (Playwright + Tests) ``` ### 5.2 Playwright Dockerfile ```dockerfile # e2e/Dockerfile FROM mcr.microsoft.com/playwright:v1.51.0-noble WORKDIR /app # pnpm installieren RUN npm install -g pnpm@9 # Workspace-Dependencies kopieren COPY frontend/package.json frontend/pnpm-workspace.yaml ./ COPY frontend/apps/e2e/package.json ./apps/e2e/ # Andere packages die e2e braucht COPY frontend/packages/types/package.json ./packages/types/ # Install RUN pnpm install --frozen-lockfile # Test-Code kopieren COPY frontend/apps/e2e/ ./apps/e2e/ COPY frontend/packages/types/ ./packages/types/ WORKDIR /app/apps/e2e ENTRYPOINT ["pnpm", "exec", "playwright", "test"] ``` ### 5.3 docker-compose.e2e.yml ```yaml version: '3.8' services: db: image: postgres:15-alpine environment: POSTGRES_DB: effigenix POSTGRES_USER: effigenix POSTGRES_PASSWORD: effigenix healthcheck: test: ["CMD-SHELL", "pg_isready -U effigenix"] interval: 5s timeout: 3s retries: 10 backend: build: context: ./backend dockerfile: Dockerfile environment: SPRING_PROFILES_ACTIVE: test SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/effigenix SPRING_DATASOURCE_USERNAME: effigenix SPRING_DATASOURCE_PASSWORD: effigenix depends_on: db: condition: service_healthy healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:8080/actuator/health || exit 1"] interval: 10s timeout: 5s retries: 12 ports: - "8080:8080" e2e-runner: build: context: . dockerfile: e2e/Dockerfile environment: BASE_URL: http://backend:8080 TEST_USER_ADMIN: admin TEST_USER_ADMIN_PASS: admin123 TEST_USER_VIEWER: viewer TEST_USER_VIEWER_PASS: viewer123 depends_on: backend: condition: service_healthy volumes: - ./e2e-results:/app/apps/e2e/playwright-report - ./e2e-results:/app/apps/e2e/test-results ``` ### 5.4 Ausführung ```bash # Alle E2E-Tests lokal starten docker compose -f docker-compose.e2e.yml up --abort-on-container-exit # Nur bestimmte Tests docker compose -f docker-compose.e2e.yml run e2e-runner --grep "TC-SUP" # Report anzeigen (nach Test-Run) pnpm exec playwright show-report e2e-results ``` --- ## 6. Playwright-Konfiguration ```typescript // frontend/apps/e2e/playwright.config.ts import { defineConfig } from '@playwright/test'; export default defineConfig({ testDir: './tests', fullyParallel: true, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 4 : undefined, reporter: [ ['html', { outputFolder: 'playwright-report' }], ['junit', { outputFile: 'test-results/junit.xml' }], // für CI ], use: { baseURL: process.env.BASE_URL ?? 'http://localhost:8080', extraHTTPHeaders: { Accept: 'application/json', 'Content-Type': 'application/json', }, }, projects: [ { name: 'api', testMatch: 'tests/api/**/*.spec.ts', // Kein Browser benötigt für API-Tests }, { name: 'web-chromium', testMatch: 'tests/web/**/*.spec.ts', use: { browserName: 'chromium' }, }, ], }); ``` --- ## 7. Auth-Fixtures ```typescript // frontend/apps/e2e/fixtures/auth.fixture.ts import { test as base, expect } from '@playwright/test'; type AuthFixtures = { adminToken: string; viewerToken: string; }; export const test = base.extend({ adminToken: async ({ request }, use) => { const res = await request.post('/api/auth/login', { data: { username: process.env.TEST_USER_ADMIN ?? 'admin', password: process.env.TEST_USER_ADMIN_PASS ?? 'admin123', }, }); expect(res.status()).toBe(200); const { token } = await res.json(); await use(token); }, viewerToken: async ({ request }, use) => { const res = await request.post('/api/auth/login', { data: { username: process.env.TEST_USER_VIEWER ?? 'viewer', password: process.env.TEST_USER_VIEWER_PASS ?? 'viewer123', }, }); expect(res.status()).toBe(200); const { token } = await res.json(); await use(token); }, }); export { expect }; ``` --- ## 8. Test-Generierung aus Issues ### 8.1 Workflow ``` GitHub Issue (body mit AC/TC) │ ▼ gh issue view --json title,body │ ▼ Claude API (claude-sonnet-4-6) Prompt: "Übersetze diese Akzeptanzkriterien in Playwright TypeScript Tests..." │ ▼ Generierter Test-Stub → Review → Commit ``` ### 8.2 Generierungs-Skript (Konzept) ```typescript // scripts/generate-tests-from-issue.ts import { execSync } from 'child_process'; import Anthropic from '@anthropic-ai/sdk'; import fs from 'fs'; const issueNumber = process.argv[2]; if (!issueNumber) throw new Error('Usage: tsx generate-tests-from-issue.ts '); // Issue-Body via gh CLI laden const issue = JSON.parse( execSync(`gh issue view ${issueNumber} --json title,body`).toString() ); const client = new Anthropic(); const prompt = ` Du bist ein Senior QA Engineer. Übersetze die folgenden GitHub Issue-Inhalte in Playwright TypeScript API-Tests (Playwright request context, kein Browser). Regeln: - Verwende test.describe() mit dem Issue-Titel - Jeden Testfall (TC-xxx oder Akzeptanzkriterium) als eigenständigen test() - Auth via Bearer Token (Fixture: adminToken oder viewerToken) - baseURL kommt aus Playwright-Config (nicht hardcoden) - Nur den Test-Code ausgeben, kein Markdown-Wrapper Issue #${issueNumber}: ${issue.title} ${issue.body} `; const response = await client.messages.create({ model: 'claude-sonnet-4-6', max_tokens: 4096, messages: [{ role: 'user', content: prompt }], }); const code = response.content[0].type === 'text' ? response.content[0].text : ''; const filename = `tests/api/generated/issue-${issueNumber}.spec.ts`; fs.mkdirSync('tests/api/generated', { recursive: true }); fs.writeFileSync(filename, code); console.log(`✓ Test generiert: ${filename}`); console.log(' → Bitte Review und manuelle Anpassung vor Commit!'); ``` ### 8.3 Nutzung ```bash # Test für ein Issue generieren cd frontend/apps/e2e tsx ../../scripts/generate-tests-from-issue.ts 63 # Output: tests/api/generated/issue-63.spec.ts # → Review → in tests/api/masterdata/suppliers.spec.ts einpflegen ``` ### 8.4 Kandidaten für sofortige Generierung Folgende Issues enthalten vollständige, abgehakte Testfälle (alle `[x]`) und sind direkt verwertbar: | Issue | Testfälle | Status | |---|---|---| | #62 TC-CAT | 6 TCs | ✅ Alle abgenommen | | #63 TC-SUP | 12 TCs | ✅ Alle abgenommen | | #65 TC-CUS | Kunden-CRUD | ✅ | | #66 TC-B2B | Rahmenverträge | ✅ | | #38 US-P13 | 3 ACs | ✅ | | #39–#42 US-P14–P17 | je 2–4 ACs | ✅ | --- ## 9. TUI-Testing (Terminal UI) ### 9.1 Machbarkeit **Ja, TUI-Testing ist möglich und bereits vorbereitet.** `ink-testing-library` ist bereits im `@effigenix/cli`-Package als `devDependency` installiert und wird in 4 bestehenden Tests genutzt (z.B. `ConfirmDialog.test.tsx`). Das Pattern ist: ```typescript import { render } from 'ink-testing-library'; const { lastFrame, stdin } = render(); ``` `lastFrame()` gibt die aktuelle Terminal-Ausgabe als String zurück. `stdin` ermöglicht simulierte Tastatureingaben. ### 9.2 Was testbar ist | Art | Testbar | Methode | |---|---|---| | Render-Output (Text, Farben) | ✅ | `lastFrame()` | | Tastaturnavigation (`↑↓`, Enter, `n`, `e`) | ✅ | `stdin.write(...)` | | Zustandsübergänge (Formular → Liste) | ✅ | Async `lastFrame()` nach Input | | API-Calls (echte HTTP) | ⚠️ | Erfordert Mock via `vi.mock()` | | Vollständiger E2E-Flow mit echtem Backend | ❌ | Nicht sinnvoll (Domäne von Playwright-API-Tests) | ### 9.3 Teststruktur für TUI ``` frontend/apps/cli/src/__tests__/ ├── shared/ # Bestehend │ ├── ConfirmDialog.test.tsx │ ├── ErrorDisplay.test.tsx │ └── SuccessDisplay.test.tsx ├── hooks/ # Bestehend │ └── useRoles.test.ts └── screens/ # Neu – aus TC-xxx Issues ableiten ├── masterdata/ │ ├── SupplierList.test.tsx # aus TC-SUP-06 (Filter) │ ├── SupplierForm.test.tsx # aus TC-SUP-01/02/03 (Formular) │ └── CategoryForm.test.tsx # aus TC-CAT-01/06 (Validierung) └── production/ └── RecipeForm.test.tsx ``` ### 9.4 Beispiel: TC-SUP-03 als TUI-Test ```typescript // __tests__/screens/masterdata/SupplierForm.test.tsx import { render } from 'ink-testing-library'; import { vi, describe, it, expect } from 'vitest'; import React from 'react'; import { SupplierCreateForm } from '../../../components/masterdata/SupplierCreateForm.js'; vi.mock('@effigenix/api-client', () => ({ suppliersApi: { create: vi.fn().mockResolvedValue({ status: 201, data: { id: '1', name: 'Frisch AG' } }), }, })); describe('TC-SUP: Lieferanten-Formular', () => { it('TC-SUP-03: Leerer Name → kein API-Call', async () => { const onSuccess = vi.fn(); const { stdin, lastFrame } = render( React.createElement(SupplierCreateForm, { onSuccess }) ); // Enter ohne Name stdin.write('\r'); await new Promise(r => setTimeout(r, 50)); expect(lastFrame()).toContain('Pflichtfeld'); expect(onSuccess).not.toHaveBeenCalled(); }); it('TC-SUP-01: Pflichtfelder ausgefüllt → Lieferant wird angelegt', async () => { const onSuccess = vi.fn(); const { stdin, lastFrame } = render( React.createElement(SupplierCreateForm, { onSuccess }) ); // Name eingeben stdin.write('Frisch AG\r'); // Telefon eingeben stdin.write('+49 30 12345\r'); // Speichern stdin.write('\r'); await new Promise(r => setTimeout(r, 100)); expect(onSuccess).toHaveBeenCalled(); }); }); ``` ### 9.5 Abgrenzung TUI-Tests vs. API-Tests ``` TUI-Tests (vitest + ink-testing-library) → "Rendert der Screen korrekt?" → "Werden Validierungsfehler angezeigt?" → "Reagiert Tastatur-Navigation richtig?" → API-Calls werden gemockt API E2E-Tests (Playwright) → "Funktioniert das Backend korrekt?" → "Werden alle Akzeptanzkriterien aus dem Ticket erfüllt?" → Kein UI-Rendering ``` Beide Ebenen ergänzen sich: TUI-Tests sichern das Verhalten der UI-Komponenten, API-Tests sichern den Backend-Vertrag. --- ## 10. Umsetzungsplan (Phasen) ### Phase 0 – TUI-Tests ausbauen (sofort, ohne neues Setup) - [ ] `__tests__/screens/` Verzeichnis anlegen - [ ] Formular-Tests für `SupplierCreateForm` (TC-SUP-01/02/03) - [ ] Formular-Tests für `CategoryForm` (TC-CAT-01/04/06) - [ ] Filter-Tests für Listen-Screens (TC-SUP-06) - [ ] Bestehende 4 Tests als Vorlage nutzen ### Phase 1 – API E2E Grundgerüst - [ ] `frontend/apps/e2e` Workspace-Package anlegen - [ ] `playwright.config.ts` einrichten (API-Projekt) - [ ] Auth-Fixture implementieren - [ ] `docker-compose.e2e.yml` erstellen - [ ] Backend `Dockerfile` (test-Profile, Seed-Daten) - [ ] Erste Spec: `api/masterdata/categories.spec.ts` (TC-CAT, Issue #62) - [ ] Erste Spec: `api/masterdata/suppliers.spec.ts` (TC-SUP, Issue #63) - [ ] `just test-e2e` Recipe in `justfile` ergänzen ### Phase 2 – Vollständige API-Coverage - [ ] Alle `manual-testing`-Issues in Specs übersetzen (#62–#67) - [ ] Production BC Specs aus US-P13–P19 (#38–#44) - [ ] Inventory BC Specs aus Story 2.x–6.x (#4–#20) - [ ] Auth/Authorization Spec (TC-AUTH, #67) - [ ] Generierungs-Skript finalisieren und in `justfile` integrieren ### Phase 3 – CI/CD Integration - [ ] `.github/workflows/e2e.yml` erstellen - [ ] Trigger: auf `main`-Push und PRs gegen `main` - [ ] JUnit-Report als CI-Artefakt - [ ] Playwright HTML-Report als GitHub Pages (optional) ### Phase 4 – Web UI Tests (wenn App ausgebaut) - [ ] Browser-Projekt in `playwright.config.ts` aktivieren - [ ] Page Object Models für die Web-App-Seiten - [ ] Visuelle Regression (optional: Playwright screenshots) --- ## 10. Offene Punkte & Entscheidungen | Punkt | Status | Anmerkung | |---|---|---| | Backend `Dockerfile` | Fehlt noch | Nötig für E2E-Docker-Setup | | Seed-Testdaten Isolation | Offen | Aktuell globale Seed-DB; für E2E per Test isolieren oder DB-Reset vor Suite | | `read:project` Scope für `gh` | Token fehlt Scope | Für Projektstatus-Abfrage (welche Issues in "Test") nötig; `gh auth refresh -s read:project` | | TUI-Testing | Out of scope | Ink-TUI-Testing via `ink-testing-library` ist möglich, aber separates Konzept | | Scanner (Tauri/mobil) | Out of scope | Appium oder Tauri-eigene Test-Tools; separates Konzept |