Die Dichtheitspruefung der Musterloesungen las die Server-Nutzlast aus self.__next_f - das Feld leert Next nach der Verarbeitung, es war also immer leer. Die Pruefung lief an dieser Stelle ins Leere, ohne dass es auffiel; nur der HTML-Quelltext wurde wirklich geprueft. Gelesen wird jetzt der Quelltext der eingebetteten Skripte. Damit zeigt die Ausgabe, was sie zeigen soll: vor der Abgabe steht fuenfmal "musterloesung":null in der Nutzlast, danach stehen dort die Loesungen. Ausserdem stellt das Skript seine Ausgangslage selbst her und laeuft gegen den Port, der ueber LERNBAR_BASIS eingestellt ist - vorher war 3311 fest eingetragen und ein zweiter Lauf scheiterte daran, dass die Kontrolle schon abgegeben war. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
143 lines
6.0 KiB
JavaScript
143 lines
6.0 KiB
JavaScript
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 = process.env.LERNBAR_BASIS ?? 'http://localhost:3000';
|
||
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)));
|
||
|
||
// --- Ausgangslage herstellen ----------------------------------------------
|
||
//
|
||
// Der entscheidende Fall ist „freigegeben, aber noch nicht abgegeben". Den gibt
|
||
// es nur einmal je Kontrolle. Damit die Prüfung wiederholbar bleibt, wird die
|
||
// Kontrolle „woche-01" vorher zurückgesetzt – das ist ein Testaufbau, kein Teil
|
||
// der Anwendung, und greift deshalb ausdrücklich nur auf die lokale
|
||
// Beispieldatenbank zu.
|
||
const { default: Datenbank } = await import('better-sqlite3');
|
||
const dbPfad = process.env.LERNBAR_DB ?? path.join(process.cwd(), 'daten', 'lernbar.sqlite');
|
||
|
||
if (fs.existsSync(dbPfad)) {
|
||
const db = new Datenbank(dbPfad);
|
||
const betroffen = db.prepare("select id from kontrolle where kontrolle_id = 'woche-01'").all();
|
||
for (const eintrag of betroffen) {
|
||
db.prepare('delete from kontroll_antwort where kontrolle_id = ?').run(eintrag.id);
|
||
db.prepare('delete from kontrolle where id = ?').run(eintrag.id);
|
||
}
|
||
db.close();
|
||
console.log(`Ausgangslage hergestellt: ${betroffen.length} frühere Bearbeitung(en) von woche-01 entfernt.`);
|
||
} else {
|
||
console.log(`Keine Datenbank unter ${dbPfad} – die Ausgangslage wird nicht zurückgesetzt.`);
|
||
}
|
||
|
||
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 Server-Nutzlast. */
|
||
async function lecksuche(beschriftung) {
|
||
const html = await seite.content();
|
||
|
||
// Die Nutzlast steht in eingebetteten Skripten, die `self.__next_f` füllen.
|
||
// Dort **nachträglich** nachzusehen bringt nichts: Next leert das Feld, sobald
|
||
// es verarbeitet ist – es wäre immer leer, und die Prüfung liefe ins Leere,
|
||
// ohne dass es auffällt. Deshalb wird der Quelltext der Skripte gelesen.
|
||
const rsc = await seite.evaluate(() =>
|
||
[...document.querySelectorAll('script:not([src])')]
|
||
.map((element) => element.textContent ?? '')
|
||
.filter((text) => text.includes('__next_f'))
|
||
.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})`}` +
|
||
` | Nutzlast ${rsc.length} Zeichen | "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');
|
||
const freigeben = seite
|
||
.locator('.kontrollzeile', { hasText: 'Woche 1' })
|
||
.locator('button:has-text("Freigeben")');
|
||
if ((await freigeben.count()) > 0) {
|
||
await freigeben.click();
|
||
await seite.waitForTimeout(1500);
|
||
} else {
|
||
// Bei einem wiederholten Lauf ist sie schon freigegeben. Das ist kein
|
||
// Grund abzubrechen – der entscheidende Fall kommt erst danach.
|
||
console.log(' (war bereits freigegeben)');
|
||
}
|
||
|
||
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);
|