import { afterEach, describe, expect, it } from 'vitest'; import { ProjectStore } from '@/app/store'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { validateProject } from '@/domain/validation'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import type { Project } from '@/domain/model/project'; import { nurHinweiseUeberschrift } from '@/ui/inputs'; import type { ViewDefinition } from '@/ui/shell'; import { exportView } from '@/ui/views/exportView'; /** * Ausgabe-Ansicht, Befunde zur Fassung 5.10.0. * * BEFUND 27: Der Balken kannte nur `exportBlocked` und `warningCount`. Ein * Bericht mit ausschliesslich Hinweisen fiel deshalb auf "Keine * Beanstandungen" zurueck, obwohl der Pruefbericht Eintraege fuehrt - und * "Beanstandung" meint in diesem Programm alle drei Schweregrade (die * Planunterlage sagt den Satz nur bei `report.findings.length === 0`, * src/services/export/pdf.ts). Der Anwender bekam die Freigabe zur Ausgabe, * ohne dass ihm die Luecke genannt wurde. * * BEFUND 29: Balken und Rueckfrage sagten unbedingt zu, die Warnungen stuenden * im Ausdruck. Ist der Schalter "Prüfbericht" abgewaehlt, druckt die Unterlage * keinen einzigen Wortlaut - nur die Anzahl auf dem Deckblatt. * * BEFUND 58: Balken und Rueckfrage setzten die Zahl unbesehen vor * "Warnungen"; bei genau einer Warnung stand dort "1 Warnungen", waehrend der * Abschnitt "Tabellen" und der Ansageraum derselben Ansicht "1 Warnung" * bildeten. */ const FESTES_DATUM = new Date('2026-01-01T00:00:00Z'); function zeichne( view: ViewDefinition, project: Project, ): { wurzel: HTMLElement; aufraeumen: () => void } { const wurzel = document.createElement('div'); document.body.append(wurzel); const store = new ProjectStore(project); const aufraeumen = view.render(wurzel, { store, refresh: () => {}, navigate: () => {}, target: null, }); return { wurzel, aufraeumen: () => { aufraeumen(); wurzel.remove(); }, }; } function mitMeta(p: Project, meta: Partial): Project { return { ...p, meta: { ...p.meta, ...meta } }; } /** * Der Normalzustand einer fertigen Planung: alle vier Pflichtangaben der * Projektdaten gefuellt, die beiden Anordnungsangaben nicht. Am Knotenpunkt * sind das genau zwei HINWEISE (engine.ts, checkAnordnungsangaben). */ function nurHinweise(): Project { const p = mitMeta(createStandardIntersectionProject('Nur Hinweise', FESTES_DATUM), { projectNumber: '2026-001', client: 'Stadt', planner: 'Bearbeiterin', location: 'Hauptstraße', }); const bericht = validateProject(p, buildSignalPlan(p), FESTES_DATUM); expect([bericht.errorCount, bericht.warningCount, bericht.hintCount]).toEqual([0, 0, 2]); return p; } /** Genau eine offene Warnung: nur der Ort fehlt. */ function eineWarnung(): Project { const p = mitMeta(createStandardIntersectionProject('Eine Warnung', FESTES_DATUM), { projectNumber: '2026-002', client: 'Stadt', planner: 'Bearbeiterin', }); const bericht = validateProject(p, buildSignalPlan(p), FESTES_DATUM); expect(bericht.errorCount).toBe(0); expect(bericht.warningCount).toBe(1); return p; } function balken(wurzel: ParentNode): HTMLElement { const gefunden = wurzel.querySelector('.hinweisbalken'); if (gefunden === null) throw new Error('Kein Hinweisbalken in der Ansicht'); return gefunden; } /** * Der Ansageraum der Ansicht - der einzige Live-Bereich, der den Neuaufbau * ueberlebt (Befund L5). Der Rechenweg-Vermerk traegt zwar auch `role="status"`, * aber kein `aria-live`. */ function ansageKnoten(wurzel: ParentNode): HTMLElement { const gefunden = wurzel.querySelector('[role="status"][aria-live]'); if (gefunden === null) throw new Error('Kein Ansageraum in der Ansicht'); return gefunden; } function ansage(wurzel: ParentNode): string { return ansageKnoten(wurzel).textContent ?? ''; } /** Die Ueberschrift des Balkens - die fette Zeile darin. */ function balkenkopf(wurzel: ParentNode): string { return balken(wurzel).querySelector('strong')?.textContent ?? ''; } function knopf(wurzel: ParentNode, beschriftung: string): HTMLButtonElement { const gefunden = [...wurzel.querySelectorAll('button')].find( (b) => (b.textContent ?? '').trim() === beschriftung, ); if (!gefunden) throw new Error(`Kein Knopf "${beschriftung}"`); return gefunden; } /** Kontrollkaestchen ueber seine sichtbare Beschriftung. */ function schalter(wurzel: ParentNode, beschriftung: string): HTMLInputElement { const marke = [...wurzel.querySelectorAll('label')].find( (l) => (l.textContent ?? '').trim() === beschriftung, ); if (!marke) throw new Error(`Kein Schalter "${beschriftung}"`); const feld = wurzel.querySelector(`#${marke.htmlFor}`); if (feld === null) throw new Error(`Schalter "${beschriftung}" ohne Eingabefeld`); return feld; } function schalte(feld: HTMLInputElement, an: boolean): void { feld.checked = an; feld.dispatchEvent(new Event('change')); } /** Laesst die angestossenen Zusagen der Reihe nach ablaufen. */ async function ruhe(): Promise { for (let i = 0; i < 6; i += 1) { await new Promise((erfuellen) => setTimeout(erfuellen, 0)); } } function dialogtext(): string { return document.querySelector('.dialog .dialog-text')?.textContent ?? ''; } function knopfImFenster(beschriftung: string): HTMLButtonElement { const fenster = document.querySelector('.dialog'); if (fenster === null) throw new Error('Es steht kein Fenster offen.'); const treffer = [...fenster.querySelectorAll('button')].find( (b) => (b.textContent ?? '').trim() === beschriftung, ); if (treffer === undefined) throw new Error(`Kein Knopf "${beschriftung}" im Fenster`); return treffer; } afterEach(() => { document.body.replaceChildren(); }); describe('Ausgabe: Balken und Rückfrage nennen den Prüfstand richtig', () => { it('nennt Hinweise, statt "Keine Beanstandungen" zu melden (Befund 27)', () => { const { wurzel, aufraeumen } = zeichne(exportView, nurHinweise()); try { const text = balken(wurzel).textContent ?? ''; expect(text, 'der Balken bestreitet vorhandene Beanstandungen').not.toContain( 'Keine Beanstandungen', ); expect(text).toContain('2 Hinweise'); // Und zwar mit dem Wortlaut, den auch der Pruefbericht fuehrt: Er steht // an EINER Stelle (`nurHinweiseUeberschrift` in src/ui/inputs.ts) und // nicht zweimal getrennt zusammengesetzt. expect(balkenkopf(wurzel)).toBe(nurHinweiseUeberschrift(2)); // Die Ausgabe bleibt moeglich - Hinweise sperren nicht, und das ist so // gewollt (engine.ts: exportBlocked haengt allein an errorCount). expect(text).toContain('Die Planunterlagen können ausgegeben werden.'); expect(knopf(wurzel, 'PDF erzeugen').disabled).toBe(false); // Und der Ansageraum sagt dasselbe wie der Balken. expect(ansage(wurzel)).not.toContain('keine Beanstandungen'); expect(ansage(wurzel)).toContain('2 Hinweise'); } finally { aufraeumen(); } }); it('bildet die Einzahl bei genau einer Warnung (Befund 58)', () => { const { wurzel, aufraeumen } = zeichne(exportView, eineWarnung()); try { const text = balken(wurzel).textContent ?? ''; expect(text).toContain('1 Warnung'); expect(text, '"1 Warnungen" widerspricht dem Abschnitt "Tabellen"').not.toContain( '1 Warnungen', ); // Was der Ansageraum schon immer richtig machte, steht jetzt auch im // sichtbaren Balken. expect(ansage(wurzel)).toContain('1 Warnung im Prüfbericht'); } finally { aufraeumen(); } }); it('bindet die Zusage "im Ausdruck" an den Schalter "Prüfbericht" (Befund 29, Balken)', () => { const { wurzel, aufraeumen } = zeichne(exportView, eineWarnung()); try { expect(balken(wurzel).textContent).toContain('Die Warnungen erscheinen im Ausdruck'); schalte(schalter(wurzel, 'Prüfbericht'), false); const text = balken(wurzel).textContent ?? ''; expect(text, 'die Zusage steht trotz abgewähltem Prüfbericht').not.toContain( 'Die Warnungen erscheinen im Ausdruck', ); expect(text).toContain('Der Prüfbericht ist abgewählt'); expect(text).toContain('Deckblatt'); // Wieder angewaehlt gilt die Zusage wieder. schalte(schalter(wurzel, 'Prüfbericht'), true); expect(balken(wurzel).textContent).toContain('Die Warnungen erscheinen im Ausdruck'); } finally { aufraeumen(); } }); it('sagt den geänderten Vorbehalt im Ansageraum an (Befund 29, Nachtrag)', () => { /* * Der Satz im Balken wechselt seinen TEXT, ohne dass der Knoten ersetzt * wird - genau der Fall, den tests/ui/bfStapelUndAnsageraeume.test.ts dem * Ansageraum zuordnet. Ein zweites `aria-live` an diesem Absatz taete es * nicht: Er entsteht in `zeichne()` und wird beim Neuaufbau ersetzt, und * ein neu eingehaengter Live-Bereich sagt nichts an (Befund L5). Der * Ansageraum der Ansicht steht ausserhalb des ausgetauschten Teils. */ const { wurzel, aufraeumen } = zeichne(exportView, eineWarnung()); try { expect(ansage(wurzel)).toBe('Ausgabe möglich – 1 Warnung im Prüfbericht.'); schalte(schalter(wurzel, 'Prüfbericht'), false); const abgewaehlt = ansage(wurzel); expect(abgewaehlt, 'der Ansageraum hat vom geänderten Vorbehalt nichts erfahren').toContain( 'Der Prüfbericht ist abgewählt', ); expect(abgewaehlt).toContain('Deckblatt'); // Er sagt woertlich das an, was jetzt im Balken steht - zwei Fassungen // desselben Vorbehalts liefen sonst auseinander. expect(balken(wurzel).textContent).toContain(abgewaehlt); // Wieder angewaehlt gilt die Zusage wieder, und auch das wird gesagt. schalte(schalter(wurzel, 'Prüfbericht'), true); expect(ansage(wurzel)).toContain('Die Warnungen erscheinen im Ausdruck'); } finally { aufraeumen(); } }); it('sagt in der Rückfrage vor dem PDF, was der abgewählte Prüfbericht bedeutet (Befund 29)', async () => { const { wurzel, aufraeumen } = zeichne(exportView, eineWarnung()); try { schalte(schalter(wurzel, 'Prüfbericht'), false); knopf(wurzel, 'PDF erzeugen').click(); await ruhe(); const text = dialogtext(); expect(text, 'die Rückfrage sagt den Wortlaut im Ausdruck zu').not.toContain( 'Sie werden im Ausdruck aufgeführt', ); expect(text).toContain('Der Prüfbericht ist abgewählt'); expect(text).toContain('Deckblatt'); // Und die Einzahl stimmt auch hier (Befund 58). expect(text).toContain('1 Warnung.'); expect(text).not.toContain('1 Warnungen'); knopfImFenster('Abbrechen').click(); await ruhe(); } finally { aufraeumen(); } }); it('behält bei angewähltem Prüfbericht die bisherige Zusage in der Rückfrage', async () => { const p = createStandardIntersectionProject('Vier Warnungen', FESTES_DATUM); const bericht = validateProject(p, buildSignalPlan(p), FESTES_DATUM); expect(bericht.warningCount).toBeGreaterThan(1); const { wurzel, aufraeumen } = zeichne(exportView, p); try { knopf(wurzel, 'PDF erzeugen').click(); await ruhe(); const text = dialogtext(); expect(text).toContain(`Die Prüfung meldet ${bericht.warningCount} Warnungen.`); expect(text).toContain('Sie werden im Ausdruck aufgeführt'); knopfImFenster('Abbrechen').click(); await ruhe(); } finally { aufraeumen(); } }); });