lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests ui uebergangszeile.test.ts
| 1 | import { afterEach, describe, expect, it } from 'vitest'; |
| 2 | import { ProjectStore } from '@/app/store'; |
| 3 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 4 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 5 | import { orderedPhases, type Project } from '@/domain/model/project'; |
| 6 | import { phasesView } from '@/ui/views/phasesView'; |
| 7 | |
| 8 | /** |
| 9 | * Befunde 25 und 56 zur Fassung 5.10.0 - die Zeile unter jeder |
| 10 | * Uebergangszeit der Ansicht "Phasen". |
| 11 | * |
| 12 | * BEFUND 25. Der Satz "Keine feindliche Beziehung erfasst." wurde aus |
| 13 | * `transition.critical === null` abgeleitet. `critical` bleibt aber auch dann |
| 14 | * null, wenn ALLE Beziehungen erfasst sind und rechnerisch 0 s ergeben: Der |
| 15 | * Planaufbau setzt es nur bei `value > duration`, und `duration` startet bei 0 |
| 16 | * (plan/signalPlan.ts). Genau diese Unterscheidung kann der Planaufbau nach |
| 17 | * eigener Aussage nicht treffen - dort wurde aus diesem Grund schon einmal |
| 18 | * eine Meldung mit derselben Bedeutung entfernt. Die Ansicht behauptete sie |
| 19 | * weiter, und zwar ausgerechnet dort, wo eine erfasste Beziehung auf 0 s |
| 20 | * zusammengefallen ist. |
| 21 | * |
| 22 | * BEFUND 56. Der Zielname stand ungefiltert in Anfuehrungszeichen; eine Phase |
| 23 | * ohne Bezeichnung ergab `Übergang zu "": 10 s`. Ueberall sonst in derselben |
| 24 | * Datei ist der Rueckfall gesetzt. |
| 25 | */ |
| 26 | |
| 27 | const STICHTAG = new Date('2026-01-01T00:00:00Z'); |
| 28 | |
| 29 | let aufraeumen: (() => void) | null = null; |
| 30 | afterEach(() => { |
| 31 | aufraeumen?.(); |
| 32 | aufraeumen = null; |
| 33 | document.body.replaceChildren(); |
| 34 | }); |
| 35 | |
| 36 | function zeichne(project: Project): HTMLElement { |
| 37 | const wurzel = document.createElement('div'); |
| 38 | document.body.append(wurzel); |
| 39 | const schliessen = phasesView.render(wurzel, { |
| 40 | store: new ProjectStore(project), |
| 41 | refresh: () => {}, |
| 42 | navigate: () => {}, |
| 43 | target: null, |
| 44 | }); |
| 45 | aufraeumen = () => { |
| 46 | schliessen(); |
| 47 | wurzel.remove(); |
| 48 | }; |
| 49 | return wurzel; |
| 50 | } |
| 51 | |
| 52 | /** Fette Ueberschrift und Erlaeuterung jeder Uebergangskarte, in Kartenfolge. */ |
| 53 | function uebergaenge(wurzel: HTMLElement): { kopf: string; hinweis: string }[] { |
| 54 | return [...wurzel.querySelectorAll('.hinweisbalken')].map((balken) => ({ |
| 55 | kopf: (balken.querySelector('strong')?.textContent ?? '').trim(), |
| 56 | hinweis: (balken.querySelector('p')?.textContent ?? '').trim(), |
| 57 | })); |
| 58 | } |
| 59 | |
| 60 | /** |
| 61 | * Standardknotenpunkt, bei dem jede Zwischenzeit rechnerisch 0 s ergibt. |
| 62 | * |
| 63 | * Kurzer Konfliktbereich, weit zurueckliegende Haltlinie - derselbe Aufbau wie |
| 64 | * `alleNull()` in tests/domain/berichtWidersprueche.test.ts, der dort belegt, |
| 65 | * dass der Pruefbericht den Uebergang dann nicht als unerfasst beanstandet. Alle sechzehn Beziehungen bleiben erfasst |
| 66 | * (zwoelf bis 5.42.1; seit 5.43.0 erfasst die Vorlage jede Furt auch gegen den |
| 67 | * Strom, der sie beim Ausfahren quert - Gruppe Furt). |
| 68 | */ |
| 69 | function alleZwischenzeitenNull(): Project { |
| 70 | const roh = createStandardIntersectionProject('Nullzwischenzeiten', STICHTAG); |
| 71 | return { |
| 72 | ...roh, |
| 73 | conflicts: roh.conflicts.map((c) => ({ ...c, clearingDistance: 3, enteringDistance: 60 })), |
| 74 | }; |
| 75 | } |
| 76 | |
| 77 | describe('Befund 25 - die Uebergangszeile spricht nur ueber das Rechenergebnis', () => { |
| 78 | it('behauptet bei erfassten Beziehungen mit 0 s nicht, es sei keine erfasst', () => { |
| 79 | const projekt = alleZwischenzeitenNull(); |
| 80 | |
| 81 | // Voraussetzungen des Ausloesers - sonst pruefte der Fall etwas anderes. |
| 82 | const plan = buildSignalPlan(projekt); |
| 83 | expect(projekt.conflicts.length, 'Es sind Konfliktbeziehungen erfasst').toBe(16); |
| 84 | expect([...plan.intergreens.values()].every((i) => i.value === 0)).toBe(true); |
| 85 | expect(plan.transitions.every((t) => t.duration === 0 && t.critical === null)).toBe(true); |
| 86 | |
| 87 | const zeilen = uebergaenge(zeichne(projekt)); |
| 88 | expect(zeilen.length, 'Der Standardknotenpunkt hat zwei Uebergaenge').toBe(2); |
| 89 | for (const zeile of zeilen) { |
| 90 | expect(zeile.hinweis).toBe( |
| 91 | 'Keine Beziehung mit einer Zwischenzeit über 0 s; ob Konflikte erfasst sind, ' + |
| 92 | 'sagt der Prüfbericht.', |
| 93 | ); |
| 94 | } |
| 95 | }); |
| 96 | |
| 97 | it('sagt dasselbe, wenn wirklich keine Beziehung erfasst ist', () => { |
| 98 | // Der Satz spricht ueber das Rechenergebnis. Ueber die Erfassung sagt er |
| 99 | // in beiden Faellen nichts - zustaendig ist der Pruefbericht, der die |
| 100 | // erfassten Beziehungen kennt (zwischenzeiten.uebergang-ungesichert). |
| 101 | const roh = createStandardIntersectionProject('Ohne Beziehungen', STICHTAG); |
| 102 | const zeilen = uebergaenge(zeichne({ ...roh, conflicts: [] })); |
| 103 | expect(zeilen.length).toBe(2); |
| 104 | for (const zeile of zeilen) { |
| 105 | expect(zeile.hinweis).toContain('Keine Beziehung mit einer Zwischenzeit über 0 s'); |
| 106 | expect(zeile.hinweis).not.toContain('erfasst.'); |
| 107 | } |
| 108 | }); |
| 109 | |
| 110 | it('nennt weiterhin die massgebende Beziehung, wo es eine gibt', () => { |
| 111 | const projekt = createStandardIntersectionProject('Regelfall', STICHTAG); |
| 112 | const plan = buildSignalPlan(projekt); |
| 113 | expect(plan.transitions.every((t) => t.critical !== null)).toBe(true); |
| 114 | |
| 115 | for (const zeile of uebergaenge(zeichne(projekt))) { |
| 116 | expect(zeile.hinweis).toContain('Maßgebend:'); |
| 117 | expect(zeile.hinweis).toContain('räumt'); |
| 118 | } |
| 119 | }); |
| 120 | }); |
| 121 | |
| 122 | describe('Befund 56 - eine Phase ohne Bezeichnung als Ziel des Uebergangs', () => { |
| 123 | /** Standardknotenpunkt, bei dem die zweite Phase keine Bezeichnung traegt. */ |
| 124 | function zweitePhaseOhneNamen(): Project { |
| 125 | const roh = createStandardIntersectionProject('Ohne Bezeichnung', STICHTAG); |
| 126 | const zweite = orderedPhases(roh)[1]!; |
| 127 | return { |
| 128 | ...roh, |
| 129 | phases: roh.phases.map((p) => (p.id === zweite.id ? { ...p, name: '' } : p)), |
| 130 | }; |
| 131 | } |
| 132 | |
| 133 | it('setzt kein leeres Anfuehrungszeichenpaar in die Uebergangszeile', () => { |
| 134 | const zeilen = uebergaenge(zeichne(zweitePhaseOhneNamen())); |
| 135 | for (const zeile of zeilen) { |
| 136 | expect(zeile.kopf).not.toContain('""'); |
| 137 | } |
| 138 | }); |
| 139 | |
| 140 | it('nennt sie so, wie die Ueberschrift ihrer Karte sie nennt', () => { |
| 141 | const wurzel = zeichne(zweitePhaseOhneNamen()); |
| 142 | const ueberschriften = [...wurzel.querySelectorAll('h3')].map((h) => h.textContent); |
| 143 | expect(ueberschriften).toContain('2. Phase (ohne Bezeichnung)'); |
| 144 | |
| 145 | const zeilen = uebergaenge(wurzel); |
| 146 | expect(zeilen[0]?.kopf.startsWith('Übergang zu 2. Phase (ohne Bezeichnung): ')).toBe(true); |
| 147 | }); |
| 148 | |
| 149 | it('laesst die Zeile mit Bezeichnung unveraendert', () => { |
| 150 | const zeilen = uebergaenge(zeichne(createStandardIntersectionProject('Regelfall', STICHTAG))); |
| 151 | expect(zeilen[0]?.kopf.startsWith('Übergang zu "Phase 2 - Ost/West": ')).toBe(true); |
| 152 | }); |
| 153 | }); |