import { readFileSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; import { HILFE, glossarEintraege } from '@/ui/help/texte'; import type { HilfeSchluessel, Hilfetext } from '@/ui/help/texte'; import { parseProject } from '@/domain/model/schema'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { compareCycleTimeMethods } from '@/domain/rilsa/cycle'; import { RILSA_DEFAULTS } from '@/domain/rilsa/constants'; /* * Fassung 5.10.0 - Hilfetexte sind Zusagen. * * Die Sammlung in src/ui/help/texte.ts wird dem Anwender am Feld und im * Glossar ausgegeben; sie leitet seine Handmessung an. Ein Satz, der etwas * anderes sagt als der Fachkern rechnet, ist deshalb kein Schoenheitsfehler, * sondern eine falsche Anweisung. Diese Datei bewacht fuenf solcher Saetze. */ const DATUM = new Date('2026-01-01T00:00:00Z'); const BEISPIELDATEI = 'beispiele/vierarmiger-knotenpunkt.lsap'; const SCHLUESSEL = Object.keys(HILFE) as HilfeSchluessel[]; /** Ein Eintrag als Hilfetext - so sind auch die freiwilligen Felder lesbar. */ function eintrag(schluessel: HilfeSchluessel): Hilfetext { return HILFE[schluessel]; } /** Alle Textfelder eines Eintrags in einer Zeichenkette. */ function ganzerText(schluessel: HilfeSchluessel): string { const e = eintrag(schluessel); return [e.wasIstDas, e.woher ?? '', e.ueblich ?? '', e.wennFalsch ?? ''].join(' '); } describe('Furt mit Mittelinsel: nur der Raeumweg geht ueber die ganze Breite (Befund 15)', () => { /* * intergreen.ts rechnet raw = tue + tr - te mit te = se / ve. Ein zu gross * angesetzter Einfahrweg VERKUERZT die Zwischenzeit - die gefaehrliche * Richtung. Der Einfahrweg endet an der vorderen Kante des * Konfliktbereichs (types.ts, vermessung.ts), an einer Furt also nach null * bis wenigen Metern. */ it('weist den Einfahrweg nicht ueber die ganze Furtbreite an', () => { const woher = HILFE.mittelinsel.woher; expect(woher).not.toContain('Einfahrweg werden über die ganze Furtbreite'); expect(woher).toContain('Räumweg wird über die ganze Furtbreite'); }); it('sagt, wo der Einfahrweg des Querenden endet', () => { expect(HILFE.mittelinsel.woher).toContain('vorderen Kante des Konfliktbereichs'); }); it('beschraenkt auch die uebliche Angabe auf den Raeumweg', () => { // "Fuer die Eingabe bleibt es bei der vollen Furtbreite" schloss den // Einfahrweg mit ein und nahm die Berichtigung im Feld darueber zurueck. expect(HILFE.mittelinsel.ueblich).not.toContain('Für die Eingabe bleibt es'); expect(HILFE.mittelinsel.ueblich).toContain('Räumweg'); }); it('laesst in der ganzen Sammlung keinen Satz stehen, der beides zusammenwirft', () => { // Die Gegenprobe steht im Eintrag "Einfahrweg" selbst: "Im Zweifel also // knapp messen." Kein zweiter Eintrag darf das Gegenteil anweisen. expect(HILFE.einfahrweg.wennFalsch).toContain('knapp messen'); for (const schluessel of SCHLUESSEL) { for (const satz of ganzerText(schluessel).split(/(?<=[.!?])\s+/)) { if (!satz.includes('Einfahrweg')) continue; expect(satz, schluessel).not.toMatch(/ganze[nr]? Furtbreite/); } } }); }); describe('Zusatzeinrichtung fuer Blinde: was eine aeltere Projektdatei erwartet (Befund 43)', () => { it('sagt einer aelteren Projektdatei keine Unveraenderlichkeit zu', () => { const ueblich = HILFE.blindenzusatz.ueblich; expect(ueblich).not.toContain('ändert sich also nicht'); expect(ueblich).not.toContain('so, wie es vor diesem'); }); it('nennt die Verschiebung, die das Einlesen bewirkt', () => { const ueblich = HILFE.blindenzusatz.ueblich; expect(ueblich).toContain('längere Mindestfreigabezeiten'); expect(ueblich).toContain('Umlaufzeit'); }); it('deckt sich mit dem, was die Beispieldatei aus Schema 7 wirklich ergibt', () => { // Belegstelle, nicht Wache: Sie zeigt, dass die neue Aussage stimmt. const { project } = parseProject(JSON.parse(readFileSync(BEISPIELDATEI, 'utf8')), DATUM); for (const gruppe of project.signalGroups.filter((g) => g.mode === 'fuss')) { expect(gruppe.blindenzusatz).toBeUndefined(); } const fussgruppen = buildSignalPlan(project).groups.filter((g) => g.mode === 'fuss'); expect(fussgruppen.length).toBeGreaterThan(0); for (const gruppe of fussgruppen) { expect(gruppe.times.minGreen).toBeGreaterThan(RILSA_DEFAULTS.minGreen.fuss); } }); }); describe('Kein Hilfeeintrag ohne Zugang (Befund 62)', () => { /** Alle Quelltextdateien ausser der Sammlung selbst. */ function quelltext(verzeichnis: string, gesammelt: string[] = []): string[] { for (const eintragImOrdner of readdirSync(verzeichnis, { withFileTypes: true })) { const pfad = join(verzeichnis, eintragImOrdner.name); if (eintragImOrdner.isDirectory()) quelltext(pfad, gesammelt); else if (pfad.endsWith('.ts') && !pfad.replace(/\\/g, '/').endsWith('src/ui/help/texte.ts')) gesammelt.push(pfad); } return gesammelt; } it('verlinkt die feste Freigabezeit einer Phase vom Eintrag Freigabezeit aus', () => { expect(HILFE.freigabezeit.siehe).toContain('festeFreigabezeit'); }); it('erreicht jeden Eintrag ueber Glossar, Verweis oder Bedienfeld', () => { const quellen = quelltext('src') .map((pfad) => readFileSync(pfad, 'utf8')) .join('\n'); const verlinkt = new Set(); for (const schluessel of SCHLUESSEL) { for (const verweis of eintrag(schluessel).siehe ?? []) { if (verweis !== schluessel) verlinkt.add(verweis); } } const imGlossar = new Set(glossarEintraege().map((e) => e.schluessel)); for (const schluessel of SCHLUESSEL) { const amBedienfeld = quellen.includes(`'${schluessel}'`); expect( imGlossar.has(schluessel) || verlinkt.has(schluessel) || amBedienfeld, `${schluessel} ist von keiner Stelle des Programms aus erreichbar`, ).toBe(true); } }); }); describe('Rechtsgrundlagen: wo die eigene Fundstelle eingetragen wird (Befund 63)', () => { it('schickt nicht in das Fundstellenverzeichnis, das nur anzeigt', () => { expect(HILFE.rechtsgrundlagen.woher).not.toContain('tragen Sie dort ein'); }); it('nennt den Ort, an dem das Eingabefeld wirklich steht', () => { const woher = HILFE.rechtsgrundlagen.woher; expect(woher).toContain('Vorgaben'); expect(woher).toContain('Herkunft'); }); }); describe('Verfahren zur Umlaufzeitermittlung: Einleitung und Aufzaehlung (Befund 64)', () => { const ZAHLWOERTER: Readonly> = { zwei: 2, drei: 3, vier: 4, fünf: 5, sechs: 6, }; function punkte(): readonly string[] { return HILFE.verfahren.wasIstDas.split('\n').filter((zeile) => zeile.startsWith('• ')); } it('nennt in der Einleitung keine Zahl, die von der Aufzaehlung abweicht', () => { const einleitung = HILFE.verfahren.wasIstDas.split('\n')[0] ?? ''; for (const [wort, zahl] of Object.entries(ZAHLWOERTER)) { if (!new RegExp(`\\b${wort}\\b`, 'i').test(einleitung)) continue; expect(zahl, `Einleitung nennt "${wort}", aufgezählt sind ${punkte().length}`).toBe( punkte().length, ); } }); it('fuehrt jeden Rechenansatz des Fachkerns als eigenen Punkt, dazu die feste Vorgabe', () => { // Die Ansaetze kommen aus dem Fachkern, nicht aus einer zweiten Liste: // Wer dort ein Verfahren ergaenzt, bringt diesen Fall zu Fall. const beschriftung: Readonly> = { webster: 'Webster', akcelik: 'Akçelik', hbs: 'HBS', hcm: 'HCM', }; const ansaetze = Object.keys( compareCycleTimeMethods({ lostTime: 12, criticalFlowRatios: [0.3, 0.3] }).results, ); for (const ansatz of ansaetze) { expect(HILFE.verfahren.wasIstDas).toContain(`• ${beschriftung[ansatz] ?? ansatz}`); } expect(HILFE.verfahren.wasIstDas).toContain('• Feste Vorgabe'); expect(punkte()).toHaveLength(ansaetze.length + 1); }); });