Files
dogfather-universe/pruef-uebersicht.mjs
T
DogFatherGitandClaude Opus 5 c1cb51884f Verwaltung: Übersicht als Startansicht, Reiter unter den Titel
Die Reiter standen neben dem Titel. Das las sich wie eine einzige lange
Zeile, in der der Titel nur der erste von sechs Knöpfen zu sein schien --
man sah nicht auf einen Blick, in welchem Bereich man war. Jetzt zwei
Zeilen: oben WO man ist, darunter WOHIN man kann.

Die Verwaltung öffnete bisher mit der Anfragenliste. Eine Liste ist eine
Ablage: Sie zeigt, WAS es gibt, nicht was zu TUN ist. Neu ist ein
Cockpit, das in drei Stufen antwortet -- was auf mich wartet, was beim
Kunden liegt, wie es ums Geld steht -- und dann laufende Projekte mit
Fortschritt sowie den Verlauf.

Zwei Grundsätze machen die Zahlen brauchbar: Getrennt nach 'wartet auf
mich' und 'wartet auf den Kunden' (zwölf offene Punkte sind entspannt,
wenn elf beim Kunden liegen). Und das ALTER färbt, nicht die Menge --
vier neue Anfragen sind kein Problem, eine seit sechs Tagen liegende
schon. Ein offener Widerruf ist immer rot, weil eine gesetzliche Frist
läuft.

Jede Kachel ist ein echter <button> und führt in den passenden Reiter.
Farbe ist nie der einzige Träger: Neben jedem farbigen Zustand steht der
Text ('älteste seit 8 Tagen').

Drei Fehler dabei gefunden und behoben:
- Der Titel wurde nur beim Klicken gesetzt. Frisch geladen zeigte die
  Seite das Cockpit, während darüber noch 'Projektanfragen' stand. Die
  Zuordnung Reiter->Titel liegt jetzt ausserhalb des Klick-Zuhörers.
- '.wd h2' überstimmte '.vw-ub-h': 44px Überschrift über 33px Zahl, die
  Seite las sich wie ein Plakat. Dieselbe Spezifitätsfalle wie früher
  bei '.wd a'.
- Eine Antwort mit ok:true aber ohne Inhalt riss die ganze Verwaltung
  mit. Wird jetzt abgefangen -- ein halber Server ist ein realistischer
  Fall.

Geprüft: 22 Serverprüfungen gegen eine echte Datenbank (inkl. vier
Fällen, die belegen, dass die Rechteprüfung wirklich greift), 56 im
Browser auf Computer und Handy, 69 im bestehenden Verwaltungstest,
0 Fundstellen im Design-/WCAG-Test, 5 im Farbsystemtest.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-23 19:21:12 +02:00

246 lines
12 KiB
JavaScript

