lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests ui hoechstfreigabezeit.test.ts
| 1 | import { afterEach, describe, expect, it } from 'vitest'; |
| 2 | import { ProjectStore } from '@/app/store'; |
| 3 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 4 | import type { Project } from '@/domain/model/project'; |
| 5 | import { RILSA_DEFAULTS } from '@/domain/rilsa/constants'; |
| 6 | import { mitBewahrtemFokus } from '@/ui/fokus'; |
| 7 | import type { ViewContext } from '@/ui/shell'; |
| 8 | import { signalGroupsView } from '@/ui/views/signalGroupsView'; |
| 9 | |
| 10 | /** |
| 11 | * Folge aus den Befunden 36 und 42 zur Fassung 5.9.0. |
| 12 | * |
| 13 | * Die Hoechstfreigabezeit je Signalgruppe (`maxGreenOverride`) wirkt seit |
| 14 | * diesen Befunden in der Freigabezeitverteilung (`phaseMaxGreen` in |
| 15 | * domain/plan/signalPlan.ts) - ein Eingabefeld hatte sie nicht. Sie konnte nur |
| 16 | * aus einer Projektdatei oder aus dem Aufstieg von 4.x kommen; ueber die |
| 17 | * Bedienung war sie weder zu setzen noch zu entfernen. Damit lief zugleich der |
| 18 | * Rat der Regel `freigabezeit-rest-umgelegt` ins Leere: "... erfassen Sie die |
| 19 | * Verkehrsstärken der maßgebenden Signalgruppen oder heben Sie deren |
| 20 | * Höchstfreigabezeit an." |
| 21 | * |
| 22 | * DIE GRENZEN DES FELDES: Eine Hoechstfreigabezeit unter der |
| 23 | * Mindestfreigabezeit ist ein Widerspruch. Der Fachkern faengt ihn ab - er |
| 24 | * rechnet mit der Mindestfreigabezeit und meldet die Vorgabe als wirkungslos |
| 25 | * ('hoechstfreigabezeit-unter-mindestfreigabezeit'). Das Feld nimmt deshalb |
| 26 | * dieselbe Untergrenze wie das Feld der Mindestfreigabezeit daneben: den Wert, |
| 27 | * der bei leerem Feld gilt. Was zwischen dieser Untergrenze und der |
| 28 | * Mindestfreigabezeit der PHASE liegt, bleibt eintragbar - dort ist die |
| 29 | * Meldung des Pruefberichts die richtige Antwort, nicht eine Sperre am Feld. |
| 30 | */ |
| 31 | |
| 32 | const FESTES_DATUM = new Date('2026-01-01T00:00:00Z'); |
| 33 | |
| 34 | let aufraeumen: (() => void) | null = null; |
| 35 | afterEach(() => { |
| 36 | aufraeumen?.(); |
| 37 | aufraeumen = null; |
| 38 | document.body.replaceChildren(); |
| 39 | }); |
| 40 | |
| 41 | interface Ansicht { |
| 42 | readonly wurzel: HTMLElement; |
| 43 | readonly store: ProjectStore; |
| 44 | } |
| 45 | |
| 46 | function zeichne(project: Project): Ansicht { |
| 47 | const wurzel = document.createElement('div'); |
| 48 | document.body.append(wurzel); |
| 49 | const store = new ProjectStore(project); |
| 50 | let abmelden: () => void = () => {}; |
| 51 | const context: ViewContext = { |
| 52 | store, |
| 53 | refresh: () => { |
| 54 | mitBewahrtemFokus(wurzel, () => { |
| 55 | abmelden(); |
| 56 | wurzel.replaceChildren(); |
| 57 | abmelden = signalGroupsView.render(wurzel, context); |
| 58 | }); |
| 59 | }, |
| 60 | navigate: () => {}, |
| 61 | target: null, |
| 62 | }; |
| 63 | abmelden = signalGroupsView.render(wurzel, context); |
| 64 | aufraeumen = () => { |
| 65 | abmelden(); |
| 66 | wurzel.remove(); |
| 67 | }; |
| 68 | return { wurzel, store }; |
| 69 | } |
| 70 | |
| 71 | function k1(project: Project): string { |
| 72 | const gruppe = project.signalGroups.find((g) => g.name === 'K1'); |
| 73 | if (gruppe === undefined) throw new Error('Keine Signalgruppe "K1"'); |
| 74 | return gruppe.id; |
| 75 | } |
| 76 | |
| 77 | function feld(ansicht: Ansicht, groupId: string, name: string): HTMLInputElement { |
| 78 | const knoten = ansicht.wurzel.querySelector<HTMLInputElement>( |
| 79 | `[data-fokus="signalgruppe|${groupId}|${name}"]`, |
| 80 | ); |
| 81 | if (knoten === null) throw new Error(`Kein Feld "${name}"`); |
| 82 | return knoten; |
| 83 | } |
| 84 | |
| 85 | function trageEin(eingabe: HTMLInputElement, wert: string): void { |
| 86 | eingabe.value = wert; |
| 87 | eingabe.dispatchEvent(new Event('change', { bubbles: true })); |
| 88 | } |
| 89 | |
| 90 | function hoechstfreigabe(ansicht: Ansicht, groupId: string): number | null { |
| 91 | return ( |
| 92 | ansicht.store.getProject().signalGroups.find((g) => g.id === groupId)?.maxGreenOverride ?? null |
| 93 | ); |
| 94 | } |
| 95 | |
| 96 | describe('Signalgruppen: die Höchstfreigabezeit ist eintragbar', () => { |
| 97 | it('zeigt ein leeres Feld mit dem Regelwert als Platzhalter', () => { |
| 98 | const projekt = createStandardIntersectionProject('Höchstfreigabe', FESTES_DATUM); |
| 99 | const ansicht = zeichne(projekt); |
| 100 | const eingabe = feld(ansicht, k1(projekt), 'hoechstfreigabe'); |
| 101 | |
| 102 | expect(eingabe.value).toBe(''); |
| 103 | expect(eingabe.placeholder).toBe(String(RILSA_DEFAULTS.maxGreen)); |
| 104 | }); |
| 105 | |
| 106 | it('übernimmt einen eingetragenen Wert in die Signalgruppe', () => { |
| 107 | const projekt = createStandardIntersectionProject('Höchstfreigabe', FESTES_DATUM); |
| 108 | const ansicht = zeichne(projekt); |
| 109 | const id = k1(projekt); |
| 110 | |
| 111 | trageEin(feld(ansicht, id, 'hoechstfreigabe'), '45'); |
| 112 | |
| 113 | expect(hoechstfreigabe(ansicht, id)).toBe(45); |
| 114 | // Nach dem Neuaufbau steht der Wert im Feld. |
| 115 | expect(feld(ansicht, id, 'hoechstfreigabe').value).toBe('45'); |
| 116 | }); |
| 117 | |
| 118 | it('entfernt die Vorgabe wieder, wenn das Feld geleert wird', () => { |
| 119 | const basis = createStandardIntersectionProject('Höchstfreigabe', FESTES_DATUM); |
| 120 | const id = k1(basis); |
| 121 | const projekt: Project = { |
| 122 | ...basis, |
| 123 | signalGroups: basis.signalGroups.map((g) => |
| 124 | g.id === id ? { ...g, maxGreenOverride: 40 } : g, |
| 125 | ), |
| 126 | }; |
| 127 | const ansicht = zeichne(projekt); |
| 128 | expect( |
| 129 | feld(ansicht, id, 'hoechstfreigabe').value, |
| 130 | 'Vorbedingung: die Vorgabe steht im Feld', |
| 131 | ).toBe('40'); |
| 132 | |
| 133 | trageEin(feld(ansicht, id, 'hoechstfreigabe'), ''); |
| 134 | |
| 135 | expect(hoechstfreigabe(ansicht, id)).toBeNull(); |
| 136 | }); |
| 137 | |
| 138 | it('hebt eine Eingabe unter der Mindestfreigabezeit auf diese an', () => { |
| 139 | const projekt = createStandardIntersectionProject('Höchstfreigabe', FESTES_DATUM); |
| 140 | const ansicht = zeichne(projekt); |
| 141 | const id = k1(projekt); |
| 142 | const untergrenze = Number(feld(ansicht, id, 'mindestfreigabe').min); |
| 143 | expect(untergrenze, 'Vorbedingung: das Nachbarfeld nennt eine Untergrenze').toBeGreaterThan(0); |
| 144 | |
| 145 | trageEin(feld(ansicht, id, 'hoechstfreigabe'), '2'); |
| 146 | |
| 147 | expect(hoechstfreigabe(ansicht, id)).toBe(untergrenze); |
| 148 | }); |
| 149 | |
| 150 | it('sagt das Feld ausgeschrieben an wie das Nachbarfeld', () => { |
| 151 | // Der Spaltenkopf ist abgekuerzt, damit die Spalte schmal bleibt; den Namen |
| 152 | // des Bedienelements bildet `beschrifteTabelle` aus ihm. Ohne Eintrag in |
| 153 | // der Karte `ausgeschrieben` hiesse das Feld einer Sprachausgabe |
| 154 | // "Höchstfreigabe, K1 ..." - eine Abkuerzung ohne Einheit, waehrend das |
| 155 | // unmittelbare Nachbarfeld derselben Zeile ausgeschrieben angesagt wird. |
| 156 | const projekt = createStandardIntersectionProject('Höchstfreigabe', FESTES_DATUM); |
| 157 | const ansicht = zeichne(projekt); |
| 158 | const id = k1(projekt); |
| 159 | const nachbar = feld(ansicht, id, 'mindestfreigabe').getAttribute('aria-label') ?? ''; |
| 160 | expect(nachbar, 'Vorbedingung: das Nachbarfeld wird ausgeschrieben angesagt').toMatch( |
| 161 | /^Mindestfreigabezeit in Sekunden, /, |
| 162 | ); |
| 163 | |
| 164 | const name = feld(ansicht, id, 'hoechstfreigabe').getAttribute('aria-label') ?? ''; |
| 165 | |
| 166 | expect(name).toBe(nachbar.replace('Mindestfreigabezeit', 'Höchstfreigabezeit')); |
| 167 | }); |
| 168 | |
| 169 | it('trägt die Spalte im Tabellenkopf', () => { |
| 170 | const projekt = createStandardIntersectionProject('Höchstfreigabe', FESTES_DATUM); |
| 171 | const ansicht = zeichne(projekt); |
| 172 | const koepfe = [...ansicht.wurzel.querySelectorAll('thead th')].map( |
| 173 | (th) => th.textContent?.replace('?', '').trim() ?? '', |
| 174 | ); |
| 175 | |
| 176 | expect(koepfe).toContain('Höchstfreigabe'); |
| 177 | }); |
| 178 | }); |