mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 08:29:36 +01:00
ok ok
This commit is contained in:
parent
061c2b4f8d
commit
5bc316ff31
6 changed files with 289 additions and 11 deletions
50
.github/workflows/e2e.yml
vendored
Normal file
50
.github/workflows/e2e.yml
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
name: E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
name: Playwright API E2E
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Run E2E Tests
|
||||
run: |
|
||||
docker compose -f test-automation/docker-compose.e2e.yml up \
|
||||
--build \
|
||||
--abort-on-container-exit \
|
||||
--exit-code-from e2e-runner
|
||||
|
||||
- name: Upload JUnit Report
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: e2e-junit-report
|
||||
path: test-automation/e2e-results/junit.xml
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload Playwright HTML Report
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: e2e-html-report
|
||||
path: test-automation/e2e-results/
|
||||
retention-days: 7
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: docker compose -f test-automation/docker-compose.e2e.yml down --volumes --remove-orphans
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
# Effigenix Fleischerei ERP
|
||||
|
||||
[](https://github.com/s-frick/effigenix/actions/workflows/e2e.yml)
|
||||
|
||||
ERP-System für Fleischereien mit HACCP-Compliance, GoBD-konform, Mehrfilialen-Support.
|
||||
|
||||
## Schnellstart
|
||||
|
|
|
|||
4
justfile
4
justfile
|
|
@ -54,6 +54,10 @@ test-backend:
|
|||
test-e2e:
|
||||
docker compose -f test-automation/docker-compose.e2e.yml up --build --abort-on-container-exit --exit-code-from e2e-runner
|
||||
|
||||
# Playwright-Spec-Stub aus GitHub Issue generieren (z.B. just generate-test 65)
|
||||
generate-test ISSUE:
|
||||
cd test-automation/web-ui && pnpm exec tsx scripts/generate-tests-from-issue.ts {{ISSUE}}
|
||||
|
||||
# ─── Code Generation ─────────────────────────────────────
|
||||
|
||||
# OpenAPI Spec + TypeScript Types generieren
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ Konzept: [`docs/ui-testing-automation.md`](./docs/ui-testing-automation.md)
|
|||
- [x] `tests/api/inventory/movements.spec.ts`
|
||||
- [x] `tests/api/inventory/reservations.spec.ts`
|
||||
- [x] `tests/api/inventory/inventory-counts.spec.ts`
|
||||
- [ ] Test-Generierungs-Skript (`scripts/generate-tests-from-issue.ts`) finalisieren
|
||||
- [ ] Skript in `justfile` als `just generate-test <issue-nr>` integrieren
|
||||
- [x] Test-Generierungs-Skript (`scripts/generate-tests-from-issue.ts`) finalisieren
|
||||
- [x] Skript in `justfile` als `just generate-test <issue-nr>` integrieren
|
||||
- [ ] Alle Specs im Docker-Stack grün
|
||||
|
||||
---
|
||||
|
|
@ -67,11 +67,11 @@ Konzept: [`docs/ui-testing-automation.md`](./docs/ui-testing-automation.md)
|
|||
|
||||
> GitHub Actions Workflow für automatische Test-Ausführung.
|
||||
|
||||
- [ ] `.github/workflows/e2e.yml` erstellen
|
||||
- [ ] Trigger: Push auf `main`, PRs gegen `main`
|
||||
- [ ] JUnit-Report als CI-Artefakt hochladen
|
||||
- [ ] Playwright HTML-Report als GitHub Pages veröffentlichen (optional)
|
||||
- [ ] Badge in README einbinden
|
||||
- [x] `.github/workflows/e2e.yml` erstellen
|
||||
- [x] Trigger: Push auf `main`, PRs gegen `main` (+ `workflow_dispatch`)
|
||||
- [x] JUnit-Report als CI-Artefakt hochladen (30 Tage Retention)
|
||||
- [x] Playwright HTML-Report als CI-Artefakt hochladen (7 Tage Retention)
|
||||
- [x] Badge in README einbinden
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -91,8 +91,9 @@ Konzept: [`docs/ui-testing-automation.md`](./docs/ui-testing-automation.md)
|
|||
|
||||
| Punkt | Status | Massnahme |
|
||||
|---|---|---|
|
||||
| Backend `Dockerfile` | Fehlt | In Phase 1 erstellen |
|
||||
| Seed-Testdaten Isolation | Offen | Strategie in Phase 1 klären (DB-Reset vor Suite oder pro Test) |
|
||||
| `gh` Token `read:project` Scope | Fehlt | `gh auth refresh -s read:project` ausführen wenn nötig |
|
||||
| TUI-Tests Abgrenzung | Klar | Vitest + ink-testing-library, gemockte API-Calls |
|
||||
| Backend `Dockerfile` | ✅ Erledigt | Multi-stage Maven + JRE, `SPRING_PROFILES_ACTIVE: e2e` |
|
||||
| Seed-Testdaten Isolation | ✅ Erledigt | Option B: UUID/Timestamp-Suffixe, kein DB-Reset nötig |
|
||||
| `gh` Token `read:project` Scope | Offen | `gh auth refresh -s read:project` ausführen wenn nötig |
|
||||
| TUI-Tests Abgrenzung | ✅ Klar | Vitest + ink-testing-library, gemockte API-Calls |
|
||||
| Scanner (Tauri/mobil) | Out of scope | Separates Konzept, ggf. `test-automation/scanner/` später |
|
||||
| Lokaler E2E-Run verifiziert | Offen | `just test-e2e` ausführen (benötigt Docker + ~5 Min. Maven-Build) |
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"devDependencies": {
|
||||
"@playwright/test": "^1.51.0",
|
||||
"@types/node": "^20.0.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.4.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
220
test-automation/web-ui/scripts/generate-tests-from-issue.ts
Normal file
220
test-automation/web-ui/scripts/generate-tests-from-issue.ts
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* generate-tests-from-issue.ts
|
||||
*
|
||||
* Generiert einen Playwright-Spec-Stub aus einem GitHub Issue.
|
||||
*
|
||||
* Aufruf (via justfile):
|
||||
* just generate-test 65
|
||||
*
|
||||
* Direkt:
|
||||
* npx tsx scripts/generate-tests-from-issue.ts 65
|
||||
*
|
||||
* Voraussetzung: `gh` CLI muss authentifiziert sein.
|
||||
* nix shell nixpkgs#gh -c gh auth status
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as readline from 'readline';
|
||||
|
||||
// ── Konfiguration ──────────────────────────────────────────────────────────────
|
||||
|
||||
/** Repo-Pfad relativ zum Repo-Root */
|
||||
const TESTS_DIR = path.join(__dirname, '..', 'tests', 'api');
|
||||
|
||||
/** Mapping: TC-Präfix → { Verzeichnis, Dateiname, API-Pfad, Beschreibung } */
|
||||
const TC_MAP: Record<string, { dir: string; file: string; apiBase: string; label: string }> = {
|
||||
'TC-CAT': { dir: 'masterdata', file: 'categories.spec.ts', apiBase: '/api/categories', label: 'Produktkategorien' },
|
||||
'TC-SUP': { dir: 'masterdata', file: 'suppliers.spec.ts', apiBase: '/api/suppliers', label: 'Lieferanten' },
|
||||
'TC-ART': { dir: 'masterdata', file: 'articles.spec.ts', apiBase: '/api/articles', label: 'Artikel' },
|
||||
'TC-CUS': { dir: 'masterdata', file: 'customers.spec.ts', apiBase: '/api/customers', label: 'Kunden' },
|
||||
'TC-B2B': { dir: 'masterdata', file: 'contracts.spec.ts', apiBase: '/api/customers/{id}/frame-contract', label: 'Rahmenverträge' },
|
||||
'TC-AUTH': { dir: 'auth', file: 'authorization.spec.ts', apiBase: '/api/auth', label: 'Autorisierung' },
|
||||
'TC-REC': { dir: 'production', file: 'recipes.spec.ts', apiBase: '/api/recipes', label: 'Rezepte' },
|
||||
'TC-BAT': { dir: 'production', file: 'batches.spec.ts', apiBase: '/api/production/batches', label: 'Produktionschargen' },
|
||||
'TC-ORD': { dir: 'production', file: 'orders.spec.ts', apiBase: '/api/production/production-orders', label: 'Produktionsaufträge' },
|
||||
'TC-TRACE': { dir: 'production', file: 'traceability.spec.ts', apiBase: '/api/production/batches/{id}/trace-*', label: 'Rückverfolgung' },
|
||||
'TC-STOCK': { dir: 'inventory', file: 'stock.spec.ts', apiBase: '/api/inventory/stocks', label: 'Lagerbestand' },
|
||||
'TC-MOV': { dir: 'inventory', file: 'movements.spec.ts', apiBase: '/api/inventory/stock-movements', label: 'Warenbewegungen' },
|
||||
'TC-RES': { dir: 'inventory', file: 'reservations.spec.ts', apiBase: '/api/inventory/stocks/{id}/reservations', label: 'Reservierungen' },
|
||||
'TC-INV': { dir: 'inventory', file: 'inventory-counts.spec.ts', apiBase: '/api/inventory/inventory-counts', label: 'Inventurzählungen' },
|
||||
};
|
||||
|
||||
// ── GitHub Issue laden ─────────────────────────────────────────────────────────
|
||||
|
||||
interface GitHubIssue {
|
||||
number: number;
|
||||
title: string;
|
||||
body: string;
|
||||
labels: { name: string }[];
|
||||
}
|
||||
|
||||
function fetchIssue(issueNumber: string): GitHubIssue {
|
||||
try {
|
||||
const raw = execSync(
|
||||
`nix shell nixpkgs#gh -c gh api repos/{owner}/{repo}/issues/${issueNumber}`,
|
||||
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
|
||||
);
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
// Fallback ohne nix
|
||||
try {
|
||||
const raw = execSync(
|
||||
`gh api repos/{owner}/{repo}/issues/${issueNumber}`,
|
||||
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
|
||||
);
|
||||
return JSON.parse(raw);
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`GitHub Issue #${issueNumber} konnte nicht geladen werden.\n` +
|
||||
`Stelle sicher, dass 'gh' authentifiziert ist: nix shell nixpkgs#gh -c gh auth status\n` +
|
||||
String(err),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Test-Cases aus Issue-Body extrahieren ──────────────────────────────────────
|
||||
|
||||
interface TestCase {
|
||||
id: string; // z.B. "TC-SUP-01"
|
||||
title: string; // z.B. "Lieferant erstellen – Pflichtfelder"
|
||||
}
|
||||
|
||||
function extractTestCases(body: string): TestCase[] {
|
||||
const cases: TestCase[] = [];
|
||||
// Erkennt: "TC-SUP-01: Beschreibung" oder "**TC-SUP-01**" oder "| TC-SUP-01 |"
|
||||
const pattern = /\b(TC-[A-Z]+-\d{2,3})[:\s\|*]+([^\n|*]{3,80})/g;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = pattern.exec(body)) !== null) {
|
||||
const id = match[1].trim();
|
||||
const title = match[2].trim().replace(/^\*+|\*+$/g, '').trim();
|
||||
if (!cases.some(c => c.id === id)) {
|
||||
cases.push({ id, title });
|
||||
}
|
||||
}
|
||||
return cases;
|
||||
}
|
||||
|
||||
/** Ermittelt den TC-Präfix aus einer Liste von TestCase-IDs */
|
||||
function detectTcPrefix(cases: TestCase[]): string | null {
|
||||
for (const tc of cases) {
|
||||
// TC-TRACE vor TC-T* prüfen (länger zuerst)
|
||||
for (const prefix of Object.keys(TC_MAP).sort((a, b) => b.length - a.length)) {
|
||||
if (tc.id.startsWith(prefix)) return prefix;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Spec-Datei generieren ──────────────────────────────────────────────────────
|
||||
|
||||
function generateSpec(issue: GitHubIssue, cases: TestCase[], prefix: string): string {
|
||||
const meta = TC_MAP[prefix];
|
||||
const describeName = `${prefix}: ${meta.label}`;
|
||||
|
||||
const testBlocks = cases.length > 0
|
||||
? cases.map(tc => `
|
||||
test('${tc.id}: ${tc.title}', async ({ request, adminToken }) => {
|
||||
// TODO: Implementierung
|
||||
// API: ${meta.apiBase}
|
||||
const res = await request.get('${meta.apiBase.replace(/\{[^}]+\}/g, 'REPLACE_ID')}', {
|
||||
headers: { Authorization: \`Bearer \${adminToken}\` },
|
||||
});
|
||||
expect(res.status()).toBe(200);
|
||||
});`).join('\n')
|
||||
: `
|
||||
test.todo('TCs aus Issue #${issue.number} noch nicht implementiert');`;
|
||||
|
||||
return `import { test, expect } from '../../../fixtures/auth.fixture.js';
|
||||
|
||||
/**
|
||||
* ${describeName}
|
||||
* Quelle: GitHub Issue #${issue.number}
|
||||
* Titel: ${issue.title}
|
||||
*
|
||||
* ACHTUNG: Automatisch generierter Stub – Implementierung erforderlich!
|
||||
*/
|
||||
test.describe('${describeName}', () => {${testBlocks}
|
||||
});
|
||||
`;
|
||||
}
|
||||
|
||||
// ── Ausgabe-Pfad bestimmen ─────────────────────────────────────────────────────
|
||||
|
||||
function resolveOutputPath(prefix: string, issueNumber: string): { full: string; relative: string } | null {
|
||||
const meta = TC_MAP[prefix];
|
||||
if (!meta) return null;
|
||||
const dir = path.join(TESTS_DIR, meta.dir);
|
||||
const full = path.join(dir, meta.file);
|
||||
const relative = path.join('tests', 'api', meta.dir, meta.file);
|
||||
return { full, relative };
|
||||
}
|
||||
|
||||
// ── Interaktive Abfrage ────────────────────────────────────────────────────────
|
||||
|
||||
async function confirm(question: string): Promise<boolean> {
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||
return new Promise(resolve => {
|
||||
rl.question(question, answer => {
|
||||
rl.close();
|
||||
resolve(answer.trim().toLowerCase().startsWith('j') || answer.trim().toLowerCase() === 'y');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Main ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const issueNumber = process.argv[2];
|
||||
if (!issueNumber || !/^\d+$/.test(issueNumber)) {
|
||||
console.error('Aufruf: npx tsx scripts/generate-tests-from-issue.ts <issue-nummer>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\nLade GitHub Issue #${issueNumber}...`);
|
||||
const issue = fetchIssue(issueNumber);
|
||||
console.log(`Titel: ${issue.title}`);
|
||||
|
||||
const cases = extractTestCases(issue.body ?? '');
|
||||
if (cases.length > 0) {
|
||||
console.log(`\nGefundene Test-Cases (${cases.length}):`);
|
||||
cases.forEach(tc => console.log(` ${tc.id}: ${tc.title}`));
|
||||
} else {
|
||||
console.log('\nKeine TC-XXX-YY Muster im Issue gefunden – Stub ohne Test-Cases wird generiert.');
|
||||
}
|
||||
|
||||
const prefix = detectTcPrefix(cases);
|
||||
if (!prefix) {
|
||||
console.error('\nTC-Präfix konnte nicht erkannt werden. Unterstützte Präfixe: ' + Object.keys(TC_MAP).join(', '));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`\nErkannter Präfix: ${prefix} → ${TC_MAP[prefix].dir}/${TC_MAP[prefix].file}`);
|
||||
|
||||
const out = resolveOutputPath(prefix, issueNumber);
|
||||
if (!out) {
|
||||
console.error('Ausgabepfad konnte nicht ermittelt werden.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (fs.existsSync(out.full)) {
|
||||
const overwrite = await confirm(`\nDatei ${out.relative} existiert bereits. Überschreiben? [j/N] `);
|
||||
if (!overwrite) {
|
||||
console.log('Abgebrochen.');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
const content = generateSpec(issue, cases, prefix);
|
||||
fs.mkdirSync(path.dirname(out.full), { recursive: true });
|
||||
fs.writeFileSync(out.full, content, 'utf-8');
|
||||
console.log(`\nSpec erstellt: ${out.relative}`);
|
||||
console.log('Nächster Schritt: TODOs im generierten Test implementieren.');
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error(err instanceof Error ? err.message : err);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue