mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 17:29:58 +01:00
ok ok
This commit is contained in:
parent
061c2b4f8d
commit
5bc316ff31
6 changed files with 289 additions and 11 deletions
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