Files
dogfather-universe/pruef-uebersicht.mjs
T
DogFatherGitandClaude Opus 5 37f1b4a8f5 Handy: Sicherheitsabstaende, Wisch-Reiter, zweispaltige Uebersicht
Der wichtigste Fund: env(safe-area-inset-*) stand bereits im CSS, lieferte
aber immer null -- viewport-fit=cover fehlte auf allen dreizehn Seiten.
Der Code sah richtig aus und tat nichts. Zusammen mit
apple-mobile-web-app-status-bar-style=black-translucent (schon gesetzt)
hiess das: installiert lag die Kopfzeile auf einem iPhone hinter Uhr und
Akkuanzeige.

Behoben:
- viewport-fit=cover auf allen 13 Seiten.
- Vier Sicherheitsabstaende zentral benannt statt an jeder Stelle
  einzeln geschrieben. Kopfzeile weicht der Statusleiste, Container dem
  seitlichen Notch im Querformat, Fusszeile der Gestenleiste.
- Der 'Ueberspringen'-Knopf des Vorspanns lag mit bottom:2rem praktisch
  AUF dem Entsperr-Strich (34px). Man haette die App verlassen statt
  uebersprungen.
- Eigener Zweig fuer den installierten Betrieb: kein Gummiband-
  Nachfedern (sieht in einer App nach einem Fehler aus), kein
  Installations-Hinweis.
- Die vier Sprungmarken auf der Rechtsseite waren 39px hoch -- fuenf
  unter dem Daumenmass. Ausgerechnet dort muss man zum Widerrufsrecht
  springen koennen.
- 'Waehle links einen Verlauf aus': Auf dem Handy gibt es kein links,
  die Liste steht darueber. Richtungswort entfernt.
- Sechs Reiter brauchten auf dem Handy drei Zeilen. Jetzt ein
  Wischstreifen mit Einrasten und Auslauf am Rand; der aktive Reiter
  wird herangeholt, wenn man ueber eine Kachel springt.
- Uebersicht zweispaltig statt zwoelf Zeilen untereinander: 2635px ->
  1924px. Eine Uebersicht, an der man vorbeiwischen muss, ist keine.

Geprueft mit 55 neuen Handy-Pruefungen auf iPhone 14 Pro, Pixel 7 und
320px Breite, jeweils im Browser und im installierten Zweig. Drei
Fehlalarme der eigenen Pruefung wurden begruendet ausgenommen (Honigtopf
bei left:-9999px, Eingabefeld in einer Beschriftung, Inline-Link im
Fliesstext -- WCAG 2.5.8 nimmt letztere ausdruecklich aus).

Zwei Selbstkorrekturen an der Pruefung dokumentiert: Die Emulation von
display-mode wirkt nicht (Chromium nimmt den Befehl an und ignoriert
ihn) -- ohne das waeren die zwoelf 'installiert'-Zeilen ein zweiter
Browser-Durchgang gewesen. Und die Messung gegen die Systemleisten mass
zuerst die Kastenkante statt der Inhaltskante und meldete elf korrekte
Seiten als fehlerhaft. Eine Selbstpruefung mit einem absichtlich falsch
gesetzten Knopf belegt jetzt, dass die Messung echte Fehler findet.

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

252 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();
/* Gemessen wird der erste KNOPF, nicht der Streifen um ihn herum.
Auf dem Handy sind die Reiter ein Wischstreifen, der bewusst 4px
ueber den Containerrand hinauslaeuft -- sonst endet er optisch
mitten auf der Seite, obwohl es weitergeht. Sichtbar ausgerichtet
ist aber der Knopf, und nur der zaehlt fuer das Auge. */
const r = document.querySelector(".vw-tabs [data-tab]").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;