Webdesign-Bereich: Fundament, oeffentliche Seiten, Zugangsschutz, PayPal
Umsetzung des "Website Masterplan" (16 Seiten) unter /webdesign. Zugangsschutz mit EIGENER Schranke (server/webdesign-gate.js) statt gate.js: gate.js laesst seit dem oeffentlichen Start am 21.08.2026 jeden durch, weil die Pruefung auf SITE_PUBLIC_LAUNCH_AT als allererste Zeile steht. Haette man /webdesign dahintergehaengt, waere der ausdruecklich nicht-oeffentliche Bereich inklusive Preisen und spaeteren Kundendaten ab der ersten Sekunde fuer jeden lesbar gewesen. Eigenes Sitzungs-Cookie, bereich="webdesign" im Token, damit ein gueltiges Universe-Cookie hier NICHT gilt. 37/37 Tests. Sieben oeffentliche Seiten in fuenf Sprachen (de, de-CH mit echtem Dialekt, en, fr, pt). Preise, Zeitrahmen, Paketnamen und die 30-%-Regel stehen an genau EINER Stelle in wd-core.js -- der Masterplan verlangt "ueberall widerspruchsfrei", und vier Kopien laufen bei der ersten Preisaenderung auseinander. Als App installierbar auf Handy und PC. Der Service Worker speichert bewusst KEINE HTML-Seite zwischen: nach dem Abmelden wuerden sonst geschuetzte Seiten weiter ausgeliefert, ohne dass der Server je gefragt wird. 18/18 Tests. Handy-Abnahme ueber alle Seiten in fuenf Breiten (320-1440) und fuenf Sprachen: 40/40. Der Test fand 35 echte Fehler (Touch-Ziele unter 44px), behoben im Designsystem statt einzeln pro Seite. PayPal (Wunsch 22.08.2026 "sofort auf meinem paypal"): Orders API mit intent=CAPTURE, also sofortiger Einzug statt blosser Reservierung. Gebuehr und Nettobetrag getrennt gespeichert. Betraege durchgehend als Ganzzahl in Cent. Gefaelschte Webhooks werden abgewiesen. Fail closed solange Zugangsdaten fehlen. 28/28 Tests gegen einen nachgebauten PayPal-Server. Datenbank: 15 Tabellen mit Praefix wd_, fachlich vollstaendig vom Universe getrennt. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
/* =====================================================================
|
||||
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",
|
||||
];
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
return { ueberlauf, raus: raus.slice(0, 5), klein: klein.slice(0, 5), roh: roh.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 (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);
|
||||
Reference in New Issue
Block a user