import { afterEach, describe, expect, it } from 'vitest'; import { numberCell } from '@/ui/inputs'; /** * Der Rueckfallwert des Zahlenfelds ist der zuletzt UEBERNOMMENE Wert, nicht * der beim Bau uebergebene. * * `numberCell` hielt den Parameter `value` fest und las ihn in beiden * Wiederherstellungswegen sowie im Meldungssatz. Fuer jeden Aufrufer, der die * Ansicht nicht nach jeder Eingabe neu baut, ist dieser Wert veraltet: Wer 60 * eintraegt und danach etwas Ungueltiges, bekommt den Bauwert 120 zurueck - im * Feld und im Satz -, obwohl 60 der zuletzt gueltige Wert war und der Aufrufer * genau damit weiterrechnet. * * Gefunden im gefuehrten Einstieg (Befund 21 zur Fassung 5.8.0): Bei der * einstreifigen Verkehrsfuehrung IST die Engstellenlaenge der Raeumweg beider * Fahrtrichtungen; Feld und Rechnung wichen um 60 m voneinander ab, die * Zwischenzeit fiel um 6,0 s kuerzer aus als die angezeigte Laenge hergab. * Der Assistent fing das mit einer eigenen Bauhilfe ab - hier ist die Ursache * bewacht, unabhaengig von jedem Aufrufer. * * Nachgestellt wird ein Aufrufer OHNE Neuaufbau: Das Feld wird einmal gebaut * und bleibt stehen; nur der Empfaenger schreibt mit. */ afterEach(() => { document.body.replaceChildren(); }); /** Der Text der zuletzt gezeigten Warnung. */ function letzteWarnung(): string { const meldungen = [...document.querySelectorAll('.meldung-warnung')]; return meldungen[meldungen.length - 1]?.textContent ?? ''; } /** Eine Eingabe so absetzen, wie sie beim Verlassen des Feldes ankommt. */ function setzeZahl(feld: HTMLInputElement, wert: string): void { feld.value = wert; feld.dispatchEvent(new Event('change')); } /** * Eine unlesbare Eingabe absetzen. * * Ein `input[type=number]` gibt sie als leere Zeichenkette heraus - in jsdom wie * im Browser. Um den zweiten Rueckfallweg zu erreichen, wird der Feldtyp fuer * die Eingabe kurz auf `text` gestellt; der Empfaenger liest nur `input.value` * und sieht davon nichts. */ function setzeUnlesbar(feld: HTMLInputElement, wert: string): void { const zuvor = feld.getAttribute('type') ?? 'number'; feld.setAttribute('type', 'text'); setzeZahl(feld, wert); feld.setAttribute('type', zuvor); } describe('Zahlenfeld: Rueckfall auf den zuletzt uebernommenen Wert', () => { it('stellt beim geleerten Pflichtfeld den uebernommenen Wert her, nicht den Bauwert', () => { // Engstellenlaenge im gefuehrten Einstieg: gebaut mit 120, eingetragen 60. let gerechnet = 120; const feld = numberCell( 120, (wert) => { if (wert !== null) gerechnet = wert; }, { min: 10, max: 400, step: 10, einheit: 'm' }, ); feld.setAttribute('aria-label', 'Länge der Engstelle'); document.body.append(feld); setzeZahl(feld, '60'); expect(gerechnet, 'der Aufrufer rechnet ab hier mit 60').toBe(60); setzeZahl(feld, ''); expect(feld.value, 'im Feld steht der zuletzt uebernommene Wert').toBe('60'); expect(letzteWarnung()).toContain('Der vorherige Wert 60 wurde wiederhergestellt'); expect(letzteWarnung(), 'der Bauwert ist nicht mehr der vorherige Wert').not.toContain('120'); expect(gerechnet, 'eine abgelehnte Eingabe aendert die Rechnung nicht').toBe(60); }); it('nennt bei unlesbarer Eingabe den uebernommenen Wert im Feld und im Satz', () => { const feld = numberCell(120, () => {}, { min: 10, max: 400, step: 10, einheit: 'm' }); feld.setAttribute('aria-label', 'Länge der Engstelle'); document.body.append(feld); setzeZahl(feld, '60'); setzeUnlesbar(feld, 'abc'); expect(feld.value).toBe('60'); expect(letzteWarnung()).toContain('Der vorherige Wert 60 gilt weiter'); expect(letzteWarnung()).not.toContain('120'); }); it('faellt nach einer begrenzten Eingabe auf den begrenzten Wert zurueck', () => { // Zulaessige Hoechstgeschwindigkeit: 200 wird auf 130 begrenzt und genau so // weitergegeben - 130 ist ab da der zuletzt uebernommene Wert. let gerechnet = 50; const feld = numberCell( 50, (wert) => { if (wert !== null) gerechnet = wert; }, { min: 5, max: 130, step: 5, einheit: 'km/h' }, ); feld.setAttribute('aria-label', 'Zulässige Höchstgeschwindigkeit'); document.body.append(feld); setzeZahl(feld, '200'); expect(feld.value, 'begrenzt auf die Obergrenze').toBe('130'); expect(gerechnet).toBe(130); setzeZahl(feld, ''); expect(feld.value).toBe('130'); expect(letzteWarnung()).toContain('Der vorherige Wert 130 wurde wiederhergestellt'); }); it('faellt bei uebernommenen Werten ausserhalb der Grenzen auf diesen Wert zurueck', () => { // `ausserhalbUebernehmen`: Der Wert wird NICHT begrenzt, sondern // unveraendert weitergereicht - dann ist er auch der Rueckfall. const feld = numberCell(15, () => {}, { min: 0, max: 100, step: 1, einheit: '%', ausserhalbUebernehmen: 'Außerhalb wird die Angabe verworfen.', }); feld.setAttribute('aria-label', 'Lastzüge am SV'); document.body.append(feld); setzeZahl(feld, '120'); expect(feld.value, 'der Wert bleibt unveraendert stehen').toBe('120'); setzeUnlesbar(feld, 'abc'); expect(feld.value).toBe('120'); expect(letzteWarnung()).toContain('Der vorherige Wert 120 gilt weiter'); expect(letzteWarnung(), 'der Bauwert ist nicht mehr der vorherige Wert').not.toContain('15'); }); it('hat nach einem zulaessig geleerten Feld keinen vorherigen Wert mehr', () => { // `allowEmpty`: Das leere Feld wird als `null` uebernommen. Danach gibt es // keinen vorherigen Wert - der Satz darf keinen nennen. const feld = numberCell(12, () => {}, { min: 0, max: 100, allowEmpty: true }); feld.setAttribute('aria-label', 'Lastzüge am SV'); document.body.append(feld); setzeZahl(feld, ''); expect(feld.value).toBe(''); setzeUnlesbar(feld, 'abc'); expect(feld.value, 'ohne vorherigen Wert bleibt das Feld leer').toBe(''); expect(letzteWarnung(), 'ohne vorherigen Wert steht keine Zahl im Satz').not.toMatch(/\d/); }); it('laesst den Bauwert stehen, solange nichts uebernommen wurde', () => { // Gegenprobe: Wo noch keine Eingabe angekommen ist, IST der Bauwert der // zuletzt gueltige Wert - an diesem Fall aendert sich nichts. const feld = numberCell(120, () => {}, { min: 10, max: 400, step: 10, einheit: 'm' }); feld.setAttribute('aria-label', 'Länge der Engstelle'); document.body.append(feld); setzeZahl(feld, ''); expect(feld.value).toBe('120'); expect(letzteWarnung()).toContain('Der vorherige Wert 120 wurde wiederhergestellt'); }); });