From 32a02021eddf2c36ee81986dd9dc331136b914b3 Mon Sep 17 00:00:00 2001 From: mhoennig Date: Tue, 21 Jul 2026 21:01:43 +0200 Subject: [PATCH] frontend: Warnungs-Modell vereinheitlichen (strukturiert: Typ + Zeile) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TASKS Phase 1 (abgeschlossen). Warnungen sind jetzt maschinenlesbare Objekte {type, line, ...data} statt vorformatierter Strings: der Renderer emittiert {type:'mixedGate', line, label}; src/warnings.js `formatWarning(w, t)` macht daraus an EINER Stelle den lokalisierten, HTML-escapten Anzeigetext. Damit sind Typ und Zeilennummer sortier-/filter-/testbar, und Phase 2 (unbekannte Statuszeichen -> {type:'unknownStatus', line, code}) dockt ohne Formatstreuung an. tests/warnings.test.js + Mixed-Gate-Test im Renderer (29 Tests grün). Ende-zu-Ende im Browser verifiziert (lokalisierte Warnung mit Zeile + Label). Co-Authored-By: Claude Opus 4.8 --- docs/TASKS.md | 6 +++++- frontend/src/app.js | 3 ++- frontend/src/render.js | 6 ++++-- frontend/src/warnings.js | 26 ++++++++++++++++++++++++++ frontend/tests/render.test.js | 14 ++++++++++++++ frontend/tests/warnings.test.js | 23 +++++++++++++++++++++++ 6 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 frontend/src/warnings.js create mode 100644 frontend/tests/warnings.test.js diff --git a/docs/TASKS.md b/docs/TASKS.md index 402fb39..d58e269 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -24,7 +24,11 @@ Abhaken beim Erledigen; neue Aufgaben unten anfügen. `tests/render.test.js` (6 Tests, Snapshots). Anm.: der Modus (horizontal/vertikal/kompakt) ist reine CSS-Container-Klasse und ändert den Renderer-String nicht — ein Snapshot deckt alle drei Modi ab. -- [ ] Warnungs-Modell vereinheitlichen (Zeilennummern, Typen). +- [x] Warnungs-Modell vereinheitlichen (Zeilennummern, Typen). + → strukturierte Objekte `{type, line, ...}` (Renderer emittiert + `mixedGate`); `src/warnings.js` `formatWarning(w, t)` macht daraus den + lokalisierten, HTML-escapten Text an einer Stelle. Vorbereitet für + Phase 2 (`unknownStatus`). `tests/warnings.test.js`. ## Phase 2 — Qualität - [ ] Barrierefreiheit: Fokusreihenfolge, aria-Labels für Status/Größe/Tags. diff --git a/frontend/src/app.js b/frontend/src/app.js index 472ea2f..44c8f67 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -2,6 +2,7 @@ import './style.css'; import { parse } from './parser.js'; import { computeCheapSet } from './model.js'; import { esc, renderTreeHtml } from './render.js'; +import { formatWarning } from './warnings.js'; const INITIAL = `%% Project structure – Sprint 14 [~] Website relaunch (XL) https://wiki.example.com/relaunch @@ -57,7 +58,7 @@ function render(){ }); out.innerHTML = html; - warnBox.innerHTML = warnings.map(w => `
⚠ ${w}
`).join(''); + warnBox.innerHTML = warnings.map(w => `
⚠ ${formatWarning(w, t)}
`).join(''); drawCheapPath(); } diff --git a/frontend/src/render.js b/frontend/src/render.js index 746c64e..b4f357f 100644 --- a/frontend/src/render.js +++ b/frontend/src/render.js @@ -47,7 +47,8 @@ function renderChildren(node, warnings, opts){ if(!kids.length) return ''; const types = new Set(kids.map(k => k.type)); if(types.size > 1){ - warnings.push(opts.t('mixedWarn', {line: kids[0].line, label: esc(node.label)})); + /* strukturierte Warnung (Typ + Zeile); Formatierung in warnings.js */ + warnings.push({type: 'mixedGate', line: kids[0].line, label: node.label}); } const gate = gateOf(kids); const items = kids.map(k => { @@ -62,7 +63,8 @@ function renderChildren(node, warnings, opts){ } /* Baut den inneren HTML-String für #out aus (bereits gefilterten) Wurzeln und - sammelt Warnungen (gemischte Gates). Leere Wurzelliste ⇒ leerer String. */ + sammelt strukturierte Warnungen ({type, line, ...}, siehe warnings.js). + Leere Wurzelliste ⇒ leerer String. */ export function renderTreeHtml(roots, opts){ const warnings = []; const html = roots.map(root => { diff --git a/frontend/src/warnings.js b/frontend/src/warnings.js new file mode 100644 index 0000000..2e60430 --- /dev/null +++ b/frontend/src/warnings.js @@ -0,0 +1,26 @@ +/* Vereinheitlichtes Warnungs-Modell. + Producer (Renderer, künftig auch Parser) liefern strukturierte Objekte + { type, line, ...data } + statt fertig formatierter Strings — so bleiben Typ und Zeilennummer + maschinenlesbar (sortierbar, filterbar, testbar). Die i18n-/HTML-Aufbereitung + passiert an genau einer Stelle: formatWarning(). Vgl. SPEC §3 (gemischte + Gates) und TASKS Phase 2 (unbekannte Statuszeichen). + + Bekannte Typen: + - mixedGate { line, label } — Geschwister mit gemischtem Gate (SPEC §3) + - unknownStatus { line, code } — unbekanntes Statuszeichen (Phase 2) */ + +import { esc } from './render.js'; + +/* Strukturierte Warnung -> lokalisierter Anzeigetext (HTML-escaped Daten). + `t` ist die i18n-Funktion (key, vars) -> String. */ +export function formatWarning(w, t){ + switch(w.type){ + case 'mixedGate': + return t('mixedWarn', {line: w.line, label: esc(w.label)}); + case 'unknownStatus': + return t('unknownStatusWarn', {line: w.line, code: esc(w.code)}); + default: + return `${esc(String(w.type))} (${w.line ?? '?'})`; + } +} diff --git a/frontend/tests/render.test.js b/frontend/tests/render.test.js index b6b064f..dcf67dc 100644 --- a/frontend/tests/render.test.js +++ b/frontend/tests/render.test.js @@ -79,6 +79,20 @@ describe('renderTreeHtml — „Untergliederung fehlt" (Geister-Knoten, SPEC §5 }); }); +describe('renderTreeHtml — strukturierte Warnungen', () => { + it('gemischte Gates ergeben eine {type:mixedGate, line, label}-Warnung', () => { + let {roots} = parse('- Eltern\n - und-Kind\n | oder-Kind'); + const {warnings} = renderTreeHtml(roots, {t, showDiscarded: false, cheapPath: false, cheapSet: new Set()}); + expect(warnings).toEqual([{type: 'mixedGate', line: 2, label: 'Eltern'}]); + }); + + it('einheitliche Gates ergeben keine Warnung', () => { + let {roots} = parse('- Eltern\n - a\n - b'); + const {warnings} = renderTreeHtml(roots, {t, showDiscarded: false, cheapPath: false, cheapSet: new Set()}); + expect(warnings).toEqual([]); + }); +}); + describe('renderTreeHtml — Moduswechsel ist CSS, nicht Renderer', () => { it('erzeugt nie eine Modus-Klasse (vertical/kompakt) — die setzt app.js am Container', () => { const {html} = renderExample(); diff --git a/frontend/tests/warnings.test.js b/frontend/tests/warnings.test.js new file mode 100644 index 0000000..456c51b --- /dev/null +++ b/frontend/tests/warnings.test.js @@ -0,0 +1,23 @@ +import { describe, it, expect } from 'vitest'; +import { formatWarning } from '../src/warnings.js'; + +/* Stub bildet key + interpolierte Variablen sichtbar ab, damit wir prüfen + können, welche Werte (inkl. Escaping) formatWarning durchreicht. */ +const t = (key, vars) => `${key}|${JSON.stringify(vars)}`; + +describe('formatWarning — vereinheitlichtes Warnungs-Modell', () => { + it('mixedGate reicht Zeile durch und HTML-escaped das Label', () => { + const out = formatWarning({type: 'mixedGate', line: 7, label: 'A & '}, t); + expect(out).toBe('mixedWarn|{"line":7,"label":"A & <B>"}'); + }); + + it('unknownStatus reicht Zeile durch und escaped den Code', () => { + const out = formatWarning({type: 'unknownStatus', line: 3, code: '<'}, t); + expect(out).toBe('unknownStatusWarn|{"line":3,"code":"<"}'); + }); + + it('unbekannter Typ fällt auf eine generische, escapte Meldung zurück', () => { + expect(formatWarning({type: 'was?', line: 9}, t)).toBe('was? (9)'); + expect(formatWarning({type: 'x'}, t)).toBe('x (?)'); + }); +});