Files
dogfather-universe/server/pruef-protokoll-klappe.mjs
T
DogFatherGitandClaude Opus 5 1894395d8f Workspace: lange Protokoll-Listen aufklappbar, zugeklappt vier Zeilen
Die "Letzten Ereignisse" im Personen-Bereich fuellten mit 25 Zeilen die
halbe Seite, obwohl sie Nachschlagewerk sind und kein Startbild. Jetzt
stehen nur die vier neuesten da, der Rest kommt auf Knopfdruck.

Der Knopf sagt, was er TUT ("Alle 25 zeigen" / "Nur die letzten 4") und
nennt die Zahl, damit man weiss, was dahintersteckt. Bei hoechstens vier
Eintraegen bleibt er ganz weg -- ein Knopf, der nichts verbirgt, verwirrt
nur. Der Pfeil dreht sich, aria-expanded stimmt.

Gemeinsamer Helfer in kopf.js statt zweimal derselbe Block: Dieselbe
Liste gab es auf der Automationen-Seite ("Zuletzt automatisch passiert"),
die verhaelt sich jetzt genauso. Zugeklappt wird per CSS
([data-klapp="zu"]) statt durch Entfernen von Zeilen -- das Aufklappen
braucht so weder Neuaufbau noch zweite Abfrage.

Der Zuhoerer wird nur einmal gesetzt (die Listen werden bei jeder
Aktualisierung neu aufgebaut), und die Anzahl wird bei jedem Umschalten
frisch gelesen statt in der Fassung des ersten Durchlaufs festzuhaengen.

Geprueft: beide Seiten, Computer und Handy, zweimaliges Hin- und
Herklappen, Gegenprobe mit drei Eintraegen -- server/pruef-protokoll-klappe.mjs

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-31 13:27:07 +02:00

166 lines
7.8 KiB
JavaScript

/* ===================================================================
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");
process.env.PORT = "4180";
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:4180";
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 } });
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);