/* =================================================================== Browserpruefung des Dashboards. Startet denselben Testserver wie pruef-uebersicht.mjs, meldet sich als DogFather bzw. als Creator an und sieht mit einem echten Browser nach: Fehler in der Konsole, ueberstehender Inhalt auf dem Handy, und ob die Karten ueberhaupt gezeichnet werden. Legt Screenshots ab. =================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; const ordner = mkdtempSync(join(tmpdir(), "ws-uebersicht-b-")); process.env.WORKSPACE_DB = join(ordner, "pruef.sqlite"); process.env.PORT = "4177"; process.env.SITE_ACCESS_SECRET = "lokaler-test"; process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; process.env.SITE_ACCESS_CODE_VANVAN = "VANVAN-TEST-1"; process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; const express = (await import("express")).default; const echteCookie = express.response.cookie; express.response.cookie = function (name, wert, opt) { return echteCookie.call(this, name, wert, { ...(opt || {}), secure: false }); }; await import("./index.js"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4177"; let fehler = 0; const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; const notbremse = setTimeout(() => { console.log("\nABBRUCH: hing über 150 Sekunden."); process.exit(1); }, 150_000); notbremse.unref?.(); /* ---------- Daten ------------------------------------------------------ */ await fetch(BASIS + "/workspace/api/anmelden", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rolle: "admin", code: "gibt-es-nicht" }), }).catch(() => {}); const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes } = await import("node:crypto"); const d = new DatabaseSync(process.env.WORKSPACE_DB); const N = 32768; const jetzt = new Date().toISOString(); function anlegen(name, rolle, code, letzterLogin) { const salt = randomBytes(16).toString("hex"); const hash = scryptSync(code, salt, 64, { N, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt, letzter_login) VALUES (?,?,?,?,?,1,?,?)") .run(name, rolle, hash, salt, N, jetzt, letzterLogin ?? null); return d.prepare("SELECT last_insert_rowid() AS id").get().id; } const idDogi = anlegen("Test-DogFather", "admin", "CODE-DOGI-0001", jetzt); const namen = ["Anna Sturm", "Ben Kohl", "Cara Weiss", "Deniz Yildiz"]; const ids = namen.map((n, i) => anlegen(n, "creator", `CODE-CRE-000${i + 1}`, i === 3 ? null : new Date(Date.now() - i * 9 * 86400_000).toISOString())); const gestern = new Date(Date.now() - 86400_000).toISOString().slice(0, 10); const aufgabe = (c, status, frist, erl) => d.prepare("INSERT INTO aufgaben (titel, creator_id, status, frist, erledigt_am, erstellt, erstellt_von) VALUES ('Test',?,?,?,?,?,?)") .run(c, status, frist, erl, jetzt, idDogi); /* Bewusst unterschiedliche Lagen, damit im Bild sowohl eine ruhige als auch eine brennende Karte zu sehen ist. */ for (let k = 0; k < 3; k++) aufgabe(ids[0], "offen", gestern, null); aufgabe(ids[0], "arbeit", null, null); aufgabe(ids[1], "offen", null, null); aufgabe(ids[1], "erledigt", null, jetzt); aufgabe(ids[2], "review", null, null); d.prepare("INSERT INTO eintraege (bereich, art, titel, datum, dringlichkeit, status, creator_id, erstellt, erstellt_von) VALUES ('live','notiz','Test',?,'hoch','offen',?,?,?)") .run(gestern, ids[0], jetzt, idDogi); d.prepare("INSERT INTO termine (titel, beginn, creator_id, erledigt, erstellt, erstellt_von) VALUES ('Test',?,?,0,?,?)") .run(new Date(Date.now() + 2 * 86400_000).toISOString().slice(0, 16), ids[1], jetzt, idDogi); for (const [i, feld] of ["a", "b", "c", "d", "e", "f", "g"].entries()) { d.prepare("INSERT INTO startcheck (creator_id, feld, punkt, bewertung, geaendert) VALUES (?,?,?,?,?)") .run(ids[0], feld, "p", i === 0 ? "handlung" : "ok", jetzt); } for (let i = 0; i < 16; i++) { d.prepare("INSERT INTO startcheck (creator_id, feld, punkt, bewertung, geaendert) VALUES (?,?,?,'ok',?)") .run(ids[1], "f" + i, "p", jetzt); } d.prepare("INSERT INTO profile (person_id, naechster_review) VALUES (?,?)").run(ids[0], "2026-09-30"); d.prepare("INSERT INTO profile (person_id, naechster_review) VALUES (?,?)").run(ids[1], null); d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(ids[0], idDogi, jetzt); d.close(); /* ---------- Browser ---------------------------------------------------- */ const { chromium } = await import( "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); const browser = await chromium.launch(); async function ansehen(name, rolle, code, groesse, datei) { const kontext = await browser.newContext({ viewport: groesse }); const seite = await kontext.newPage(); const konsole = []; seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); }); seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message)); await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); await seite.click(`.rolle[data-rolle="${rolle}"]`); await seite.fill("#code", code); await seite.click("button[type=submit]"); await seite.waitForURL("**/start.html", { timeout: 15000 }); await seite.goto(BASIS + "/workspace/uebersicht.html", { waitUntil: "networkidle" }); await seite.waitForTimeout(500); const bild = await seite.evaluate(() => ({ karten: document.querySelectorAll(".creator").length, summen: document.querySelectorAll(".summe").length, titel: document.getElementById("titel")?.textContent, unterzeile: document.getElementById("unterzeile")?.textContent, fehler: document.getElementById("fehler")?.textContent, /* Ueberstehender Inhalt: das ist der Fehler, den man am Rechner nie sieht und der auf dem Handy die Seite unbrauchbar macht. */ ueberstand: document.documentElement.scrollWidth - document.documentElement.clientWidth, breit: [...document.querySelectorAll("main *")] .filter((e) => e.getBoundingClientRect().right > window.innerWidth + 1) .map((e) => e.className).slice(0, 5), /* Der Balken muss eine echte Breite haben, sonst ist der Start-Check nur eine leere Rinne. */ balken: [...document.querySelectorAll(".fortschritt__fuellung")] .map((e) => e.style.width), zahlen: [...document.querySelectorAll(".creator")].map((k) => ({ name: k.querySelector(".creator__name")?.textContent, achtung: k.dataset.achtung === "ja", werte: [...k.querySelectorAll(".wert__zahl")].map((z) => z.textContent).join("/"), naechstes: k.querySelector(".naechstes__wert")?.textContent, unter: k.querySelector(".creator__unter")?.textContent, })), })); await seite.screenshot({ path: datei, fullPage: true }); console.log(`\n${name}`); ok(konsole.length === 0, `keine Fehler in der Konsole${konsole.length ? " -> " + konsole.join(" | ") : ""}`); ok(bild.ueberstand <= 0, `nichts steht über (${bild.ueberstand}px, ${bild.breit.join(", ")})`); ok(!bild.fehler, `keine Fehlermeldung auf der Seite ("${bild.fehler}")`); console.log(" ->", JSON.stringify(bild.zahlen, null, 1).replace(/\n\s*/g, " ")); console.log(" -> Titel:", bild.titel, "| Summen:", bild.summen, "| Balken:", bild.balken.join(" ")); await kontext.close(); return bild; } const gross = await ansehen("DogFather · Computer", "admin", "CODE-DOGI-0001", { width: 1440, height: 1000 }, "pruef-uebersicht-computer.png"); ok(gross.karten === 4, `vier Karten (${gross.karten})`); ok(gross.summen === 6, `sechs Summen (${gross.summen})`); /* Sortierung: Anna hat 3 ueberfaellige + 1 dringend und muss oben stehen. */ ok(gross.zahlen[0]?.name === "Anna Sturm", `die brennendste Karte steht oben (${gross.zahlen[0]?.name})`); ok(gross.zahlen[0]?.achtung === true, "sie ist als Achtung markiert"); ok(gross.zahlen.at(-1)?.achtung === false, "eine ruhige Karte ist nicht markiert"); ok(gross.zahlen.some((z) => /nicht da|nie angemeldet/.test(z.unter || "")), "lange Abwesenheit wird benannt"); const klein = await ansehen("DogFather · Handy", "admin", "CODE-DOGI-0001", { width: 390, height: 844 }, "pruef-uebersicht-handy.png"); ok(klein.karten === 4, `vier Karten auch auf dem Handy (${klein.karten})`); const eigen = await ansehen("Creator · Handy", "creator", "CODE-CRE-0001", { width: 390, height: 844 }, "pruef-uebersicht-creator.png"); ok(eigen.karten === 1, `Creator sieht genau seine Karte (${eigen.karten})`); ok(eigen.titel === "Mein Stand", `Titel heisst "Mein Stand" (${eigen.titel})`); ok(eigen.summen === 0, "keine Summenzeile bei nur einer Karte"); ok(!/betreut von/.test(eigen.zahlen[0]?.unter || ""), "dem Creator wird die Betreuung nicht vorgehalten"); await browser.close(); console.log(`\n${fehler === 0 ? "Alles in Ordnung." : fehler + " Prüfung(en) fehlgeschlagen."}`); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } process.exit(fehler === 0 ? 0 : 1);