/* ===================================================================== Prüfung: das Formular "Neue Person" und die Rechte auf den beiden Verwaltungsbereichen (02.09.2026). Zwei Meldungen: "das sieht ja mal richtig scheisse aus. mach das besser bitte." -- Die Rollenauswahl lief unten aus dem Formular heraus und legte sich über die Personenliste. Ursache war keine schlampige Gestaltung, sondern eine Raster-Regel für EINZEILIGE Felder (feste Zeilenhöhe 44 px), die auch auf den Block aus vier hohen Karten angewandt wurde. "die manager sollen diese kategorien garnicht sehen" -- "Personen & Zugänge" und "Automationen" gehören allein DogFather. Der zweite Punkt wird auf DREI Ebenen geprüft, nicht nur an der Kachel: Wegnehmen, was man sieht, ist kein Rechteentzug. Es zählt, was der Server antwortet. ===================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; const ordner = mkdtempSync(join(tmpdir(), "ws-pform-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); process.env.PORT = "4198"; 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 ec = express.response.cookie; express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; await import("./index.js"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4198"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 300_000).unref?.(); let fehler = 0; let geprueft = 0; const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes } = await import("node:crypto"); const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); function anlegen(name, rolle, code) { const salt = randomBytes(16).toString("hex"); const hash = scryptSync(code, salt, 64, { N: 32768, 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) VALUES (?,?,?,?,?,1,?)") .run(name, rolle, hash, salt, 32768, jetzt); return d.prepare("SELECT last_insert_rowid() AS id").get().id; } anlegen("Dogfather", "admin", "CODE-DOGI-0001"); anlegen("Cigdem", "manager", "CODE-CIGDEM-01"); anlegen("Tili", "creator", "CODE-TILI-0001"); d.close(); const { chromium } = await import( "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); const browser = await chromium.launch(); async function anmelden(rolle, code, breite = 1440) { const kontext = await browser.newContext({ viewport: { width: breite, height: 1000 } }); const seite = await kontext.newPage(); 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 }); return seite; } /* =================================================================== TEIL 1 — Das Formular läuft nicht mehr über =================================================================== */ console.log("\n— Das Formular „Neue Person“ —"); const dogi = await anmelden("admin", "CODE-DOGI-0001"); await dogi.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" }); await dogi.waitForTimeout(700); await dogi.click("#neu-oeffnen").catch(() => {}); await dogi.waitForTimeout(500); const mass = await dogi.evaluate(() => { const form = document.getElementById("neu"); const wahl = document.getElementById("f-rolle"); const karten = [...document.querySelectorAll(".rollenwahl__knopf")]; const f = form.getBoundingClientRect(); const w = wahl.getBoundingClientRect(); /* Das erste Element NACH dem Formular -- über das die Karten vorher hinweggelaufen sind. */ const danach = document.querySelector("#liste"); const dn = danach ? danach.getBoundingClientRect() : null; return { formUnten: Math.round(f.bottom), wahlUnten: Math.round(w.bottom), wahlOben: Math.round(w.top), listeOben: dn ? Math.round(dn.top) : null, karten: karten.length, kartenBreiten: karten.map((k) => Math.round(k.getBoundingClientRect().width)), /* Liegt eine Karte weiter rechts als das Formular breit ist? */ ueberRand: karten.some((k) => k.getBoundingClientRect().right > f.right + 1), }; }); ok(mass.karten === 4, `alle vier Rollen stehen zur Wahl (${mass.karten})`); ok(mass.wahlUnten <= mass.formUnten, `die Rollenauswahl endet INNERHALB des Formulars (${mass.wahlUnten} ≤ ${mass.formUnten})`); ok(!mass.ueberRand, "keine Karte ragt seitlich aus dem Formular"); ok(mass.listeOben === null || mass.wahlUnten <= mass.listeOben, `die Karten liegen nicht über der Personenliste (${mass.wahlUnten} ≤ ${mass.listeOben})`); /* Alle Karten gleich breit -- ungleiche Kacheln sind das, was "sieht scheisse aus" in Zahlen bedeutet. */ const breiteste = Math.max(...mass.kartenBreiten); const schmalste = Math.min(...mass.kartenBreiten); ok(breiteste - schmalste <= 2, `alle Karten sind gleich breit (${schmalste}–${breiteste} px)`); /* Die gewählte Rolle muss man SEHEN, nicht erraten. */ const vorher = await dogi.evaluate(() => getComputedStyle(document.querySelector('.rollenwahl__knopf[data-an="ja"]'), "::after").content); ok(vorher.includes("✓"), `die gewählte Rolle trägt ein Häkchen (${vorher})`); await dogi.screenshot({ path: "pruef-personen-formular.png", fullPage: false }); /* Auf dem Handy darf nichts herausragen. */ const handy = await anmelden("admin", "CODE-DOGI-0001", 390); await handy.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" }); await handy.waitForTimeout(700); await handy.click("#neu-oeffnen").catch(() => {}); await handy.waitForTimeout(500); const engUeber = await handy.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth); ok(engUeber <= 0, `auf dem Handy ragt nichts heraus (${engUeber}px)`); await handy.screenshot({ path: "pruef-personen-formular-handy.png", fullPage: false }); /* =================================================================== TEIL 2 — Die beiden Bereiche gehören DogFather allein =================================================================== */ console.log("\n— Personen & Automationen: nur DogFather —"); const cigdem = await anmelden("manager", "CODE-CIGDEM-01"); await cigdem.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" }); await cigdem.waitForTimeout(800); const kacheln = await cigdem.evaluate(() => [...document.querySelectorAll(".kachel")].map((k) => k.innerText.split("\n")[0].trim())); ok(!kacheln.some((t) => /Personen/.test(t)), "1. die Kachel „Personen & Zugänge“ fehlt bei der Managerin"); ok(!kacheln.some((t) => /Automationen/.test(t)), "1. die Kachel „Automationen“ fehlt ebenfalls"); ok(kacheln.length > 8, ` – aber der Rest steht noch da (${kacheln.length} Kacheln), es ist nicht alles weg`); /* 2. Die SEITE, falls jemand die Adresse kennt. */ for (const pfad of ["personen.html", "automation.html"]) { const a = await cigdem.goto(BASIS + "/workspace/" + pfad, { waitUntil: "networkidle" }); const gelandet = cigdem.url(); ok(gelandet.endsWith("/start.html"), `2. ${pfad} direkt aufgerufen führt zurück zur Startseite (${gelandet.split("/").pop()})`); } /* 3. Die SCHNITTSTELLE, falls jemand ganz an der Seite vorbeigeht. Das ist die einzige Ebene, die wirklich zählt -- die beiden darüber sind Bequemlichkeit. */ const api = (seite, pfad, optionen) => seite.evaluate(async ([p, o]) => { const a = await fetch(p, { credentials: "same-origin", ...(o || {}) }); return a.status; }, [pfad, optionen]); await cigdem.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" }); ok(await api(cigdem, "/workspace/api/verwaltung/personen") === 404, "3. die Personenverwaltung antwortet ihr mit 404"); ok(await api(cigdem, "/workspace/api/zustand") === 404, "3. der Systemzustand ebenfalls"); ok(await api(cigdem, "/workspace/api/ki/schalter", { method: "PUT" }) !== 200, "3. und den KI-Schalter darf sie nicht umlegen"); /* GEGENPROBE: Bei DogFather geht das alles weiterhin. Ohne sie wäre jede Zeile oben auch dann grün, wenn die Bereiche für ALLE kaputt wären -- und das würde erst auffallen, wenn er sie braucht. */ ok(await api(dogi, "/workspace/api/verwaltung/personen") === 200, "GEGENPROBE: DogFather kommt weiterhin an die Personenverwaltung"); ok(await api(dogi, "/workspace/api/zustand") === 200, "GEGENPROBE: und an den Systemzustand"); /* Und die Managerin behält, was sie behalten soll. */ ok(await api(cigdem, "/workspace/api/aufgaben") === 200, "die Managerin arbeitet ansonsten unverändert weiter (Aufgaben)"); ok(await api(cigdem, "/workspace/api/calls") === 200, " und die Gespräche auch"); const ERWARTET = 17; console.log(""); ok(geprueft >= ERWARTET, `es sind ${geprueft} Prüfungen gelaufen (erwartet mindestens ${ERWARTET})`); await browser.close(); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } console.log(`\n${fehler ? "FEHLER: " + fehler : "Alles in Ordnung"} — ${geprueft} Prüfungen`); process.exit(fehler ? 1 : 0);