import { beforeAll, describe, expect, it, vi } from 'vitest'; import { ladeSitzung, saveProjectToFile, saveSession } from '@/services/storage'; import { createEmptyProject } from '@/domain/model/factory'; /** * Befund 23 zur Fassung 5.9.0. * * Ohne Desktop-Bruecke - `npm run dev` im Browser, und im Auslieferungsstand * dann, wenn `preload.js` unter `sandbox: true` stumm scheitert - faellt * `saveProjectToFile` auf `downloadInBrowser` zurueck. Diese Funktion ist * `void` und klickt nur ein `` an; danach wurde fest * `{ ok: true, ... }` zurueckgegeben. Ob die Ablage zustande kam - der Anwender * bricht den Speichern-unter-Kasten des Browsers ab, eine Unternehmensrichtlinie * sperrt den Download -, laesst sich von aussen nicht feststellen. * * In `src/main.ts` folgte daraus `store.markSaved(...)`: Die Aenderungsmarke * fiel, Kopfzeile und Statusleiste sagten "gespeichert", und der naechste * Griff zu "Neu" oder "Öffnen" verwarf die Arbeit ohne Rueckfrage - genau die * Lage, die der Kommentar bei `speichereEinmal` fuer den Wettlauffall bereits * als unzulaessig beschreibt. * * Geprueft wird dreifach: die Auskunft der Speicherschicht selbst, was der * Programmstart daraus macht - und dass der zweite Weg durch denselben * Rueckfall, `saveDataToFile` fuer PDF, CSV und die ausgegebene Projektdatei, * dieselbe Auskunft gibt. Er meldete zuvor "Heruntergeladen: ..." und damit * denselben Vorgang in derselben Datei anders als 30 Zeilen darueber. */ vi.mock('@/services/storage', async (importOriginal) => { const echt = await importOriginal(); return { ...echt, ladeSitzung: vi.fn(), saveSession: vi.fn(), addRecoveryPoint: vi.fn(), saveProjectToFile: vi.fn(), }; }); async function warte(ms = 0): Promise { await new Promise((fertig) => setTimeout(fertig, ms)); } function offeneFenster(): string[] { return [...document.querySelectorAll('.dialog-hintergrund h2')].map( (h) => h.textContent?.trim() ?? '', ); } function feld(beschriftung: string): HTMLInputElement { const label = [...document.querySelectorAll('label')].find( (l) => l.textContent?.trim() === beschriftung, ); const id = label?.getAttribute('for'); const eingabe = id === null || id === undefined ? null : document.getElementById(id); if (!(eingabe instanceof HTMLInputElement)) { throw new Error(`Eingabefeld "${beschriftung}" nicht gefunden.`); } return eingabe; } function druecke(taste: string): void { window.dispatchEvent( new KeyboardEvent('keydown', { key: taste, ctrlKey: true, bubbles: true, cancelable: true }), ); } beforeAll(async () => { // jsdom kennt weder createObjectURL noch revokeObjectURL; der Rueckfallweg // braucht beide, um ueberhaupt bis zu seiner Rueckgabe zu kommen. Object.defineProperty(URL, 'createObjectURL', { value: () => 'blob:pruefung', configurable: true, }); Object.defineProperty(URL, 'revokeObjectURL', { value: () => undefined, configurable: true }); vi.mocked(ladeSitzung).mockResolvedValue({ art: 'leer' }); vi.mocked(saveSession).mockResolvedValue({ ok: true, message: '' }); vi.mocked(saveProjectToFile).mockResolvedValue({ ok: true, canceled: false, filePath: null, unbestaetigt: true, message: 'Herunterladen angestoßen: Projekt.lsap', }); document.body.replaceChildren(); const wurzel = document.createElement('div'); wurzel.id = 'anwendung'; document.body.append(wurzel); await import('@/main'); await warte(20); }); describe('Speichern ohne Desktop-Brücke', () => { it('behauptet keinen bestätigten Schreibvorgang', async () => { // Die echte Funktion, nicht die Attrappe: Ohne `window.lsaPlaner` faellt sie // auf das Herunterladen im Browser zurueck. const { saveProjectToFile: echtesSpeichern } = await vi.importActual('@/services/storage'); const ergebnis = await echtesSpeichern(createEmptyProject('Bahnhofstraße'), null); expect( ergebnis.unbestaetigt, 'Der Rueckfallweg gibt einen Schreibvorgang als bestaetigt aus, den niemand bestaetigt hat', ).toBe(true); expect(ergebnis.filePath).toBeNull(); expect(ergebnis.message).not.toContain('Gespeichert'); }); it('lässt die Änderungsmarke stehen, statt "gespeichert" zu melden', async () => { const name = feld('Projektbezeichnung'); name.value = 'Bahnhofstraße / Ringstraße'; name.dispatchEvent(new Event('change')); await warte(0); druecke('s'); await warte(20); expect(saveProjectToFile).toHaveBeenCalledTimes(1); // Die Probe: Steht der Stand in keiner nachweislich geschriebenen Datei, // muss "Neu" zurueckfragen. druecke('n'); await warte(10); expect(offeneFenster(), 'Der Knotenpunkt liess sich ohne Rueckfrage verwerfen').toEqual([ 'Ungespeicherte Änderungen', ]); document.querySelector('.dialog-hintergrund button')?.click(); await warte(10); }); }); describe('Ausgabe von PDF, CSV und Projektdatei ohne Desktop-Brücke', () => { it('gibt denselben Vorbehalt aus wie das Speichern', async () => { const { saveDataToFile } = await vi.importActual('@/services/storage'); const ergebnis = await saveDataToFile( 'Signalgruppe;Räumweg\nK1;12,0\n', 'Bahnhofstraße - Zwischenzeiten.csv', [{ name: 'CSV-Datei', extensions: ['csv'] }], 'text/csv', ); expect( ergebnis.unbestaetigt, 'Der Ausgabeweg gibt einen Schreibvorgang als bestaetigt aus, den niemand bestaetigt hat', ).toBe(true); expect( ergebnis.message, 'Derselbe Vorgang wird in derselben Datei zweimal verschieden beschrieben', ).toBe('Herunterladen angestoßen: Bahnhofstraße - Zwischenzeiten.csv'); }); });