import { afterEach, describe, expect, it } from 'vitest'; import { ProjectStore } from '@/app/store'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { ANLAGENART_GRENZEN, RILSA_DEFAULTS } from '@/domain/rilsa/constants'; import { resolveRilsaSettings, settingFields } from '@/domain/rilsa/settings'; import { fundstellenkennwerte } from '@/services/export/bewertung'; import { settingsView } from '@/ui/views/settingsView'; import type { Anlagenart, Project } from '@/domain/model/project'; import type { RilsaOverrides } from '@/domain/rilsa/settings'; /* * Der letzte offene Rest von Befund 48 (tests/domain/regelbereichBezug.test.ts) * und seiner ersten Behebung (tests/export/regelbereichFundstelle.test.ts). * * Die Abweichungsliste misst die beiden Regelbereichsfelder der Umlaufzeit * seit Befund 48 gegen den Wert, der OHNE Eintrag fuer die ANLAGENART gilt, und * das Fundstellenverzeichnis des Ausdrucks seit der ersten Behebung * ebenfalls. Die VORGABENANSICHT am Bildschirm tat es nicht: Spalte * "Regelwert", Platzhalter des leeren Feldes und die Sichtbarkeit des Knopfes * "Zuruecksetzen" lasen `field.standard` aus `settingFields`, also den * Knotenpunktwert. * * WAS DER ANWENDER SONST ERLEBT - an einer Fussgaengerschutzanlage, deren * Regelbereich 40 bis 70 s betraegt und mit der der Plan auch rechnet: * * (a) Die Spalte "Regelwert" nennt 60 s und der Platzhalter des leeren * Feldes ebenfalls - beides fuer einen Wert, den weder der Plan noch die * Abweichungsliste noch der Ausdruck kennen. Wer den Platzhalter fuer den * geltenden Wert haelt und ihn eintraegt, verschiebt den Regelbereich von * 40 auf 60 s, ohne es zu merken. * (b) Der Knopf "Zuruecksetzen" stand an der falschen Zeile: bei einem * Eintrag von 40 s - dem Wert, der dort ohnehin gilt - erschien er, bei * einem Eintrag von 60 s, der den Regelbereich wirklich verschiebt, * nicht. Genau dort, wo etwas zurueckzunehmen war, fehlte er. * * DIESE WACHE HAELT DIE AUSGABEN GEGENEINANDER; ihr Auseinanderlaufen ist der * Befund. Geprueft wird die Ansicht deshalb nicht gegen eine hier * hingeschriebene Zahl, sondern gegen `plan.grenzen` (womit gerechnet wird), * gegen `resolveRilsaSettings(...).deviations` (die gedruckte Aufstellung * "Abweichungen von den Regelwerten") und gegen `fundstellenkennwerte` (das * Fundstellenverzeichnis desselben Ausdrucks). * * GEGEN DEN ALTSTAND schlagen sechs der zehn Faelle fehl. Gruen bleiben dort * die Vorbedingung, die Gegenprobe (Knotenpunkt und groesste Umlaufzeit) und * die beiden Faelle zum Vermerk "wirksam N s". * * DER UMBAU VON `wirksamerWert` IST FUER SICH WIRKUNGSGLEICH, und das ist * gemessen: Setzt man allein seinen Rumpf auf die fruehere Fassung zurueck * (`bezug = eingetragen ?? feld.standard`) und laesst Spalte, Platzhalter und * Knopf umgestellt, bleiben alle zehn Faelle gruen. Rot wird der Fall * "behauptet am leeren Feld keinen abweichenden wirksamen Wert" erst gegen die * HALBE Umstellung, bei der nur die BEZUGSSEITE des Vergleichs auf `regelwert` * gezogen wird (`bezug = eingetragen ?? feld.regelwert`, `wirksam` * unveraendert): Dann steht unter dem leeren Feld "wirksam 60 s", waehrend die * Zeile daneben 40 s als Regelwert fuehrt - gemessen, 1 von 10 rot. Genau das * bewacht der Fall. Der Fall daneben ("meldet den gekappten Wert weiterhin") * ist die Gegenprobe dazu und bleibt in allen drei Staenden gruen. */ const FESTES_DATUM = new Date('2026-01-01T00:00:00Z'); const VON = 'Regelbereich Umlaufzeit von'; const BIS = 'Regelbereich Umlaufzeit bis'; afterEach(() => { document.body.replaceChildren(); }); function projekt(anlagenart: Anlagenart, rilsa: RilsaOverrides = {}): Project { const roh = createStandardIntersectionProject('Vorgaben', FESTES_DATUM); return { ...roh, anlagenart, settings: { ...roh.settings, rilsa: { ...roh.settings.rilsa, ...rilsa } }, }; } interface Ansicht { readonly wurzel: HTMLElement; readonly aufraeumen: () => void; } function zeichne(project: Project): Ansicht { const wurzel = document.createElement('div'); document.body.append(wurzel); const schliessen = settingsView.render(wurzel, { store: new ProjectStore(project), refresh: () => {}, navigate: () => {}, target: null, }); return { wurzel, aufraeumen: () => { schliessen(); wurzel.remove(); }, }; } /** 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; } /** Was die Zeile in Spalte "Regelwert", im Platzhalter und am Knopf zeigt. */ interface Zeilenbild { readonly spalte: string; readonly platzhalter: string; readonly zuruecksetzen: boolean; readonly text: string; } function bild(wurzel: ParentNode, label: string): Zeilenbild { const zeile = zeileZu(wurzel, label); return { spalte: zeile.children[2]?.textContent?.trim() ?? '', platzhalter: zeile.querySelector('input')?.placeholder ?? '', zuruecksetzen: [...zeile.querySelectorAll('button')].some( (b) => b.textContent?.trim() === 'Zurücksetzen', ), text: zeile.textContent ?? '', }; } /** Der Bezugswert der gedruckten Aufstellung "Abweichungen von den Regelwerten". */ function bezugDerAufstellung( anlagenart: Anlagenart, key: keyof RilsaOverrides, eintrag: number, ): number | undefined { return resolveRilsaSettings({ [key]: eintrag }, RILSA_DEFAULTS, anlagenart).deviations.find( (d) => d.key === key, )?.standardValue; } describe('Der Regelwert der Vorgabenzeile', () => { it('haelt an der Fussgaengerschutzanlage die Zahlen der Anlagenart bereit', () => { // Vorbedingung des Aufbaus, wie in regelbereichBezug: Die Anlagenart // fuehrt einen anderen Regelbereich als der Knotenpunkt. expect(ANLAGENART_GRENZEN.fussgaengerschutzanlage.cycleTime.recommendedMin).toBe(40); expect(ANLAGENART_GRENZEN.fussgaengerschutzanlage.cycleTime.recommendedMax).toBe(70); // Seit 5.27.0 der Richtwert der RiLSA (Abschnitt 2.6) statt der 60 s. expect(RILSA_DEFAULTS.cycleTime.recommendedMin).toBe(30); expect(RILSA_DEFAULTS.cycleTime.recommendedMax).toBe(90); }); it('nennt in Spalte und Platzhalter die Zahl, mit der der Plan rechnet', () => { const p = projekt('fussgaengerschutzanlage'); // Womit gerechnet wird - nicht behauptet, sondern am Plan abgelesen. const grenzen = buildSignalPlan(p).grenzen.cycleTime; expect([grenzen.recommendedMin, grenzen.recommendedMax]).toEqual([40, 70]); const ansicht = zeichne(p); try { const von = bild(ansicht.wurzel, VON); expect(von.spalte, 'Spalte "Regelwert"').toBe(String(grenzen.recommendedMin)); expect(von.platzhalter, 'Platzhalter des leeren Feldes').toBe(String(grenzen.recommendedMin)); const bis = bild(ansicht.wurzel, BIS); expect(bis.spalte).toBe(String(grenzen.recommendedMax)); expect(bis.platzhalter).toBe(String(grenzen.recommendedMax)); } finally { ansicht.aufraeumen(); } }); it('nennt dieselbe Zahl wie die Aufstellung "Abweichungen von den Regelwerten"', () => { // Ein Eintrag von 60 s verschiebt den Regelbereich; die Aufstellung nennt // dafuer ihren Bezugswert. Die Ansicht darf keinen anderen fuehren. const bezug = bezugDerAufstellung('fussgaengerschutzanlage', 'cycleTimeRecommendedMin', 60); expect(bezug, 'Vorbedingung: die Aufstellung fuehrt die Zeile').toBeDefined(); const ansicht = zeichne(projekt('fussgaengerschutzanlage', { cycleTimeRecommendedMin: 60 })); try { expect(bild(ansicht.wurzel, VON).spalte).toBe(String(bezug)); } finally { ansicht.aufraeumen(); } }); it('nennt dieselbe Zahl wie das Fundstellenverzeichnis desselben Ausdrucks', () => { // Bildschirm und Ausdruck bilden ihren Bezugswert nicht mehr unabhaengig // voneinander - das war der Befund, dreimal hintereinander. const gedruckt = fundstellenkennwerte({}, 'fussgaengerschutzanlage'); const zeile = settingFields(RILSA_DEFAULTS, 'fussgaengerschutzanlage'); for (const key of ['cycleTimeRecommendedMin', 'cycleTimeRecommendedMax'] as const) { const feld = zeile.find((f) => f.key === key); expect(feld?.regelwert, `Vorgabenzeile "${key}"`).toBe(gedruckt.get(key)?.regelwert); } const ansicht = zeichne(projekt('fussgaengerschutzanlage')); try { expect(bild(ansicht.wurzel, VON).spalte).toBe( String(gedruckt.get('cycleTimeRecommendedMin')?.regelwert), ); expect(bild(ansicht.wurzel, BIS).spalte).toBe( String(gedruckt.get('cycleTimeRecommendedMax')?.regelwert), ); } finally { ansicht.aufraeumen(); } }); it('bietet "Zuruecksetzen" bei der wirksamen Eingabe an', () => { // 60 s verschieben den Regelbereich von 40 auf 60 - hier gibt es etwas // zurueckzunehmen, und die Aufstellung fuehrt die Zeile. expect(bezugDerAufstellung('fussgaengerschutzanlage', 'cycleTimeRecommendedMin', 60)).toBe(40); const ansicht = zeichne(projekt('fussgaengerschutzanlage', { cycleTimeRecommendedMin: 60 })); try { expect(bild(ansicht.wurzel, VON).zuruecksetzen).toBe(true); } finally { ansicht.aufraeumen(); } }); it('bietet "Zuruecksetzen" bei der wirkungslosen Eingabe nicht an', () => { // 40 s gelten dort ohnehin; die Aufstellung fuehrt die Zeile deshalb nicht. expect( bezugDerAufstellung('fussgaengerschutzanlage', 'cycleTimeRecommendedMin', 40), ).toBeUndefined(); const ansicht = zeichne(projekt('fussgaengerschutzanlage', { cycleTimeRecommendedMin: 40 })); try { expect(bild(ansicht.wurzel, VON).zuruecksetzen).toBe(false); } finally { ansicht.aufraeumen(); } }); it('behauptet am leeren Feld keinen abweichenden wirksamen Wert', () => { /* * Der Vermerk "wirksam N s" vergleicht gegen das, was die Zeile als geltend * darstellt. Mit dem Regelwert der Anlagenart in Spalte und Platzhalter * muss ihm der Vergleich AUF BEIDEN SEITEN folgen: `resolveRilsaSettings` * fuehrt im aufgeloesten Kennwertsatz weiterhin die Knotenpunktzahl, weil * der Regelbereich der Anlagenart erst in `anlagenartGrenzen` einsetzt - * und zwar nur, solange nichts eingetragen ist. Zieht nur die Bezugsseite * auf den Regelwert um, steht unter dem leeren Feld "wirksam 60 s", * waehrend der Plan mit 40 s rechnet (gemessen; siehe Kopfkommentar). */ const ansicht = zeichne(projekt('fussgaengerschutzanlage')); try { expect(bild(ansicht.wurzel, VON).text).not.toContain('wirksam'); expect(bild(ansicht.wurzel, BIS).text).not.toContain('wirksam'); } finally { ansicht.aufraeumen(); } }); it('meldet den gekappten Wert weiterhin, wo einer gekappt wird', () => { // Gegenprobe zum Fall darueber: Ein Eintrag ueber der Schranke der // Anlagenart wird auf 120 s gekappt, und das steht neben dem Feld // (Befund 56 zur Fassung 5.9.0). const aufgeloest = resolveRilsaSettings( { cycleTimeRecommendedMin: 500 }, RILSA_DEFAULTS, 'fussgaengerschutzanlage', ); expect(aufgeloest.defaults.cycleTime.recommendedMin).toBe(120); const ansicht = zeichne(projekt('fussgaengerschutzanlage', { cycleTimeRecommendedMin: 500 })); try { expect(bild(ansicht.wurzel, VON).text).toContain('wirksam 120 s'); } finally { ansicht.aufraeumen(); } }); it('folgt auch bei einstreifiger Verkehrsfuehrung der Anlagenart', () => { // Nicht nur die Fussgaengerschutzanlage: Den Regelbereich der einstreifigen // Verkehrsfuehrung (60 bis 180 s) setzt dieses Programm ebenso selbst. const p = projekt('einstreifig'); const grenzen = buildSignalPlan(p).grenzen.cycleTime; expect([grenzen.recommendedMin, grenzen.recommendedMax]).toEqual([60, 180]); const ansicht = zeichne(p); try { expect(bild(ansicht.wurzel, BIS).spalte).toBe(String(grenzen.recommendedMax)); expect(bild(ansicht.wurzel, BIS).platzhalter).toBe(String(grenzen.recommendedMax)); } finally { ansicht.aufraeumen(); } }); it('laesst Knotenpunkt und groesste Umlaufzeit unveraendert', () => { /* * Die Gegenproben, in beiden Staenden gruen. Am Knotenpunkt sind * Anlagenart- und Regelwerkszahl dieselben. Und bei der kleinsten und * groessten Umlaufzeit bleibt der Knotenpunktwert der Bezug, auch wo die * Anlagenart mehr zulaesst: Er ist dort ein Regelwerkswert (30 bis 120 s, * von der RiLSA nicht nach Anlagenart gestaffelt), waehrend die 300 s der * einstreifigen Fuehrung dieses Programm setzt. */ const amKnoten = zeichne(projekt('knotenpunkt')); try { expect(bild(amKnoten.wurzel, VON).spalte).toBe('30'); expect(bild(amKnoten.wurzel, VON).platzhalter).toBe('30'); expect(bild(amKnoten.wurzel, BIS).spalte).toBe('90'); } finally { amKnoten.aufraeumen(); } expect(ANLAGENART_GRENZEN.einstreifig.cycleTime.max).toBe(300); const einstreifig = zeichne(projekt('einstreifig')); try { const groesste = bild(einstreifig.wurzel, 'Größte Umlaufzeit'); expect(groesste.spalte, 'Regelwerkswert, nicht die Schranke der Anlagenart').toBe('120'); expect(groesste.platzhalter).toBe('120'); } finally { einstreifig.aufraeumen(); } }); });