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]>
246 lines
12 KiB
JavaScript
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;
|