lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain stilleErsetzungenFachkern.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { |
| 3 | UMLAUFZEIT_ERSATZWERT_VERMERK, |
| 4 | buildSignalPlan, |
| 5 | intergreenKey, |
| 6 | umlaufzeitMitVermerk, |
| 7 | } from '@/domain/plan/signalPlan'; |
| 8 | import { validateProject } from '@/domain/validation/engine'; |
| 9 | import { computeIntergreen } from '@/domain/rilsa/intergreen'; |
| 10 | import { resolveRilsaSettings, settingFields } from '@/domain/rilsa/settings'; |
| 11 | import { |
| 12 | ANLAGENART_GRENZEN, |
| 13 | RILSA_DEFAULTS, |
| 14 | SAFETY_FLOORS, |
| 15 | fahrzeugartBeschriftung, |
| 16 | } from '@/domain/rilsa/constants'; |
| 17 | import { |
| 18 | createConflict, |
| 19 | createEmptyProject, |
| 20 | createPhase, |
| 21 | createSignalGroup, |
| 22 | createStandardIntersectionProject, |
| 23 | } from '@/domain/model/factory'; |
| 24 | import type { Anlagenart, Project, SignalGroup } from '@/domain/model/project'; |
| 25 | import type { VehicleClass } from '@/domain/rilsa/types'; |
| 26 | |
| 27 | /* |
| 28 | * Befunde C7 bis C12 - weitere Fachkern-Befunde: |
| 29 | * |
| 30 | * - Der Ersatzwert-Vermerk traegt den Gedankenstrich der Anwendertexte, und |
| 31 | * das Zusammenfuegen "Zahl – Vermerk" steht EINMAL im Fachkern. |
| 32 | * - Der Ersatzwert ist die groesste ZULAESSIGE Umlaufzeit - eine engere eigene |
| 33 | * Vorgabe gilt, nicht die Schranke der Anlagenart. |
| 34 | * - Eine unzulaessige Vorgabe steht EINMAL im Bericht, nicht zweimal. |
| 35 | * - Die Umlaufzeitfelder begruenden die verletzte Schranke, nicht die andere. |
| 36 | * - Widerspruch kleinste > groesste Umlaufzeit: die Notiz ueberlebt den |
| 37 | * Rueckfall auf die Regelwerte. |
| 38 | * - Schwerverkehrsanteil: 0 bis 100 % wie das Schema; ausserhalb begrenzt UND |
| 39 | * gemeldet - vorher stumm bei 90 % gekappt. |
| 40 | * - Zwischenzeit von Hand: Aufrunden wird gemeldet, ein negativer Wert ist ein |
| 41 | * Fehler und ersetzt die Rechnung nicht mehr durch 0 s. |
| 42 | * - Beschriftung einer unbekannten Fahrzeugart nennt keine falsche Zahl. |
| 43 | * |
| 44 | * Gegen den Altstand schlagen alle Faelle fehl, bis auf die als Waechter |
| 45 | * gekennzeichneten. |
| 46 | */ |
| 47 | |
| 48 | const DATUM = new Date('2026-01-01T00:00:00Z'); |
| 49 | |
| 50 | /** Zwei Kfz-Gruppen in zwei Phasen; Konflikte und Gruppe 2 anpassbar. */ |
| 51 | function zweiGruppen( |
| 52 | zweite: Partial<SignalGroup> = {}, |
| 53 | anlagenart: Anlagenart = 'knotenpunkt', |
| 54 | rilsa: Project['settings']['rilsa'] = {}, |
| 55 | konflikt: Partial<Project['conflicts'][number]> = {}, |
| 56 | ): Project { |
| 57 | const basis = createEmptyProject('Zwei Gruppen', DATUM, anlagenart); |
| 58 | const a = createSignalGroup({ name: 'K1', mode: 'kfz', index: 0 }); |
| 59 | const b: SignalGroup = { ...createSignalGroup({ name: 'K2', mode: 'kfz', index: 1 }), ...zweite }; |
| 60 | const phase1 = createPhase('Phase 1', [a.id]); |
| 61 | const phase2 = createPhase('Phase 2', [b.id]); |
| 62 | return { |
| 63 | ...basis, |
| 64 | signalGroups: [a, b], |
| 65 | conflicts: [ |
| 66 | { ...createConflict(a.id, b.id), clearingDistance: 22, enteringDistance: 8, ...konflikt }, |
| 67 | { ...createConflict(b.id, a.id), clearingDistance: 22, enteringDistance: 8 }, |
| 68 | ], |
| 69 | phases: [phase1, phase2], |
| 70 | program: { ...basis.program, phaseOrder: [phase1.id, phase2.id] }, |
| 71 | settings: { ...basis.settings, rilsa }, |
| 72 | }; |
| 73 | } |
| 74 | |
| 75 | /** Der Musterknotenpunkt mit Verkehrsstaerken weit ueber der Kapazitaet. */ |
| 76 | function uebersaettigt(rilsa: Project['settings']['rilsa'] = {}): Project { |
| 77 | const roh = createStandardIntersectionProject('Uebersaettigt', DATUM); |
| 78 | return { |
| 79 | ...roh, |
| 80 | settings: { ...roh.settings, rilsa }, |
| 81 | demands: roh.signalGroups |
| 82 | .filter((g) => g.mode === 'kfz') |
| 83 | .map((g) => ({ signalGroupId: g.id, volume: 1800, heavyVehicleShare: 0.1 })), |
| 84 | }; |
| 85 | } |
| 86 | |
| 87 | /** Musterknotenpunkt mit einem Schwerverkehrsanteil an K1. */ |
| 88 | function mitSchwerverkehr(anteil: number): Project { |
| 89 | const roh = createStandardIntersectionProject('Schwerverkehr', DATUM); |
| 90 | const k1 = roh.signalGroups.find((g) => g.name === 'K1')!; |
| 91 | return { |
| 92 | ...roh, |
| 93 | demands: roh.demands.map((d) => |
| 94 | d.signalGroupId === k1.id ? { ...d, heavyVehicleShare: anteil } : d, |
| 95 | ), |
| 96 | }; |
| 97 | } |
| 98 | |
| 99 | function saettigungK1(project: Project): number { |
| 100 | const k1 = project.signalGroups.find((g) => g.name === 'K1')!; |
| 101 | return buildSignalPlan(project).groups.find((g) => g.groupId === k1.id)!.capacity!.saturationFlow; |
| 102 | } |
| 103 | |
| 104 | // --- Vermerk ------------------------------------------------------------------ |
| 105 | |
| 106 | describe('Ersatzwert-Vermerk: EIN Wortlaut, EIN Zusammenfuegen', () => { |
| 107 | it('traegt den Gedankenstrich der Anwendertexte - kein Bindestrich', () => { |
| 108 | expect(UMLAUFZEIT_ERSATZWERT_VERMERK).toBe('nicht bemessbar (übersättigt) – Ersatzwert'); |
| 109 | expect(UMLAUFZEIT_ERSATZWERT_VERMERK).not.toContain(' - '); |
| 110 | }); |
| 111 | |
| 112 | it('haengt den Vermerk nur bei Uebersaettigung an die formatierte Zahl', () => { |
| 113 | const ersatz = buildSignalPlan(uebersaettigt()); |
| 114 | expect(umlaufzeitMitVermerk(ersatz, '120 s')).toBe(`120 s – ${UMLAUFZEIT_ERSATZWERT_VERMERK}`); |
| 115 | const normal = buildSignalPlan(createStandardIntersectionProject('Normal', DATUM)); |
| 116 | expect(umlaufzeitMitVermerk(normal, '80 s')).toBe('80 s'); |
| 117 | // Waechter: keine Zahl im Fachkern - die formatiert der Aufrufer. |
| 118 | expect(umlaufzeitMitVermerk(ersatz, 'x')).toBe(`x – ${UMLAUFZEIT_ERSATZWERT_VERMERK}`); |
| 119 | }); |
| 120 | |
| 121 | it('ist die groesste ZULAESSIGE Umlaufzeit: eine engere eigene Vorgabe gilt vor der Anlagenart', () => { |
| 122 | // Knotenpunkt (Schranke 120 s) mit "Groesste Umlaufzeit 90 s", uebersaettigt. |
| 123 | const plan = buildSignalPlan(uebersaettigt({ cycleTimeMax: 90 })); |
| 124 | expect(plan.cycleResult?.bounded).toBe('uebersaettigt'); |
| 125 | expect(ANLAGENART_GRENZEN.knotenpunkt.cycleTime.max).toBe(120); |
| 126 | expect(plan.cycleResult?.cycleTime).toBe(90); |
| 127 | expect(plan.grenzen.cycleTime.max).toBe(90); |
| 128 | // Die Fehlermeldung nennt die 90 s - nicht die 120 s der Anlagenart. |
| 129 | const notiz = plan.notes.find((n) => n.code === 'uebersaettigt'); |
| 130 | expect(notiz?.message).toContain('Obergrenze von 90 s'); |
| 131 | }); |
| 132 | }); |
| 133 | |
| 134 | // --- Doppelmeldung ------------------------------------------------------------ |
| 135 | |
| 136 | describe('Unzulaessige Vorgabe: einmal im Bericht, nicht zweimal', () => { |
| 137 | it('fuehrt cycleTimeMax 150 s am Knotenpunkt als EINEN Fehler unter Vorgaben', () => { |
| 138 | const projekt = zweiGruppen({}, 'knotenpunkt', { cycleTimeMax: 150 }); |
| 139 | const plan = buildSignalPlan(projekt); |
| 140 | // Der Plan traegt die Notiz weiterhin - er ist fuer sich vollstaendig. |
| 141 | expect(plan.notes.filter((n) => n.code === 'vorgabe-ueberschritten:cycleTimeMax')).toHaveLength( |
| 142 | 1, |
| 143 | ); |
| 144 | |
| 145 | const bericht = validateProject(projekt, plan, DATUM); |
| 146 | const treffer = bericht.findings.filter((f) => |
| 147 | f.rule.endsWith('vorgabe-ueberschritten:cycleTimeMax'), |
| 148 | ); |
| 149 | expect(treffer).toHaveLength(1); |
| 150 | expect(treffer[0]!.rule).toBe('vorgaben.vorgabe-ueberschritten:cycleTimeMax'); |
| 151 | expect(treffer[0]!.category).toBe('vorgaben'); |
| 152 | expect(treffer[0]!.title).toBe('Unzulässige Vorgabe'); |
| 153 | expect(bericht.findings.some((f) => f.rule.startsWith('signalplan.vorgabe-'))).toBe(false); |
| 154 | // Und sie zaehlt genau einmal: Der Plan hat sonst keinen Fehler. |
| 155 | expect(bericht.errorCount).toBe(1); |
| 156 | expect(bericht.exportBlocked).toBe(true); |
| 157 | }); |
| 158 | |
| 159 | it('gilt fuer jedes Vorgabenfeld - auch fuer die Gelbzeit', () => { |
| 160 | const projekt = zweiGruppen({}, 'knotenpunkt', { yellowKfzUpTo50: 1 }); |
| 161 | const bericht = validateProject(projekt, undefined, DATUM); |
| 162 | expect( |
| 163 | bericht.findings.filter((f) => f.rule.endsWith('vorgabe-unterschritten:yellowKfzUpTo50')), |
| 164 | ).toHaveLength(1); |
| 165 | expect(bericht.findings.some((f) => f.rule.startsWith('signalplan.vorgabe-'))).toBe(false); |
| 166 | }); |
| 167 | }); |
| 168 | |
| 169 | // --- Begruendung der Schranke ----------------------------------------------- |
| 170 | |
| 171 | describe('Umlaufzeitfelder begruenden die verletzte Schranke', () => { |
| 172 | it('nennt bei "Kleinste Umlaufzeit 150 s" die Obergrenze der Anlagenart, nicht "nicht schaltbar"', () => { |
| 173 | const { notes } = resolveRilsaSettings({ cycleTimeMin: 150 }, undefined, 'knotenpunkt'); |
| 174 | const notiz = notes.find((n) => n.code === 'vorgabe-ueberschritten:cycleTimeMin'); |
| 175 | expect(notiz).toBeDefined(); |
| 176 | expect(notiz!.message).toContain('höchstens 120 s'); |
| 177 | expect(notiz!.message).toContain('Umlaufzeiten über 120 s sind nach RiLSA nicht vorzusehen.'); |
| 178 | expect(notiz!.message).not.toContain('nicht schaltbar'); |
| 179 | }); |
| 180 | |
| 181 | it('nennt bei "Groesste Umlaufzeit 10 s" die Schaltbarkeit, nicht die RiLSA-Obergrenze', () => { |
| 182 | const { notes } = resolveRilsaSettings({ cycleTimeMax: 10 }, undefined, 'knotenpunkt'); |
| 183 | const notiz = notes.find((n) => n.code === 'vorgabe-unterschritten:cycleTimeMax'); |
| 184 | expect(notiz).toBeDefined(); |
| 185 | expect(notiz!.message).toContain('mindestens 30 s'); |
| 186 | expect(notiz!.message).toContain('Umlaufzeiten unter 30 s sind nicht schaltbar.'); |
| 187 | expect(notiz!.message).not.toContain('nicht vorzusehen'); |
| 188 | }); |
| 189 | |
| 190 | it('begruendet auch den Regelbereich - und bei einstreifiger Fuehrung mit dem RSA-Vorbehalt', () => { |
| 191 | const { notes } = resolveRilsaSettings( |
| 192 | { cycleTimeRecommendedMax: 400, cycleTimeRecommendedMin: 10 }, |
| 193 | undefined, |
| 194 | 'einstreifig', |
| 195 | ); |
| 196 | expect( |
| 197 | notes.find((n) => n.code === 'vorgabe-ueberschritten:cycleTimeRecommendedMax')?.message, |
| 198 | ).toContain('RSA'); |
| 199 | expect( |
| 200 | notes.find((n) => n.code === 'vorgabe-unterschritten:cycleTimeRecommendedMin')?.message, |
| 201 | ).toContain('nicht schaltbar'); |
| 202 | }); |
| 203 | |
| 204 | it('nennt in der Vorgabenansicht beide Gruende, wenn sie sich unterscheiden', () => { |
| 205 | const feld = settingFields(undefined, 'knotenpunkt').find((f) => f.key === 'cycleTimeMax')!; |
| 206 | expect(feld.schrankenBegruendung).toContain('nicht schaltbar'); |
| 207 | expect(feld.schrankenBegruendung).toContain('nach RiLSA nicht vorzusehen'); |
| 208 | // Waechter: Felder mit EINER Begruendung bleiben bei ihr. |
| 209 | const gelb = settingFields().find((f) => f.key === 'yellowKfzUpTo50')!; |
| 210 | expect(gelb.schrankenBegruendung).toBe( |
| 211 | 'Die Gelbzeit für Kraftfahrzeuge darf 3 s nicht unterschreiten.', |
| 212 | ); |
| 213 | }); |
| 214 | }); |
| 215 | |
| 216 | // --- Widerspruch -------------------------------------------------------------- |
| 217 | |
| 218 | describe('Kleinste Umlaufzeit groesser als groesste: der Widerspruch bleibt gemeldet', () => { |
| 219 | it('faellt auf die Regelwerte zurueck UND behaelt die Notiz', () => { |
| 220 | const { defaults, notes, deviations } = resolveRilsaSettings( |
| 221 | { cycleTimeMin: 100, cycleTimeMax: 50 }, |
| 222 | undefined, |
| 223 | 'knotenpunkt', |
| 224 | ); |
| 225 | expect(defaults.cycleTime.min).toBe(RILSA_DEFAULTS.cycleTime.min); |
| 226 | expect(defaults.cycleTime.max).toBe(RILSA_DEFAULTS.cycleTime.max); |
| 227 | expect(deviations).toHaveLength(0); |
| 228 | const notiz = notes.find((n) => n.code === 'vorgabe-umlaufzeit-widerspruch'); |
| 229 | expect(notiz).toBeDefined(); |
| 230 | expect(notiz!.severity).toBe('fehler'); |
| 231 | // Und im Bericht - als Fehler mit Sperre. |
| 232 | const bericht = validateProject( |
| 233 | zweiGruppen({}, 'knotenpunkt', { cycleTimeMin: 100, cycleTimeMax: 50 }), |
| 234 | undefined, |
| 235 | DATUM, |
| 236 | ); |
| 237 | expect( |
| 238 | bericht.findings.filter((f) => f.rule === 'vorgaben.vorgabe-umlaufzeit-widerspruch'), |
| 239 | ).toHaveLength(1); |
| 240 | expect(bericht.exportBlocked).toBe(true); |
| 241 | }); |
| 242 | }); |
| 243 | |
| 244 | // --- Schwerverkehrsanteil ----------------------------------------------------- |
| 245 | |
| 246 | describe('Schwerverkehrsanteil: 0 bis 100 % wie das Schema, kein stiller Deckel bei 90 %', () => { |
| 247 | it('rechnet mit 95 %, wenn 95 % eingetragen sind', () => { |
| 248 | const neunzig = saettigungK1(mitSchwerverkehr(0.9)); |
| 249 | const fuenfundneunzig = saettigungK1(mitSchwerverkehr(0.95)); |
| 250 | const hundert = saettigungK1(mitSchwerverkehr(1)); |
| 251 | /* |
| 252 | * Altstand: alle drei gleich (Deckel 0,9). |
| 253 | * |
| 254 | * ANPASSUNG (Fassung 5.5.0, Frage E6): Gerechnet wird nicht |
| 255 | * mehr `base / (1 + Anteil)` - das war der Pkw-Gleichwert 2,0 des HCM -, |
| 256 | * sondern `base / fSV` mit fSV = 1 + 0,90 * Anteil nach HBS 2015 |
| 257 | * (Gl. 2-6). Die GEPRUEFTE AUSSAGE bleibt dieselbe: Es gibt keinen stillen |
| 258 | * Deckel bei 90 %, ein hoeherer Anteil senkt die |
| 259 | * Saettigungsverkehrsstaerke tatsaechlich. Nur die Zahlen wandern. |
| 260 | */ |
| 261 | expect(fuenfundneunzig).toBeLessThan(neunzig); |
| 262 | expect(hundert).toBeLessThan(fuenfundneunzig); |
| 263 | expect(hundert / neunzig).toBeCloseTo(1.81 / 1.9, 6); |
| 264 | const ohne = saettigungK1(mitSchwerverkehr(0)); |
| 265 | // fSV(0,95) = 1 + 0,9 * 0,95 = 1,855; fSV(0,90) = 1,81. |
| 266 | expect(fuenfundneunzig).toBeCloseTo(ohne / 1.855, 6); |
| 267 | expect(neunzig).toBeCloseTo(ohne / 1.81, 6); |
| 268 | expect( |
| 269 | buildSignalPlan(mitSchwerverkehr(0.95)).notes.some( |
| 270 | (n) => n.code === 'schwerverkehrsanteil-ausserhalb', |
| 271 | ), |
| 272 | ).toBe(false); |
| 273 | }); |
| 274 | |
| 275 | it('begrenzt einen Anteil ueber 100 % auf 100 % und MELDET das an der Gruppe', () => { |
| 276 | const projekt = mitSchwerverkehr(1.2); |
| 277 | const plan = buildSignalPlan(projekt); |
| 278 | expect(saettigungK1(projekt)).toBe(saettigungK1(mitSchwerverkehr(1))); |
| 279 | const notiz = plan.notes.find((n) => n.code === 'schwerverkehrsanteil-ausserhalb'); |
| 280 | expect(notiz).toBeDefined(); |
| 281 | expect(notiz!.severity).toBe('warnung'); |
| 282 | expect(notiz!.message).toContain('"K1"'); |
| 283 | expect(notiz!.message).toContain('120 %'); |
| 284 | expect(notiz!.message).toContain('Gerechnet wird mit 100 %'); |
| 285 | |
| 286 | const bericht = validateProject(projekt, plan, DATUM); |
| 287 | const befund = bericht.findings.find( |
| 288 | (f) => f.rule === 'signalplan.schwerverkehrsanteil-ausserhalb', |
| 289 | ); |
| 290 | expect(befund).toBeDefined(); |
| 291 | expect(befund!.category).toBe('leistungsfaehigkeit'); |
| 292 | expect(befund!.target).toMatchObject({ kind: 'signalgruppe', label: 'K1' }); |
| 293 | expect(befund!.title).toContain('Schwerverkehrsanteil'); |
| 294 | expect(befund!.suggestion).toContain('0 und 100 %'); |
| 295 | }); |
| 296 | |
| 297 | it('meldet einen negativen Anteil ebenso und rechnet mit 0 %', () => { |
| 298 | const projekt = mitSchwerverkehr(-0.2); |
| 299 | expect(saettigungK1(projekt)).toBe(saettigungK1(mitSchwerverkehr(0))); |
| 300 | const notiz = buildSignalPlan(projekt).notes.find( |
| 301 | (n) => n.code === 'schwerverkehrsanteil-ausserhalb', |
| 302 | ); |
| 303 | expect(notiz?.message).toContain('-20 %'); |
| 304 | expect(notiz?.message).toContain('Gerechnet wird mit 0 %'); |
| 305 | }); |
| 306 | }); |
| 307 | |
| 308 | // --- Zwischenzeit von Hand ---------------------------------------------------- |
| 309 | |
| 310 | describe('Zwischenzeit von Hand: Aufrunden gemeldet, negativ ist ein Fehler', () => { |
| 311 | it('rundet 4,2 s auf 5 s auf und sagt es als Hinweis an der Konfliktbeziehung', () => { |
| 312 | const projekt = zweiGruppen({}, 'knotenpunkt', {}, { manualIntergreen: 4.2 }); |
| 313 | const plan = buildSignalPlan(projekt); |
| 314 | const [k1, k2] = projekt.signalGroups; |
| 315 | const eintrag = plan.intergreens.get(intergreenKey(k1!.id, k2!.id))!; |
| 316 | expect(eintrag.value).toBe(5); |
| 317 | expect(eintrag.source).toBe('vorgegeben'); |
| 318 | const notiz = plan.notes.find((n) => n.code === 'zwischenzeit-vorgabe-aufgerundet'); |
| 319 | expect(notiz).toBeDefined(); |
| 320 | expect(notiz!.severity).toBe('hinweis'); |
| 321 | expect(notiz!.konfliktId).toBe(projekt.conflicts[0]!.id); |
| 322 | expect(notiz!.message).toContain('4,2 s'); |
| 323 | expect(notiz!.message).toContain('angesetzt werden 5 s'); |
| 324 | |
| 325 | const befund = validateProject(projekt, plan, DATUM).findings.find( |
| 326 | (f) => f.rule === 'signalplan.zwischenzeit-vorgabe-aufgerundet', |
| 327 | ); |
| 328 | expect(befund).toBeDefined(); |
| 329 | expect(befund!.category).toBe('zwischenzeiten'); |
| 330 | expect(befund!.target).toMatchObject({ kind: 'konflikt' }); |
| 331 | }); |
| 332 | |
| 333 | it('schweigt bei ganzen Sekunden (Waechter)', () => { |
| 334 | const plan = buildSignalPlan(zweiGruppen({}, 'knotenpunkt', {}, { manualIntergreen: 9 })); |
| 335 | expect(plan.notes.some((n) => n.code === 'zwischenzeit-vorgabe-aufgerundet')).toBe(false); |
| 336 | }); |
| 337 | |
| 338 | it('nimmt eine negative Vorgabe nicht an: Fehler, gerechnet wird aus den Wegen - nicht 0 s', () => { |
| 339 | const projekt = zweiGruppen({}, 'knotenpunkt', {}, { manualIntergreen: -3 }); |
| 340 | const plan = buildSignalPlan(projekt); |
| 341 | const [k1, k2] = projekt.signalGroups; |
| 342 | const eintrag = plan.intergreens.get(intergreenKey(k1!.id, k2!.id))!; |
| 343 | // Altstand: Math.max(0, ceil(-3)) = 0 s als VORGABE - keine Zwischenzeit. |
| 344 | expect(eintrag.source).toBe('berechnet'); |
| 345 | expect(eintrag.value).toBe( |
| 346 | computeIntergreen( |
| 347 | { mode: 'kfz', movement: 'geradeaus', clearingDistance: 22, vehicleClass: 'pkw', vZul: 50 }, |
| 348 | { mode: 'kfz', movement: 'geradeaus', enteringDistance: 8 }, |
| 349 | ).intergreen, |
| 350 | ); |
| 351 | expect(eintrag.value).toBeGreaterThan(0); |
| 352 | const notiz = plan.notes.find((n) => n.code === 'zwischenzeit-vorgabe-ungueltig'); |
| 353 | expect(notiz).toBeDefined(); |
| 354 | expect(notiz!.severity).toBe('fehler'); |
| 355 | expect(notiz!.message).toContain('-3 s'); |
| 356 | expect(notiz!.message).toContain('nicht übernommen'); |
| 357 | |
| 358 | const bericht = validateProject(projekt, plan, DATUM); |
| 359 | expect( |
| 360 | bericht.findings.some((f) => f.rule === 'signalplan.zwischenzeit-vorgabe-ungueltig'), |
| 361 | ).toBe(true); |
| 362 | expect(bericht.exportBlocked).toBe(true); |
| 363 | }); |
| 364 | }); |
| 365 | |
| 366 | // --- Fahrzeugart -------------------------------------------------------------- |
| 367 | |
| 368 | describe('Beschriftung einer unbekannten Fahrzeugart', () => { |
| 369 | it('nennt keine erfundene Zahl und kein "undefined"', () => { |
| 370 | const text = fahrzeugartBeschriftung('traktor' as VehicleClass); |
| 371 | expect(text).not.toContain('undefined'); |
| 372 | expect(text).not.toContain('kein Zuschlag'); |
| 373 | expect(text).toContain('unbekannt'); |
| 374 | expect(text).toContain('traktor'); |
| 375 | expect(text).toContain('Prüfbericht'); |
| 376 | // Waechter: bekannte Fahrzeugarten wie bisher. |
| 377 | expect(fahrzeugartBeschriftung('pkw')).toBe(`Pkw (${SAFETY_FLOORS.minVehicleLengthKfz} m)`); |
| 378 | expect(fahrzeugartBeschriftung('keine')).toBe('ohne Längenzuschlag'); |
| 379 | }); |
| 380 | }); |