Baustein 2: Inhalte einlesen und pruefen, Handouts, Suche, Glossar

Die fachlichen Inhalte liegen als Dateien unter content/ und werden beim
Start eingelesen, geprueft und in den Speicher geladen.

- Inhaltslader mit Zod-Schemata und Querverweispruefung
- Fehlermeldungen nennen Datei und Zeile, gesammelt statt beim ersten
  Fehler geworfen; die Anwendung startet bei Fehlern nicht
- npm run inhalte:pruefen prueft ohne Start
- Handout-Ansicht mit festem Aufbau, Uebersicht nach Themenbloecken
- Volltextsuche mit Gewichtung und Trefferausschnitt
- Gelesen-Markierung je Handout
- Glossar mit Suche und automatischer Verlinkung in den Handouts
- Beispielinhalte: 4 Wochen, 12 Handouts, 24 Merkkarten, 4 Aufgaben,
  4 Checklisten, 2 Lernzielkontrollen, 24 Glossareintraege
- rahmenplan.yaml aus FIAusbV 2020 und KMK-Rahmenlehrplan, mit Fundstellen
- gray-matter entfernt, YAML-Kopf wird selbst herausgeloest
- npm run screenshots nimmt die Bilder fuer die Anleitung auf
- 39 neue Tests, insgesamt 68

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-10 07:56:38 +02:00
co-authored by Claude Opus 5
parent e856349cd4
commit 1af5613e9b
70 changed files with 4779 additions and 124 deletions
+131
View File
@@ -0,0 +1,131 @@
/**
* Nimmt die Bildschirmfotos für die Anleitung auf.
*
* npm run build && npm run start (in einer zweiten Konsole)
* npm run screenshots
*
* Die Bilder landen unter docs/bilder/ und werden bei jedem Lauf überschrieben.
* So veralten sie nicht – wer die Oberfläche ändert, lässt das Skript neu laufen.
*
* Umgebungsvariablen:
* LERNBAR_BASIS Adresse der laufenden Anwendung (Vorgabe http://localhost:3000)
* LERNBAR_AUSBILDER E-Mail des Ausbilderkontos
* LERNBAR_AZUBI E-Mail des Azubikontos
* LERNBAR_PASSWORT_* zugehörige Passwörter
*/
import fs from 'node:fs';
import path from 'node:path';
import { chromium, type Page } from '@playwright/test';
const BASIS = process.env.LERNBAR_BASIS ?? 'http://localhost:3000';
const ZIEL = path.join(process.cwd(), 'docs', 'bilder');
const AUSBILDER = {
email: process.env.LERNBAR_AUSBILDER ?? 'ausbilder@betrieb.de',
passwort: process.env.LERNBAR_PASSWORT_AUSBILDER ?? 'ausbilder-start',
};
const AZUBI = {
email: process.env.LERNBAR_AZUBI ?? 'azubi@betrieb.de',
passwort: process.env.LERNBAR_PASSWORT_AZUBI ?? 'azubi-start-2026',
};
const SCHREIBTISCH = { width: 1280, height: 900 };
const HANDY = { width: 390, height: 844 };
async function anmelden(seite: Page, konto: { email: string; passwort: string }) {
await seite.goto(`${BASIS}/anmelden`);
await seite.waitForSelector('#email');
await seite.fill('#email', konto.email);
await seite.fill('#passwort', konto.passwort);
await seite.click('button[type=submit]');
await seite.waitForURL(`${BASIS}/`);
}
async function abmelden(seite: Page) {
await seite.click('button:has-text("Abmelden")');
await seite.waitForURL(/anmelden/);
}
async function bild(seite: Page, name: string, ganzeSeite = true) {
const datei = path.join(ZIEL, `${name}.png`);
await seite.screenshot({ path: datei, fullPage: ganzeSeite });
console.log(` ${path.relative(process.cwd(), datei)}`);
}
async function main() {
fs.mkdirSync(ZIEL, { recursive: true });
const browser = await chromium.launch();
const umgebung = await browser.newContext({ viewport: SCHREIBTISCH, locale: 'de-DE', colorScheme: 'light' });
const seite = await umgebung.newPage();
console.log('Bildschirmfotos werden aufgenommen …');
// --- Anmeldung ---
await seite.goto(`${BASIS}/anmelden`);
await seite.waitForSelector('#email');
await bild(seite, '01-anmelden');
// --- Ausbilder ---
await anmelden(seite, AUSBILDER);
await bild(seite, '02-ausbilder-ueberblick');
await seite.goto(`${BASIS}/verwaltung`);
await seite.waitForSelector('#neu-name');
await bild(seite, '03-verwaltung');
await abmelden(seite);
// --- Azubi ---
await anmelden(seite, AZUBI);
await bild(seite, '10-azubi-start');
await seite.goto(`${BASIS}/handouts`);
await seite.waitForSelector('h1');
await bild(seite, '11-handouts-liste');
await seite.goto(`${BASIS}/handouts?suche=subnetting`);
await seite.waitForSelector('h2');
await bild(seite, '12-handouts-suche');
await seite.goto(`${BASIS}/handouts/N3`);
await seite.waitForSelector('h1');
await seite.click('.pruefungsfrage > summary');
await bild(seite, '13-handout-n3');
await seite.goto(`${BASIS}/glossar`);
await seite.waitForSelector('h1');
await bild(seite, '14-glossar');
// --- Dunkle Darstellung ---
await seite.goto(`${BASIS}/handouts/N3`);
await seite.waitForSelector('button[aria-pressed]');
await seite.click('button[aria-pressed="false"]');
await seite.waitForTimeout(200);
await bild(seite, '20-dunkel');
await seite.click('button[aria-pressed="true"]');
// --- Handy ---
const handy = await browser.newContext({
viewport: HANDY,
locale: 'de-DE',
isMobile: true,
hasTouch: true,
deviceScaleFactor: 2,
});
const handySeite = await handy.newPage();
await anmelden(handySeite, AZUBI);
await handySeite.goto(`${BASIS}/handouts/N3`);
await handySeite.waitForSelector('h1');
await bild(handySeite, '30-handy-handout');
await browser.close();
console.log('Fertig.');
}
main().catch((fehler) => {
console.error('Die Bildschirmfotos konnten nicht aufgenommen werden.');
console.error(fehler instanceof Error ? fehler.message : fehler);
console.error(`Läuft die Anwendung unter ${BASIS}? Stimmen LERNBAR_AUSBILDER und LERNBAR_AZUBI?`);
process.exit(1);
});