Files
dogfather-universe/server/pruef-live-abnahme.mjs
T
DogFatherGitandClaude Opus 5 80639d0bab Abnahme vor der Vorstellung: eine tote Kachel gefunden und geschlossen
Morgen wird die Seite als Produkt vorgestellt. Deshalb eine Pruefung,
die es bisher nicht gab: server/pruef-live-abnahme.mjs geht gegen die
ECHTE Domain statt gegen einen Server auf diesem Rechner.

Der Unterschied ist nicht theoretisch. Zwischen "laeuft bei mir" und
"laeuft 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 haengengeblieben, das lokal tadellos war.

Sie geht 26 Seiten in DREI Gestalten durch:
  RECHNER      1440 px
  HANDY        390 px, mit Fingerbedienung
  INSTALLIERT  als App vom Startbildschirm -- ohne Adresszeile und ohne
               Zurueck-Knopf des Browsers. Dort faellt auf, was man mit
               Browserleiste nie merkt.

Gemessen je Seite: Konsolenfehler, fehlgeschlagene Anfragen, seitlicher
Ueberlauf, tote Verweise, fehlende Bilder, ueberhaupt Inhalt, Titel.
Dazu die App selbst: Manifest, Name, Start ohne Browserleiste, alle vier
Startbildschirm-Symbole einzeln abgefragt, Hintergrunddienst angemeldet
und aktiv.

DER EINE ECHTE FUND: Auf links.html war die Kachel "Merchandise / Shop"
ein <a href="#"> -- sie sah aus wie ein Verweis, liess sich anklicken
und tat nichts. Auf der Kachel steht "Folgt in Kuerze"; genau dann darf
sie nicht klickbar sein. Ein Knopf, der nichts tut, ist schlimmer als
gar keiner: Man drueckt ihn zweimal und haelt die Seite fuer kaputt.
Jetzt ein <div> mit denselben Klassen -- gleiches Aussehen, kein
Versprechen mehr, das die Seite nicht halten kann.

ZWEI FEHLALARME, beide in MEINER Pruefung, beide aufgeschrieben:
  * Sie meldete "fehlende Bilder" auf zwei Seiten. Es waren versteckte
    Platzhalter (<img hidden> ohne src), die JavaScript erst fuellt --
    der Normalzustand. Jetzt zaehlen nur sichtbare Bilder mit Quelle.
  * Sie meldete "manifest.json nicht ladbar", waehrend curl gleichzeitig
    200 lieferte. Die Probe stand noch auf about:blank und holte von
    dort -- fremde Herkunft, der Browser lehnt ab. Sie hat also nicht
    die Seite geprueft, sondern sich selbst. Erst navigieren, dann
    messen.

Die beiden anderen href="#" auf der Seite (index.html, streamplan.html)
bleiben: Sie sind versteckt und werden von JavaScript gefuellt, sobald
es etwas zu verlinken gibt. Geprueft wird nur, was man auch sieht.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-03 09:48:38 +02:00

222 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
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 (<img hidden> 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);