import { afterEach, describe, expect, it } from 'vitest'; import { ProjectStore } from '@/app/store'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import type { Project } from '@/domain/model/project'; import { RILSA_DEFAULTS } from '@/domain/rilsa/constants'; import { mitBewahrtemFokus } from '@/ui/fokus'; import type { ViewContext } from '@/ui/shell'; import { signalGroupsView } from '@/ui/views/signalGroupsView'; /** * Folge aus den Befunden 36 und 42 zur Fassung 5.9.0. * * Die Hoechstfreigabezeit je Signalgruppe (`maxGreenOverride`) wirkt seit * diesen Befunden in der Freigabezeitverteilung (`phaseMaxGreen` in * domain/plan/signalPlan.ts) - ein Eingabefeld hatte sie nicht. Sie konnte nur * aus einer Projektdatei oder aus dem Aufstieg von 4.x kommen; ueber die * Bedienung war sie weder zu setzen noch zu entfernen. Damit lief zugleich der * Rat der Regel `freigabezeit-rest-umgelegt` ins Leere: "... erfassen Sie die * Verkehrsstärken der maßgebenden Signalgruppen oder heben Sie deren * Höchstfreigabezeit an." * * DIE GRENZEN DES FELDES: Eine Hoechstfreigabezeit unter der * Mindestfreigabezeit ist ein Widerspruch. Der Fachkern faengt ihn ab - er * rechnet mit der Mindestfreigabezeit und meldet die Vorgabe als wirkungslos * ('hoechstfreigabezeit-unter-mindestfreigabezeit'). Das Feld nimmt deshalb * dieselbe Untergrenze wie das Feld der Mindestfreigabezeit daneben: den Wert, * der bei leerem Feld gilt. Was zwischen dieser Untergrenze und der * Mindestfreigabezeit der PHASE liegt, bleibt eintragbar - dort ist die * Meldung des Pruefberichts die richtige Antwort, nicht eine Sperre am Feld. */ const FESTES_DATUM = new Date('2026-01-01T00:00:00Z'); let aufraeumen: (() => void) | null = null; afterEach(() => { aufraeumen?.(); aufraeumen = null; document.body.replaceChildren(); }); interface Ansicht { readonly wurzel: HTMLElement; readonly store: ProjectStore; } function zeichne(project: Project): Ansicht { const wurzel = document.createElement('div'); document.body.append(wurzel); const store = new ProjectStore(project); let abmelden: () => void = () => {}; const context: ViewContext = { store, refresh: () => { mitBewahrtemFokus(wurzel, () => { abmelden(); wurzel.replaceChildren(); abmelden = signalGroupsView.render(wurzel, context); }); }, navigate: () => {}, target: null, }; abmelden = signalGroupsView.render(wurzel, context); aufraeumen = () => { abmelden(); wurzel.remove(); }; return { wurzel, store }; } function k1(project: Project): string { const gruppe = project.signalGroups.find((g) => g.name === 'K1'); if (gruppe === undefined) throw new Error('Keine Signalgruppe "K1"'); return gruppe.id; } function feld(ansicht: Ansicht, groupId: string, name: string): HTMLInputElement { const knoten = ansicht.wurzel.querySelector( `[data-fokus="signalgruppe|${groupId}|${name}"]`, ); if (knoten === null) throw new Error(`Kein Feld "${name}"`); return knoten; } function trageEin(eingabe: HTMLInputElement, wert: string): void { eingabe.value = wert; eingabe.dispatchEvent(new Event('change', { bubbles: true })); } function hoechstfreigabe(ansicht: Ansicht, groupId: string): number | null { return ( ansicht.store.getProject().signalGroups.find((g) => g.id === groupId)?.maxGreenOverride ?? null ); } describe('Signalgruppen: die Höchstfreigabezeit ist eintragbar', () => { it('zeigt ein leeres Feld mit dem Regelwert als Platzhalter', () => { const projekt = createStandardIntersectionProject('Höchstfreigabe', FESTES_DATUM); const ansicht = zeichne(projekt); const eingabe = feld(ansicht, k1(projekt), 'hoechstfreigabe'); expect(eingabe.value).toBe(''); expect(eingabe.placeholder).toBe(String(RILSA_DEFAULTS.maxGreen)); }); it('übernimmt einen eingetragenen Wert in die Signalgruppe', () => { const projekt = createStandardIntersectionProject('Höchstfreigabe', FESTES_DATUM); const ansicht = zeichne(projekt); const id = k1(projekt); trageEin(feld(ansicht, id, 'hoechstfreigabe'), '45'); expect(hoechstfreigabe(ansicht, id)).toBe(45); // Nach dem Neuaufbau steht der Wert im Feld. expect(feld(ansicht, id, 'hoechstfreigabe').value).toBe('45'); }); it('entfernt die Vorgabe wieder, wenn das Feld geleert wird', () => { const basis = createStandardIntersectionProject('Höchstfreigabe', FESTES_DATUM); const id = k1(basis); const projekt: Project = { ...basis, signalGroups: basis.signalGroups.map((g) => g.id === id ? { ...g, maxGreenOverride: 40 } : g, ), }; const ansicht = zeichne(projekt); expect( feld(ansicht, id, 'hoechstfreigabe').value, 'Vorbedingung: die Vorgabe steht im Feld', ).toBe('40'); trageEin(feld(ansicht, id, 'hoechstfreigabe'), ''); expect(hoechstfreigabe(ansicht, id)).toBeNull(); }); it('hebt eine Eingabe unter der Mindestfreigabezeit auf diese an', () => { const projekt = createStandardIntersectionProject('Höchstfreigabe', FESTES_DATUM); const ansicht = zeichne(projekt); const id = k1(projekt); const untergrenze = Number(feld(ansicht, id, 'mindestfreigabe').min); expect(untergrenze, 'Vorbedingung: das Nachbarfeld nennt eine Untergrenze').toBeGreaterThan(0); trageEin(feld(ansicht, id, 'hoechstfreigabe'), '2'); expect(hoechstfreigabe(ansicht, id)).toBe(untergrenze); }); it('sagt das Feld ausgeschrieben an wie das Nachbarfeld', () => { // Der Spaltenkopf ist abgekuerzt, damit die Spalte schmal bleibt; den Namen // des Bedienelements bildet `beschrifteTabelle` aus ihm. Ohne Eintrag in // der Karte `ausgeschrieben` hiesse das Feld einer Sprachausgabe // "Höchstfreigabe, K1 ..." - eine Abkuerzung ohne Einheit, waehrend das // unmittelbare Nachbarfeld derselben Zeile ausgeschrieben angesagt wird. const projekt = createStandardIntersectionProject('Höchstfreigabe', FESTES_DATUM); const ansicht = zeichne(projekt); const id = k1(projekt); const nachbar = feld(ansicht, id, 'mindestfreigabe').getAttribute('aria-label') ?? ''; expect(nachbar, 'Vorbedingung: das Nachbarfeld wird ausgeschrieben angesagt').toMatch( /^Mindestfreigabezeit in Sekunden, /, ); const name = feld(ansicht, id, 'hoechstfreigabe').getAttribute('aria-label') ?? ''; expect(name).toBe(nachbar.replace('Mindestfreigabezeit', 'Höchstfreigabezeit')); }); it('trägt die Spalte im Tabellenkopf', () => { const projekt = createStandardIntersectionProject('Höchstfreigabe', FESTES_DATUM); const ansicht = zeichne(projekt); const koepfe = [...ansicht.wurzel.querySelectorAll('thead th')].map( (th) => th.textContent?.replace('?', '').trim() ?? '', ); expect(koepfe).toContain('Höchstfreigabe'); }); });