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]>
175 lines
8.3 KiB
JavaScript
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;
|