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