/** * Die redaktionelle Feingliederung der Kapitel II bis IV. * * **Der Anlass.** Der amtliche Katalog gliedert nur Kapitel I in Abschnitte. * Für 230 Fragen steht `abschnitt` auf `null`; wer in der Simulation an * Munitionskunde scheiterte, sah bis 0.22.0 nur „Kapitel II schwach“ und * konnte nur das ganze Kapitel nachüben – 92 Fragen. * * Die eigentliche Zusage steht ganz unten: **Vollständigkeit**. Eine * lückenhafte Gliederung wäre schlimmer als keine, weil „Thema üben“ dann * Fragen stillschweigend überginge. */ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; import type { Katalog } from '../src/shared/katalog'; import { gruppenZuKapitel, gruppeZuFrage, THEMEN_LEER, type Themen } from '../src/shared/themen'; const WURZEL = join(process.cwd(), '..'); const THEMEN = JSON.parse(readFileSync(join(WURZEL, 'content', 'themen.json'), 'utf8')) as Themen; const KATALOG = JSON.parse( readFileSync(join(WURZEL, 'content', 'katalog', 'katalog.json'), 'utf8'), ) as Katalog; /** Kapitel ohne amtliche Abschnittsgliederung. */ const OHNE_ABSCHNITTE = ['II', 'III', 'IV']; describe('gruppenZuKapitel', () => { it('liefert die Gruppen eines Kapitels in der Reihenfolge der Datei', () => { const gruppen = gruppenZuKapitel(THEMEN, 'III'); expect(gruppen.length).toBeGreaterThan(3); expect(gruppen.every((gruppe) => gruppe.kapitel === 'III')).toBe(true); }); it('liefert für Kapitel I nichts', () => { /* Dort gibt es die amtlichen Abschnitte. Eine zweite Gliederung daneben wäre die nächste Zahl, die still auseinanderläuft. */ expect(gruppenZuKapitel(THEMEN, 'I')).toEqual([]); }); it('kommt ohne geladene Gliederung zurecht', () => { expect(gruppenZuKapitel(THEMEN_LEER, 'II')).toEqual([]); expect(gruppeZuFrage(THEMEN_LEER, 'II-01')).toBeNull(); }); }); describe('gruppeZuFrage', () => { it('findet die Gruppe einer Frage', () => { expect(gruppeZuFrage(THEMEN, 'II-34')?.titel).toBe('Schalldämpfer und Schallentstehung'); expect(gruppeZuFrage(THEMEN, 'III-08')?.kapitel).toBe('III'); }); it('gibt für Fragen aus Kapitel I nichts zurück', () => { expect(gruppeZuFrage(THEMEN, 'I.1-01')).toBeNull(); }); }); describe('Die ausgelieferte Gliederung', () => { const zugeordnet = new Map(); for (const gruppe of THEMEN.gruppen) { for (const frageId of gruppe.fragen) { zugeordnet.set(frageId, gruppe.id); } } it('ordnet jede Frage der Kapitel II bis IV genau einer Gruppe zu', () => { /* Die eigentliche Zusage. Ohne sie überginge „Thema üben“ Fragen, ohne es zu sagen – und niemandem fiele auf, welche. */ const fehlend = KATALOG.fragen .filter((frage) => OHNE_ABSCHNITTE.includes(frage.kapitel) && !zugeordnet.has(frage.id)) .map((frage) => frage.id); expect(fehlend).toEqual([]); }); it('lässt Kapitel I in Ruhe', () => { const zuviel = KATALOG.fragen .filter((frage) => !OHNE_ABSCHNITTE.includes(frage.kapitel) && zugeordnet.has(frage.id)) .map((frage) => frage.id); expect(zuviel).toEqual([]); }); it('nennt keine Frage in zwei Gruppen', () => { const alle = THEMEN.gruppen.flatMap((gruppe) => gruppe.fragen); expect(alle.length).toBe(new Set(alle).size); expect(alle.length).toBe(230); }); it('kennt nur Fragen, die es gibt', () => { const bekannt = new Set(KATALOG.fragen.map((frage) => frage.id)); const erfunden = [...zugeordnet.keys()].filter((frageId) => !bekannt.has(frageId)); expect(erfunden).toEqual([]); }); it('mischt in keiner Gruppe zwei Kapitel', () => { const kapitelJeFrage = new Map(KATALOG.fragen.map((frage) => [frage.id, frage.kapitel])); const gemischt = THEMEN.gruppen .filter( (gruppe) => !gruppe.fragen.every((frageId) => kapitelJeFrage.get(frageId) === gruppe.kapitel), ) .map((gruppe) => gruppe.id); expect(gemischt).toEqual([]); }); it('führt keine Gruppe mit weniger als drei Fragen', () => { // Eine Gruppe mit einer Frage ist kein Thema, sondern ein Einzelfall. const winzig = THEMEN.gruppen .filter((gruppe) => gruppe.fragen.length < 3) .map((gruppe) => `${gruppe.id} (${String(gruppe.fragen.length)})`); expect(winzig).toEqual([]); }); it('vergibt eindeutige Kennungen und nichtleere Titel', () => { const kennungen = THEMEN.gruppen.map((gruppe) => gruppe.id); expect(kennungen.length).toBe(new Set(kennungen).size); expect(THEMEN.gruppen.every((gruppe) => gruppe.titel.trim().length > 0)).toBe(true); }); it('weist sich selbst als Ergänzung dieser Software aus', () => { /* Der amtliche Katalog darf nicht verändert wiedergegeben werden; was eigener Zusatz ist, muss erkennbar sein – auch in der Datei. */ expect(THEMEN.meta.hinweis).toContain('nicht Teil des amtlichen'); }); });