Baustein 8: Lernzielkontrollen mit Freigabe, Abgabe und Korrektur

- Ausbilder gibt frei; vorher sieht der Azubi die Fragen nicht
- Freigabe laesst sich zuruecknehmen, solange nicht abgegeben wurde
- Antworten werden beim Tippen je Frage zwischengespeichert
- Musterloesungen erst nach dem Absenden: sichtbareKontrolle() entfernt sie
  serverseitig, bevor die Daten in die Ausgabe gelangen
- Korrektur mit Punkten und Anmerkung je Frage, Summe und Note laufen mit
- Notenschluessel der IHK; solange eine Frage offen ist, bleibt das Ergebnis
  vorlaeufig und die Kontrolle gilt nicht als korrigiert
- Ergebnis bei beiden sichtbar, Schnitt ueber alle Kontrollen
- Abgegebene Kontrollen unter "Was auf dich wartet" und im Wochenfortschritt
- scripts/pruefungen/musterloesung-dichtheit.mjs prueft die Sperre gegen den
  echten Wortlaut aus der Inhaltsdatei im rohen Seitenquelltext

36 neue Tests, davon 21 fuer die Sichtbarkeit der Musterloesung.
Insgesamt 277.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-11 08:57:00 +02:00
co-authored by Claude Opus 5
parent 7c4a051347
commit d226a6bae8
42 changed files with 2132 additions and 9 deletions
@@ -0,0 +1,104 @@
import { chromium } from '@playwright/test';
import fs from 'node:fs';
import path from 'node:path';
/**
* Gezielte Prüfung der einen Zusage, auf die es bei den Lernzielkontrollen
* ankommt: Die Musterlösung darf vor dem Absenden nirgends herauskommen.
*
* Geprüft wird gegen den **echten** Wortlaut aus der Inhaltsdatei, nicht gegen
* ausgedachte Zeichenketten – und im rohen Quelltext samt RSC-Nutzlast, nicht
* im sichtbaren Text.
*/
const BASIS = 'http://localhost:3311';
const AZUBI = { email: 'jonas.berger@betrieb.de', pw: 'azubi-start-2026' };
const AUSBILDER = { email: 'fw@wildeflorian.de', pw: 'ausbilder-start' };
// Musterlösungen direkt aus der Datei lesen – so kann der Test nicht veralten.
const yaml = fs.readFileSync(path.join(process.cwd(), 'content/lernzielkontrollen/woche-01.yaml'), 'utf8');
const loesungen = [...yaml.matchAll(/musterloesung:\s*>-\s*\n((?:\s{6}.+\n)+)/g)].map((t) =>
t[1].replace(/\s+/g, ' ').trim(),
);
// Aussagekräftige Bruchstücke: lang genug, um nicht zufällig vorzukommen.
const bruchstuecke = loesungen.map((l) => l.slice(40, 110)).filter((b) => b.length > 40);
console.log(`${loesungen.length} Musterlösungen aus woche-01.yaml gelesen.`);
console.log('Geprüft wird auf Bruchstücke wie:', JSON.stringify(bruchstuecke[0]?.slice(0, 60)));
const browser = await chromium.launch();
const seite = await (await browser.newContext({ locale: 'de-DE' })).newPage();
async function anmelden(k) {
await seite.goto(`${BASIS}/anmelden`);
await seite.fill('#email', k.email);
await seite.fill('#passwort', k.pw);
await seite.click('button[type=submit]');
await seite.waitForURL(`${BASIS}/`);
}
async function abmelden() {
await seite.click('button:has-text("Abmelden")');
await seite.waitForURL(/anmelden/);
}
/** Sucht im rohen HTML **und** in der RSC-Nutzlast. */
async function lecksuche(beschriftung) {
const html = await seite.content();
const rsc = await seite.evaluate(() =>
// Next legt die Server-Nutzlast in self.__next_f ab – dort steht alles,
// was der Server an den Browser geschickt hat.
(globalThis.self?.__next_f ?? []).map((t) => (Array.isArray(t) ? String(t[1] ?? '') : '')).join(''),
);
const treffer = bruchstuecke.filter((b) => html.includes(b) || rsc.includes(b));
const nullFelder = (rsc.match(/musterloesung.{0,12}null/g) ?? []).length;
console.log(
` ${beschriftung.padEnd(34)} Leck: ${treffer.length === 0 ? 'nein' : `JA (${treffer.length})`}` +
` | RSC-Größe ${rsc.length} | "musterloesung":null ${nullFelder}x`,
);
return treffer;
}
console.log('\n1) Vor der Freigabe');
await anmelden(AZUBI);
await seite.goto(`${BASIS}/kontrollen/woche-01`);
await seite.waitForSelector('h1');
await lecksuche('gesperrt');
console.log('\n2) Ausbilder gibt frei');
await abmelden();
await anmelden(AUSBILDER);
await seite.goto(`${BASIS}/kontrollen`);
await seite.waitForSelector('.kontrollzeile');
await seite.locator('.kontrollzeile', { hasText: 'Woche 1' }).locator('button:has-text("Freigeben")').click();
await seite.waitForTimeout(1500);
console.log('\n3) Nach der Freigabe, vor der Abgabe – der entscheidende Fall');
await abmelden();
await anmelden(AZUBI);
await seite.goto(`${BASIS}/kontrollen/woche-01`);
await seite.waitForSelector('.frage-text');
console.log(` Fragen sichtbar: ${(await seite.$$('.frage-text')).length}`);
const leck = await lecksuche('freigegeben, nicht abgegeben');
console.log('\n4) Nach dem Zwischenspeichern einer Antwort');
await seite.locator('textarea[id^=antwort-]').first().fill('Person, Gerät, Fehlerbild, Zeitpunkt.');
await seite.waitForTimeout(1600);
await seite.reload();
await seite.waitForSelector('.frage-text');
const leck2 = await lecksuche('nach Zwischenspeichern');
console.log('\n5) Nach der Abgabe muss sie da sein');
await seite.click('button:has-text("Kontrolle abgeben")');
await seite.waitForTimeout(2500);
await seite.reload();
await seite.waitForSelector('h1');
const nachher = await lecksuche('abgegeben');
console.log(` Musterlösungen jetzt sichtbar: ${nachher.length} von ${bruchstuecke.length}`);
await browser.close();
const gescheitert = leck.length + leck2.length;
console.log(`\n${gescheitert === 0 ? 'BESTANDEN' : 'DURCHGEFALLEN'}: vor der Abgabe ${gescheitert} Leck(s).`);
process.exit(gescheitert === 0 && nachher.length === bruchstuecke.length ? 0 : 1);