lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests ui datenschutzText.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import { gliedereText, textbloeckeAlsKnoten, type Textblock } from '@/ui/datenschutzText'; |
| 3 | |
| 4 | /** |
| 5 | * Der kleine Leser fuer docs/datenschutz.md - als Einheit, ohne Fenster. |
| 6 | * |
| 7 | * Die Faelle folgen den Zusagen im Kopf von src/ui/datenschutzText.ts: das |
| 8 | * Format der Datei, die drei Abweichungen, die der Leser wie CommonMark liest, |
| 9 | * und die, die er anders liest. Am echten Fenster mit der echten Datei - und |
| 10 | * die Wache, dass die Datei keine der anders gelesenen enthaelt - prueft |
| 11 | * tests/ui/datenschutzAnsicht.test.ts. |
| 12 | */ |
| 13 | |
| 14 | const ERSATZ = 'Ersatztitel'; |
| 15 | |
| 16 | function absatz(text: string): Textblock { |
| 17 | return { art: 'absatz', text }; |
| 18 | } |
| 19 | |
| 20 | function abschnitt(text: string): Textblock { |
| 21 | return { art: 'abschnitt', text }; |
| 22 | } |
| 23 | |
| 24 | function aufzaehlung(...punkte: string[]): Textblock { |
| 25 | return { art: 'aufzaehlung', punkte }; |
| 26 | } |
| 27 | |
| 28 | describe('gliedereText: Titel', () => { |
| 29 | it('nimmt die Titelzeile als Titel und nicht als Block', () => { |
| 30 | const text = gliedereText('# Hinweise\n\nErster Absatz.\n', ERSATZ); |
| 31 | expect(text.titel).toBe('Hinweise'); |
| 32 | expect(text.bloecke).toEqual([absatz('Erster Absatz.')]); |
| 33 | }); |
| 34 | |
| 35 | it('nimmt den Ersatztitel, wenn die Titelzeile fehlt', () => { |
| 36 | const text = gliedereText('## Abschnitt\n\nAbsatz.', ERSATZ); |
| 37 | expect(text.titel).toBe(ERSATZ); |
| 38 | expect(text.bloecke).toEqual([abschnitt('Abschnitt'), absatz('Absatz.')]); |
| 39 | }); |
| 40 | |
| 41 | it('verwirft einen zweiten Titel nicht, sondern fuehrt ihn als Abschnitt', () => { |
| 42 | const text = gliedereText('# Erster\n\n# Zweiter\n\nText.', ERSATZ); |
| 43 | expect(text.titel).toBe('Erster'); |
| 44 | expect(text.bloecke).toEqual([abschnitt('Zweiter'), absatz('Text.')]); |
| 45 | }); |
| 46 | |
| 47 | it('liest ein Nummernzeichen ohne Leerzeichen nicht als Ueberschrift', () => { |
| 48 | // "#Hinweise" ist kein Titel und "###" keine Ebene, die der Leser kennt - |
| 49 | // beides bleibt sichtbar Text, statt still verschluckt zu werden. |
| 50 | const text = gliedereText('#Hinweise\n\n### Dritte Ebene', ERSATZ); |
| 51 | expect(text.titel).toBe(ERSATZ); |
| 52 | expect(text.bloecke).toEqual([absatz('#Hinweise'), absatz('### Dritte Ebene')]); |
| 53 | }); |
| 54 | }); |
| 55 | |
| 56 | describe('gliedereText: Absaetze', () => { |
| 57 | it('fuegt die Zeilen eines Absatzes mit einem Leerzeichen zusammen', () => { |
| 58 | const text = gliedereText('Die erste Zeile\nlaeuft hier weiter\nund endet hier.', ERSATZ); |
| 59 | expect(text.bloecke).toEqual([absatz('Die erste Zeile laeuft hier weiter und endet hier.')]); |
| 60 | }); |
| 61 | |
| 62 | it('trennt Absaetze an der Leerzeile und macht aus mehreren Leerzeilen keinen leeren Absatz', () => { |
| 63 | const text = gliedereText('Eins.\n\n\n \nZwei\nZeilen.', ERSATZ); |
| 64 | expect(text.bloecke).toEqual([absatz('Eins.'), absatz('Zwei Zeilen.')]); |
| 65 | }); |
| 66 | |
| 67 | it('laesst Leerzeilen und Leerzeichen am Anfang und am Ende nicht als Block stehen', () => { |
| 68 | const text = gliedereText('\n\n Absatz mit Rand. \n\n\n\n', ERSATZ); |
| 69 | expect(text.bloecke).toEqual([absatz('Absatz mit Rand.')]); |
| 70 | }); |
| 71 | |
| 72 | it('liest Wagenruecklaeufe am Zeilenende nicht mit', () => { |
| 73 | const text = gliedereText('# Titel\r\n\r\nZeile eins\r\nZeile zwei\r\n\r\n- Punkt\r\n', ERSATZ); |
| 74 | expect(text.titel).toBe('Titel'); |
| 75 | expect(text.bloecke).toEqual([absatz('Zeile eins Zeile zwei'), aufzaehlung('Punkt')]); |
| 76 | }); |
| 77 | |
| 78 | it('beendet einen Absatz an einer Ueberschrift auch ohne Leerzeile', () => { |
| 79 | const text = gliedereText('Absatz.\n## Abschnitt\nWeiter.', ERSATZ); |
| 80 | expect(text.bloecke).toEqual([absatz('Absatz.'), abschnitt('Abschnitt'), absatz('Weiter.')]); |
| 81 | }); |
| 82 | }); |
| 83 | |
| 84 | describe('gliedereText: Aufzaehlungen', () => { |
| 85 | it('fasst aufeinanderfolgende Punkte zu einer Aufzaehlung zusammen', () => { |
| 86 | const text = gliedereText('- eins\n- zwei\n- drei', ERSATZ); |
| 87 | expect(text.bloecke).toEqual([aufzaehlung('eins', 'zwei', 'drei')]); |
| 88 | }); |
| 89 | |
| 90 | it('beginnt nach einem Absatz auch ohne Leerzeile eine Aufzaehlung', () => { |
| 91 | const text = gliedereText('Einleitung:\n- erster Punkt\n- zweiter Punkt', ERSATZ); |
| 92 | expect(text.bloecke).toEqual([ |
| 93 | absatz('Einleitung:'), |
| 94 | aufzaehlung('erster Punkt', 'zweiter Punkt'), |
| 95 | ]); |
| 96 | }); |
| 97 | |
| 98 | it('setzt einen Punkt mit der unmittelbar folgenden Zeile fort', () => { |
| 99 | const text = gliedereText('- erster Punkt,\n umbrochen\n- zweiter Punkt', ERSATZ); |
| 100 | expect(text.bloecke).toEqual([aufzaehlung('erster Punkt, umbrochen', 'zweiter Punkt')]); |
| 101 | }); |
| 102 | |
| 103 | it('beendet eine Aufzaehlung an der Leerzeile, der Absatz danach steht fuer sich', () => { |
| 104 | const text = gliedereText('- Punkt\n\nAbsatz danach.', ERSATZ); |
| 105 | expect(text.bloecke).toEqual([aufzaehlung('Punkt'), absatz('Absatz danach.')]); |
| 106 | }); |
| 107 | |
| 108 | it('beendet eine Aufzaehlung an einer Ueberschrift', () => { |
| 109 | const text = gliedereText('- Punkt\n## Abschnitt', ERSATZ); |
| 110 | expect(text.bloecke).toEqual([aufzaehlung('Punkt'), abschnitt('Abschnitt')]); |
| 111 | }); |
| 112 | |
| 113 | it('schliesst eine Aufzaehlung am Dateiende ohne Zeilenumbruch ab', () => { |
| 114 | const text = gliedereText('Absatz.\n\n- letzter Punkt', ERSATZ); |
| 115 | expect(text.bloecke).toEqual([absatz('Absatz.'), aufzaehlung('letzter Punkt')]); |
| 116 | }); |
| 117 | |
| 118 | it('liest einen Bindestrich ohne Leerzeichen nicht als Punkt', () => { |
| 119 | const text = gliedereText('-kein Punkt', ERSATZ); |
| 120 | expect(text.bloecke).toEqual([absatz('-kein Punkt')]); |
| 121 | }); |
| 122 | }); |
| 123 | |
| 124 | describe('gliedereText: anders als CommonMark', () => { |
| 125 | /* |
| 126 | * Die Liste im Kopf von src/ui/datenschutzText.ts, Fall fuer Fall: was dieser |
| 127 | * Leser liest. Nach CommonMark gliedert sich jeder dieser Texte anders, und |
| 128 | * deshalb darf docs/datenschutz.md keinen davon enthalten |
| 129 | * (`gliederungsabweichungen` in tests/ui/datenschutzAnsicht.test.ts). Liest |
| 130 | * der Leser einen davon eines Tages anders, gehoeren Kopf und Wache mit |
| 131 | * geaendert. |
| 132 | */ |
| 133 | it.each<[string, string, Textblock[]]>([ |
| 134 | [ |
| 135 | 'zwei Punkte mit einer Leerzeile dazwischen: zwei Aufzaehlungen', |
| 136 | '- a\n\n- b', |
| 137 | [aufzaehlung('a'), aufzaehlung('b')], |
| 138 | ], |
| 139 | [ |
| 140 | 'eine eingerueckte Zeile nach Leerzeile hinter einem Punkt: ein eigener Absatz', |
| 141 | '- a\n\n b', |
| 142 | [aufzaehlung('a'), absatz('b')], |
| 143 | ], |
| 144 | ['Bindestriche unter einer Absatzzeile: Text', 'Titel\n---', [absatz('Titel ---')]], |
| 145 | ['Gleichheitszeichen unter einer Absatzzeile: Text', 'Titel\n===', [absatz('Titel ===')]], |
| 146 | [ |
| 147 | 'eine Zeile nur aus Sternen oder Unterstrichen: ein Absatz', |
| 148 | 'Eins.\n\n***\n\n___', |
| 149 | [absatz('Eins.'), absatz('***'), absatz('___')], |
| 150 | ], |
| 151 | ['ein Bindestrich allein hinter einem Punkt: Fortsetzung', '- a\n-', [aufzaehlung('a -')]], |
| 152 | ['ein Zitat: Text', '> Zitat', [absatz('> Zitat')]], |
| 153 | ['Tilden: Text', '~~~\nCode\n~~~', [absatz('~~~ Code ~~~')]], |
| 154 | [ |
| 155 | 'eine Verweisdefinition: sichtbarer Text', |
| 156 | '[name]: https://example.org', |
| 157 | [absatz('[name]: https://example.org')], |
| 158 | ], |
| 159 | ['ein Tabulator nach dem Bindestrich: kein Punkt', '-\tPunkt', [absatz('-\tPunkt')]], |
| 160 | ])('%s', (_fall, wortlaut, erwartet) => { |
| 161 | expect(gliedereText(wortlaut, ERSATZ).bloecke).toEqual(erwartet); |
| 162 | }); |
| 163 | }); |
| 164 | |
| 165 | describe('textbloeckeAlsKnoten', () => { |
| 166 | it('macht aus einem Abschnitt h3, aus einem Absatz p und aus einer Aufzaehlung ul mit li', () => { |
| 167 | const knoten = textbloeckeAlsKnoten( |
| 168 | [abschnitt('Abschnitt'), absatz('Absatz.'), aufzaehlung('eins', 'zwei')], |
| 169 | 'de', |
| 170 | ); |
| 171 | expect(knoten.map((k) => k.tagName)).toEqual(['H3', 'P', 'UL']); |
| 172 | expect(knoten.map((k) => k.textContent)).toEqual(['Abschnitt', 'Absatz.', 'einszwei']); |
| 173 | const punkte = [...knoten[2]!.children]; |
| 174 | expect(punkte.map((p) => p.tagName)).toEqual(['LI', 'LI']); |
| 175 | expect(punkte.map((p) => p.textContent)).toEqual(['eins', 'zwei']); |
| 176 | }); |
| 177 | |
| 178 | it('zeichnet jeden Block mit der uebergebenen Sprache aus', () => { |
| 179 | for (const sprache of ['de', 'en'] as const) { |
| 180 | const knoten = textbloeckeAlsKnoten([abschnitt('A'), absatz('B'), aufzaehlung('C')], sprache); |
| 181 | expect(knoten.map((k) => k.getAttribute('lang'))).toEqual([sprache, sprache, sprache]); |
| 182 | } |
| 183 | }); |
| 184 | |
| 185 | it('setzt Text als Text: Eine spitze Klammer wird kein Element', () => { |
| 186 | const knoten = textbloeckeAlsKnoten( |
| 187 | [ |
| 188 | abschnitt('<em>Abschnitt</em>'), |
| 189 | absatz('<img src=x onerror=alert(1)>'), |
| 190 | aufzaehlung('<b>fett</b>'), |
| 191 | ], |
| 192 | 'de', |
| 193 | ); |
| 194 | expect(knoten[0]!.children).toHaveLength(0); |
| 195 | expect(knoten[0]!.textContent).toBe('<em>Abschnitt</em>'); |
| 196 | expect(knoten[1]!.children).toHaveLength(0); |
| 197 | expect(knoten[1]!.textContent).toBe('<img src=x onerror=alert(1)>'); |
| 198 | expect(knoten[2]!.querySelector('b')).toBeNull(); |
| 199 | expect(knoten[2]!.textContent).toBe('<b>fett</b>'); |
| 200 | }); |
| 201 | |
| 202 | it('liefert fuer keine Bloecke keine Knoten', () => { |
| 203 | expect(textbloeckeAlsKnoten([], 'de')).toEqual([]); |
| 204 | }); |
| 205 | }); |