/* =================================================================== Pruefung: Lange Protokoll-Listen lassen sich auf- und zuklappen und zeigen zugeklappt genau vier Zeilen. Geprueft an BEIDEN Stellen, die den gemeinsamen Helfer aus kopf.js benutzen -- Personen und Automationen. =================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; const ordner = mkdtempSync(join(tmpdir(), "ws-klappe-")); process.env.WORKSPACE_DB = join(ordner, "pruef.sqlite"); /* DER PORT MUSS MEINER SEIN (nachgeruestet 19.09.2026). Ohne diese Zeile startet der eigene Server bei belegtem Port STILL nicht -- und alles Folgende misst gegen einen fremden Stand. Das ist der dritte Ausgang: belegt ist etwas anderes als kaputt. */ const PORT = await eigenerPort(import.meta, "pruef-protokoll-klappe"); process.env.PORT = `${PORT}`; 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 }); }; import { notbremse } from "./helfer-notbremse.mjs"; import { eigenerPort } from "./helfer-port.mjs"; await import("./index.js"); /* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort zwei Auffangnetze ein, die einen Absturz nur protokollieren -- richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */ notbremse(480_000, "pruef-protokoll-klappe"); await new Promise((r) => setTimeout(r, 700)); const BASIS = `http://127.0.0.1:${PORT}`; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 200_000).unref?.(); let fehler = 0; const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; await fetch(BASIS + "/workspace/api/anmelden", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {}); const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes } = await import("node:crypto"); const d = new DatabaseSync(process.env.WORKSPACE_DB); const salt = randomBytes(16).toString("hex"); const hash = scryptSync("CODE-DOGI-0001", 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("Dogfather", "admin", hash, salt, 32768, new Date().toISOString()); /* Reichlich Eintraege, damit der Unterschied zwischen zu und auf sichtbar wird. 'betreuung_gesetzt' zaehlt auch die Automationen-Seite als Automation -- damit dort ebenfalls genug zusammenkommt. */ for (let i = 0; i < 30; i++) { d.prepare("INSERT INTO protokoll (zeitpunkt, aktion, detail, ip) VALUES (?,?,?,?)") .run(new Date(Date.now() - i * 3600_000).toISOString(), "betreuung_gesetzt", "Nr. " + i, "1.2.3.4"); } 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(kontext) { const seite = await kontext.newPage(); await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" }); await seite.click('.rolle[data-rolle="admin"]'); await seite.fill("#code", "CODE-DOGI-0001"); await seite.click("button[type=submit]"); await seite.waitForURL("**/start.html", { timeout: 15000 }); return seite; } /* seite, Liste, Knopf, Textfeld -> was gerade zu sehen ist */ const lesen = (seite, liste, knopf, text) => seite.evaluate(([l, k, t]) => { const box = document.querySelector(l); const kn = document.querySelector(k); const zeilen = box ? [...box.children] : []; return { sichtbar: zeilen.filter((z) => z.offsetParent !== null).length, gesamt: zeilen.length, text: document.querySelector(t)?.textContent, auf: kn?.getAttribute("aria-expanded"), knopfDa: kn ? !kn.hidden : false, ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth, pfeil: getComputedStyle(document.querySelector(k + " .klapp-pfeil")).transform, }; }, [liste, knopf, text]); async function pruefe(seite, name, pfad, liste, knopf, text) { await seite.goto(BASIS + pfad, { waitUntil: "networkidle" }); await seite.waitForTimeout(600); console.log(`\n--- ${name}`); const zu = await lesen(seite, liste, knopf, text); ok(zu.knopfDa, "der Knopf ist da"); ok(zu.sichtbar === 4, `zugeklappt vier Zeilen sichtbar (${zu.sichtbar} von ${zu.gesamt})`); ok(zu.auf === "false", `aria-expanded=false (${zu.auf})`); ok(/^Alle \d+ zeigen$/.test(zu.text || ""), `Knopf nennt die Zahl ("${zu.text}")`); ok(zu.ueber <= 0, `nichts steht über (${zu.ueber}px)`); await seite.click(knopf); await seite.waitForTimeout(350); const auf = await lesen(seite, liste, knopf, text); ok(auf.sichtbar === auf.gesamt && auf.gesamt > 4, `aufgeklappt alle Zeilen sichtbar (${auf.sichtbar} von ${auf.gesamt})`); ok(auf.auf === "true", `aria-expanded=true (${auf.auf})`); ok(auf.text === "Nur die letzten 4", `Knopf bietet das Zuklappen an ("${auf.text}")`); ok(auf.pfeil !== zu.pfeil, "der Pfeil hat sich gedreht"); ok(auf.ueber <= 0, `auch aufgeklappt steht nichts über (${auf.ueber}px)`); /* Zweimal klicken -- der haeufigste Fehler bei so etwas ist ein doppelt gesetzter Zuhoerer, der beim zweiten Mal gar nichts tut. */ await seite.click(knopf); await seite.waitForTimeout(350); const wiederZu = await lesen(seite, liste, knopf, text); ok(wiederZu.sichtbar === 4, `wieder zugeklappt vier Zeilen (${wiederZu.sichtbar})`); await seite.click(knopf); await seite.waitForTimeout(350); ok((await lesen(seite, liste, knopf, text)).sichtbar > 4, "und wieder auf"); return zu.gesamt; } for (const [breite, wie] of [[1440, "Computer"], [390, "Handy"]]) { console.log(`\n=== ${wie}`); const kontext = await browser.newContext({ viewport: { width: breite, height: 900 }, hasTouch: breite <= 860 }); const seite = await anmelden(kontext); const konsole = []; seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); }); seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message)); await pruefe(seite, "Personen · Letzte Ereignisse", "/workspace/personen.html", "#protokoll", "#protokoll-schalter", "#protokoll-schalter-text"); await pruefe(seite, "Automationen · Zuletzt automatisch passiert", "/workspace/automation.html", "#verlauf .verlauf", "#verlauf-schalter", "#verlauf-schalter-text"); ok(konsole.length === 0, `keine Fehler in der Konsole${konsole.length ? " -> " + konsole.join(" | ") : ""}`); if (breite === 1440) { await seite.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" }); await seite.waitForTimeout(500); await seite.screenshot({ path: "pruef-protokoll-zu.png" }); await seite.click("#protokoll-schalter"); await seite.waitForTimeout(400); await seite.screenshot({ path: "pruef-protokoll-auf.png", fullPage: true }); } await kontext.close(); } /* Gegenprobe: Bei hoechstens vier Eintraegen darf der Knopf nicht da sein -- ein Knopf, der nichts verbirgt, verwirrt nur. */ { const d2 = new DatabaseSync(process.env.WORKSPACE_DB); d2.prepare("DELETE FROM protokoll WHERE aktion = 'betreuung_gesetzt'").run(); d2.prepare("DELETE FROM protokoll WHERE id NOT IN (SELECT id FROM protokoll ORDER BY id DESC LIMIT 2)").run(); d2.close(); console.log("\n=== Wenige Einträge"); const kontext = await browser.newContext({ viewport: { width: 1440, height: 900 } }); const seite = await anmelden(kontext); await seite.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" }); await seite.waitForTimeout(500); const r = await seite.evaluate(() => ({ gesamt: document.querySelectorAll("#protokoll .protokoll-zeile").length, knopfDa: !document.getElementById("protokoll-schalter")?.hidden, klapp: document.getElementById("protokoll")?.dataset.klapp, })); ok(r.gesamt <= 4 && !r.knopfDa, `kein Knopf bei ${r.gesamt} Einträgen (Knopf da: ${r.knopfDa})`); ok(!r.klapp, `und nichts bleibt versteckt (data-klapp=${r.klapp})`); await kontext.close(); } await browser.close(); console.log(`\n${fehler === 0 ? "Alles in Ordnung." : fehler + " Prüfung(en) fehlgeschlagen."}`); try { rmSync(ordner, { recursive: true, force: true }); } catch {} process.exit(fehler === 0 ? 0 : 1);