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);