import { afterEach, describe, expect, it } from 'vitest'; import { ProjectStore } from '@/app/store'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { ANLAGENART_GRENZEN } from '@/domain/rilsa/constants'; import type { Project } from '@/domain/model/project'; import type { ViewDefinition } from '@/ui/shell'; import { HILFE } from '@/ui/help/texte'; import { ERSATZWERT_ERLAEUTERUNG, planView } from '@/ui/views/planView'; import { phasesView } from '@/ui/views/phasesView'; import { projectView } from '@/ui/views/projectView'; import { settingsView } from '@/ui/views/settingsView'; import { signalGroupsView } from '@/ui/views/signalGroupsView'; /** * Die stillen Ersetzungen an der Oberflaeche: * * - Vorgaben-Ansicht: Eine Vorgabe ausserhalb der Schranken steht nicht nur * im Pruefbericht - der Balken oben nennt die Meldung, "wirksam N s" steht * neben dem Feld (wie an der Mindestfreigabezeit, Befund C9). * - Projekt-Ansicht: Der Dialog vor dem Anlagenart-Wechsel nennt auch die * Umlaufzeit-Vorgaben, die durch den Wechsel zum Fehler mit Sperre werden. * - Signalzeitenplan und Phasen: Der Ersatzwert ist "die groesste zulaessige * Umlaufzeit", nicht "die Obergrenze der Anlagenart" - eine engere eigene * Vorgabe gilt ebenso. * - Signalgruppen: Schwerverkehrsanteil bis 100 %, wie Schema und Rechnung. * * Gegen den Altstand schlagen die Faelle fehl: kein Balken, kein "wirksam", * kein Satz zur Vorgabe im Dialog, "Obergrenze der Anlagenart", max 90. */ const FESTES_DATUM = new Date('2026-01-01T00:00:00Z'); 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(); }, }; } function mitVorgabe( anlagenart: Project['anlagenart'], rilsa: Project['settings']['rilsa'], ): Project { const roh = createStandardIntersectionProject('Vorgaben', FESTES_DATUM); return { ...roh, anlagenart, settings: { ...roh.settings, rilsa: { ...roh.settings.rilsa, ...rilsa } }, }; } function uebersaettigt(rilsa: Project['settings']['rilsa'] = {}): Project { const p = createStandardIntersectionProject('Ersatzwert', FESTES_DATUM); return { ...p, settings: { ...p.settings, rilsa: { ...p.settings.rilsa, ...rilsa } }, demands: p.demands.map((d) => ({ ...d, volume: 1500 })), }; } /** Zeile der Vorgabentabelle zu einem Kennwert. */ function zeileZu(wurzel: ParentNode, label: string): HTMLTableRowElement { const zeile = [...wurzel.querySelectorAll('tbody tr')].find( (tr) => tr.children[0]?.textContent?.trim() === label, ); if (!zeile) throw new Error(`Keine Zeile "${label}"`); return zeile; } function knopfMit(text: string): HTMLButtonElement | undefined { return [...document.querySelectorAll('button')].find( (b) => (b.textContent ?? '').trim() === text, ); } async function abwarten(): Promise { await new Promise((fertig) => setTimeout(fertig, 0)); } afterEach(() => { document.body.replaceChildren(); }); describe('Vorgaben: unzulaessige Vorgabe sichtbar an Ort und Stelle', () => { it('nennt am Knotenpunkt bei "Groesste Umlaufzeit 250 s" die Meldung im Balken und "wirksam 120 s" am Feld', () => { const p = mitVorgabe('knotenpunkt', { cycleTimeMax: 250 }); const { wurzel, aufraeumen } = zeichne(settingsView, p); try { const balken = wurzel.querySelector('.hinweisbalken-fehler'); expect(balken, 'Kein Fehlerbalken fuer die unzulaessige Vorgabe').not.toBeNull(); expect(balken!.textContent).toContain('1 Vorgabe wird nicht wie eingetragen angesetzt'); expect(balken!.textContent).toContain('"Größte Umlaufzeit" wurde auf 250 s gesetzt'); expect(balken!.textContent).toContain('höchstens 120 s'); const zeile = zeileZu(wurzel, 'Größte Umlaufzeit'); const feld = zeile.querySelector('input')!; // Der eingetragene Wert bleibt stehen - nichts wird stumm ueberschrieben. expect(feld.value).toBe('250'); expect(zeile.textContent).toContain('wirksam 120 s'); } finally { aufraeumen(); } }); it('zeigt bei einstreifiger Fuehrung mit 250 s weder Balken noch "wirksam"', () => { const p = mitVorgabe('einstreifig', { cycleTimeMax: 250 }); expect(ANLAGENART_GRENZEN.einstreifig.cycleTime.max).toBeGreaterThanOrEqual(250); const { wurzel, aufraeumen } = zeichne(settingsView, p); try { expect(wurzel.querySelector('.hinweisbalken-fehler')).toBeNull(); expect(wurzel.textContent).not.toContain('nicht wie eingetragen angesetzt'); expect(zeileZu(wurzel, 'Größte Umlaufzeit').textContent).not.toContain('wirksam'); } finally { aufraeumen(); } }); it('nennt "wirksam" nur an dem Feld, das abweicht', () => { const p = mitVorgabe('knotenpunkt', { cycleTimeMax: 250, cycleTimeMin: 40 }); const { wurzel, aufraeumen } = zeichne(settingsView, p); try { expect(zeileZu(wurzel, 'Größte Umlaufzeit').textContent).toContain('wirksam 120 s'); expect(zeileZu(wurzel, 'Kleinste Umlaufzeit').textContent).not.toContain('wirksam'); } finally { aufraeumen(); } }); }); describe('Projekt: Anlagenart-Wechsel warnt vor Vorgaben, die zum Fehler werden', () => { 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"').not.toBeNull(); return feld as HTMLSelectElement; } it('nennt beim Wechsel einstreifig -> Knotenpunkt die groesste Umlaufzeit von 250 s', async () => { const p = mitVorgabe('einstreifig', { cycleTimeMax: 250 }); const { wurzel, store, aufraeumen } = zeichne(projectView, p); try { const feld = auswahl(wurzel); feld.value = 'knotenpunkt'; feld.dispatchEvent(new Event('change')); const dialog = document.querySelector('.dialog'); expect(dialog, 'Die Umstellung geschah ohne Rueckfrage').not.toBeNull(); const text = dialog?.textContent ?? ''; expect(text).toContain('„Größte Umlaufzeit“ von 250 s'); expect(text).toContain('30 bis 120 s'); expect(text).toContain('gerechnet wird mit 120 s'); expect(text).toContain('Export ist gesperrt'); knopfMit('Anlagenart behalten')?.click(); await abwarten(); expect(store.getProject().anlagenart).toBe('einstreifig'); expect(store.getProject().settings.rilsa.cycleTimeMax).toBe(250); } finally { aufraeumen(); } }); it('fragt nicht nach, wenn die Vorgabe schon jetzt ausserhalb liegt oder nach dem Wechsel passt', async () => { // Knotenpunkt mit 250 s ist bereits ein Fehler; einstreifig nimmt 250 s an. const p = mitVorgabe('knotenpunkt', { cycleTimeMax: 250 }); const { wurzel, store, aufraeumen } = zeichne(projectView, p); try { const feld = auswahl(wurzel); feld.value = 'einstreifig'; feld.dispatchEvent(new Event('change')); await abwarten(); expect(document.querySelector('.dialog')).toBeNull(); expect(store.getProject().anlagenart).toBe('einstreifig'); } finally { aufraeumen(); } }); }); describe('Ersatzwert: "groesste zulaessige Umlaufzeit", nicht "Obergrenze der Anlagenart"', () => { it('sagt es in der Fussnote der Kennzahl und im Phasen-Hinweis - mit EINEM Text', () => { // Knotenpunkt mit engerer Vorgabe 90 s: der Ersatzwert ist 90 s, nicht 120 s. const p = uebersaettigt({ cycleTimeMax: 90 }); const plan = buildSignalPlan(p); expect(plan.cycleResult?.bounded, 'Vorbedingung').toBe('uebersaettigt'); expect(plan.cycleTime).toBe(90); expect(ERSATZWERT_ERLAEUTERUNG).toContain('größte zulässige Umlaufzeit'); expect(ERSATZWERT_ERLAEUTERUNG).toContain('engere eigene Vorgabe'); expect(ERSATZWERT_ERLAEUTERUNG).not.toContain('Obergrenze der Anlagenart'); const plan1 = zeichne(planView, p); try { const fussnote = plan1.wurzel.querySelector('.kennzahl-fussnote'); expect(fussnote?.textContent).toBe(ERSATZWERT_ERLAEUTERUNG); expect(plan1.wurzel.textContent).not.toContain('Obergrenze der Anlagenart'); } finally { plan1.aufraeumen(); } const phasen = zeichne(phasesView, p); try { expect(phasen.wurzel.textContent).toContain(ERSATZWERT_ERLAEUTERUNG); expect(phasen.wurzel.textContent).not.toContain('Obergrenze der Anlagenart'); } finally { phasen.aufraeumen(); } }); it('sagt es auch in der Hilfe - und nennt die Zeichnung als Ausgabe mit Vermerk', () => { expect(HILFE.umlaufzeit.woher).toContain('größte zulässige Umlaufzeit'); expect(HILFE.umlaufzeit.woher).toContain('engere eigene Vorgabe'); expect(HILFE.umlaufzeit.woher).toContain('die Zeichnung'); expect(HILFE.umlaufzeit.woher).not.toContain('Obergrenze der Anlagenart'); }); }); describe('Signalgruppen: Schwerverkehrsanteil bis 100 %', () => { it('laesst das Feld bis 100 % zu - wie Schema und Rechnung', () => { const p = createStandardIntersectionProject('SV', FESTES_DATUM); const { wurzel, aufraeumen } = zeichne(signalGroupsView, p); try { const felder = [...wurzel.querySelectorAll('input[type="number"]')].filter( (f) => f.getAttribute('max') === '100' && f.getAttribute('min') === '0', ); expect(felder.length).toBeGreaterThan(0); expect(wurzel.querySelector('input[max="90"]')).toBeNull(); } finally { aufraeumen(); } }); });