Masterplan S.10 "Projektanfrage ohne Informationsverlust" umgesetzt.
End-to-End gegen die echte Datenbank belegt: Anfrage A-2608-0002 liegt drin.
38/38 Browsertests, 45/45 Handy-Abnahme.
Aufbau: 4 Schritte + Zusammenfassung + Dankeseite.
- Zwischenspeicherung nach jedem Tastendruck. Geschlossener Tab, leerer
Akku oder versehentliches Zurueck kosten keine Arbeit. Entwuerfe
verfallen nach 30 Tagen -- ein halbes Jahr alter Entwurf verwirrt mehr
als er hilft.
- Folgefragen erscheinen nur passend zur Auswahl und werden beim
Zurueckwechseln NICHT mitgeschickt, sonst landen alte Shop-Antworten in
einer Onepager-Anfrage.
- Die Zusammenfassung wird aus den Feldern gelesen, nicht aus einem
nebenher gepflegten Objekt -- sie kann dadurch nie etwas anderes
behaupten als das, was tatsaechlich abgeschickt wird.
- Telefon wird zur Pflicht, sobald "Per Telefon" gewaehlt ist. Ein Wunsch,
der nicht erfuellbar ist, ist schlimmer als eine Pflichtangabe.
Drei Fehler, die erst der Test gefunden hat:
1. Der "Zurueck"-Knopf stand auch auf Schritt 1 da. Ursache: das
hidden-Attribut wird nur ueber "display:none" umgesetzt und verliert
gegen jedes eigene display -- und Knoepfe sind inline-flex. Zentral
behoben ueber ".wd [hidden] { display:none !important }".
2. Nach erfolgreichem Absenden legte zeigeSeite() den geloeschten Entwurf
sofort wieder an. Beim naechsten Besuch haette eine bereits gesendete
Anfrage erneut im Formular gestanden.
3. Der Server meldet bei Spamverdacht bewusst Erfolg ohne Nummer. Ein
echter Mensch, der zufaellig sehr schnell war, haette eine Dankeseite
mit "—" gesehen und auf eine Antwort gewartet, die nie kommt. Jetzt
ein ehrlicher Hinweis mit zweitem Weg, und der Entwurf bleibt erhalten.
Pruefskripte geschaerft:
- i18n-Pruefung liest jetzt auch assets/js/wd-<seite>.js, sonst meldet sie
reihenweise "unbenutzt" fuer Schluessel, die aus dem Seitenskript kommen.
- Handy-Abnahme ueberspringt absichtlich unsichtbare Eingabefelder
(Auswahlkacheln, Honigtopf). Fehlalarme sind das Ende jeder Pruefung,
weil man sie irgendwann pauschal ignoriert.
Co-Authored-By: Claude Opus 5 <[email protected]>
236 lines
10 KiB
JavaScript
236 lines
10 KiB
JavaScript
/* =====================================================================
|
|
pruefe-webdesign-handy.mjs — Handy-Abnahme für alle Webdesign-Seiten.
|
|
|
|
Prüft je Seite und je Bildschirmbreite (320 / 390 / 430 / 768 / 1440):
|
|
- kein waagerechter Überlauf (der häufigste Handy-Fehler überhaupt)
|
|
- kein Element ragt über den rechten Rand hinaus
|
|
- alle Knöpfe und Links sind mindestens 44px hoch (Touch-Ziel)
|
|
- keine Konsolenfehler
|
|
- in allen fünf Sprachen: keine unübersetzten Schlüssel sichtbar
|
|
|
|
320px ist bewusst dabei: das ist ein iPhone SE der ersten Generationen
|
|
und immer noch die Breite, bei der Layouts zuerst brechen.
|
|
|
|
Aufruf (lokaler Testserver muss laufen):
|
|
node pruefe-webdesign-handy.mjs
|
|
===================================================================== */
|
|
import { chromium } from "playwright";
|
|
|
|
const BASIS = process.env.WD_BASIS || "http://127.0.0.1:4199";
|
|
const CODE = process.env.WD_CODE || "DOGI-TEST-1";
|
|
|
|
const SEITEN = [
|
|
"/webdesign/",
|
|
"/webdesign/leistungen.html",
|
|
"/webdesign/preise.html",
|
|
"/webdesign/portfolio.html",
|
|
"/webdesign/ablauf.html",
|
|
"/webdesign/ueber.html",
|
|
"/webdesign/faq.html",
|
|
"/webdesign/anfrage.html",
|
|
];
|
|
const BREITEN = [320, 390, 430, 768, 1440];
|
|
const SPRACHEN = ["de", "de-CH", "en", "fr", "pt"];
|
|
|
|
let ok = 0;
|
|
const probleme = [];
|
|
|
|
const browser = await chromium.launch();
|
|
const kontext = await browser.newContext();
|
|
const seite = await kontext.newPage();
|
|
|
|
const konsole = [];
|
|
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
|
|
seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message));
|
|
|
|
// --- Anmelden ---
|
|
await seite.goto(BASIS + "/webdesign/zugang.html", { waitUntil: "networkidle" });
|
|
await seite.fill("#zg-code-dogfather", CODE);
|
|
await Promise.all([
|
|
seite.waitForURL((u) => !u.pathname.endsWith("zugang.html"), { timeout: 15000 }),
|
|
seite.click("#zg-form-dogfather button[type=submit]"),
|
|
]);
|
|
|
|
console.log("\n=== Handy-Abnahme ===\n");
|
|
|
|
for (const pfad of SEITEN) {
|
|
console.log(pfad);
|
|
for (const breite of BREITEN) {
|
|
await seite.setViewportSize({ width: breite, height: 900 });
|
|
await seite.goto(BASIS + pfad, { waitUntil: "networkidle" });
|
|
// Marken-Intro wegklicken, es liegt sonst über allem.
|
|
const skip = seite.locator("#wd-intro-skip");
|
|
if (await skip.isVisible().catch(() => false)) await skip.click();
|
|
await seite.waitForTimeout(250);
|
|
|
|
const befund = await seite.evaluate((vp) => {
|
|
const raus = [];
|
|
// Waagerechter Überlauf des ganzen Dokuments
|
|
const ueberlauf = document.documentElement.scrollWidth > vp + 1;
|
|
|
|
// Einzelne Elemente, die über den rechten Rand ragen
|
|
for (const el of document.querySelectorAll("body *")) {
|
|
const s = getComputedStyle(el);
|
|
if (s.display === "none" || s.visibility === "hidden") continue;
|
|
// Absichtlich waagerecht scrollbare Leisten ausnehmen
|
|
if (el.closest(".ls-sprung, .fq-sprung, .wd-tabelle-rahmen")) continue;
|
|
const r = el.getBoundingClientRect();
|
|
if (r.width === 0 || r.height === 0) continue;
|
|
if (r.right > vp + 1.5) {
|
|
raus.push((el.tagName.toLowerCase() + "." + (el.className || "").toString().split(" ")[0])
|
|
.slice(0, 50) + " bis " + Math.round(r.right) + "px");
|
|
}
|
|
}
|
|
|
|
// Zu kleine Touch-Ziele
|
|
const klein = [];
|
|
for (const el of document.querySelectorAll("a, button, summary, input, select")) {
|
|
const s = getComputedStyle(el);
|
|
if (s.display === "none" || s.visibility === "hidden") continue;
|
|
const r = el.getBoundingClientRect();
|
|
if (r.height === 0 || r.width === 0) continue;
|
|
|
|
/* Absichtlich unsichtbare Eingabefelder ueberspringen.
|
|
Bei Auswahlkacheln (.wd-wahl-kachel) liegt das echte Ankreuzfeld
|
|
unsichtbar hinter der Kachel -- angetippt wird die KACHEL, und
|
|
die ist gross genug. Ebenso der Honigtopf im Anfrageformular,
|
|
der fuer Menschen gar nicht existieren soll.
|
|
Ohne diese Ausnahme meldet die Pruefung fuenf Fehler, die keine
|
|
sind -- und Fehlalarme sind das Ende jeder Pruefung, weil man
|
|
sie irgendwann pauschal ignoriert. */
|
|
if (el.tagName === "INPUT") {
|
|
if (parseFloat(s.opacity) === 0) continue; // unsichtbar hinter der Kachel
|
|
if (r.right < 0 || r.left > vp) continue; // aus dem Bild geschoben
|
|
const label = el.closest("label");
|
|
if (label && label.getBoundingClientRect().height >= 44) continue; // Label ist das Ziel
|
|
}
|
|
|
|
if (r.height < 44) {
|
|
klein.push((el.tagName.toLowerCase() + " „" + (el.textContent || "").trim().slice(0, 24) + "“ ")
|
|
+ Math.round(r.height) + "px");
|
|
}
|
|
}
|
|
|
|
// Unübersetzte Schlüssel: erscheinen als roher Schlüsselname im Text
|
|
const roh = [];
|
|
for (const el of document.querySelectorAll("[data-i18n]")) {
|
|
const txt = (el.textContent || "").trim();
|
|
if (txt === el.getAttribute("data-i18n")) roh.push(txt);
|
|
}
|
|
|
|
/* Verzerrte Bilder.
|
|
Am 22.08.2026 gemeldet: zwei Projektbilder standen 576px breit,
|
|
aber 750px bzw. 696px hoch da -- hochkant gestreckt und ungleich,
|
|
obwohl im CSS 16:10 stand. Ursache waren die height-Attribute im
|
|
HTML, die wie eine CSS-Hoehe wirken und aspect-ratio schlagen.
|
|
Diese Pruefung vergleicht das im CSS gewuenschte Verhaeltnis mit
|
|
dem tatsaechlich gerenderten -- damit faellt so etwas sofort auf
|
|
und nicht erst per Bildschirmfoto. */
|
|
const verzerrt = [];
|
|
for (const bild of document.querySelectorAll("img")) {
|
|
const s = getComputedStyle(bild);
|
|
if (s.display === "none") continue;
|
|
const r = bild.getBoundingClientRect();
|
|
if (r.width < 2 || r.height < 2) continue;
|
|
|
|
const gewuenscht = s.aspectRatio;
|
|
if (!gewuenscht || gewuenscht === "auto") continue;
|
|
const teile = String(gewuenscht).split("/").map((t) => parseFloat(t));
|
|
if (teile.length !== 2 || !teile[0] || !teile[1]) continue;
|
|
|
|
const soll = teile[0] / teile[1];
|
|
const ist = r.width / r.height;
|
|
// 2 % Toleranz für Rundung auf ganze Pixel
|
|
if (Math.abs(soll - ist) / soll > 0.02) {
|
|
verzerrt.push(
|
|
(bild.className || bild.src.split("/").pop()).toString().slice(0, 40) +
|
|
` soll ${soll.toFixed(2)}, ist ${ist.toFixed(2)} (${Math.round(r.width)}x${Math.round(r.height)})`
|
|
);
|
|
}
|
|
}
|
|
|
|
/* Gleich große Karten in einer Reihe. "die sollen alle gleich
|
|
aussehen und nicht einer größer oder kleiner" -- ungleiche Höhen
|
|
in EINER Zeile sind der haeufigste Grund, warum ein Raster
|
|
unaufgeraeumt wirkt. */
|
|
const ungleich = [];
|
|
for (const raster of document.querySelectorAll(".wd-grid")) {
|
|
const kinder = [...raster.children].filter((k) => k.getBoundingClientRect().height > 0);
|
|
if (kinder.length < 2) continue;
|
|
// Nur Elemente vergleichen, die tatsächlich nebeneinander stehen
|
|
const reihen = new Map();
|
|
for (const k of kinder) {
|
|
const oben = Math.round(k.getBoundingClientRect().top);
|
|
const schluessel = Math.round(oben / 8) * 8;
|
|
if (!reihen.has(schluessel)) reihen.set(schluessel, []);
|
|
reihen.get(schluessel).push(Math.round(k.getBoundingClientRect().height));
|
|
}
|
|
for (const [, hoehen] of reihen) {
|
|
if (hoehen.length < 2) continue;
|
|
if (Math.max(...hoehen) - Math.min(...hoehen) > 2) {
|
|
ungleich.push(`${raster.className.slice(0, 30)}: ${hoehen.join(", ")}px`);
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
ueberlauf,
|
|
raus: raus.slice(0, 5),
|
|
klein: klein.slice(0, 5),
|
|
roh: roh.slice(0, 5),
|
|
verzerrt: verzerrt.slice(0, 5),
|
|
ungleich: ungleich.slice(0, 5),
|
|
};
|
|
}, breite);
|
|
|
|
const zeile = ` ${String(breite).padStart(4)}px`;
|
|
const fehler = [];
|
|
if (befund.ueberlauf) fehler.push("waagerechter Ueberlauf");
|
|
if (befund.raus.length) fehler.push("ragt heraus: " + befund.raus.join(", "));
|
|
if (befund.klein.length) fehler.push("Touch-Ziel < 44px: " + befund.klein.join(", "));
|
|
if (befund.roh.length) fehler.push("unuebersetzt: " + befund.roh.join(", "));
|
|
if (befund.verzerrt.length) fehler.push("Bild verzerrt: " + befund.verzerrt.join(" | "));
|
|
if (befund.ungleich.length) fehler.push("ungleich hohe Karten: " + befund.ungleich.join(" | "));
|
|
|
|
if (fehler.length) {
|
|
probleme.push(`${pfad} @ ${breite}px -> ${fehler.join(" | ")}`);
|
|
console.log(`${zeile} FEHLER: ${fehler.join(" | ")}`);
|
|
} else {
|
|
ok++;
|
|
console.log(`${zeile} ok`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Sprachen: einmal je Sprache die Startseite prüfen ---
|
|
console.log("\nSprachen (Startseite, 390px)");
|
|
await seite.setViewportSize({ width: 390, height: 900 });
|
|
for (const sprache of SPRACHEN) {
|
|
await seite.goto(BASIS + "/webdesign/", { waitUntil: "networkidle" });
|
|
const skip = seite.locator("#wd-intro-skip");
|
|
if (await skip.isVisible().catch(() => false)) await skip.click();
|
|
await seite.evaluate((s) => window.WD.setSprache(s), sprache);
|
|
await seite.waitForTimeout(350);
|
|
const befund = await seite.evaluate((vp) => {
|
|
const roh = [];
|
|
for (const el of document.querySelectorAll("[data-i18n]")) {
|
|
const txt = (el.textContent || "").trim();
|
|
if (txt === el.getAttribute("data-i18n")) roh.push(txt);
|
|
}
|
|
return { roh: roh.slice(0, 5), ueberlauf: document.documentElement.scrollWidth > vp + 1, lang: document.documentElement.lang };
|
|
}, 390);
|
|
const fehler = [];
|
|
if (befund.lang !== sprache) fehler.push("lang-Attribut ist " + befund.lang);
|
|
if (befund.roh.length) fehler.push("unuebersetzt: " + befund.roh.join(", "));
|
|
if (befund.ueberlauf) fehler.push("waagerechter Ueberlauf");
|
|
if (fehler.length) { probleme.push(`Sprache ${sprache} -> ${fehler.join(" | ")}`); console.log(` ${sprache.padEnd(6)} FEHLER: ${fehler.join(" | ")}`); }
|
|
else { ok++; console.log(` ${sprache.padEnd(6)} ok`); }
|
|
}
|
|
|
|
await browser.close();
|
|
|
|
console.log(konsole.length ? "\nKONSOLENFEHLER:\n" + [...new Set(konsole)].join("\n") : "\nKeine Konsolenfehler.");
|
|
console.log(`\n=== ${ok} ok, ${probleme.length} Probleme ===`);
|
|
if (probleme.length) probleme.forEach((p) => console.log(" " + p));
|
|
process.exit(probleme.length === 0 && konsole.length === 0 ? 0 : 1);
|