import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import { defaultMovement, fahrbeziehungZurVerkehrsart, zulaessigeFahrbeziehungen, } from '@/domain/model/factory'; import { parseProject } from '@/domain/model/schema'; import { clearingSpeedFor } from '@/domain/rilsa/intergreen'; import type { Movement, TrafficMode } from '@/domain/rilsa/types'; /* * Fassung 5.10.0 - die Zuordnung Fahrbeziehung zu Verkehrsart gehoert in den * Fachkern. * * Befund 44 hat die Auswahlliste an die Verkehrsart gebunden, die Zuordnung * dabei aber in der Oberflaeche abgelegt: Sie stand in * `src/ui/views/signalGroupsView.ts`, und `lageplanView.ts` holte * `movementOptions` und `fahrbeziehungZurVerkehrsart` von dort. Damit war sie * fuer das Einlesen nicht erreichbar - `oneOf` in `model/schema.ts` prueft die * Fahrbeziehung gegen die Werteliste, nicht gegen die Verkehrsart. Eine * Projektdatei mit "querung" an einer Kfz-Gruppe oder "links" an einer Furt kam * ohne ein Wort durch, waehrend die Oberflaeche denselben Eintrag abfing. * * WAS DIE ERSETZUNG AENDERT: die AUSGEWIESENE Fahrbeziehung, nicht die * Zwischenzeit. `clearingSpeedFor` verzweigt ueber `mode`; eine Furt raeumt mit * 1,2 m/s, auch wenn "geradeaus" daran steht. Der letzte Fall hier haelt das * fest - die Behebung geht in keine der beiden Sicherheitsrichtungen. */ const ALLE_ARTEN: readonly TrafficMode[] = ['kfz', 'rad', 'fuss', 'oepnv']; /** Ein eingelesenes Projekt mit genau einer Signalgruppe. */ function gruppeEinlesen(mode: TrafficMode, movement: unknown) { const { project, issues, repaired } = parseProject({ schemaVersion: 13, signalGroups: [ { id: 'sg-1', name: 'K1', mode, movement, armId: null, vZul: 50, lanes: 1, vehicleClass: mode === 'fuss' ? 'keine' : 'pkw', minGreenOverride: null, maxGreenOverride: null, reducedMobility: false, color: '#2f6fd0', }, ], }); const meldungen = issues.filter((i) => i.path === 'signalGroups[0].movement'); return { movement: project.signalGroups[0]?.movement, meldungen, repaired }; } /** Ein eingelesenes Projekt mit genau einer Fahrlinie im Lageplan. */ function linieEinlesen(mode: TrafficMode, movement: unknown) { const { project, issues } = parseProject({ schemaVersion: 13, lageplan: { bild: null, kalibrierung: null, haltlinien: [], linien: [ { id: 'pl-1', name: 'Linie 1', mode, movement, breiteMeter: 3.25, punkte: [ { x: 0, y: 0 }, { x: 100, y: 0 }, ], haltlinieId: null, startT: 0.5, signalGroupId: null, }, ], }, }); const meldungen = issues.filter((i) => i.path === 'lageplan.linien[0].movement'); return { movement: project.lageplan.linien[0]?.movement, meldungen }; } describe('Der Fachkern sagt, welche Fahrbeziehung zu welcher Verkehrsart passt', () => { it('laesst nur die Furt queren', () => { expect(zulaessigeFahrbeziehungen('fuss')).toEqual(['querung']); }); it('gibt Kfz- und OePNV-Strömen drei Fahrbeziehungen', () => { for (const mode of ['kfz', 'oepnv'] as const) { expect(zulaessigeFahrbeziehungen(mode), mode).toEqual(['geradeaus', 'rechts', 'links']); } }); it('laesst den Radverkehr auch queren', () => { /* * NICHT wie vorgeschlagen dieselbe Liste wie beim Kraftfahrzeugverkehr: * Die Radquerung ist ein gefuehrter Fall des Programms - * `geometrie/lageplan.ts` nennt zur Haltlinienart 'querung' "Furt oder * Radquerung", und `validation/engine.ts` verlangt von einer Anlage "nur * aus Furten und Radfurten" keine Verkehrsstaerken. Auch die Hin- und * Rueckwegprobe der Projektdatei fuehrt eine Radgruppe mit 'querung' und * erwartet dazu keine Meldung. */ expect(zulaessigeFahrbeziehungen('rad')).toEqual(['geradeaus', 'rechts', 'links', 'querung']); // Die Vorbelegung bleibt trotzdem 'geradeaus' - so steht es in // validation/rules.ts. expect(defaultMovement('rad')).toBe('geradeaus'); }); it('nennt keine Verkehrsart ohne Fahrbeziehung', () => { for (const mode of ALLE_ARTEN) { expect(zulaessigeFahrbeziehungen(mode).length, mode).toBeGreaterThan(0); } }); it('setzt die Standardfahrbeziehung als erste der zulaessigen', () => { // Ein zweiter, danebenstehender Rueckfallwert waere eine zweite Fassung // derselben Zuordnung. for (const mode of ALLE_ARTEN) { expect(defaultMovement(mode), mode).toBe(zulaessigeFahrbeziehungen(mode)[0]); } }); it('behaelt beim Wechsel der Verkehrsart, was weiter passt', () => { expect(fahrbeziehungZurVerkehrsart('kfz', 'links')).toBe('links'); expect(fahrbeziehungZurVerkehrsart('rad', 'rechts')).toBe('rechts'); }); it('ersetzt beim Wechsel der Verkehrsart, was nicht mehr passt', () => { expect(fahrbeziehungZurVerkehrsart('kfz', 'querung')).toBe('geradeaus'); expect(fahrbeziehungZurVerkehrsart('fuss', 'links')).toBe('querung'); }); it('macht aus einer Furt beim Wechsel auf Radverkehr eine Radquerung', () => { expect(fahrbeziehungZurVerkehrsart('rad', 'querung')).toBe('querung'); }); }); describe('Das Einlesen prueft die Fahrbeziehung gegen die Verkehrsart', () => { it('ersetzt "querung" an der Kfz-Gruppe und meldet die Ersetzung', () => { const { movement, meldungen, repaired } = gruppeEinlesen('kfz', 'querung'); expect(movement).toBe('geradeaus'); expect(meldungen).toHaveLength(1); expect(meldungen[0]?.severity).toBe('warnung'); expect(meldungen[0]?.message).toContain('querung'); expect(meldungen[0]?.message).toContain('unzulässig'); expect(meldungen[0]?.message).toContain('Signalgruppe'); // Die Datei ist veraendert worden; der Anwender muss das beim Speichern // wissen. expect(repaired).toBe(true); }); it('ersetzt "links" an der Fussgaengergruppe und meldet die Ersetzung', () => { const { movement, meldungen } = gruppeEinlesen('fuss', 'links'); expect(movement).toBe('querung'); expect(meldungen).toHaveLength(1); expect(meldungen[0]?.message).toContain('querung'); }); it('ersetzt "querung" an der Kfz-Fahrlinie des Lageplans und meldet es', () => { const { movement, meldungen } = linieEinlesen('kfz', 'querung'); expect(movement).toBe('geradeaus'); expect(meldungen).toHaveLength(1); expect(meldungen[0]?.message).toContain('Fahrlinie'); }); it('ersetzt "geradeaus" an der Fusslinie des Lageplans und meldet es', () => { const { movement, meldungen } = linieEinlesen('fuss', 'geradeaus'); expect(movement).toBe('querung'); expect(meldungen).toHaveLength(1); }); it('meldet eine unbekannte Fahrbeziehung weiterhin nur einmal', () => { // `oneOf` ersetzt sie bereits durch die Standardfahrbeziehung. Zwei // Meldungen zu einem Eintrag waeren eine zu viel. const { movement, meldungen } = gruppeEinlesen('fuss', 'wenden'); expect(movement).toBe('querung'); expect(meldungen).toHaveLength(1); expect(meldungen[0]?.message).toContain('unbekannt'); }); }); describe('Gegenproben - was unveraendert bleibt', () => { it('laesst jede zulaessige Fahrbeziehung stehen und schweigt dazu', () => { for (const mode of ALLE_ARTEN) { for (const movement of zulaessigeFahrbeziehungen(mode)) { const gelesen = gruppeEinlesen(mode, movement); expect(gelesen.movement, `${mode}/${movement}`).toBe(movement); expect(gelesen.meldungen, `${mode}/${movement}`).toHaveLength(0); } } }); it('aendert an der Raeumgeschwindigkeit nichts - die entscheidet die Verkehrsart', () => { // Beide Ersetzungen wiegen die Zwischenzeit nicht auf: Die Furt raeumt mit // ihren 1,2 m/s, gleich welche Fahrbeziehung an ihr steht, und der // querende Kfz-Strom raeumte schon vorher wie ein geradeausfahrender. const furt = (movement: Movement) => clearingSpeedFor({ mode: 'fuss', movement, reducedMobility: false }); expect(furt('geradeaus')).toBe(furt('querung')); const kfz = (movement: Movement) => clearingSpeedFor({ mode: 'kfz', movement, reducedMobility: false }); expect(kfz('querung')).toBe(kfz('geradeaus')); }); }); describe('Die Zuordnung steht an einer Stelle', () => { const SIGNALGRUPPEN = readFileSync('src/ui/views/signalGroupsView.ts', 'utf8'); const LAGEPLAN = readFileSync('src/ui/views/lageplanView.ts', 'utf8'); it('fuehrt in keiner der beiden Ansichten eine eigene Werteliste', () => { // Gesucht wird die Liste selbst, nicht ihr Name: Eine zweite Fassung // entstuende als Aufzaehlung im Quelltext. for (const [name, quelle] of [ ['signalGroupsView.ts', SIGNALGRUPPEN], ['lageplanView.ts', LAGEPLAN], ] as const) { expect(quelle, name).not.toMatch(/'geradeaus',\s*'rechts',\s*'links'/); expect(quelle, name).not.toMatch(/'querung'\s*\]/); } }); it('bindet die Lageplanansicht die Signalgruppentabelle nicht mehr ein', () => { expect(LAGEPLAN).not.toContain("from './signalGroupsView'"); }); });