lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests services fremdeDateiAbweisen.test.ts
| 1 | import { afterEach, describe, expect, it } from 'vitest'; |
| 2 | import { openProjectFromFile, serializeProject } from '@/services/storage'; |
| 3 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 4 | import { CURRENT_SCHEMA_VERSION } from '@/domain/model/project'; |
| 5 | import type { DesktopBridge, OpenFileResult } from '@/platform/bridge'; |
| 6 | |
| 7 | /** |
| 8 | * Was das Einlesen einer Datei mit dem Dateipfad tun darf. |
| 9 | * |
| 10 | * Befunde 41 und 15 zur Fassung 5.10.0. Beide enden im selben Schaden: Der Pfad |
| 11 | * einer Datei, deren Inhalt das Programm gerade NICHT vollstaendig uebernommen |
| 12 | * hat, wird als Ziel des naechsten Speicherns gefuehrt. Weil `store.replace` |
| 13 | * mit Pfad die Aenderungsmarke loescht, meldet die Kopfzeile "gespeichert", und |
| 14 | * Strg+S laeuft ohne Dialog ueber `bridge.writeFile` auf genau diese Datei - |
| 15 | * der Hauptprozess hat den Pfad beim Oeffnen mit `approvePath` |
| 16 | * schreibberechtigt gemacht. |
| 17 | * |
| 18 | * 41: Jede beliebige JSON-Datei wurde angenommen. Die Zerlegehelfer in |
| 19 | * schema.ts melden nur vorhandene, unpassende Werte; fehlende Schluessel |
| 20 | * erzeugen null Meldungen, `showImportIssues` unterdrueckt den Hinweiskasten |
| 21 | * daraufhin ganz. Der Anwender bekam "Projekt geladen.", sah ein leeres |
| 22 | * Projekt namens "Importiertes Projekt" - und sein naechstes Strg+S ersetzte |
| 23 | * die fremde Datei durch dieses leere Projekt. Die Wache `looksLikeProject` |
| 24 | * gab es, sie hatte nur keinen Aufrufer. |
| 25 | * |
| 26 | * 15: Eine Datei aus einer neueren Programmversion erzeugte nur eine |
| 27 | * Hinweiszeile. Danach galt sie als heil: Pfad uebernommen, Aenderungsmarke |
| 28 | * geloescht. Das naechste Speichern schrieb sie mit der eigenen Schemaversion |
| 29 | * zurueck, und jedes Feld, das diese Fassung nicht kennt, war fort. Beide |
| 30 | * Sitzungswege haben laengst eine Wache vor `loadProject` (siehe UNBRAUCHBAR |
| 31 | * in src/services/storage.ts); der Dateiweg hatte keine. |
| 32 | */ |
| 33 | |
| 34 | function mitBruecke(antwort: OpenFileResult): void { |
| 35 | const bruecke = { |
| 36 | openFile: () => Promise.resolve(antwort), |
| 37 | } as unknown as DesktopBridge; |
| 38 | (globalThis as unknown as { window?: { lsaPlaner?: DesktopBridge } }).window = { |
| 39 | lsaPlaner: bruecke, |
| 40 | }; |
| 41 | } |
| 42 | |
| 43 | function antwort(teil: Partial<OpenFileResult>): OpenFileResult { |
| 44 | return { |
| 45 | ok: true, |
| 46 | canceled: false, |
| 47 | filePath: 'C:/Plaene/Fremd.json', |
| 48 | content: null, |
| 49 | error: null, |
| 50 | ...teil, |
| 51 | }; |
| 52 | } |
| 53 | |
| 54 | afterEach(() => { |
| 55 | delete (globalThis as unknown as { window?: unknown }).window; |
| 56 | }); |
| 57 | |
| 58 | describe('Fremde JSON-Datei (Befund 41)', () => { |
| 59 | it('weist eine Datei ab, die kein Projekt ist, statt ein leeres zu bauen', async () => { |
| 60 | mitBruecke(antwort({ content: '{"einkaufsliste":["Milch","Brot"]}' })); |
| 61 | |
| 62 | const ergebnis = await openProjectFromFile(); |
| 63 | |
| 64 | expect(ergebnis.ok).toBe(false); |
| 65 | expect(ergebnis.result).toBeNull(); |
| 66 | expect(ergebnis.message).toContain('kein Projekt'); |
| 67 | }); |
| 68 | |
| 69 | it('bindet den Pfad der fremden Datei nicht an das Programm', async () => { |
| 70 | mitBruecke(antwort({ content: '{"einkaufsliste":["Milch","Brot"]}' })); |
| 71 | |
| 72 | const ergebnis = await openProjectFromFile(); |
| 73 | |
| 74 | // Ohne Pfad kein rueckfragefreies Ueberschreiben: Das ist der eigentliche |
| 75 | // Schaden des Befundes. |
| 76 | expect(ergebnis.filePath).toBeNull(); |
| 77 | }); |
| 78 | |
| 79 | it('nimmt eine 4.x-Datei weiterhin an - sie traegt keine Schemaversion', async () => { |
| 80 | mitBruecke( |
| 81 | antwort({ |
| 82 | content: JSON.stringify({ |
| 83 | projektdaten: { name: 'Altbestand' }, |
| 84 | signalgruppen: [{ name: 'K1', typ: 'kfz' }], |
| 85 | }), |
| 86 | }), |
| 87 | ); |
| 88 | |
| 89 | const ergebnis = await openProjectFromFile(); |
| 90 | |
| 91 | expect(ergebnis.ok).toBe(true); |
| 92 | expect(ergebnis.result?.migrated).toBe(true); |
| 93 | }); |
| 94 | |
| 95 | it('nimmt eine selbst geschriebene Datei weiterhin an', async () => { |
| 96 | mitBruecke(antwort({ content: serializeProject(createStandardIntersectionProject()) })); |
| 97 | |
| 98 | const ergebnis = await openProjectFromFile(); |
| 99 | |
| 100 | expect(ergebnis.ok).toBe(true); |
| 101 | expect(ergebnis.filePath).toBe('C:/Plaene/Fremd.json'); |
| 102 | }); |
| 103 | }); |
| 104 | |
| 105 | describe('Datei aus einer neueren Programmversion (Befund 15)', () => { |
| 106 | function neuereDatei(): string { |
| 107 | return JSON.stringify({ |
| 108 | ...createStandardIntersectionProject(), |
| 109 | schemaVersion: CURRENT_SCHEMA_VERSION + 1, |
| 110 | }); |
| 111 | } |
| 112 | |
| 113 | it('uebernimmt den Dateipfad nicht - Speichern muss nach einem Ziel fragen', async () => { |
| 114 | mitBruecke(antwort({ content: neuereDatei(), filePath: 'C:/Plaene/Kollege.lsap' })); |
| 115 | |
| 116 | const ergebnis = await openProjectFromFile(); |
| 117 | |
| 118 | expect(ergebnis.ok).toBe(true); |
| 119 | expect(ergebnis.filePath).toBeNull(); |
| 120 | }); |
| 121 | |
| 122 | it('sagt in der Meldung, dass der Pfad nicht uebernommen wurde', async () => { |
| 123 | mitBruecke(antwort({ content: neuereDatei(), filePath: 'C:/Plaene/Kollege.lsap' })); |
| 124 | |
| 125 | const ergebnis = await openProjectFromFile(); |
| 126 | |
| 127 | expect(ergebnis.message).toContain('neueren Programmversion'); |
| 128 | expect(ergebnis.message).toContain('nicht überschrieben'); |
| 129 | }); |
| 130 | |
| 131 | it('liest die Datei trotzdem ein und behaelt die Hinweiszeile', async () => { |
| 132 | mitBruecke(antwort({ content: neuereDatei(), filePath: 'C:/Plaene/Kollege.lsap' })); |
| 133 | |
| 134 | const ergebnis = await openProjectFromFile(); |
| 135 | |
| 136 | expect(ergebnis.result?.project.signalGroups.length).toBeGreaterThan(0); |
| 137 | expect(ergebnis.result?.issues.some((i) => i.path === 'schemaVersion')).toBe(true); |
| 138 | }); |
| 139 | |
| 140 | it('laesst den Pfad einer Datei der eigenen Fassung unangetastet', async () => { |
| 141 | mitBruecke( |
| 142 | antwort({ |
| 143 | content: serializeProject(createStandardIntersectionProject()), |
| 144 | filePath: 'C:/Plaene/Eigen.lsap', |
| 145 | }), |
| 146 | ); |
| 147 | |
| 148 | const ergebnis = await openProjectFromFile(); |
| 149 | |
| 150 | expect(ergebnis.filePath).toBe('C:/Plaene/Eigen.lsap'); |
| 151 | }); |
| 152 | }); |