import { chromium } from '@playwright/test'; /** * Durchstich für Baustein 9. * * Geprüft werden die beiden Zusagen, die in diesem Baustein zählen: * * 1. Die Rahmenplanabdeckung unterscheidet vier Lagen – und weist Positionen * ohne Material im Lehrgang getrennt aus (A4, A10, C1, C2, C5). * 2. Das Feedbackgespräch bewertet beide Seiten getrennt, hebt Abweichungen * hervor, ist nach beidseitiger Bestätigung unveränderlich und liefert * ein PDF. */ 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' }; let fehler = 0; function pruefe(bedingung, text, zusatz = '') { console.log(` ${bedingung ? '✓' : '✗'} ${text}${zusatz ? ` – ${zusatz}` : ''}`); if (!bedingung) fehler += 1; } const browser = await chromium.launch(); const UEBRIGE = [ 'sorgfalt', 'dokumentation', 'fachwissen', 'fehlersuche', 'kundenkontakt', 'zuverlaessigkeit', 'lernbereitschaft', ]; /** * Die Radios selbst sind für das Auge weggeblendet; angeklickt wird die * Beschriftung – genau wie ein Mensch es täte. */ async function waehle(seite, kriterium, wert) { await seite.click(`label.stufe:has(input[name="stufe-${kriterium}"][value="${wert}"])`); } async function neueSitzung(konto) { const kontext = await browser.newContext({ locale: 'de-DE' }); const seite = await kontext.newPage(); await seite.goto(`${BASIS}/anmelden`); await seite.fill('#email', konto.email); await seite.fill('#passwort', konto.pw); await seite.click('button[type=submit]'); await seite.waitForURL(`${BASIS}/`); return { kontext, seite }; } // --- 1. Auswertung --------------------------------------------------------- console.log('\n1. Auswertung und Rahmenplanabdeckung (Ausbilder)'); const ausbilder = await neueSitzung(AUSBILDER); await ausbilder.seite.goto(`${BASIS}/auswertung`); await ausbilder.seite.waitForLoadState('networkidle'); const text = await ausbilder.seite.locator('body').innerText(); pruefe(text.includes('Abdeckung'), 'Die Auswertung zeigt die Rahmenplanabdeckung'); pruefe(/ohne Inhalt/i.test(text), 'Die vierte Lage „ohne Inhalt" kommt vor'); // Die fünf Lücken im Beispielinhalt müssen als eigene Gruppe auftauchen. const luecken = ['A4', 'A10', 'C1', 'C2', 'C5']; const gefunden = []; for (const kuerzel of luecken) { const treffer = await ausbilder.seite .locator(`[data-stufe="ohne_inhalt"]:has-text("${kuerzel}")`) .count() .catch(() => 0); if (treffer > 0) gefunden.push(kuerzel); } pruefe( gefunden.length === luecken.length, 'Alle fünf Positionen ohne Material sind als Lücke im Lehrgang gekennzeichnet', `gefunden: ${gefunden.join(', ') || 'keine'}`, ); pruefe(/Lernfeld/i.test(text), 'Die Lernfeldabdeckung ist vorhanden'); pruefe(/§|Verordnung|Abschnitt/i.test(text), 'Die Fundstelle in der Verordnung wird genannt'); // --- 2. Feedbackgespräch anlegen ------------------------------------------ console.log('\n2. Feedbackgespräch – Ausbilder legt an und bewertet'); await ausbilder.seite.goto(`${BASIS}/feedback`); await ausbilder.seite.click('button:has-text("Neues Gespräch anlegen")'); await ausbilder.seite.waitForURL(/\/feedback\/[0-9a-f-]{36}$/); const gespraechUrl = ausbilder.seite.url(); const gespraechId = gespraechUrl.split('/').pop(); pruefe(Boolean(gespraechId), 'Gespräch angelegt', gespraechId); // Ausbilder bewertet: Selbstständigkeit 2, alle anderen 4. await waehle(ausbilder.seite, 'selbststaendigkeit', 2); for (const k of UEBRIGE) await waehle(ausbilder.seite, k, 4); await ausbilder.seite.fill('#staerken', 'Arbeitet ruhig und fragt nach, bevor etwas kaputtgeht.'); await ausbilder.seite.fill('#entwicklungsfelder', 'Bei unklarer Lage zu lange allein – früher melden.'); await ausbilder.seite.fill('#ziele', 'Bis Ende November eine Rücksicherung eigenständig durchführen.'); await ausbilder.seite.click('button:has-text("Speichern")'); await ausbilder.seite.waitForSelector('text=Gespeichert', { timeout: 15000 }); pruefe(true, 'Einschätzung des Ausbilders gespeichert'); // --- 3. Azubi sieht das Gespräch und schätzt sich selbst ein --------------- console.log('\n3. Feedbackgespräch – Azubi schätzt sich selbst ein'); const azubi = await neueSitzung(AZUBI); await azubi.seite.goto(gespraechUrl); await azubi.seite.waitForLoadState('networkidle'); const azubiText = await azubi.seite.locator('body').innerText(); pruefe(azubiText.includes('Feedbackgespräch'), 'Der Azubi sieht sein Gespräch'); pruefe( azubiText.includes('Arbeitet ruhig und fragt nach'), 'Der Azubi liest die Freitexte des Ausbilders', ); pruefe( (await azubi.seite.locator('#staerken').count()) === 0, 'Der Azubi kann die Freitexte nicht bearbeiten', ); // Azubi schätzt sich bei Selbstständigkeit deutlich höher ein: 5 gegen 2. await waehle(azubi.seite, 'selbststaendigkeit', 5); for (const k of UEBRIGE) await waehle(azubi.seite, k, 4); await azubi.seite.click('button:has-text("Speichern")'); await azubi.seite.waitForSelector('text=Selbsteinschätzung ist gespeichert', { timeout: 15000 }); await azubi.seite.reload(); const nachSpeichern = await azubi.seite.locator('body').innerText(); pruefe( /Unterschiedliche Sicht/i.test(nachSpeichern), 'Die Abweichung von drei Stufen wird hervorgehoben', ); // Gegenprobe: Die Spalte des Ausbilders blieb unangetastet. await ausbilder.seite.reload(); pruefe( await ausbilder.seite.isChecked('input[name="stufe-selbststaendigkeit"][value="2"]'), 'Der Azubi hat die Einschätzung des Ausbilders nicht überschrieben', ); // --- 4. Beidseitige Bestätigung sperrt ------------------------------------- console.log('\n4. Bestätigung durch beide Seiten'); await azubi.seite.click('button:has-text("Gespräch bestätigen")'); await azubi.seite.waitForSelector('text=Bestätigt', { timeout: 15000 }); pruefe(true, 'Azubi hat bestätigt'); // Erst ohne, dann mit Neuladen – so ist ein Rennen von einem echten Fehler // zu unterscheiden. const sofort = await azubi.seite .locator('button:has-text("Bestätigung zurücknehmen")') .waitFor({ timeout: 5000 }) .then(() => true, () => false); await azubi.seite.reload(); const nachLaden = (await azubi.seite.locator('button:has-text("Bestätigung zurücknehmen")').count()) > 0; pruefe( nachLaden, 'Solange nur eine Seite bestätigt hat, lässt sich das zurücknehmen', sofort ? 'sofort sichtbar' : 'erst nach dem Neuladen sichtbar', ); await ausbilder.seite.reload(); await ausbilder.seite.click('button:has-text("Als Ausbilder bestätigen")'); await ausbilder.seite.waitForSelector('text=unveränderlich', { timeout: 15000 }); await ausbilder.seite.reload(); const gesperrt = await ausbilder.seite.locator('body').innerText(); pruefe(/abgeschlossen und unveränderlich/.test(gesperrt), 'Das Gespräch ist jetzt abgeschlossen'); pruefe( (await ausbilder.seite.locator('button:has-text("Speichern")').count()) === 0, 'Es gibt keinen Speichern-Knopf mehr', ); pruefe( (await ausbilder.seite.locator('button:has-text("Bestätigung zurücknehmen")').count()) === 0, 'Die Bestätigung lässt sich nicht mehr zurücknehmen', ); // Serverseitige Gegenprobe: Schreiben trotz Sperre muss abgewiesen werden. await azubi.seite.reload(); pruefe( await azubi.seite.isDisabled('input[name="stufe-selbststaendigkeit"][value="5"]'), 'Auch beim Azubi sind die Felder gesperrt', ); // --- 5. PDF ---------------------------------------------------------------- console.log('\n5. PDF für die Akte'); const pdfAntwort = await ausbilder.kontext.request.get(`${gespraechUrl}/pdf`); const pdfKoerper = await pdfAntwort.body(); pruefe(pdfAntwort.status() === 200, 'PDF wird ausgeliefert', `HTTP ${pdfAntwort.status()}`); pruefe(pdfKoerper.subarray(0, 5).toString() === '%PDF-', 'Die Antwort ist wirklich ein PDF'); pruefe(pdfKoerper.byteLength > 3000, 'Das PDF hat Inhalt', `${pdfKoerper.byteLength} Bytes`); // Und ohne Anmeldung? Ein PDF aus der Ausbildungsakte darf nicht herausfallen. const fremd = await browser.newContext(); const ohne = await fremd.request.get(`${gespraechUrl}/pdf`, { maxRedirects: 0 }); pruefe( ohne.status() >= 300 && ohne.status() < 400, 'Ohne Anmeldung kommt kein PDF heraus', `HTTP ${ohne.status()} → ${ohne.headers()['location'] ?? '—'}`, ); await fremd.close(); // --- 6. Fremdzugriff ------------------------------------------------------- console.log('\n6. Rechte'); const fremderAzubi = await browser.newContext({ locale: 'de-DE' }); const fremdSeite = await fremderAzubi.newPage(); await fremdSeite.goto(`${BASIS}/anmelden`); await fremdSeite.fill('#email', 'lea.hoffmann@betrieb.de'); await fremdSeite.fill('#passwort', 'azubi-start-2026'); await fremdSeite.click('button[type=submit]'); const angemeldet = await fremdSeite.waitForURL(`${BASIS}/`, { timeout: 8000 }).then( () => true, () => false, ); if (angemeldet) { const antwort = await fremdSeite.goto(gespraechUrl); pruefe(antwort?.status() === 404, 'Ein anderer Azubi bekommt 404', `HTTP ${antwort?.status()}`); const fremdPdf = await fremderAzubi.request.get(`${gespraechUrl}/pdf`); pruefe(fremdPdf.status() === 404, 'Auch das PDF bleibt verschlossen', `HTTP ${fremdPdf.status()}`); } else { console.log(' – zweites Azubikonto nicht vorhanden, Fremdzugriff übersprungen'); } await fremderAzubi.close(); await browser.close(); console.log(`\n${fehler === 0 ? '✓ Baustein 9 steht.' : `✗ ${fehler} Prüfung(en) fehlgeschlagen.`}`); process.exit(fehler === 0 ? 0 : 1);