Files
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

175 lines
8.3 KiB
JavaScript

/* Prüft die neue Detailansicht der Verwaltung im echten Browser.
Die Seite spricht mit dem internen Dienst, den es hier nicht gibt.
Deshalb werden alle Netzanfragen abgefangen und mit erfundenen, aber
REALISTISCHEN Daten beantwortet — insbesondere einer sehr knappen
Anfrage, denn genau die sah vorher schlecht aus (halbe Ansicht
"keine Angabe").
Geprüft wird nicht "sieht gut aus", sondern messbar:
- stehen die Spalten wirklich nebeneinander
- ist die rechte Spalte nicht breiter als die linke
- ragt nichts aus dem Fenster
- ist die Bildlaufleiste dunkel statt weiss
*/
import { chromium } from "playwright";
import { createServer } from "node:http";
import { readFileSync, existsSync } from "node:fs";
import { join, extname } from "node:path";
const WURZEL = process.cwd();
const TYPEN = { ".html": "text/html", ".css": "text/css", ".js": "text/javascript",
".json": "application/json", ".png": "image/png", ".jpg": "image/jpeg",
".svg": "image/svg+xml", ".webp": "image/webp", ".ico": "image/x-icon" };
const server = createServer((req, res) => {
const pfad = join(WURZEL, decodeURIComponent(req.url.split("?")[0]));
if (existsSync(pfad) && !pfad.endsWith("/")) {
res.writeHead(200, { "content-type": TYPEN[extname(pfad)] || "text/plain" });
return res.end(readFileSync(pfad));
}
res.writeHead(404).end("nix");
});
await new Promise((r) => server.listen(4321, r));
const ANFRAGE_KNAPP = {
id: "t1", nummer: "A-2608-0002", name: "Selbsttest", email: "[email protected]",
telefon: null, kontaktweg: "email", sprache: "de", paket: "onepager",
ziel: "TESTANFRAGE aus dem Selbsttest - bitte archivieren.",
zielgruppe: null, seiten: null, funktionen: [], details: {},
hat_texte: null, hat_bilder: null, hat_logo: null, hat_domain: null,
budget: null, termin: null, bemerkung: null, status: "neu",
archiviert: 0, kunde_id: null,
erstellt_am: new Date(Date.now() - 3 * 86400000).toISOString(),
};
const browser = await chromium.launch();
const ergebnisse = [];
function pruefe(name, ok, zusatz) {
ergebnisse.push({ name, ok, zusatz });
console.log((ok ? " ok " : " FEHL ") + name + (zusatz ? " -> " + zusatz : ""));
}
for (const [bezeichnung, breite, hoehe] of [["Computer", 1440, 900], ["Handy", 390, 844]]) {
const seite = await browser.newPage({ viewport: { width: breite, height: hoehe } });
await seite.route("**/webdesign/admin/**", (route) => {
const u = route.request().url();
if (u.includes("/anfragen/t1")) {
return route.fulfill({ json: { ok: true, anfrage: ANFRAGE_KNAPP, notizen: [] } });
}
if (u.includes("/anfragen")) {
return route.fulfill({ json: { ok: true, anfragen: [ANFRAGE_KNAPP] } });
}
/* Die Uebersicht ist seit dem 23.08.2026 die Startansicht und wird
bei jedem Laden abgerufen. Ohne eigene Ersatzantwort landet sie im
Auffang unten, bekommt ein blankes {ok:true} und der Aufbau bricht
ab, bevor irgendetwas geprueft werden kann. */
if (u.includes("/uebersicht")) {
return route.fulfill({ json: { ok: true,
beiMir: { anfragen: { n: 1 }, nachrichten: { n: 0 }, wuensche: { n: 0 },
widerrufe: { n: 0 }, projekte: 0 },
beiKunde: { projekte: 0, aufgaben: 0, zahlungen: { n: 0, summe: 0 }, zahlungenText: "0,00 €" },
geld: { monat: { n: 0, brutto: 0, netto: 0, bruttoText: "0,00 €", nettoText: "0,00 €" },
gesamt: { n: 0, bruttoText: "0,00 €" }, gebuehrenText: "0,00 €" },
projekte: [], verlauf: [], bestand: {} } });
}
return route.fulfill({ json: { ok: true, kunden: [], projekte: [] } });
});
await seite.route("**/auth/**", (r) => r.fulfill({ json: { ok: true, user: { name: "Test" } } }));
await seite.addInitScript(() => {
sessionStorage.setItem("wd-vw-token", "testtoken");
sessionStorage.setItem("wd-sitzung-aktiv", "1");
});
const fehlerImProtokoll = [];
seite.on("pageerror", (e) => fehlerImProtokoll.push(e.message));
await seite.goto("http://127.0.0.1:4321/webdesign/verwaltung.html");
await seite.waitForTimeout(900);
console.log("\n=== " + bezeichnung + " (" + breite + "x" + hoehe + ") ===");
/* Erst in die Anfragen wechseln. Die Verwaltung oeffnet seit dem
23.08.2026 im Cockpit, nicht mehr in der Anfragenliste -- ohne
diesen Klick sucht der Test eine Karte, die es noch gar nicht gibt. */
await seite.click('[data-tab="anfragen"]');
await seite.waitForTimeout(600);
const karte = seite.locator(".vw-karte").first();
if (await karte.count()) {
await karte.click();
await seite.waitForTimeout(700);
}
const da = await seite.locator(".vw-detail-spalten").count();
pruefe(bezeichnung + ": Detailansicht öffnet sich", da === 1);
if (!da) { await seite.close(); continue; }
const mass = await seite.evaluate(() => {
const li = document.querySelector(".vw-dt-links");
const re = document.querySelector(".vw-dt-rechts");
const blatt = document.querySelector(".vw-detail-blatt");
const leere = document.querySelectorAll(".vw-ohne");
const blick = document.querySelectorAll(".vw-blick-feld");
const keineAngabe = Array.from(document.querySelectorAll(".vw-paar dd.vw-leer")).length;
return {
li: li?.getBoundingClientRect(), re: re?.getBoundingClientRect(),
blatt: blatt?.getBoundingClientRect(),
leereZeilen: leere.length, blickFelder: blick.length, keineAngabe,
fensterBreite: innerWidth, fensterHoehe: innerHeight,
ueberlauf: blatt ? blatt.scrollWidth > blatt.clientWidth + 1 : false,
};
});
if (breite >= 900) {
pruefe(bezeichnung + ": Spalten stehen NEBENeinander",
Math.abs(mass.li.top - mass.re.top) < 40, `links y=${Math.round(mass.li.top)}, rechts y=${Math.round(mass.re.top)}`);
pruefe(bezeichnung + ": rechte Spalte ist schmaler als die linke",
mass.re.width < mass.li.width, `${Math.round(mass.li.width)} vs ${Math.round(mass.re.width)}`);
pruefe(bezeichnung + ": Spalten überlappen sich nicht",
mass.li.right <= mass.re.left + 1, `links endet ${Math.round(mass.li.right)}, rechts beginnt ${Math.round(mass.re.left)}`);
} else {
pruefe(bezeichnung + ": Spalten stehen UNTEReinander",
mass.re.top > mass.li.top, `links y=${Math.round(mass.li.top)}, rechts y=${Math.round(mass.re.top)}`);
}
pruefe(bezeichnung + ": Fenster ragt nicht über den Bildschirm",
mass.blatt.left >= -1 && mass.blatt.right <= mass.fensterBreite + 1,
`${Math.round(mass.blatt.left)} … ${Math.round(mass.blatt.right)} bei ${mass.fensterBreite}`);
pruefe(bezeichnung + ": kein waagerechtes Schieben nötig", !mass.ueberlauf);
pruefe(bezeichnung + ": „Auf einen Blick“ zeigt vier Felder", mass.blickFelder === 4, "n=" + mass.blickFelder);
pruefe(bezeichnung + ": leere Angaben sind zusammengefasst", mass.leereZeilen >= 3, "n=" + mass.leereZeilen);
pruefe(bezeichnung + ": keine einzelne „keine Angabe“-Zeile mehr", mass.keineAngabe === 0, "n=" + mass.keineAngabe);
pruefe(bezeichnung + ": keine Fehler im Protokoll", fehlerImProtokoll.length === 0, fehlerImProtokoll.join(" | "));
await seite.screenshot({ path: `pruef-detail-${bezeichnung.toLowerCase()}.png`, fullPage: false });
await seite.close();
}
/* Bildlaufleiste: dunkel statt weiss? */
const s2 = await browser.newPage({ viewport: { width: 1200, height: 700 } });
await s2.goto("http://127.0.0.1:4321/webdesign/verwaltung.html");
await s2.waitForTimeout(400);
const bl = await s2.evaluate(() => {
const stil = getComputedStyle(document.body);
const d = document.createElement("div");
d.style.cssText = "overflow:auto;height:40px";
d.innerHTML = "<div style='height:400px'></div>";
document.body.appendChild(d);
return { farbschema: stil.colorScheme, hatRegel: !!Array.from(document.styleSheets)
.flatMap((s) => { try { return Array.from(s.cssRules); } catch { return []; } })
.find((r) => r.selectorText && r.selectorText.includes("scrollbar-thumb")) };
});
console.log("\n=== Bildlaufleiste ===");
pruefe("color-scheme steht auf dark (Firefox/Safari)", bl.farbschema === "dark", bl.farbschema);
pruefe("eigene Regel für den Griff vorhanden (Chrome/Edge)", bl.hatRegel);
await s2.close();
await browser.close();
server.close();
const fehler = ergebnisse.filter((e) => !e.ok).length;
console.log(`\n===== ${ergebnisse.length - fehler} bestanden, ${fehler} fehlgeschlagen =====`);
process.exitCode = fehler ? 1 : 0;