Files
dogfather-universe/server/pruefe-webdesign-pwa.mjs
DogFatherGitandClaude Opus 5 4b3ec450d5 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]>
2026-08-22 18:06:52 +02:00

89 lines
4.2 KiB
JavaScript

/* Prüft die App-Installierbarkeit des Webdesign-Bereichs:
- Manifest erreichbar, gültig, Icons laden
- Service Worker registriert sich und wird aktiv
- HTML wird NICHT zwischengespeichert (Sicherheitsvorgabe)
- Bausteine unter /assets/ werden zwischengespeichert */
import { chromium } from "playwright";
const BASIS = "http://127.0.0.1:4199";
const browser = await chromium.launch();
const kontext = await browser.newContext();
const seite = await kontext.newPage();
const fehler = [];
seite.on("console", (m) => { if (m.type() === "error") fehler.push(m.text()); });
seite.on("pageerror", (e) => fehler.push("pageerror: " + e.message));
let ok = 0, schlecht = 0;
const pruefe = (name, b, zusatz = "") => {
if (b) { ok++; console.log(" ok " + name); }
else { schlecht++; console.log(" FEHLT " + name + " " + zusatz); }
};
console.log("\n=== App-Installierbarkeit ===\n");
// --- Manifest ohne Anmeldung erreichbar (Installation von der Zugangswand) ---
console.log("1. Manifest");
const mRes = await seite.request.get(BASIS + "/webdesign/app.webmanifest");
pruefe("ohne Anmeldung erreichbar", mRes.status() === 200, "(war " + mRes.status() + ")");
const manifest = await mRes.json().catch(() => null);
pruefe("gueltiges JSON", !!manifest);
if (manifest) {
pruefe("start_url im Webdesign-Bereich", manifest.start_url === "/webdesign/");
pruefe("scope begrenzt auf /webdesign/", manifest.scope === "/webdesign/");
pruefe("display standalone", manifest.display === "standalone");
pruefe("192er-Symbol vorhanden", manifest.icons.some((i) => i.sizes === "192x192"));
pruefe("512er-Symbol vorhanden", manifest.icons.some((i) => i.sizes === "512x512"));
pruefe("maskierbares Symbol vorhanden", manifest.icons.some((i) => (i.purpose || "").includes("maskable")));
for (const symbol of manifest.icons) {
const r = await seite.request.get(BASIS + symbol.src);
pruefe("Symbol laedt: " + symbol.src, r.status() === 200);
}
}
const swRes = await seite.request.get(BASIS + "/webdesign/sw.js");
pruefe("Service Worker ohne Anmeldung erreichbar", swRes.status() === 200, "(war " + swRes.status() + ")");
pruefe("wird als JavaScript ausgeliefert",
(swRes.headers()["content-type"] || "").includes("javascript"),
"(war " + swRes.headers()["content-type"] + ")");
// --- Registrierung im Browser ---
console.log("\n2. Service Worker im Browser");
await seite.goto(BASIS + "/webdesign/zugang.html", { waitUntil: "networkidle" });
await seite.fill("#zg-code-dogfather", "DOGI-TEST-1");
await Promise.all([
seite.waitForURL((u) => !u.pathname.endsWith("zugang.html"), { timeout: 15000 }),
seite.click("#zg-form-dogfather button[type=submit]"),
]);
await seite.waitForLoadState("networkidle");
const aktiv = await seite.evaluate(async () => {
const reg = await navigator.serviceWorker.ready.catch(() => null);
return reg ? { scope: reg.scope, aktiv: !!reg.active } : null;
});
pruefe("registriert und aktiv", !!(aktiv && aktiv.aktiv));
pruefe("Geltungsbereich ist /webdesign/", !!(aktiv && aktiv.scope.endsWith("/webdesign/")), aktiv ? aktiv.scope : "");
// --- Sicherheitsvorgabe: keine HTML-Seiten im Zwischenspeicher ---
console.log("\n3. Sicherheit des Zwischenspeichers");
await seite.reload({ waitUntil: "networkidle" });
await seite.waitForTimeout(1200);
const inhalt = await seite.evaluate(async () => {
const namen = await caches.keys();
const alles = [];
for (const n of namen) {
const c = await caches.open(n);
for (const anfrage of await c.keys()) alles.push(anfrage.url);
}
return alles;
});
console.log(" Zwischenspeicher enthaelt " + inhalt.length + " Eintraege");
const htmlDrin = inhalt.filter((u) => u.endsWith(".html") || new URL(u).pathname === "/webdesign/");
pruefe("KEINE geschuetzte HTML-Seite zwischengespeichert", htmlDrin.length === 0, htmlDrin.join(", "));
pruefe("Stilvorlage ist zwischengespeichert", inhalt.some((u) => u.includes("webdesign.css")));
pruefe("Kern-Skript ist zwischengespeichert", inhalt.some((u) => u.includes("wd-core.js")));
console.log(fehler.length ? "\nKONSOLENFEHLER:\n" + fehler.join("\n") : "\nKeine Konsolenfehler.");
await browser.close();
console.log(`\n=== ${ok} bestanden, ${schlecht} gescheitert ===\n`);
process.exit(schlecht === 0 && fehler.length === 0 ? 0 : 1);