/* ===================================================================== ABNAHME VOR DER VORSTELLUNG (03.09.2026) Alle anderen Prüfungen laufen gegen einen Server auf diesem Rechner. Diese hier geht gegen die ECHTE Domain -- so, wie sie morgen jemand im Publikum aufruft. Der Unterschied ist nicht theoretisch: Zwischen "läuft bei mir" und "läuft im Netz" liegen Caddy, Cloudflare, der Cache-Stempel, das Zertifikat und der Dienst-Neustart. Genau dort ist am 18.08. und am 22.08.2026 schon zweimal etwas hängengeblieben, das lokal tadellos war (siehe CLAUDE.md, "Grundregel für ALLE Websites"). Geprüft wird in DREI Gestalten, weil das Publikum sie alle benutzt: RECHNER 1440 px HANDY 390 px, mit Fingerbedienung INSTALLIERT als App vom Startbildschirm -- ohne Adresszeile, ohne Zurück-Knopf des Browsers. Dort fällt auf, was man mit Browserleiste nie merkt. Gemessen wird je Seite: * Fehler in der Konsole * fehlgeschlagene Anfragen (4xx/5xx) * ob etwas seitlich aus dem Bild ragt * tote Verweise (href="#" oder leer) * fehlende Bilder * ob die Seite überhaupt Inhalt hat ===================================================================== */ const BASIS = process.env.ABNAHME_BASIS || "https://dogfather-universe.com"; const SEITEN = [ "/", "/dogfather.html", "/casper.html", "/hasidog.html", "/vanvan.html", "/community.html", "/streamer.html", "/supporter.html", "/abonnieren.html", "/bewerben.html", "/bewerben-creator.html", "/bewerben-scout.html", "/bewerben-modi.html", "/bewerben-kooperation.html", "/kontakt.html", "/werte.html", "/team-scouts.html", "/team-modis.html", "/stimmen.html", "/links.html", "/rabattcodes.html", "/streamplan.html", "/zeitreise.html", "/agb.html", "/webdesign", "/404.html", ]; let fehler = 0; let geprueft = 0; const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; const befunde = []; const EGAL = [ /favicon/i, /net::ERR_ABORTED/i, /Download the React DevTools/i, ]; const istEgal = (t) => EGAL.some((r) => r.test(t)); const { chromium } = await import( "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); const browser = await chromium.launch(); async function rundgang(name, kontextOptionen, seiten) { const kontext = await browser.newContext(kontextOptionen); const seite = await kontext.newPage(); const konsole = []; const schlecht = []; seite.on("console", (m) => { if (m.type() === "error" && !istEgal(m.text())) konsole.push(m.text()); }); seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message)); seite.on("response", (a) => { if (a.status() >= 400 && !istEgal(a.url())) { schlecht.push(`${a.status()} ${a.url().replace(BASIS, "")}`); } }); console.log(`\n===== ${name} =====`); for (const pfad of seiten) { konsole.length = 0; schlecht.length = 0; let antwort = null; try { antwort = await seite.goto(BASIS + pfad, { waitUntil: "networkidle", timeout: 40000 }); } catch (e) { ok(false, `${pfad.padEnd(28)} nicht erreichbar (${e.message.slice(0, 40)})`); befunde.push({ name, pfad, was: "nicht erreichbar" }); continue; } await seite.waitForTimeout(500); const z = await seite.evaluate(() => { const sichtbar = (e) => { const k = e.getBoundingClientRect(); return k.width > 0 && k.height > 0 && getComputedStyle(e).visibility !== "hidden"; }; const tote = [...document.querySelectorAll("a[href]")].filter(sichtbar) .filter((a) => { const h = a.getAttribute("href"); return !h || h === "#"; }) .map((a) => a.textContent.trim().slice(0, 30)); /* Nur SICHTBARE Bilder mit einer Quelle. Die Seite hat Platzhalter ( ohne src), die JavaScript erst füllt, wenn es etwas zu zeigen gibt -- die sind kein Fehler, sondern der Normalzustand. Beim ersten Lauf hat meine Prüfung genau die gemeldet und damit zwei Seiten falsch beschuldigt. */ const bilderKaputt = [...document.querySelectorAll("img")] .filter((b) => sichtbar(b) && (b.getAttribute("src") || "").trim()) .filter((b) => b.complete && b.naturalWidth === 0) .map((b) => (b.getAttribute("src") || "").slice(-40)); return { tote, bilderKaputt, laenge: (document.body.innerText || "").trim().length, ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth, titel: document.title, }; }); const probleme = []; if (antwort && antwort.status() >= 400) probleme.push(`Seite antwortet ${antwort.status()}`); if (konsole.length) probleme.push("Konsole: " + konsole.slice(0, 2).join(" | ").slice(0, 130)); if (schlecht.length) probleme.push("Server: " + [...new Set(schlecht)].slice(0, 3).join(", ")); if (z.tote.length) probleme.push("tote Verweise: " + z.tote.slice(0, 3).join(", ")); if (z.bilderKaputt.length) probleme.push("fehlende Bilder: " + z.bilderKaputt.slice(0, 2).join(", ")); if (z.ueber > 0) probleme.push(`ragt ${z.ueber}px heraus`); if (z.laenge < 200 && pfad !== "/404.html") probleme.push(`fast kein Inhalt (${z.laenge})`); if (!z.titel) probleme.push("kein Seitentitel"); ok(!probleme.length, `${pfad.padEnd(28)} ${probleme.join(" · ")}`); if (probleme.length) befunde.push({ name, pfad, was: probleme.join(" · ") }); } await kontext.close(); } /* ---------- 1. Rechner ---------------------------------------------- */ await rundgang("RECHNER (1440 px)", { viewport: { width: 1440, height: 950 } }, SEITEN); /* ---------- 2. Handy ------------------------------------------------- */ await rundgang("HANDY (390 px, Finger)", { viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true, deviceScaleFactor: 3, userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 " + "(KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1", }, SEITEN); /* ---------- 3. Installiert ------------------------------------------- */ /* Als App vom Startbildschirm: kein Adressfeld, kein Zurück-Knopf des Browsers. Wer sich dort verläuft, kommt nicht mehr heraus -- deshalb ist diese Gestalt die unbarmherzigste. */ console.log("\n===== INSTALLIERT (App-Ansicht) ====="); const app = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true, deviceScaleFactor: 3, }); const appSeite = await app.newPage(); /* ERST auf die Domain gehen, DANN das Manifest holen. Beim ersten Lauf stand die Probe noch auf about:blank und holte von dort -- das ist für den Browser eine fremde Herkunft, und er lehnt ab. Gemeldet wurde "manifest.json nicht ladbar", während curl gleichzeitig 200 lieferte. Die Prüfung hat also nicht die Seite geprüft, sondern sich selbst. */ await appSeite.goto(BASIS + "/", { waitUntil: "domcontentloaded", timeout: 40000 }); const manifest = await appSeite.evaluate(async (basis) => { const a = await fetch(basis + "/manifest.json"); if (!a.ok) return { fehler: a.status }; return a.json(); }, BASIS).catch(() => ({ fehler: "nicht ladbar" })); ok(!manifest.fehler, `manifest.json wird ausgeliefert${manifest.fehler ? " (" + manifest.fehler + ")" : ""}`); ok(!!manifest.name || !!manifest.short_name, `die App hat einen Namen ("${manifest.short_name || manifest.name}")`); ok(manifest.display === "standalone" || manifest.display === "fullscreen", `sie startet ohne Browserleiste (display: ${manifest.display})`); ok(!!manifest.start_url, `sie weiss, wo sie startet (${manifest.start_url})`); ok(Array.isArray(manifest.icons) && manifest.icons.length > 0, `sie hat ein Symbol für den Startbildschirm (${manifest.icons?.length || 0})`); /* Jedes Symbol muss wirklich da sein -- ein fehlendes fällt erst auf, wenn es als graues Viereck auf dem Startbildschirm klebt. */ for (const symbol of (manifest.icons || []).slice(0, 4)) { const pfad = symbol.src.startsWith("http") ? symbol.src : BASIS + "/" + symbol.src.replace(/^\//, ""); const status = await appSeite.evaluate(async (u) => { try { const a = await fetch(u); return a.status; } catch { return 0; } }, pfad); ok(status === 200, ` Symbol ${symbol.sizes || ""} vorhanden (${status})`); } await appSeite.goto(BASIS + (manifest.start_url || "/"), { waitUntil: "networkidle", timeout: 40000 }); await appSeite.waitForTimeout(1500); const dienst = await appSeite.evaluate(async () => { if (!("serviceWorker" in navigator)) return { da: false, grund: "nicht unterstützt" }; const r = await navigator.serviceWorker.getRegistration(); return { da: !!r, umfang: r?.scope || null, aktiv: !!r?.active }; }); ok(dienst.da, `der Hintergrunddienst ist angemeldet${dienst.umfang ? " (" + dienst.umfang + ")" : ""}`); ok(dienst.aktiv, "und aktiv – die App startet auch ohne Netz"); const appStand = await appSeite.evaluate(() => ({ ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth, laenge: (document.body.innerText || "").trim().length, /* In der App gibt es keinen Zurück-Knopf des Browsers. Es MUSS also auf der Seite selbst ein Weg zurück oder ein Menü geben. */ wege: document.querySelectorAll("a[href]").length, })); ok(appStand.ueber <= 0, `nichts ragt seitlich heraus (${appStand.ueber}px)`); ok(appStand.laenge > 200, `die Startseite hat Inhalt (${appStand.laenge} Zeichen)`); ok(appStand.wege > 3, `es gibt Wege weiter (${appStand.wege} Verweise) – in der App gibt es keinen Browser-Zurück`); await appSeite.screenshot({ path: "abnahme-installiert.png", fullPage: false }); await app.close(); /* ---------- Abschluss ------------------------------------------------ */ console.log(""); const ERWARTET = SEITEN.length * 2 + 12; ok(geprueft >= ERWARTET, `es sind ${geprueft} Prüfungen gelaufen (erwartet mindestens ${ERWARTET})`); if (befunde.length) { console.log("\n--- WAS MORGEN AUFFALLEN WÜRDE ---"); for (const b of befunde) console.log(` ${b.name} · ${b.pfad}\n ${b.was}`); } await browser.close(); console.log(`\n${fehler ? "FEHLER: " + fehler : "Alles in Ordnung"} — ${geprueft} Prüfungen`); process.exit(fehler ? 1 : 0);