/* ===================================================================== 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); } /* 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);