lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests ui anlagenartWechsel.test.ts
| 1 | import { afterEach, describe, expect, it } from 'vitest'; |
| 2 | import { ProjectStore } from '@/app/store'; |
| 3 | import { ASSISTENT_VORGABE, projektAusAngaben } from '@/ui/assistent'; |
| 4 | import { ANLAGENART_GRENZEN } from '@/domain/rilsa/constants'; |
| 5 | import { CURRENT_SCHEMA_VERSION, type Project } from '@/domain/model/project'; |
| 6 | import { loadProject } from '@/domain/model/migrate'; |
| 7 | import type { ViewDefinition } from '@/ui/shell'; |
| 8 | import { projectView } from '@/ui/views/projectView'; |
| 9 | |
| 10 | /** |
| 11 | * Der Dialog beim Wechsel der Anlagenart muss den ZEITPUNKT der Kuerzung |
| 12 | * richtig nennen (Befund 12 zur Fassung 5.8.0). |
| 13 | * |
| 14 | * WAS DER ANWENDER SONST ERLEBT: Er stellt eine einstreifige Fuehrung mit |
| 15 | * 400 m Raeumweg auf "Fussgaengerschutzanlage" um. Der Dialog nennt als |
| 16 | * Zeitpunkt der Kuerzung nur "beim naechsten Aendern eines dieser Felder" - er |
| 17 | * fasst also kein Feld mehr an und glaubt die vermessene Laenge sicher. |
| 18 | * Gekuerzt wird aber schon beim naechsten EINLESEN: Nach einem Programmstart |
| 19 | * holt die Sitzungswiederherstellung das Projekt mit 100 m zurueck, und die |
| 20 | * Zwischenzeit steht rund 30 s zu kurz da. |
| 21 | */ |
| 22 | |
| 23 | const FESTES_DATUM = '2026-01-01T00:00:00.000Z'; |
| 24 | const FESTES_DATUM_OBJEKT = new Date(FESTES_DATUM); |
| 25 | |
| 26 | /** Einstreifige Fuehrung, alle Konfliktwege am Lageplan mit 400 m vermessen. */ |
| 27 | function projektMitLangenWegen(): Project { |
| 28 | const basis = projektAusAngaben( |
| 29 | { ...ASSISTENT_VORGABE, fussgaenger: true, rad: true, linksabbieger: true }, |
| 30 | FESTES_DATUM, |
| 31 | ); |
| 32 | return { |
| 33 | ...basis, |
| 34 | anlagenart: 'einstreifig', |
| 35 | conflicts: basis.conflicts.map((c) => ({ ...c, clearingDistance: 400 })), |
| 36 | }; |
| 37 | } |
| 38 | |
| 39 | function zeichne( |
| 40 | view: ViewDefinition, |
| 41 | project: Project, |
| 42 | ): { wurzel: HTMLElement; store: ProjectStore; aufraeumen: () => void } { |
| 43 | const wurzel = document.createElement('div'); |
| 44 | document.body.append(wurzel); |
| 45 | const store = new ProjectStore(project); |
| 46 | const aufraeumen = view.render(wurzel, { |
| 47 | store, |
| 48 | refresh: () => {}, |
| 49 | navigate: () => {}, |
| 50 | target: null, |
| 51 | }); |
| 52 | return { |
| 53 | wurzel, |
| 54 | store, |
| 55 | aufraeumen: () => { |
| 56 | aufraeumen(); |
| 57 | wurzel.remove(); |
| 58 | }, |
| 59 | }; |
| 60 | } |
| 61 | |
| 62 | /** Das Auswahlfeld der Anlagenart in der Projektansicht. */ |
| 63 | function auswahl(wurzel: ParentNode): HTMLSelectElement { |
| 64 | const label = [...wurzel.querySelectorAll('label')].find( |
| 65 | (l) => (l.textContent ?? '').trim() === 'Anlagenart', |
| 66 | ); |
| 67 | const feld = wurzel.querySelector<HTMLSelectElement>(`#${label?.htmlFor ?? ''}`); |
| 68 | expect(feld, 'Kein Auswahlfeld "Anlagenart" in der Projektansicht').not.toBeNull(); |
| 69 | return feld as HTMLSelectElement; |
| 70 | } |
| 71 | |
| 72 | function knopfMit(text: string): HTMLButtonElement | undefined { |
| 73 | return [...document.querySelectorAll('button')].find( |
| 74 | (b) => (b.textContent ?? '').trim() === text, |
| 75 | ); |
| 76 | } |
| 77 | |
| 78 | /** Laesst die Warteschlange der Mikroaufgaben leerlaufen. */ |
| 79 | async function abwarten(): Promise<void> { |
| 80 | await new Promise((fertig) => setTimeout(fertig, 0)); |
| 81 | } |
| 82 | |
| 83 | /** |
| 84 | * Das Projekt so einlesen, wie es die Sitzungswiederherstellung tut: ueber die |
| 85 | * gespeicherte Rohgestalt und `loadProject`. Kein Feld wird dabei angefasst. |
| 86 | */ |
| 87 | function wiedereingelesen(project: Project): ReturnType<typeof loadProject> { |
| 88 | const roh: unknown = JSON.parse( |
| 89 | JSON.stringify({ ...project, schemaVersion: CURRENT_SCHEMA_VERSION }), |
| 90 | ); |
| 91 | return loadProject(roh, FESTES_DATUM_OBJEKT); |
| 92 | } |
| 93 | |
| 94 | afterEach(() => { |
| 95 | // Dialoge und Kurzmeldungen haengen an document.body, nicht an der Ansicht. |
| 96 | document.body.replaceChildren(); |
| 97 | }); |
| 98 | |
| 99 | describe('Anlagenartwechsel: der Dialog nennt den Zeitpunkt der Kuerzung', () => { |
| 100 | it('nennt das naechste Oeffnen des Projekts und die Sitzungswiederherstellung, nicht nur das Aendern eines Feldes', async () => { |
| 101 | const { wurzel, aufraeumen } = zeichne(projectView, projektMitLangenWegen()); |
| 102 | try { |
| 103 | const feld = auswahl(wurzel); |
| 104 | feld.value = 'fussgaengerschutzanlage'; |
| 105 | feld.dispatchEvent(new Event('change')); |
| 106 | |
| 107 | const text = document.querySelector('.dialog')?.textContent ?? ''; |
| 108 | expect(text, 'Die Umstellung geschah ohne Rueckfrage').not.toBe(''); |
| 109 | // Die betroffenen Zahlen stehen weiterhin im Dialog. |
| 110 | expect(text).toContain('400'); |
| 111 | expect(text).toContain( |
| 112 | String(ANLAGENART_GRENZEN.fussgaengerschutzanlage.maxClearingDistance), |
| 113 | ); |
| 114 | |
| 115 | // Der eigentliche Punkt: Gekuerzt wird beim Einlesen, also auch ohne |
| 116 | // jede Bedienhandlung. Steht das nicht da, verlaesst sich der Bearbeiter |
| 117 | // darauf, dass die vermessenen 400 m bleiben, solange er sie in Ruhe |
| 118 | // laesst. |
| 119 | expect( |
| 120 | text, |
| 121 | 'Der Dialog nennt das Oeffnen des Projekts nicht als Zeitpunkt der Kuerzung', |
| 122 | ).toContain('nächsten Öffnen'); |
| 123 | expect( |
| 124 | text, |
| 125 | 'Der Dialog nennt die Sitzungswiederherstellung nicht, obwohl sie ebenso kuerzt', |
| 126 | ).toContain('Sitzung'); |
| 127 | |
| 128 | knopfMit('Umstellen')?.click(); |
| 129 | await abwarten(); |
| 130 | } finally { |
| 131 | aufraeumen(); |
| 132 | } |
| 133 | }); |
| 134 | |
| 135 | it('kuerzt den vermessenen Weg tatsaechlich schon beim Wiedereinlesen, ohne dass ein Feld angefasst wurde', async () => { |
| 136 | const { wurzel, store, aufraeumen } = zeichne(projectView, projektMitLangenWegen()); |
| 137 | try { |
| 138 | const feld = auswahl(wurzel); |
| 139 | feld.value = 'fussgaengerschutzanlage'; |
| 140 | feld.dispatchEvent(new Event('change')); |
| 141 | knopfMit('Umstellen')?.click(); |
| 142 | await abwarten(); |
| 143 | |
| 144 | // Im Speicher steht weiterhin die vermessene Laenge - so weit haelt die |
| 145 | // Zusage des Dialogs. |
| 146 | const umgestellt = store.getProject(); |
| 147 | expect(umgestellt.anlagenart).toBe('fussgaengerschutzanlage'); |
| 148 | expect(umgestellt.conflicts[0]?.clearingDistance).toBe(400); |
| 149 | |
| 150 | // Nach dem Wiedereinlesen nicht mehr. Das ist der Zeitpunkt, den der |
| 151 | // Dialog nennen muss. |
| 152 | const ergebnis = wiedereingelesen(umgestellt); |
| 153 | expect(ergebnis.project.conflicts[0]?.clearingDistance).toBe( |
| 154 | ANLAGENART_GRENZEN.fussgaengerschutzanlage.maxClearingDistance, |
| 155 | ); |
| 156 | expect(ergebnis.issues.some((i) => i.message.includes('Räumweg'))).toBe(true); |
| 157 | } finally { |
| 158 | aufraeumen(); |
| 159 | } |
| 160 | }); |
| 161 | }); |