waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
| 1 | /** |
| 2 | * Die redaktionelle Feingliederung der Kapitel II bis IV. |
| 3 | * |
| 4 | * **Der Anlass.** Der amtliche Katalog gliedert nur Kapitel I in Abschnitte. |
| 5 | * Für 230 Fragen steht `abschnitt` auf `null`; wer in der Simulation an |
| 6 | * Munitionskunde scheiterte, sah bis 0.22.0 nur „Kapitel II schwach“ und |
| 7 | * konnte nur das ganze Kapitel nachüben – 92 Fragen. |
| 8 | * |
| 9 | * Die eigentliche Zusage steht ganz unten: **Vollständigkeit**. Eine |
| 10 | * lückenhafte Gliederung wäre schlimmer als keine, weil „Thema üben“ dann |
| 11 | * Fragen stillschweigend überginge. |
| 12 | */ |
| 13 | |
| 14 | import { readFileSync } from 'node:fs'; |
| 15 | import { join } from 'node:path'; |
| 16 | |
| 17 | import { describe, expect, it } from 'vitest'; |
| 18 | |
| 19 | import type { Katalog } from '../src/shared/katalog'; |
| 20 | import { gruppenZuKapitel, gruppeZuFrage, THEMEN_LEER, type Themen } from '../src/shared/themen'; |
| 21 | |
| 22 | const WURZEL = join(process.cwd(), '..'); |
| 23 | const THEMEN = JSON.parse(readFileSync(join(WURZEL, 'content', 'themen.json'), 'utf8')) as Themen; |
| 24 | const KATALOG = JSON.parse( |
| 25 | readFileSync(join(WURZEL, 'content', 'katalog', 'katalog.json'), 'utf8'), |
| 26 | ) as Katalog; |
| 27 | |
| 28 | /** Kapitel ohne amtliche Abschnittsgliederung. */ |
| 29 | const OHNE_ABSCHNITTE = ['II', 'III', 'IV']; |
| 30 | |
| 31 | describe('gruppenZuKapitel', () => { |
| 32 | it('liefert die Gruppen eines Kapitels in der Reihenfolge der Datei', () => { |
| 33 | const gruppen = gruppenZuKapitel(THEMEN, 'III'); |
| 34 | |
| 35 | expect(gruppen.length).toBeGreaterThan(3); |
| 36 | expect(gruppen.every((gruppe) => gruppe.kapitel === 'III')).toBe(true); |
| 37 | }); |
| 38 | |
| 39 | it('liefert für Kapitel I nichts', () => { |
| 40 | /* Dort gibt es die amtlichen Abschnitte. Eine zweite Gliederung daneben |
| 41 | wäre die nächste Zahl, die still auseinanderläuft. */ |
| 42 | expect(gruppenZuKapitel(THEMEN, 'I')).toEqual([]); |
| 43 | }); |
| 44 | |
| 45 | it('kommt ohne geladene Gliederung zurecht', () => { |
| 46 | expect(gruppenZuKapitel(THEMEN_LEER, 'II')).toEqual([]); |
| 47 | expect(gruppeZuFrage(THEMEN_LEER, 'II-01')).toBeNull(); |
| 48 | }); |
| 49 | }); |
| 50 | |
| 51 | describe('gruppeZuFrage', () => { |
| 52 | it('findet die Gruppe einer Frage', () => { |
| 53 | expect(gruppeZuFrage(THEMEN, 'II-34')?.titel).toBe('Schalldämpfer und Schallentstehung'); |
| 54 | expect(gruppeZuFrage(THEMEN, 'III-08')?.kapitel).toBe('III'); |
| 55 | }); |
| 56 | |
| 57 | it('gibt für Fragen aus Kapitel I nichts zurück', () => { |
| 58 | expect(gruppeZuFrage(THEMEN, 'I.1-01')).toBeNull(); |
| 59 | }); |
| 60 | }); |
| 61 | |
| 62 | describe('Die ausgelieferte Gliederung', () => { |
| 63 | const zugeordnet = new Map<string, string>(); |
| 64 | for (const gruppe of THEMEN.gruppen) { |
| 65 | for (const frageId of gruppe.fragen) { |
| 66 | zugeordnet.set(frageId, gruppe.id); |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | it('ordnet jede Frage der Kapitel II bis IV genau einer Gruppe zu', () => { |
| 71 | /* Die eigentliche Zusage. Ohne sie überginge „Thema üben“ Fragen, ohne es |
| 72 | zu sagen – und niemandem fiele auf, welche. */ |
| 73 | const fehlend = KATALOG.fragen |
| 74 | .filter((frage) => OHNE_ABSCHNITTE.includes(frage.kapitel) && !zugeordnet.has(frage.id)) |
| 75 | .map((frage) => frage.id); |
| 76 | |
| 77 | expect(fehlend).toEqual([]); |
| 78 | }); |
| 79 | |
| 80 | it('lässt Kapitel I in Ruhe', () => { |
| 81 | const zuviel = KATALOG.fragen |
| 82 | .filter((frage) => !OHNE_ABSCHNITTE.includes(frage.kapitel) && zugeordnet.has(frage.id)) |
| 83 | .map((frage) => frage.id); |
| 84 | |
| 85 | expect(zuviel).toEqual([]); |
| 86 | }); |
| 87 | |
| 88 | it('nennt keine Frage in zwei Gruppen', () => { |
| 89 | const alle = THEMEN.gruppen.flatMap((gruppe) => gruppe.fragen); |
| 90 | |
| 91 | expect(alle.length).toBe(new Set(alle).size); |
| 92 | expect(alle.length).toBe(230); |
| 93 | }); |
| 94 | |
| 95 | it('kennt nur Fragen, die es gibt', () => { |
| 96 | const bekannt = new Set(KATALOG.fragen.map((frage) => frage.id)); |
| 97 | const erfunden = [...zugeordnet.keys()].filter((frageId) => !bekannt.has(frageId)); |
| 98 | |
| 99 | expect(erfunden).toEqual([]); |
| 100 | }); |
| 101 | |
| 102 | it('mischt in keiner Gruppe zwei Kapitel', () => { |
| 103 | const kapitelJeFrage = new Map(KATALOG.fragen.map((frage) => [frage.id, frage.kapitel])); |
| 104 | const gemischt = THEMEN.gruppen |
| 105 | .filter( |
| 106 | (gruppe) => |
| 107 | !gruppe.fragen.every((frageId) => kapitelJeFrage.get(frageId) === gruppe.kapitel), |
| 108 | ) |
| 109 | .map((gruppe) => gruppe.id); |
| 110 | |
| 111 | expect(gemischt).toEqual([]); |
| 112 | }); |
| 113 | |
| 114 | it('führt keine Gruppe mit weniger als drei Fragen', () => { |
| 115 | // Eine Gruppe mit einer Frage ist kein Thema, sondern ein Einzelfall. |
| 116 | const winzig = THEMEN.gruppen |
| 117 | .filter((gruppe) => gruppe.fragen.length < 3) |
| 118 | .map((gruppe) => `${gruppe.id} (${String(gruppe.fragen.length)})`); |
| 119 | |
| 120 | expect(winzig).toEqual([]); |
| 121 | }); |
| 122 | |
| 123 | it('vergibt eindeutige Kennungen und nichtleere Titel', () => { |
| 124 | const kennungen = THEMEN.gruppen.map((gruppe) => gruppe.id); |
| 125 | |
| 126 | expect(kennungen.length).toBe(new Set(kennungen).size); |
| 127 | expect(THEMEN.gruppen.every((gruppe) => gruppe.titel.trim().length > 0)).toBe(true); |
| 128 | }); |
| 129 | |
| 130 | it('weist sich selbst als Ergänzung dieser Software aus', () => { |
| 131 | /* Der amtliche Katalog darf nicht verändert wiedergegeben werden; was |
| 132 | eigener Zusatz ist, muss erkennbar sein – auch in der Datei. */ |
| 133 | expect(THEMEN.meta.hinweis).toContain('nicht Teil des amtlichen'); |
| 134 | }); |
| 135 | }); |