lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests export bfUnterlagen.test.ts
| 1 | import { inflateSync } from 'node:zlib'; |
| 2 | import { describe, expect, it } from 'vitest'; |
| 3 | import { buildProjectPdf, DEFAULT_PDF_OPTIONS } from '@/services/export/pdf'; |
| 4 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 5 | import { validateProject } from '@/domain/validation'; |
| 6 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 7 | import { toWinAnsi } from '@/render/pdfSurface'; |
| 8 | import type { Project, SignalGroup } from '@/domain/model/project'; |
| 9 | |
| 10 | /* |
| 11 | * Sofortmassnahmen an den erzeugten Unterlagen (Befunde L10, M12, S1/D-3, |
| 12 | * M5/D-6, D-5). |
| 13 | * |
| 14 | * WAS DIESE PRUEFUNG NICHT BEHAUPTET: Sie weist nicht nach, dass die erzeugten |
| 15 | * Unterlagen barrierefrei sind. Sie sind es nicht - sie sind ungetaggt, und mit |
| 16 | * jsPDF 2.5.2 ist daran nichts zu aendern. Geprueft wird ausschliesslich, was |
| 17 | * im Bestand zugesagt wurde: Titelausweisung, Lesezeichen, der Verweis auf die |
| 18 | * gleichwertige Textfassung, die Fussnote zu den unbeschrifteten Abschnitten, |
| 19 | * die Erklaerung zur Barrierefreiheit im Dokument - und dass die Zeichnung |
| 20 | * samt Legende auf der Seite bleibt. |
| 21 | * |
| 22 | * Der letzte Punkt ist der einzige mit einer Geometrie: Befund M12 war nicht |
| 23 | * daran zu erkennen, dass etwas fehlt, sondern daran, dass gezeichnet wurde, |
| 24 | * wo keine Seite mehr war. Ein Prueffall, der nur die Legendentexte im |
| 25 | * Dokument sucht, geht auch gegen den Altstand durch - die Texte SIND dort, |
| 26 | * nur ausserhalb des Papiers. Deshalb wird hier gemessen. |
| 27 | */ |
| 28 | |
| 29 | const STICHTAG = new Date('2026-01-01T12:00:00Z'); |
| 30 | |
| 31 | const PAGE_MARGIN = 36; |
| 32 | const FOOTER_HEIGHT = 26; |
| 33 | /** Unterkante des bedruckbaren Bereichs, in PDF-Koordinaten (Ursprung unten). */ |
| 34 | const UNTERKANTE = PAGE_MARGIN + FOOTER_HEIGHT; |
| 35 | |
| 36 | /** So, wie der Text im PDF ankommt - Anfuehrungszeichen und Striche gewandelt. */ |
| 37 | function gedruckt(text: string): string { |
| 38 | return toWinAnsi(text); |
| 39 | } |
| 40 | |
| 41 | interface Seite { |
| 42 | readonly nummer: number; |
| 43 | readonly text: string; |
| 44 | /** Unterster Rand aller gezeichneten Rechtecke dieser Seite. */ |
| 45 | readonly unterster: number; |
| 46 | } |
| 47 | |
| 48 | function entpacke(roh: string, von: number): string | null { |
| 49 | const anfang = roh.indexOf('stream', von); |
| 50 | if (anfang < 0) return null; |
| 51 | const start = anfang + (roh.startsWith('stream\r\n', anfang) ? 8 : 7); |
| 52 | const ende = roh.indexOf('endstream', start); |
| 53 | if (ende < 0) return null; |
| 54 | try { |
| 55 | return inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1'); |
| 56 | } catch { |
| 57 | return null; |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | /** |
| 62 | * Seiten des Dokuments mit ihrem sichtbaren Text und ihrer Zeichengeometrie. |
| 63 | * |
| 64 | * Die Inhaltsstroeme sind komprimiert; eine Pruefung, die nur die Rohbytes |
| 65 | * durchsucht, findet keinen einzigen Buchstaben und prueft nichts. |
| 66 | */ |
| 67 | function seiten(bytes: Uint8Array): readonly Seite[] { |
| 68 | const roh = new TextDecoder('latin1').decode(bytes); |
| 69 | const raus: Seite[] = []; |
| 70 | |
| 71 | for (const treffer of roh.matchAll(/\/Type \/Page\b[^>]*?\/Contents (\d+) 0 R/g)) { |
| 72 | const objekt = roh.indexOf(`\n${treffer[1]!} 0 obj`); |
| 73 | const inhalt = objekt < 0 ? null : entpacke(roh, objekt); |
| 74 | if (inhalt === null) continue; |
| 75 | |
| 76 | const stuecke: string[] = []; |
| 77 | for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) { |
| 78 | stuecke.push(t[1]!.replace(/\\([()\\])/g, '$1')); |
| 79 | } |
| 80 | |
| 81 | let unterster = Number.POSITIVE_INFINITY; |
| 82 | for (const t of inhalt.matchAll(/(-?[\d.]+) (-?[\d.]+) (-?[\d.]+) (-?[\d.]+) re/g)) { |
| 83 | unterster = Math.min(unterster, Number(t[2])); |
| 84 | } |
| 85 | |
| 86 | raus.push({ |
| 87 | nummer: raus.length + 1, |
| 88 | text: stuecke.join(' ').replace(/\s+/g, ' '), |
| 89 | unterster, |
| 90 | }); |
| 91 | } |
| 92 | return raus; |
| 93 | } |
| 94 | |
| 95 | function volltext(bytes: Uint8Array): string { |
| 96 | return seiten(bytes) |
| 97 | .map((s) => s.text) |
| 98 | .join(' '); |
| 99 | } |
| 100 | |
| 101 | /** Klartextanteile der Datei - Katalog, Lesezeichen, Anzeigevorgaben. */ |
| 102 | function struktur(bytes: Uint8Array): string { |
| 103 | return new TextDecoder('latin1').decode(bytes); |
| 104 | } |
| 105 | |
| 106 | function mitSignalgruppen(anzahl: number): Project { |
| 107 | const basis = createStandardIntersectionProject('Musterkreuzung', STICHTAG); |
| 108 | const gruppen: SignalGroup[] = []; |
| 109 | for (let i = 0; i < anzahl; i += 1) { |
| 110 | const vorlage = basis.signalGroups[i % basis.signalGroups.length]!; |
| 111 | gruppen.push({ ...vorlage, id: `sg${i + 1}`, name: `K${i + 1}` }); |
| 112 | } |
| 113 | return { ...basis, signalGroups: gruppen }; |
| 114 | } |
| 115 | |
| 116 | /** |
| 117 | * Dieselbe Anlage mit Namen, wie sie in einer Anordnungsunterlage stehen. |
| 118 | * |
| 119 | * Die Namen sind kein Kunstgriff: Wer eine Zufahrt samt Fahrbeziehung und |
| 120 | * Furtlage in den Signalgruppennamen schreibt, kommt muehelos auf diese Laenge - |
| 121 | * und die Blattangabe des Verweisabsatzes nennt den ersten und den letzten. |
| 122 | */ |
| 123 | function mitLangenNamen(anzahl: number, teile: number): Project { |
| 124 | const basis = mitSignalgruppen(anzahl); |
| 125 | const zusatz = [ |
| 126 | 'Zufahrt Nord, Hauptstraße stadteinwärts, Geradeaus und Rechtsabbieger gemeinsam geführt', |
| 127 | 'Zufahrt Süd, Bahnhofstraße Richtung Innenstadt, Linksabbieger mit eigener Signalgruppe', |
| 128 | 'Furt West mit Mittelinsel, gemeinsame Führung von Fuß- und Radverkehr, Blindenzusatz', |
| 129 | ]; |
| 130 | return { |
| 131 | ...basis, |
| 132 | signalGroups: basis.signalGroups.map((g) => ({ |
| 133 | ...g, |
| 134 | name: `${g.name} ${zusatz.slice(0, teile).join(', ')}`, |
| 135 | })), |
| 136 | }; |
| 137 | } |
| 138 | |
| 139 | function ausdruck(project: Project): Uint8Array { |
| 140 | const plan = buildSignalPlan(project); |
| 141 | const report = validateProject(project, plan, STICHTAG); |
| 142 | return buildProjectPdf(project, plan, report, DEFAULT_PDF_OPTIONS, STICHTAG); |
| 143 | } |
| 144 | |
| 145 | /** Blaetter, auf denen die Zeichnung steht - erkennbar am Verweisabsatz. */ |
| 146 | function zeichenblaetter(bytes: Uint8Array): readonly Seite[] { |
| 147 | return seiten(bytes).filter((s) => s.text.includes('Balkendiagramm')); |
| 148 | } |
| 149 | |
| 150 | describe('L10 - Dokumenttitel im Anzeigeprogramm', () => { |
| 151 | it('setzt DisplayDocTitle, damit nicht der Dateiname angesagt wird', () => { |
| 152 | const roh = struktur(ausdruck(createStandardIntersectionProject('Musterkreuzung', STICHTAG))); |
| 153 | |
| 154 | expect(roh).toContain('/ViewerPreferences'); |
| 155 | expect(roh).toContain('/DisplayDocTitle true'); |
| 156 | // Der Titel selbst war schon immer gesetzt; ohne ihn bewirkt der Schalter |
| 157 | // nichts. |
| 158 | expect(roh).toContain(gedruckt('/Title (Musterkreuzung – Signalzeitenplan)')); |
| 159 | }); |
| 160 | }); |
| 161 | |
| 162 | describe('S1 - keine Auszeichnung behaupten, die es nicht gibt', () => { |
| 163 | /* |
| 164 | * Diese Pruefung behebt nichts; sie haelt eine Zusage fest. "/MarkInfo << |
| 165 | * /Marked true >>" liesse sich ueber die Ereignishaken von jsPDF in drei |
| 166 | * Zeilen setzen, und das Dokument bestuende danach manche automatische |
| 167 | * Pruefung. Ein Strukturbaum entstuende dabei nicht, und Sprachausgaben |
| 168 | * schalten bei "/Marked true" von ihren Positionsheuristiken auf den - dann |
| 169 | * leeren - Strukturbaum um: Sie lesen danach WENIGER vor als heute. |
| 170 | */ |
| 171 | it('behauptet weder Marked Content noch einen Strukturbaum', () => { |
| 172 | const roh = struktur(ausdruck(createStandardIntersectionProject('Musterkreuzung', STICHTAG))); |
| 173 | |
| 174 | expect(roh).not.toContain('/MarkInfo'); |
| 175 | expect(roh).not.toContain('/Marked'); |
| 176 | expect(roh).not.toContain('/StructTreeRoot'); |
| 177 | }); |
| 178 | }); |
| 179 | |
| 180 | describe('D-5 - Lesezeichen', () => { |
| 181 | it('legt fuer jeden Abschnitt ein Lesezeichen an', () => { |
| 182 | const roh = struktur(ausdruck(createStandardIntersectionProject('Musterkreuzung', STICHTAG))); |
| 183 | |
| 184 | expect(roh).toContain('/Outlines'); |
| 185 | expect(roh).toContain('/Type /Outlines'); |
| 186 | |
| 187 | for (const titel of [ |
| 188 | 'Deckblatt', |
| 189 | 'Bestandteile der Anordnungsunterlage', |
| 190 | 'Signalzeitenplan', |
| 191 | 'Signalzeiten je Signalgruppe', |
| 192 | 'Zwischenzeitenmatrix', |
| 193 | 'Prüfbericht', |
| 194 | 'Barrierefreiheit dieser Datei', |
| 195 | ]) { |
| 196 | expect(roh, titel).toContain(`/Title (${gedruckt(titel)})`); |
| 197 | } |
| 198 | }); |
| 199 | }); |
| 200 | |
| 201 | describe('S1/D-3 - Verweis auf die gleichwertige Textfassung der Zeichnung', () => { |
| 202 | it('benennt die Zeichnung als Zeichnung und die Tabelle als ihre Textfassung', () => { |
| 203 | const bytes = ausdruck(createStandardIntersectionProject('Musterkreuzung', STICHTAG)); |
| 204 | const text = volltext(bytes); |
| 205 | |
| 206 | expect(text).toContain(gedruckt('Über diesem Absatz steht eine Zeichnung')); |
| 207 | expect(text).toContain( |
| 208 | gedruckt('Tabelle „Signalzeiten je Signalgruppe", die auf die Zeichnung folgt'), |
| 209 | ); |
| 210 | expect(text).toContain(gedruckt('gleichwertige Textfassung der Zeichnung')); |
| 211 | // Der Textschluessel zu den Farben steht im Absatz selbst, nicht nur in der |
| 212 | // Zeichnung - er muss auch dann tragen, wenn die Zeichnung nicht ankommt. |
| 213 | expect(text).toContain(gedruckt('(R = Rot, RG = Rot-Gelb, F = Freigabe, GE = Gelb)')); |
| 214 | }); |
| 215 | |
| 216 | /* |
| 217 | * Ein Verweis, den man erst auf der naechsten Seite findet, ist keiner. |
| 218 | * Der Platz fuer beide Absaetze wird deshalb abgezogen, BEVOR die Hoehe der |
| 219 | * Zeichnung feststeht. Beim ersten Anlauf fehlte in dieser Reserve der |
| 220 | * Abstand von 14 pt zwischen Zeichnung und Absatz; bei 13 Signalgruppen |
| 221 | * landete die Fussnote daraufhin allein auf einer eigenen Seite. Deshalb |
| 222 | * wird hier ueber die Zahl der Signalgruppen geprueft und nicht nur am |
| 223 | * Beispielprojekt. |
| 224 | */ |
| 225 | /* |
| 226 | * UND MIT NAMEN, WIE SIE IN EINER ANORDNUNGSUNTERLAGE VORKOMMEN |
| 227 | * (Befund B1). |
| 228 | * |
| 229 | * Die Reserve unter der Zeichnung wurde mit der Fassung OHNE Blattangabe |
| 230 | * gemessen, plus pauschal 18 pt "fuer eine Zeile und eine halbe zum |
| 231 | * Abrunden". Gedruckt wird bei mehreren Blaettern aber "Blatt 1 von 2 der |
| 232 | * Zeichnung, Signalgruppen <erste> bis <letzte>" - und Signalgruppennamen |
| 233 | * sind freier Anwendertext. Bei 16 langen Namen wuchs die Blattangabe ueber |
| 234 | * die eine reservierte Zeile hinaus, und die Fussnote landete umgeblaettert: |
| 235 | * gemessen Zeichnung auf 20 und 22, Fussnote auf 21 und 23. Auf dem |
| 236 | * Zeichnungsblatt stand dann nur noch der uneingeschraenkte Satz "die |
| 237 | * farbigen Abschnitte tragen ihr Kürzel und ihre Dauer" - die |
| 238 | * Richtigstellung fehlte. |
| 239 | * |
| 240 | * Die Prueffaelle darunter fuehrten ausschliesslich K1 bis Kn; mit so kurzen |
| 241 | * Namen tritt der Fall nicht auf. |
| 242 | */ |
| 243 | for (const [anzahl, teile] of [ |
| 244 | [20, 2], |
| 245 | [16, 3], |
| 246 | ] as const) { |
| 247 | it(`${anzahl} Signalgruppen mit langen Namen: Fussnote bleibt beim Blatt`, () => { |
| 248 | // Nachgestellt gegen die alte Reserve: Dort steht der Verweis auf |
| 249 | // Blatt 35 und 37, die Fussnote auf 36 und 38 (20 Gruppen) bzw. 43/45 |
| 250 | // gegen 44/46 (16 Gruppen). |
| 251 | const blaetter = zeichenblaetter(ausdruck(mitLangenNamen(anzahl, teile))); |
| 252 | |
| 253 | expect(blaetter.length).toBeGreaterThanOrEqual(1); |
| 254 | for (const blatt of blaetter) { |
| 255 | expect(blatt.text, `Blatt ${blatt.nummer}, Verweis`).toContain( |
| 256 | gedruckt('gleichwertige Textfassung der Zeichnung'), |
| 257 | ); |
| 258 | expect(blatt.text, `Blatt ${blatt.nummer}, Fussnote`).toContain( |
| 259 | gedruckt('in ihnen steht dann keine Beschriftung'), |
| 260 | ); |
| 261 | } |
| 262 | }); |
| 263 | } |
| 264 | |
| 265 | for (const anzahl of [1, 6, 9, 12, 13, 16, 20, 26]) { |
| 266 | it(`${anzahl} Signalgruppen: Verweis und Fussnote stehen auf dem Blatt der Zeichnung`, () => { |
| 267 | const blaetter = zeichenblaetter(ausdruck(mitSignalgruppen(anzahl))); |
| 268 | |
| 269 | expect(blaetter.length).toBeGreaterThanOrEqual(1); |
| 270 | for (const blatt of blaetter) { |
| 271 | expect(blatt.text, `Blatt ${blatt.nummer}, Verweis`).toContain( |
| 272 | gedruckt('gleichwertige Textfassung der Zeichnung'), |
| 273 | ); |
| 274 | expect(blatt.text, `Blatt ${blatt.nummer}, Fussnote`).toContain( |
| 275 | gedruckt('in ihnen steht dann keine Beschriftung'), |
| 276 | ); |
| 277 | } |
| 278 | }); |
| 279 | } |
| 280 | }); |
| 281 | |
| 282 | describe('M5/D-6 - Fussnote zu den unbeschrifteten Abschnitten', () => { |
| 283 | it('sagt, dass zu schmale Abschnitte unbeschriftet bleiben und wo ihre Dauer steht', () => { |
| 284 | const bytes = ausdruck(createStandardIntersectionProject('Musterkreuzung', STICHTAG)); |
| 285 | const blaetter = zeichenblaetter(bytes); |
| 286 | |
| 287 | expect(blaetter[0]!.text).toContain( |
| 288 | gedruckt( |
| 289 | 'ergeben Abschnitte, die dafür zu schmal sind; in ihnen steht dann keine Beschriftung', |
| 290 | ), |
| 291 | ); |
| 292 | expect(blaetter[0]!.text).toContain( |
| 293 | gedruckt( |
| 294 | 'führt die Tabelle „Signalzeiten je Signalgruppe" das Signalbild mit seiner Dauer auf', |
| 295 | ), |
| 296 | ); |
| 297 | expect(blaetter[0]!.text).toContain( |
| 298 | gedruckt( |
| 299 | 'Bei Farbfehlsichtigkeit und im Schwarzweißausdruck ist sie deshalb die verlässliche Quelle', |
| 300 | ), |
| 301 | ); |
| 302 | }); |
| 303 | |
| 304 | it('nimmt der Zeichnung ihren eigenen Hinweis nicht vorweg', () => { |
| 305 | /* |
| 306 | * Die Zeichnung benennt seit Fassung 5.7.0 unter ihrer Legende, welche |
| 307 | * Signalbilder es diesmal getroffen hat - das kann nur sie, weil nur dort |
| 308 | * gemessen wird, was noch hineinpasst. Die Fussnote hier sagt die Regel |
| 309 | * und nennt die Tabelle. Beruefe sie sich auf den Hinweis der Zeichnung, |
| 310 | * waere sie falsch, sobald die Zeichnung ihn weglaesst (und sie laesst ihn |
| 311 | * weg, wenn jeder Abschnitt sein Kuerzel bekommen hat). |
| 312 | */ |
| 313 | const bytes = ausdruck(createStandardIntersectionProject('Musterkreuzung', STICHTAG)); |
| 314 | const blatt = zeichenblaetter(bytes)[0]!; |
| 315 | |
| 316 | expect(blatt.text).not.toContain(gedruckt('vermerkt die Zeichnung')); |
| 317 | expect(blatt.text).not.toContain(gedruckt('unter ihrer Legende')); |
| 318 | }); |
| 319 | }); |
| 320 | |
| 321 | describe('M12 - die Zeichnung bleibt mit ihrer Legende auf der Seite', () => { |
| 322 | /* |
| 323 | * Der Altstand kappte die Hoehe mit Math.min, ohne der Zeichnung davon zu |
| 324 | * sagen: drawSignalPlan rechnet seine Zeilen aus dem Layout, nicht aus der |
| 325 | * Flaechenhoehe, und PdfSurface beschneidet nicht. Gemessen am Altstand lag |
| 326 | * der unterste gezeichnete Rand bei 16 Signalgruppen 11,7 pt UNTERHALB der |
| 327 | * Blattkante (-11,7 in PDF-Koordinaten), bei 20 Signalgruppen 123,7 pt. |
| 328 | * Die Legende steht ganz unten und fiel als Erstes weg; danach trug in der |
| 329 | * Zeichnung nur noch die Farbe. |
| 330 | */ |
| 331 | for (const anzahl of [6, 12, 13, 16, 20, 26]) { |
| 332 | it(`${anzahl} Signalgruppen: nichts wird unter die Blattkante gezeichnet`, () => { |
| 333 | const bytes = ausdruck(mitSignalgruppen(anzahl)); |
| 334 | const blaetter = zeichenblaetter(bytes); |
| 335 | |
| 336 | expect(blaetter.length).toBeGreaterThanOrEqual(1); |
| 337 | for (const blatt of blaetter) { |
| 338 | expect(blatt.unterster, `Blatt ${blatt.nummer}`).toBeGreaterThanOrEqual(UNTERKANTE); |
| 339 | } |
| 340 | }); |
| 341 | |
| 342 | it(`${anzahl} Signalgruppen: die Legende ist auf jedem Blatt lesbar`, () => { |
| 343 | const bytes = ausdruck(mitSignalgruppen(anzahl)); |
| 344 | const blaetter = zeichenblaetter(bytes); |
| 345 | |
| 346 | // Ohne diese Zeile prueft die Schleife darunter nichts, sobald es kein |
| 347 | // Zeichenblatt gibt - und ginge damit auch gegen den Altstand durch. |
| 348 | expect(blaetter.length).toBeGreaterThanOrEqual(1); |
| 349 | for (const blatt of blaetter) { |
| 350 | for (const eintrag of ['R = Rot', 'RG = Rot-Gelb', 'F = Freigabe', 'GE = Gelb']) { |
| 351 | expect(blatt.text, `Blatt ${blatt.nummer}, ${eintrag}`).toContain(gedruckt(eintrag)); |
| 352 | } |
| 353 | } |
| 354 | }); |
| 355 | } |
| 356 | |
| 357 | it('teilt erst auf, wenn es sein muss - und dann gleichmaessig', () => { |
| 358 | // Bis zu 13 Signalgruppen traegt ein Blatt; darueber wird geteilt, aber |
| 359 | // nicht in 14 und 2: Zeilen zweier so verschiedener Blaetter liessen sich |
| 360 | // nicht mehr nebeneinanderlegen. |
| 361 | expect(zeichenblaetter(ausdruck(mitSignalgruppen(6)))).toHaveLength(1); |
| 362 | expect(zeichenblaetter(ausdruck(mitSignalgruppen(13)))).toHaveLength(1); |
| 363 | |
| 364 | const bytes = ausdruck(mitSignalgruppen(16)); |
| 365 | const blaetter = zeichenblaetter(bytes); |
| 366 | expect(blaetter).toHaveLength(2); |
| 367 | expect(blaetter[0]!.text).toContain( |
| 368 | gedruckt('Blatt 1 von 2 der Zeichnung, Signalgruppen K1 bis K8'), |
| 369 | ); |
| 370 | expect(blaetter[1]!.text).toContain( |
| 371 | gedruckt('Blatt 2 von 2 der Zeichnung, Signalgruppen K9 bis K16'), |
| 372 | ); |
| 373 | // Jedes Blatt traegt seinen eigenen Verweis auf die Textfassung. |
| 374 | for (const blatt of blaetter) { |
| 375 | expect(blatt.text).toContain(gedruckt('gleichwertige Textfassung der Zeichnung')); |
| 376 | } |
| 377 | }); |
| 378 | |
| 379 | it('nennt das Fortsetzungsblatt im Lesezeichenverzeichnis', () => { |
| 380 | const roh = struktur(ausdruck(mitSignalgruppen(16))); |
| 381 | expect(roh).toContain('/Title (Signalzeitenplan \\(Fortsetzung\\))'); |
| 382 | }); |
| 383 | }); |
| 384 | |
| 385 | describe('Erklaerung zur Barrierefreiheit im Dokument', () => { |
| 386 | const bytes = ausdruck(createStandardIntersectionProject('Musterkreuzung', STICHTAG)); |
| 387 | const text = volltext(bytes); |
| 388 | |
| 389 | it('nennt die Luecke beim Namen, statt sie zu verschweigen', () => { |
| 390 | expect(text).toContain(gedruckt('Barrierefreiheit dieser Datei')); |
| 391 | expect(text).toContain(gedruckt('Diese Datei ist nicht getaggt')); |
| 392 | expect(text).toContain(gedruckt('erfüllt PDF/UA (DIN ISO 14289-1) nicht')); |
| 393 | expect(text).toContain( |
| 394 | gedruckt('1.3.1 Info und Beziehungen, 1.3.2 Bedeutungstragende Reihenfolge und 1.1.1'), |
| 395 | ); |
| 396 | }); |
| 397 | |
| 398 | it('sagt, was das fuer Bildschirmleseprogramme bedeutet', () => { |
| 399 | expect(text).toContain(gedruckt('Überschriften werden nicht als Überschriften angesagt')); |
| 400 | expect(text).toContain(gedruckt('ohne Bezug zu Zeilen- und Spaltenköpfen vorgelesen')); |
| 401 | expect(text).toContain(gedruckt('Kopf- und Fußzeile jeder Seite stehen im Lesefluss')); |
| 402 | }); |
| 403 | |
| 404 | it('weist die Signalzeitentabelle als Textfassung der Zeichnung aus', () => { |
| 405 | expect(text).toContain( |
| 406 | gedruckt('Die Signalbilder der Zeichnung stehen vollständig in Textform in der Tabelle'), |
| 407 | ); |
| 408 | // Und sagt zugleich, was sie NICHT ist - sonst waere der Satz eine Zusage, |
| 409 | // die das Dokument nicht halten kann. |
| 410 | expect(text).toContain( |
| 411 | gedruckt('ein Alternativtext im Sinne von 1.1.1 ist sie mangels Auszeichnung nicht'), |
| 412 | ); |
| 413 | }); |
| 414 | |
| 415 | /* |
| 416 | * Die Zusage ist auf das eingegrenzt, was die Tabelle wirklich enthaelt. |
| 417 | * |
| 418 | * Zuvor stand da: "Der Inhalt der Zeichnung steht VOLLSTAENDIG in Textform in |
| 419 | * der Tabelle „Signalzeiten je Signalgruppe"". Die Zeichnung traegt ueber der |
| 420 | * Zeitachse aber ein Phasenband - Phasennamen, Phasenbeginn, |
| 421 | * Uebergangszeiten -, und davon steht in dieser Tabelle nichts. Der Abschnitt |
| 422 | * ist ein Lueckenausweis; er darf als einziger Teil der Datei keine Angabe |
| 423 | * machen, die einer Nachpruefung nicht standhaelt. |
| 424 | */ |
| 425 | it('nennt fuer das Phasenband die zweite Tabelle und behauptet nicht, eine reiche', () => { |
| 426 | expect(text).toContain(gedruckt('Das Phasenband über der Zeitachse')); |
| 427 | expect(text).toContain(gedruckt('Abschnitt „Phasen und Phasenübergänge"')); |
| 428 | expect(text).toContain(gedruckt('Diese beiden Tabellen zusammen sind die gleichwertige')); |
| 429 | // Die zu weit gefasste Fassung darf nicht wieder auftauchen. |
| 430 | expect(text).not.toContain(gedruckt('Der Inhalt der Zeichnung steht vollständig')); |
| 431 | }); |
| 432 | |
| 433 | it('benennt die Luecke, wenn der Phasenabschnitt abgewaehlt wurde', () => { |
| 434 | // Ein Verweis auf einen Abschnitt, den diese Ausgabe gar nicht enthaelt, |
| 435 | // waere genau die Art Zusage, die der Abschnitt vermeiden soll. |
| 436 | const projekt = createStandardIntersectionProject('Musterkreuzung', STICHTAG); |
| 437 | const plan = buildSignalPlan(projekt); |
| 438 | const ohnePhasen = volltext( |
| 439 | buildProjectPdf( |
| 440 | projekt, |
| 441 | plan, |
| 442 | validateProject(projekt, plan, STICHTAG), |
| 443 | { ...DEFAULT_PDF_OPTIONS, includePhases: false }, |
| 444 | STICHTAG, |
| 445 | ), |
| 446 | ); |
| 447 | expect(ohnePhasen).toContain(gedruckt('wurde für diese Ausgabe abgewählt')); |
| 448 | expect(ohnePhasen).not.toContain(gedruckt('Diese beiden Tabellen zusammen')); |
| 449 | }); |
| 450 | |
| 451 | it('sagt der veroeffentlichenden Stelle, was sie zu tun hat', () => { |
| 452 | expect(text).toContain(gedruckt('§ 27a VwVfG')); |
| 453 | expect(text).toContain(gedruckt('EN 301 549 Kapitel 10')); |
| 454 | expect(text).toContain(gedruckt('PAC oder veraPDF')); |
| 455 | expect(text).toContain(gedruckt('zugängliche Fassung des Inhalts bereitzustellen')); |
| 456 | expect(text).toContain(gedruckt('keine Rechtsberatung')); |
| 457 | }); |
| 458 | |
| 459 | it('behauptet an keiner Stelle Konformitaet', () => { |
| 460 | // Erst nachweisen, dass die Erklaerung ueberhaupt da ist: Ein Dokument |
| 461 | // ohne sie besteht die Suche nach unzulaessigen Zusagen von selbst. |
| 462 | expect(text).toContain(gedruckt('Barrierefreiheit dieser Datei')); |
| 463 | |
| 464 | for (const zusage of [ |
| 465 | 'ist barrierefrei', |
| 466 | 'barrierefreie PDF', |
| 467 | 'WCAG 2.1 AA erfüllt', |
| 468 | 'erfüllt die Anforderungen der EN 301 549', |
| 469 | 'PDF/UA-konform', |
| 470 | ]) { |
| 471 | expect(text, zusage).not.toContain(gedruckt(zusage)); |
| 472 | } |
| 473 | }); |
| 474 | |
| 475 | it('steht in der Uebersicht der Bestandteile, damit man sie findet', () => { |
| 476 | expect(text).toContain( |
| 477 | gedruckt( |
| 478 | 'Erklärung zur Barrierefreiheit dieser Datei ja – Abschnitt „Barrierefreiheit dieser Datei"', |
| 479 | ), |
| 480 | ); |
| 481 | }); |
| 482 | }); |