waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
/ app tests kernpunkte.test.ts
| 1 | /** |
| 2 | * Die Prüfliste zur Selbstbewertung offener Fragen. |
| 3 | * |
| 4 | * **Der Anlass.** Bei 63 der 104 offenen Fragen ist in der amtlichen |
| 5 | * Musterantwort nichts unterstrichen, und die Anwendung gibt die |
| 6 | * Unterstreichungen seit 0.17.0 zu Recht nicht mehr als Pflichtinhalt aus. |
| 7 | * Damit stand der Lernende bei der vierstufigen Selbstbewertung – die |
| 8 | * unmittelbar die Wiedervorlage steuert – ohne jedes Kriterium da. |
| 9 | * |
| 10 | * **Die Leitplanke.** `docs/entscheidung-freitext.md` Abschnitt 8 verlangt, |
| 11 | * dass über einer solchen Liste **nie wieder** „muss Ihre Antwort enthalten“ |
| 12 | * steht. Der letzte Test dieser Datei hält das am ganzen Bestand fest. |
| 13 | */ |
| 14 | |
| 15 | import { readFileSync } from 'node:fs'; |
| 16 | import { join } from 'node:path'; |
| 17 | |
| 18 | import { describe, expect, it } from 'vitest'; |
| 19 | |
| 20 | import type { Erklaerungen } from '../src/shared/erklaerungen'; |
| 21 | import type { Katalog } from '../src/shared/katalog'; |
| 22 | |
| 23 | const WURZEL = join(process.cwd(), '..'); |
| 24 | const ERKLAERUNGEN = JSON.parse( |
| 25 | readFileSync(join(WURZEL, 'content', 'erklaerungen.json'), 'utf8'), |
| 26 | ) as Erklaerungen; |
| 27 | const KATALOG = JSON.parse( |
| 28 | readFileSync(join(WURZEL, 'content', 'katalog', 'katalog.json'), 'utf8'), |
| 29 | ) as Katalog; |
| 30 | |
| 31 | const OFFEN = new Set(KATALOG.fragen.filter((frage) => frage.typ !== 'mc').map((f) => f.id)); |
| 32 | const MIT_PUNKTEN = Object.entries(ERKLAERUNGEN.zuFrage).filter( |
| 33 | ([, eintrag]) => eintrag.kernpunkte !== undefined, |
| 34 | ); |
| 35 | |
| 36 | describe('Die ausgelieferten Kernpunkte', () => { |
| 37 | it('decken den größeren Teil der offenen Fragen ab', () => { |
| 38 | /* Nicht alle: Wo die Musterantwort aus einer einzigen Tatsache besteht |
| 39 | („Unbefristet“, „Nein“), wäre eine Prüfliste mit einem Punkt kein |
| 40 | Werkzeug, sondern Beiwerk. */ |
| 41 | expect(OFFEN.size).toBe(104); |
| 42 | expect(MIT_PUNKTEN.length).toBeGreaterThan(70); |
| 43 | expect(MIT_PUNKTEN.length).toBeLessThanOrEqual(OFFEN.size); |
| 44 | }); |
| 45 | |
| 46 | it('stehen ausschließlich an offenen Fragen', () => { |
| 47 | /* Bei einer Auswahlfrage stehen die Antwortmöglichkeiten schon da – eine |
| 48 | Prüfliste wäre dort die Lösung, nicht eine Hilfe beim Bewerten. */ |
| 49 | const falschePlatzierung = MIT_PUNKTEN.filter(([frageId]) => !OFFEN.has(frageId)).map( |
| 50 | ([frageId]) => frageId, |
| 51 | ); |
| 52 | |
| 53 | expect(falschePlatzierung).toEqual([]); |
| 54 | }); |
| 55 | |
| 56 | it('führen zwischen zwei und acht Punkte', () => { |
| 57 | const daneben = MIT_PUNKTEN.filter(([, eintrag]) => { |
| 58 | const anzahl = eintrag.kernpunkte?.length ?? 0; |
| 59 | return anzahl < 2 || anzahl > 8; |
| 60 | }).map(([frageId]) => frageId); |
| 61 | |
| 62 | expect(daneben).toEqual([]); |
| 63 | }); |
| 64 | |
| 65 | it('wiederholen keinen Punkt innerhalb einer Frage', () => { |
| 66 | const doppelt = MIT_PUNKTEN.filter(([, eintrag]) => { |
| 67 | const punkte = eintrag.kernpunkte ?? []; |
| 68 | return new Set(punkte).size !== punkte.length; |
| 69 | }).map(([frageId]) => frageId); |
| 70 | |
| 71 | expect(doppelt).toEqual([]); |
| 72 | }); |
| 73 | |
| 74 | it('bleiben Stichworte statt Satzgefüge', () => { |
| 75 | const zuLang = MIT_PUNKTEN.flatMap(([frageId, eintrag]) => |
| 76 | (eintrag.kernpunkte ?? []) |
| 77 | .filter((punkt) => punkt.length > 140 || punkt.trim().length === 0) |
| 78 | .map((punkt) => `${frageId}: ${punkt.slice(0, 40)}`), |
| 79 | ); |
| 80 | |
| 81 | expect(zuLang).toEqual([]); |
| 82 | }); |
| 83 | |
| 84 | it('behaupten nirgends, was eine Antwort enthalten muss', () => { |
| 85 | /* Die Leitplanke aus `docs/entscheidung-freitext.md` Abschnitt 8. Bis |
| 86 | Fassung 0.17.0 stand über einer ähnlichen Liste „Diese Kernelemente muss |
| 87 | Ihre Antwort enthalten“ – unbelegt, und ausdrücklich abgeschafft. Dieser |
| 88 | Test hält fest, dass die Behauptung nicht durch die Hintertür |
| 89 | zurückkommt. */ |
| 90 | const verboten = /muss\s+(ihre|die)\s+antwort|zwingend\s+erforderlich/iu; |
| 91 | const verstoesse = MIT_PUNKTEN.flatMap(([frageId, eintrag]) => |
| 92 | (eintrag.kernpunkte ?? []).filter((punkt) => verboten.test(punkt)).map(() => frageId), |
| 93 | ); |
| 94 | |
| 95 | expect(verstoesse).toEqual([]); |
| 96 | }); |
| 97 | |
| 98 | it('sind bei den Fragen gesetzt, an denen der Befund hing', () => { |
| 99 | // Vier Beispiele quer durch die Kapitel, damit ein stilles Verschwinden |
| 100 | // auffällt. |
| 101 | expect(ERKLAERUNGEN.zuFrage['I.5-02']?.kernpunkte).toContain( |
| 102 | 'gegen einen gegenwärtigen rechtswidrigen Angriff', |
| 103 | ); |
| 104 | expect(ERKLAERUNGEN.zuFrage['I.1-30']?.kernpunkte).toHaveLength(4); |
| 105 | expect(ERKLAERUNGEN.zuFrage['II-20']?.kernpunkte).toEqual([ |
| 106 | 'Hülse', |
| 107 | 'Geschoss', |
| 108 | 'Zündsatz', |
| 109 | 'Treibladung', |
| 110 | ]); |
| 111 | expect(ERKLAERUNGEN.zuFrage['IV-41']?.kernpunkte).toHaveLength(6); |
| 112 | }); |
| 113 | |
| 114 | it('lassen die einteiligen Musterantworten aus', () => { |
| 115 | /* Beispiele: „Unbefristet“, „Ja“, „Mindestens alle drei Jahre“. Eine |
| 116 | Prüfliste mit einem Punkt wäre Beiwerk. */ |
| 117 | for (const frageId of ['I.1-71', 'I.1-72', 'I.1-65']) { |
| 118 | expect(ERKLAERUNGEN.zuFrage[frageId]?.kernpunkte, frageId).toBeUndefined(); |
| 119 | } |
| 120 | }); |
| 121 | }); |