/* =====================================================================
pruef-uebersicht.mjs — die neue Startansicht der Verwaltung
Zwei Dinge werden hier geprüft, und beide haben eine Vorgeschichte:
1. STEHEN DIE REITER WIRKLICH UNTER DEM TITEL?
Das ist der ausdrückliche Wunsch vom 23.08.2026. Es wäre bequem,
dafür das Vorhandensein einer CSS-Regel zu prüfen — das beweist
aber nichts. Am 22.08.2026 waren 37 von 37 Prüfungen grün, während
jede Aufgabenzeile im Browser verkehrt herum stand: Die Regeln
waren da, das Ergebnis war falsch. Deshalb wird hier die
tatsächliche Lage im Fenster gemessen — der Reiter MUSS weiter
unten liegen als der Titel.
2. FÜHREN DIE KACHELN AUCH WIRKLICH IRGENDWOHIN?
Eine Kachel, die aussieht wie ein Knopf und keiner ist, ist
schlimmer als gar kein Knopf. Also wird geklickt und geprüft, dass
danach die richtige Ansicht offen ist.
===================================================================== */
import { chromium } from "playwright";
import { createServer } from "node:http";
import { readFileSync, existsSync } from "node:fs";
import { join, extname } from "node:path";
const W = process.cwd();
const T = { ".html": "text/html", ".css": "text/css", ".js": "text/javascript",
".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml",
".webp": "image/webp", ".ico": "image/x-icon", ".json": "application/json" };
const srv = createServer((q, r) => {
const p = join(W, decodeURIComponent(q.url.split("?")[0]));
if (existsSync(p) && !p.endsWith("/")) {
r.writeHead(200, { "content-type": T[extname(p)] || "text/plain" });
return r.end(readFileSync(p));
}
r.writeHead(404).end("");
});
await new Promise((r) => srv.listen(4337, r));
const ergebnisse = [];
function pruefe(name, ok, zusatz) {
ergebnisse.push({ name, ok });
console.log((ok ? " ok " : " FEHL ") + name + (zusatz !== undefined ? " -> " + zusatz : ""));
}
const vorTagen = (n) => new Date(Date.now() - n * 864e5).toISOString();
/* Absichtlich unbequeme Testdaten: eine Anfrage, die seit acht Tagen
liegt (muss rot werden), eine Nachricht seit drei Tagen (gelb), und
ein offener Widerruf von heute (muss trotzdem rot sein, weil eine
gesetzliche Frist läuft). Ein Datensatz, in dem alles in Ordnung ist,
würde die Warnstufen gar nicht erst auslösen. */
const UEBERSICHT = {
ok: true,
beiMir: {
anfragen: { n: 3, aeltestes: vorTagen(8) },
nachrichten: { n: 2, aeltestes: vorTagen(3) },
wuensche: { n: 0, aeltestes: null },
widerrufe: { n: 1, aeltestes: vorTagen(0) },
projekte: 2,
},
beiKunde: {
projekte: 1,
aufgaben: 4,
zahlungen: { n: 2, summe: 120000, aeltestes: vorTagen(5) },
zahlungenText: "1.200,00 €",
},
geld: {
monat: { n: 3, brutto: 250000, netto: 241000, bruttoText: "2.500,00 €", nettoText: "2.410,00 €" },
gesamt: { n: 11, brutto: 980000, bruttoText: "9.800,00 €" },
gebuehrenText: "90,00 €",
},
projekte: [
{ id: "p1", nummer: "P-2026-001", titel: "Website für Musterbau", status: "bau",
wartet_auf: "dogfather", richttermin: "2026-09-15", kunde_name: "Anna Muster",
aufg_gesamt: 20, aufg_fertig: 13 },
{ id: "p2", nummer: "P-2026-002", titel: "Shop für Beispiel GmbH", status: "bau",
wartet_auf: "kunde", richttermin: null, kunde_name: "Bert Beispiel",
aufg_gesamt: 18, aufg_fertig: 4 },
],
verlauf: [
{ bereich: "projekt", aktion: "Aufgabe erledigt: Startseite gebaut", wer: "dogfather", erstellt_am: vorTagen(0) },
{ bereich: "zahlung", aktion: "Zahlung eingegangen", wer: "paypal", erstellt_am: vorTagen(1) },
],
bestand: { kunden: 5, projekte: 2, anfragenGesamt: 9 },
paypal: { eingerichtet: true, live: true },
};
const browser = await chromium.launch();
for (const [wo, breite, hoehe] of [["Computer", 1440, 950], ["Handy", 390, 844]]) {
const seite = await browser.newPage({ viewport: { width: breite, height: hoehe } });
const fehlerImProtokoll = [];
seite.on("pageerror", (e) => fehlerImProtokoll.push(e.message));
await seite.route("**/webdesign/admin/**", (route) => {
const u = new URL(route.request().url()).pathname;
if (u.endsWith("/admin/uebersicht")) return route.fulfill({ json: UEBERSICHT });
if (u.includes("/admin/postfach-zaehler")) return route.fulfill({ json: { ok: true, ungelesen: 2 } });
if (u.includes("/admin/anfragen")) return route.fulfill({ json: { ok: true, anfragen: [] } });
if (u.includes("/admin/kunden")) return route.fulfill({ json: { ok: true, kunden: [] } });
if (u.includes("/admin/projekte")) return route.fulfill({ json: { ok: true, projekte: [] } });
if (u.includes("/admin/zahlungen")) return route.fulfill({ json: { ok: true, zahlungen: [] } });
if (u.includes("/admin/postfach")) return route.fulfill({ json: { ok: true, ungelesen: 2, verlaeufe: [] } });
return route.fulfill({ json: { ok: true } });
});
await seite.route("**/auth/**", (r) => r.fulfill({ json: { ok: true, user: { name: "Test" } } }));
await seite.addInitScript(() => {
sessionStorage.setItem("wd-vw-token", "t");
sessionStorage.setItem("wd-sitzung-aktiv", "1");
});
await seite.goto("http://127.0.0.1:4337/webdesign/verwaltung.html");
await seite.waitForTimeout(900);
console.log("\n=== " + wo + " ===");
/* ---- 1. Die Verwaltung startet in der Übersicht ---- */
const titel = await seite.locator("#vw-titel").innerText();
pruefe(wo + ": die Verwaltung öffnet in der Übersicht", titel.trim() === "Übersicht", titel);
pruefe(wo + ": das Cockpit ist sichtbar",
await seite.locator("#vw-ub-inhalt").isVisible());
pruefe(wo + ": die Anfragenliste liegt NICHT darunter",
!(await seite.locator("#vw-liste").isVisible()));
/* ---- 2. Reiter UNTER dem Titel — gemessen, nicht behauptet ---- */
const lage = await seite.evaluate(() => {
const t = document.getElementById("vw-titel").getBoundingClientRect();
const r = document.querySelector(".vw-tabs").getBoundingClientRect();
return { titelUnten: t.bottom, titelLinks: t.left, reiterOben: r.top, reiterLinks: r.left };
});
pruefe(wo + ": die Reiter beginnen UNTERHALB des Titels",
lage.reiterOben >= lage.titelUnten - 1,
"Titel endet bei " + Math.round(lage.titelUnten) + ", Reiter beginnen bei " + Math.round(lage.reiterOben));
/* Zusatzbedingung, sonst wäre "darunter" auch erfüllt, wenn die Reiter
weit nach rechts eingerückt stünden statt am Seitenrand zu beginnen. */
pruefe(wo + ": die Reiter beginnen am selben linken Rand wie der Titel",
Math.abs(lage.reiterLinks - lage.titelLinks) < 3,
"Titel " + Math.round(lage.titelLinks) + " vs. Reiter " + Math.round(lage.reiterLinks));
/* ---- 2b. Groessenverhaeltnisse ----
Eine Zwischenueberschrift darf nicht groesser sein als die Zahl, um
die es geht. Genau das war der Fall, solange die Regel .wd h2 die
eigene ueberstimmte: 44px Ueberschrift ueber 33px Zahl -- die Seite
las sich wie ein Plakat statt wie ein Arbeitsplatz. */
const groessen = await seite.evaluate(() => ({
h: parseFloat(getComputedStyle(document.querySelector(".vw-ub-h")).fontSize),
zahl: parseFloat(getComputedStyle(document.querySelector(".vw-ub-zahl")).fontSize),
}));
pruefe(wo + ": die Zwischenueberschrift ist kleiner als die Zahl darunter",
groessen.h < groessen.zahl,
"Ueberschrift " + groessen.h + "px, Zahl " + groessen.zahl + "px");
/* ---- 3. Inhalt der Kacheln ---- */
const kacheln = await seite.locator("#vw-ub-beimir .vw-ub-kachel").allInnerTexts();
pruefe(wo + ": alle fünf Kacheln 'bei mir' sind da (inkl. Widerruf)",
kacheln.length === 5, kacheln.length);
pruefe(wo + ": die drei offenen Anfragen stehen drin",
kacheln.some((k) => k.includes("3") && k.includes("offene Anfragen")));
pruefe(wo + ": das Alter steht als TEXT dabei, nicht nur als Farbe",
kacheln.some((k) => k.includes("seit 8 Tagen")),
kacheln.find((k) => k.includes("Anfragen")));
/* ---- 4. Die Warnstufen greifen wirklich ---- */
const stufen = await seite.evaluate(() => {
const raus = {};
document.querySelectorAll("#vw-ub-beimir .vw-ub-kachel").forEach((k) => {
const t = (k.querySelector(".vw-ub-titel") || {}).textContent || "";
raus[t] = k.className;
});
return raus;
});
pruefe(wo + ": 8 Tage alte Anfrage ist rot",
/vw-ub-kachel--rot/.test(stufen["offene Anfragen"] || ""), stufen["offene Anfragen"]);
pruefe(wo + ": 3 Tage alte Nachricht ist nur gelb, nicht rot",
/--warn/.test(stufen["ungelesene Nachrichten"] || "") &&
!/--rot/.test(stufen["ungelesene Nachrichten"] || ""), stufen["ungelesene Nachrichten"]);
pruefe(wo + ": ein Widerruf von HEUTE ist trotzdem rot (Frist läuft)",
/--rot/.test(stufen["Widerrufe unbearbeitet"] || ""), stufen["Widerrufe unbearbeitet"]);
pruefe(wo + ": eine Null-Kachel nimmt sich zurück statt zu warnen",
/--leer/.test(stufen["Änderungswünsche zu prüfen"] || ""),
stufen["Änderungswünsche zu prüfen"]);
/* ---- 5. Geld ---- */
const geld = (await seite.locator("#vw-ub-geld").innerText());
pruefe(wo + ": der Monatsumsatz steht da", geld.includes("2.500,00"));
pruefe(wo + ": die PayPal-Gebühr wird offen ausgewiesen", geld.includes("90,00"));
pruefe(wo + ": das tatsächlich angekommene Netto steht dabei", geld.includes("2.410,00"));
/* ---- 6. Projekte mit Fortschritt ---- */
const proj = await seite.locator("#vw-ub-projekte .vw-ub-projekt").allInnerTexts();
pruefe(wo + ": beide laufenden Projekte erscheinen", proj.length === 2, proj.length);
pruefe(wo + ": der Fortschritt steht als Zahl da, nicht nur als Balken",
proj[0].includes("13 von 20") && proj[0].includes("65"), proj[0].replace(/\n/g, " | "));
pruefe(wo + ": man sieht, wer am Zug ist",
proj[0].includes("du bist dran") && proj[1].includes("wartet auf Kunden"));
const balken = await seite.evaluate(() =>
document.querySelector(".vw-ub-balken-voll").style.width);
pruefe(wo + ": der Balken zeigt denselben Wert wie der Text", balken === "65%", balken);
/* ---- 7. Die Kacheln sind echte Knöpfe und führen irgendwohin ---- */
const istKnopf = await seite.evaluate(() =>
document.querySelector("#vw-ub-beimir .vw-ub-kachel").tagName);
pruefe(wo + ": eine Kachel ist ein <button>, kein <div>", istKnopf === "BUTTON", istKnopf);
await seite.locator('#vw-ub-beimir .vw-ub-kachel[data-ziel="postfach"]').first().click();
await seite.waitForTimeout(450);
pruefe(wo + ": ein Klick auf 'ungelesene Nachrichten' öffnet das Postfach",
(await seite.locator("#vw-titel").innerText()).trim() === "Postfach",
await seite.locator("#vw-titel").innerText());
/* ---- 8. Zurück in die Übersicht, ohne dass etwas hängen bleibt ---- */
await seite.click('[data-tab="uebersicht"]');
await seite.waitForTimeout(450);
pruefe(wo + ": zurück in der Übersicht",
(await seite.locator("#vw-titel").innerText()).trim() === "Übersicht");
pruefe(wo + ": beim Zurückkommen steht kein leerer Ladehinweis",
!(await seite.locator("#vw-ub-laedt").isVisible()));
pruefe(wo + ": das Postfach liegt nicht mehr darunter",
!(await seite.locator("#vw-postfach-bereich").isVisible()));
/* ---- 9. Nichts überlappt, nichts läuft aus dem Bild ---- */
const ueberlauf = await seite.evaluate(() =>
Math.max(0, document.documentElement.scrollWidth - document.documentElement.clientWidth));
pruefe(wo + ": nichts ragt seitlich aus dem Fenster", ueberlauf <= 1, ueberlauf + "px");
/* ---- 10. Tastaturbedienung ---- */
const fokus = await seite.evaluate(() => {
const k = document.querySelector("#vw-ub-beimir .vw-ub-kachel");
k.focus();
const st = getComputedStyle(k);
return { aktiv: document.activeElement === k, umriss: st.outlineWidth };
});
pruefe(wo + ": eine Kachel lässt sich mit der Tastatur anspringen", fokus.aktiv);
pruefe(wo + ": kein Fehler im Protokoll", fehlerImProtokoll.length === 0,
fehlerImProtokoll.join(" | "));
await seite.screenshot({ path: "bild-uebersicht-" + wo.toLowerCase() + ".png", fullPage: true });
await seite.close();
}
await browser.close();
srv.close();
const f = ergebnisse.filter((e) => !e.ok).length;
console.log(`\n===== ${ergebnisse.length - f} bestanden, ${f} fehlgeschlagen =====`);
process.exitCode = f ? 1 : 0;