import { afterEach, describe, expect, it } from 'vitest'; import { ProjectStore } from '@/app/store'; import { ASSISTENT_VORGABE, projektAusAngaben } from '@/ui/assistent'; import { ANLAGENART_GRENZEN } from '@/domain/rilsa/constants'; import { CURRENT_SCHEMA_VERSION, type Project } from '@/domain/model/project'; import { loadProject } from '@/domain/model/migrate'; import type { ViewDefinition } from '@/ui/shell'; import { projectView } from '@/ui/views/projectView'; /** * Der Dialog beim Wechsel der Anlagenart muss den ZEITPUNKT der Kuerzung * richtig nennen (Befund 12 zur Fassung 5.8.0). * * WAS DER ANWENDER SONST ERLEBT: Er stellt eine einstreifige Fuehrung mit * 400 m Raeumweg auf "Fussgaengerschutzanlage" um. Der Dialog nennt als * Zeitpunkt der Kuerzung nur "beim naechsten Aendern eines dieser Felder" - er * fasst also kein Feld mehr an und glaubt die vermessene Laenge sicher. * Gekuerzt wird aber schon beim naechsten EINLESEN: Nach einem Programmstart * holt die Sitzungswiederherstellung das Projekt mit 100 m zurueck, und die * Zwischenzeit steht rund 30 s zu kurz da. */ const FESTES_DATUM = '2026-01-01T00:00:00.000Z'; const FESTES_DATUM_OBJEKT = new Date(FESTES_DATUM); /** Einstreifige Fuehrung, alle Konfliktwege am Lageplan mit 400 m vermessen. */ function projektMitLangenWegen(): Project { const basis = projektAusAngaben( { ...ASSISTENT_VORGABE, fussgaenger: true, rad: true, linksabbieger: true }, FESTES_DATUM, ); return { ...basis, anlagenart: 'einstreifig', conflicts: basis.conflicts.map((c) => ({ ...c, clearingDistance: 400 })), }; } function zeichne( view: ViewDefinition, project: Project, ): { wurzel: HTMLElement; store: ProjectStore; 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, store, aufraeumen: () => { aufraeumen(); wurzel.remove(); }, }; } /** Das Auswahlfeld der Anlagenart in der Projektansicht. */ function auswahl(wurzel: ParentNode): HTMLSelectElement { const label = [...wurzel.querySelectorAll('label')].find( (l) => (l.textContent ?? '').trim() === 'Anlagenart', ); const feld = wurzel.querySelector(`#${label?.htmlFor ?? ''}`); expect(feld, 'Kein Auswahlfeld "Anlagenart" in der Projektansicht').not.toBeNull(); return feld as HTMLSelectElement; } function knopfMit(text: string): HTMLButtonElement | undefined { return [...document.querySelectorAll('button')].find( (b) => (b.textContent ?? '').trim() === text, ); } /** Laesst die Warteschlange der Mikroaufgaben leerlaufen. */ async function abwarten(): Promise { await new Promise((fertig) => setTimeout(fertig, 0)); } /** * Das Projekt so einlesen, wie es die Sitzungswiederherstellung tut: ueber die * gespeicherte Rohgestalt und `loadProject`. Kein Feld wird dabei angefasst. */ function wiedereingelesen(project: Project): ReturnType { const roh: unknown = JSON.parse( JSON.stringify({ ...project, schemaVersion: CURRENT_SCHEMA_VERSION }), ); return loadProject(roh, FESTES_DATUM_OBJEKT); } afterEach(() => { // Dialoge und Kurzmeldungen haengen an document.body, nicht an der Ansicht. document.body.replaceChildren(); }); describe('Anlagenartwechsel: der Dialog nennt den Zeitpunkt der Kuerzung', () => { it('nennt das naechste Oeffnen des Projekts und die Sitzungswiederherstellung, nicht nur das Aendern eines Feldes', async () => { const { wurzel, aufraeumen } = zeichne(projectView, projektMitLangenWegen()); try { const feld = auswahl(wurzel); feld.value = 'fussgaengerschutzanlage'; feld.dispatchEvent(new Event('change')); const text = document.querySelector('.dialog')?.textContent ?? ''; expect(text, 'Die Umstellung geschah ohne Rueckfrage').not.toBe(''); // Die betroffenen Zahlen stehen weiterhin im Dialog. expect(text).toContain('400'); expect(text).toContain( String(ANLAGENART_GRENZEN.fussgaengerschutzanlage.maxClearingDistance), ); // Der eigentliche Punkt: Gekuerzt wird beim Einlesen, also auch ohne // jede Bedienhandlung. Steht das nicht da, verlaesst sich der Bearbeiter // darauf, dass die vermessenen 400 m bleiben, solange er sie in Ruhe // laesst. expect( text, 'Der Dialog nennt das Oeffnen des Projekts nicht als Zeitpunkt der Kuerzung', ).toContain('nächsten Öffnen'); expect( text, 'Der Dialog nennt die Sitzungswiederherstellung nicht, obwohl sie ebenso kuerzt', ).toContain('Sitzung'); knopfMit('Umstellen')?.click(); await abwarten(); } finally { aufraeumen(); } }); it('kuerzt den vermessenen Weg tatsaechlich schon beim Wiedereinlesen, ohne dass ein Feld angefasst wurde', async () => { const { wurzel, store, aufraeumen } = zeichne(projectView, projektMitLangenWegen()); try { const feld = auswahl(wurzel); feld.value = 'fussgaengerschutzanlage'; feld.dispatchEvent(new Event('change')); knopfMit('Umstellen')?.click(); await abwarten(); // Im Speicher steht weiterhin die vermessene Laenge - so weit haelt die // Zusage des Dialogs. const umgestellt = store.getProject(); expect(umgestellt.anlagenart).toBe('fussgaengerschutzanlage'); expect(umgestellt.conflicts[0]?.clearingDistance).toBe(400); // Nach dem Wiedereinlesen nicht mehr. Das ist der Zeitpunkt, den der // Dialog nennen muss. const ergebnis = wiedereingelesen(umgestellt); expect(ergebnis.project.conflicts[0]?.clearingDistance).toBe( ANLAGENART_GRENZEN.fussgaengerschutzanlage.maxClearingDistance, ); expect(ergebnis.issues.some((i) => i.message.includes('Räumweg'))).toBe(true); } finally { aufraeumen(); } }); });