lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ tests services dateinamensvorschlag.test.ts

4,4 KB Rohdatei
tests/services/dateinamensvorschlag.test.ts — 102 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { suggestFileName } from '@/services/storage';
3 import { createEmptyProject } from '@/domain/model/factory';
4 import type { Project } from '@/domain/model/project';
5
6 /**
7 * Der Dateinamensvorschlag muss unter Windows anlegbar sein.
8 *
9 * Befunde 71 und 72 zur Fassung 5.10.0. Beide Faelle haengen
10 * an derselben Quelle: `str` in src/domain/model/schema.ts reicht jede
11 * Zeichenkette einer Projektdatei ungefiltert an `meta.name` und
12 * `meta.projectNumber` durch, und `suggestFileName` baut daraus den Namen, der
13 * ueber `bridge.saveFile({ defaultName })` unveraendert in den
14 * Speichern-Dialog geht - `path.basename` im Hauptprozess reinigt nichts nach.
15 *
16 * 71: Windows verbietet neben den neun druckbaren Zeichen \ / : * ? " < > |
17 * auch den ganzen Bereich U+0000 bis U+001F. Ersetzt wurden nur die neun; von
18 * den Steuerzeichen fielen allein \t \n \v \f \r auf, weil sie als Leerraum
19 * gelten. U+0001 oder U+0007 standen unveraendert im Vorschlag - unsichtbar
20 * fuer den Anwender, und `open()` scheitert damit unter Windows mit ENOENT.
21 *
22 * 72: Die Begrenzung schnitt mit `slice(0, 120)` nach UTF-16-Codeeinheiten.
23 * Faellt ein Zeichen ausserhalb der Grundebene auf die Schnittstelle, blieb die
24 * einsame hohe Ersatzhaelfte stehen; Windows legt daraufhin eine Datei mit
25 * U+FFFD an, und der zurueckgemeldete Pfad weicht von dem ab, was das Programm
26 * vorgeschlagen hat.
27 *
28 * Die Steuerzeichen stehen hier durchgaengig als Escape und nie als Zeichen:
29 * Im Quelltext waeren sie unsichtbar, und Git fuehrt eine Datei mit einem
30 * Nullbyte als Binaerdatei (dieselbe Begruendung wie in src/render/pdfSurface.ts).
31 */
32
33 const JETZT = new Date('2026-09-06T09:00:00.000Z');
34
35 function mitMeta(projectNumber: string, name: string): Project {
36 const basis = createEmptyProject('x', JETZT);
37 return { ...basis, meta: { ...basis.meta, projectNumber, name } };
38 }
39
40 /** Irgendein Steuerzeichen U+0000 bis U+001F. */
41 // eslint-disable-next-line no-control-regex
42 const STEUERZEICHEN = /[\x00-\x1f]/;
43
44 /** Eine Ersatzhaelfte ohne ihre Gegenhaelfte. */
45 const EINSAME_ERSATZHAELFTE =
46 /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
47
48 describe('Dateinamensvorschlag: Steuerzeichen (Befund 71)', () => {
49 it('ersetzt auch die nicht als Leerraum geltenden Steuerzeichen', () => {
50 // U+0007 und U+0001 - beide ueberlebten die Ersetzung der neun druckbaren
51 // Zeichen, und beide lehnt Windows in einem Dateinamen ab.
52 const bel = String.fromCharCode(0x07);
53 const soh = String.fromCharCode(0x01);
54 const name = suggestFileName(mitMeta('', `Knoten${bel}B7${soh} Nord`));
55
56 expect(name).toBe('Knoten-B7- Nord.lsap');
57 expect(name).not.toMatch(STEUERZEICHEN);
58 });
59
60 it('laesst kein einziges Steuerzeichen von U+0000 bis U+001F stehen', () => {
61 const stehengeblieben: string[] = [];
62 for (let code = 0; code <= 0x1f; code += 1) {
63 const name = suggestFileName(mitMeta('', `A${String.fromCharCode(code)}B`));
64 if (STEUERZEICHEN.test(name)) stehengeblieben.push(`U+${code.toString(16).padStart(4, '0')}`);
65 }
66
67 expect(stehengeblieben).toEqual([]);
68 });
69
70 it('zieht Zeilenumbruch und Tabulator weiterhin zu einem Leerzeichen zusammen', () => {
71 // Die Zusage des Bestands bleibt: Leerraum wird zusammengezogen, nicht
72 // durch Bindestriche ersetzt. Deshalb laeuft die Ersetzung der
73 // Steuerzeichen NACH dem Zusammenziehen.
74 expect(suggestFileName(mitMeta('', 'Zeile eins\nZeile zwei\tdrei'))).toBe(
75 'Zeile eins Zeile zwei drei.lsap',
76 );
77 });
78 });
79
80 describe('Dateinamensvorschlag: Grenze von 120 Zeichen (Befund 72)', () => {
81 it('zerteilt an der Grenze kein Zeichen ausserhalb der Grundebene', () => {
82 const lang = `${'K'.repeat(119)}\u{1F6A6} und noch etwas dahinter`;
83
84 const name = suggestFileName(mitMeta('', lang));
85
86 expect(name).toBe(`${'K'.repeat(119)}\u{1F6A6}.lsap`);
87 expect(name).not.toMatch(EINSAME_ERSATZHAELFTE);
88 });
89
90 it('zaehlt die Grenze in Zeichen, nicht in Codeeinheiten', () => {
91 const lang = '\u{1F6A6}'.repeat(200);
92
93 const name = suggestFileName(mitMeta('', lang));
94
95 expect([...name.slice(0, -'.lsap'.length)]).toHaveLength(120);
96 expect(name).not.toMatch(EINSAME_ERSATZHAELFTE);
97 });
98
99 it('begrenzt reinen Grundebenentext weiterhin auf 120 Zeichen', () => {
100 expect(suggestFileName(mitMeta('', 'K'.repeat(200)))).toBe(`${'K'.repeat(120)}.lsap`);
101 });
102 });