import { afterEach, describe, expect, it } from 'vitest'; import { openProjectFromFile, serializeProject } from '@/services/storage'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { CURRENT_SCHEMA_VERSION } from '@/domain/model/project'; import type { DesktopBridge, OpenFileResult } from '@/platform/bridge'; /** * Was das Einlesen einer Datei mit dem Dateipfad tun darf. * * Befunde 41 und 15 zur Fassung 5.10.0. Beide enden im selben Schaden: Der Pfad * einer Datei, deren Inhalt das Programm gerade NICHT vollstaendig uebernommen * hat, wird als Ziel des naechsten Speicherns gefuehrt. Weil `store.replace` * mit Pfad die Aenderungsmarke loescht, meldet die Kopfzeile "gespeichert", und * Strg+S laeuft ohne Dialog ueber `bridge.writeFile` auf genau diese Datei - * der Hauptprozess hat den Pfad beim Oeffnen mit `approvePath` * schreibberechtigt gemacht. * * 41: Jede beliebige JSON-Datei wurde angenommen. Die Zerlegehelfer in * schema.ts melden nur vorhandene, unpassende Werte; fehlende Schluessel * erzeugen null Meldungen, `showImportIssues` unterdrueckt den Hinweiskasten * daraufhin ganz. Der Anwender bekam "Projekt geladen.", sah ein leeres * Projekt namens "Importiertes Projekt" - und sein naechstes Strg+S ersetzte * die fremde Datei durch dieses leere Projekt. Die Wache `looksLikeProject` * gab es, sie hatte nur keinen Aufrufer. * * 15: Eine Datei aus einer neueren Programmversion erzeugte nur eine * Hinweiszeile. Danach galt sie als heil: Pfad uebernommen, Aenderungsmarke * geloescht. Das naechste Speichern schrieb sie mit der eigenen Schemaversion * zurueck, und jedes Feld, das diese Fassung nicht kennt, war fort. Beide * Sitzungswege haben laengst eine Wache vor `loadProject` (siehe UNBRAUCHBAR * in src/services/storage.ts); der Dateiweg hatte keine. */ function mitBruecke(antwort: OpenFileResult): void { const bruecke = { openFile: () => Promise.resolve(antwort), } as unknown as DesktopBridge; (globalThis as unknown as { window?: { lsaPlaner?: DesktopBridge } }).window = { lsaPlaner: bruecke, }; } function antwort(teil: Partial): OpenFileResult { return { ok: true, canceled: false, filePath: 'C:/Plaene/Fremd.json', content: null, error: null, ...teil, }; } afterEach(() => { delete (globalThis as unknown as { window?: unknown }).window; }); describe('Fremde JSON-Datei (Befund 41)', () => { it('weist eine Datei ab, die kein Projekt ist, statt ein leeres zu bauen', async () => { mitBruecke(antwort({ content: '{"einkaufsliste":["Milch","Brot"]}' })); const ergebnis = await openProjectFromFile(); expect(ergebnis.ok).toBe(false); expect(ergebnis.result).toBeNull(); expect(ergebnis.message).toContain('kein Projekt'); }); it('bindet den Pfad der fremden Datei nicht an das Programm', async () => { mitBruecke(antwort({ content: '{"einkaufsliste":["Milch","Brot"]}' })); const ergebnis = await openProjectFromFile(); // Ohne Pfad kein rueckfragefreies Ueberschreiben: Das ist der eigentliche // Schaden des Befundes. expect(ergebnis.filePath).toBeNull(); }); it('nimmt eine 4.x-Datei weiterhin an - sie traegt keine Schemaversion', async () => { mitBruecke( antwort({ content: JSON.stringify({ projektdaten: { name: 'Altbestand' }, signalgruppen: [{ name: 'K1', typ: 'kfz' }], }), }), ); const ergebnis = await openProjectFromFile(); expect(ergebnis.ok).toBe(true); expect(ergebnis.result?.migrated).toBe(true); }); it('nimmt eine selbst geschriebene Datei weiterhin an', async () => { mitBruecke(antwort({ content: serializeProject(createStandardIntersectionProject()) })); const ergebnis = await openProjectFromFile(); expect(ergebnis.ok).toBe(true); expect(ergebnis.filePath).toBe('C:/Plaene/Fremd.json'); }); }); describe('Datei aus einer neueren Programmversion (Befund 15)', () => { function neuereDatei(): string { return JSON.stringify({ ...createStandardIntersectionProject(), schemaVersion: CURRENT_SCHEMA_VERSION + 1, }); } it('uebernimmt den Dateipfad nicht - Speichern muss nach einem Ziel fragen', async () => { mitBruecke(antwort({ content: neuereDatei(), filePath: 'C:/Plaene/Kollege.lsap' })); const ergebnis = await openProjectFromFile(); expect(ergebnis.ok).toBe(true); expect(ergebnis.filePath).toBeNull(); }); it('sagt in der Meldung, dass der Pfad nicht uebernommen wurde', async () => { mitBruecke(antwort({ content: neuereDatei(), filePath: 'C:/Plaene/Kollege.lsap' })); const ergebnis = await openProjectFromFile(); expect(ergebnis.message).toContain('neueren Programmversion'); expect(ergebnis.message).toContain('nicht überschrieben'); }); it('liest die Datei trotzdem ein und behaelt die Hinweiszeile', async () => { mitBruecke(antwort({ content: neuereDatei(), filePath: 'C:/Plaene/Kollege.lsap' })); const ergebnis = await openProjectFromFile(); expect(ergebnis.result?.project.signalGroups.length).toBeGreaterThan(0); expect(ergebnis.result?.issues.some((i) => i.path === 'schemaVersion')).toBe(true); }); it('laesst den Pfad einer Datei der eigenen Fassung unangetastet', async () => { mitBruecke( antwort({ content: serializeProject(createStandardIntersectionProject()), filePath: 'C:/Plaene/Eigen.lsap', }), ); const ergebnis = await openProjectFromFile(); expect(ergebnis.filePath).toBe('C:/Plaene/Eigen.lsap'); }); });