Wunsch 27.08.2026: "soll nur noch hochwertiger und professioneller
aussehen." Kein neues Design, sondern die Details, die den Unterschied
zwischen "gemacht" und "gesetzt" ausmachen:
- Ueberschrift enger gesetzt (-.022em, Zeilenabstand 1.08, text-wrap:
balance). Bei 4rem wirkt normale Laufweite auseinandergefallen; die
drei Zeilen verteilen sich jetzt gleichmaessig statt mit kurzer
Restzeile.
- Jede Welt-Kachel traegt die Farbe IHRER Welt statt dreimal derselben:
Babyblau, Silber, das gedeckte Rot von Spicy Media. Die Werte sind
nicht erfunden, sondern die --accent-Toene der drei Themendateien.
- Plaketten ("WELT 1") klein, in Versalien, weit gesperrt -- liest sich
als Kapitelmarke statt als Beschriftung.
- Event-Karte: Datum als gesperrte Versalzeile (ordnet sich dem Titel
unter), "Mehr erfahren" als ruhiger Knopf statt nacktem Textlink,
Haarlinie am Bild, Text auf 5 Zeilen begrenzt -- damit nicht die
Textlaenge aus der Verwaltung das Aussehen der Startseite bestimmt und
bei zwei Events beide Kacheln gleich hoch sind (geprueft: 572/572px).
ZWEI FUNDE BEIM PRUEFEN, BEIDE WICHTIGER ALS DER FEINSCHLIFF:
1. INHALTSRICHTLINIE UND ZEILENENDEN. Der Browser rechnet die Pruefsumme
eines Inline-Skripts NICHT ueber die Bytes der Datei, sondern ueber
den Text im Dokument -- und der HTML-Parser ersetzt beim Einlesen
jedes CR LF durch LF (HTML-Spezifikation, "preprocessing the input
stream"). Eine Datei mit Windows-Zeilenenden ergibt serverseitig also
eine ANDERE Summe, und die Seite fuehrt ihr eigenes Skript nicht mehr
aus: kein Live-Status, keine Events, nichts. Live war es unauffaellig
(Linux-Auscheckung hat LF), auf dem Windows-Rechner sofort tot
(core.autocrlf=true). inhaltsrichtlinie.js vereinheitlicht jetzt vor
dem Rechnen auf LF -- das ist die Summe, die der Browser wirklich
bildet, und macht die Richtlinie unabhaengig davon, mit welchem
Werkzeug eine Datei zuletzt gespeichert wurde.
2. TESTS MIT FESTEM PORT KOENNEN LUEGEN. Drei Server aus frueheren
Laeufen liefen noch. Neue Testlaeufe konnten ihren Port nicht belegen,
starben still -- und massen weiter gegen den ALTEN Code. Ergebnis
waren Fehlermeldungen zu einem laengst behobenen Fehler. pruef-musik,
pruef-kasse und pruef-startseite suchen sich jetzt einen freien Port.
Neuer Test pruef-startseite.mjs (19 Pruefungen) mit FESTEN Event-Daten:
Beim Pruefen kam einmal nichts vom Server, der Abschnitt blieb leer und
der Test haette "kein Fehler" gemeldet, obwohl er nichts gesehen hat.
Geprueft werden Maske am Artwork, genau eine Kachel bei einem Event,
volle Breite, saubere Textkuerzung auf ganze Zeilen, eigene Weltfarben,
zwei gleich hohe Kacheln bei zwei Events, Handy ohne Ueberlauf.
Alles gruen: startseite 19, musik 17, kasse 15, inhaltsrichtlinie 22,
handy 56.
Co-Authored-By: Claude Opus 5 <[email protected]>
196 lines
9.5 KiB
JavaScript
196 lines
9.5 KiB
JavaScript
/* =====================================================================
|
|
Kasse & Anmeldung gegen die Inhaltsrichtlinie (27.08.2026)
|
|
|
|
WORUM ES GEHT
|
|
|
|
Auf abonnieren.html kommen zwei Dinge von fremden Servern: die
|
|
PayPal-Kasse und die Google-Anmeldung. Beide werden erst NACH einer
|
|
Nutzerhandlung nachgeladen (Klick bzw. Anmeldung) und beide sind
|
|
derzeit nicht scharf geschaltet -- googleClientId und paypalClientId
|
|
stehen auf null. Ein Fehler dort fällt deshalb heute NICHT auf. Er
|
|
fällt erst an dem Tag auf, an dem Dogi seine echten Zugangsdaten
|
|
einträgt: Dann stünde an der Kasse ein leerer Bereich, ohne
|
|
Fehlermeldung, ohne Eintrag im Server-Protokoll.
|
|
|
|
Genau dieses Muster hat schon einmal zugeschlagen (Musik-Knopf,
|
|
26.-27.08.2026). Deshalb wird hier vorgezogen geprüft, statt zu warten.
|
|
|
|
WIE ECHT DIESER TEST IST
|
|
|
|
- Er startet den ECHTEN server/index.js, damit die echte Kopfzeile
|
|
ausgeliefert wird. Ein Datei-Server erzeugt sie gar nicht.
|
|
- Er lädt das ECHTE PayPal-SDK mit deren offizieller Testkennung
|
|
("client-id=test") und baut die Knöpfe wirklich auf.
|
|
- Er lädt die ECHTE Google-Bibliothek und rendert den Knopf wirklich.
|
|
- Verstöße werden nicht aus Konsolentext geraten, sondern über das
|
|
Ereignis "securitypolicyviolation" gezählt -- dort stehen Richtlinie
|
|
und blockierte Adresse maschinenlesbar drin.
|
|
|
|
Kein echter Zugangscode und kein echtes Geld sind beteiligt.
|
|
===================================================================== */
|
|
import { chromium } from "playwright";
|
|
import { spawn } from "node:child_process";
|
|
import { createServer } from "node:net";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const HIER = dirname(fileURLToPath(import.meta.url));
|
|
/* Freien Port suchen statt festen nehmen: Laeuft noch ein Server aus einem
|
|
frueheren Lauf, kann der neue seinen Port nicht belegen, stirbt still --
|
|
und der Test misst gegen den ALTEN Code. Genau das ist am 27.08.2026
|
|
passiert und hat fast zu einer Fehlersuche in die falsche Richtung
|
|
gefuehrt. */
|
|
const PORT = await new Promise((auf, ab) => {
|
|
const probe = createServer();
|
|
probe.on("error", ab);
|
|
probe.listen(0, "127.0.0.1", () => { const p = probe.address().port; probe.close(() => auf(p)); });
|
|
});
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
let ok = 0, fehl = 0;
|
|
const funde = [];
|
|
const pruefe = (name, gut, info) => {
|
|
if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + info : "")); }
|
|
else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); funde.push(name + " — " + info); }
|
|
};
|
|
|
|
const server = spawn(process.execPath, ["index.js"], {
|
|
cwd: join(HIER, "server"),
|
|
env: { ...process.env, PORT: String(PORT) },
|
|
stdio: "ignore",
|
|
});
|
|
const beenden = (code) => { try { server.kill(); } catch { /* schon weg */ } process.exit(code); };
|
|
/* Notbremse: PayPals render() wartet ohne echten Bezahlvorgang unbegrenzt.
|
|
Ohne diese Grenze bliebe der Test im Zweifel ewig hängen. */
|
|
const notbremse = setTimeout(() => { console.log("\nZEIT ABGELAUFEN — Testlauf abgebrochen."); beenden(1); }, 180000);
|
|
notbremse.unref();
|
|
|
|
async function warteAufServer() {
|
|
for (let i = 0; i < 40; i++) {
|
|
try {
|
|
const res = await fetch(BASIS + "/abonnieren.html");
|
|
if (res.ok) return res;
|
|
} catch { /* noch nicht oben */ }
|
|
await new Promise((r) => setTimeout(r, 250));
|
|
}
|
|
throw new Error("Server ist nicht hochgekommen");
|
|
}
|
|
|
|
(async () => {
|
|
const antwort = await warteAufServer();
|
|
const richtlinie = antwort.headers.get("content-security-policy") || "";
|
|
const regel = (name) => (richtlinie.split(";").find((t) => t.trim().startsWith(name + " ")) || "").trim();
|
|
|
|
console.log("\n== Was die Kopfzeile verspricht ==");
|
|
pruefe("script-src kennt PayPal", /www\.paypal\.com/.test(regel("script-src")), regel("script-src").slice(0, 120));
|
|
pruefe("script-src kennt Google", /accounts\.google\.com\/gsi\/client/.test(regel("script-src")), "");
|
|
pruefe("frame-src kennt PayPal (Betrieb + Übung)", /www\.paypal\.com/.test(regel("frame-src")) && /sandbox\.paypal\.com/.test(regel("frame-src")), regel("frame-src"));
|
|
pruefe("frame-src kennt Google", /accounts\.google\.com\/gsi\//.test(regel("frame-src")), "");
|
|
pruefe("style-src kennt Googles Stilvorlage", /accounts\.google\.com\/gsi\/style/.test(regel("style-src")), regel("style-src"));
|
|
pruefe("Musik bleibt erlaubt", /open\.spotify\.com/.test(regel("frame-src")), "");
|
|
pruefe("Kein Sternchen in frame-src", !/\*/.test(regel("frame-src")), regel("frame-src"));
|
|
pruefe("Kein 'unsafe-inline' bei Skripten", !/unsafe-inline/.test(regel("script-src")), "");
|
|
|
|
const browser = await chromium.launch();
|
|
const seite = await browser.newPage({ viewport: { width: 1280, height: 900 } });
|
|
await seite.goto(BASIS + "/abonnieren.html", { waitUntil: "domcontentloaded" });
|
|
await seite.evaluate(() => {
|
|
window.__verstoesse = [];
|
|
document.addEventListener("securitypolicyviolation", (e) =>
|
|
window.__verstoesse.push({ regel: e.violatedDirective, blockiert: e.blockedURI || "(inline)" })
|
|
);
|
|
});
|
|
|
|
/* ---------- PayPal ---------- */
|
|
console.log("\n== PayPal-Kasse (offizielle Testkennung) ==");
|
|
const paypal = await seite.evaluate(async () => {
|
|
const geladen = await new Promise((res) => {
|
|
const sk = document.createElement("script");
|
|
sk.src = "https://www.paypal.com/sdk/js?client-id=test¤cy=EUR";
|
|
sk.onload = () => res(true);
|
|
sk.onerror = () => res(false);
|
|
document.body.appendChild(sk);
|
|
setTimeout(() => res(false), 20000);
|
|
});
|
|
if (!geladen || !window.paypal) return { geladen, knoepfe: false };
|
|
const ziel = document.createElement("div");
|
|
ziel.id = "kassen-test";
|
|
ziel.style.width = "320px";
|
|
document.body.appendChild(ziel);
|
|
/* Absichtlich NICHT abwarten: render() loest ohne echten Bezahlvorgang
|
|
nie auf. Es geht nur darum, ob der Rahmen entstehen darf. */
|
|
try { window.paypal.Buttons({}).render("#kassen-test").catch(() => {}); } catch { /* egal */ }
|
|
await new Promise((r) => setTimeout(r, 9000));
|
|
const rahmen = ziel.querySelector("iframe");
|
|
return { geladen, knoepfe: !!rahmen, hoehe: rahmen ? Math.round(rahmen.getBoundingClientRect().height) : 0 };
|
|
});
|
|
pruefe("PayPal-SDK wird geladen", paypal.geladen, paypal.geladen ? "geladen" : "BLOCKIERT");
|
|
pruefe("Bezahlknöpfe werden aufgebaut", paypal.knoepfe, paypal.knoepfe ? paypal.hoehe + "px hoch" : "kein Rahmen entstanden");
|
|
|
|
/* ---------- Google ---------- */
|
|
console.log("\n== Google-Anmeldung ==");
|
|
const google = await seite.evaluate(async () => {
|
|
const geladen = await new Promise((res) => {
|
|
const sk = document.createElement("script");
|
|
sk.src = "https://accounts.google.com/gsi/client";
|
|
sk.onload = () => res(true);
|
|
sk.onerror = () => res(false);
|
|
document.head.appendChild(sk);
|
|
setTimeout(() => res(false), 20000);
|
|
});
|
|
if (!geladen || !window.google) return { geladen, knopf: false };
|
|
const ziel = document.createElement("div");
|
|
ziel.id = "anmelde-test";
|
|
document.body.appendChild(ziel);
|
|
try {
|
|
window.google.accounts.id.initialize({
|
|
client_id: "000000000000-testtesttesttesttesttest.apps.googleusercontent.com",
|
|
callback: () => {},
|
|
});
|
|
window.google.accounts.id.renderButton(ziel, { type: "standard" });
|
|
} catch { return { geladen, knopf: false }; }
|
|
await new Promise((r) => setTimeout(r, 5000));
|
|
return { geladen, knopf: ziel.children.length > 0 };
|
|
});
|
|
pruefe("Google-Bibliothek wird geladen", google.geladen, google.geladen ? "geladen" : "BLOCKIERT");
|
|
pruefe("Anmelde-Knopf wird aufgebaut", google.knopf, google.knopf ? "gerendert" : "leer geblieben");
|
|
const googleRahmen = seite.frames().filter((f) => f.url().startsWith("https://accounts.google.com/gsi/")).length;
|
|
pruefe("Googles Rahmen darf laden", googleRahmen >= 1, googleRahmen + " Rahmen");
|
|
|
|
/* ---------- Das eigentliche Urteil ---------- */
|
|
console.log("\n== Verstöße gegen die Inhaltsrichtlinie ==");
|
|
const verstoesse = await seite.evaluate(() => window.__verstoesse);
|
|
const gezaehlt = {};
|
|
for (const v of verstoesse) {
|
|
const schluessel = v.regel + " <- " + v.blockiert.split("?")[0];
|
|
gezaehlt[schluessel] = (gezaehlt[schluessel] || 0) + 1;
|
|
}
|
|
Object.entries(gezaehlt).forEach(([k, n]) => console.log(" " + n + "x " + k));
|
|
pruefe("Kein einziger Verstoß", verstoesse.length === 0, verstoesse.length + " Verstöße");
|
|
|
|
const fehlerRahmen = seite.frames().filter((f) => f.url().startsWith("chrome-error://")).length;
|
|
pruefe("Kein Rahmen endet als Fehlerseite", fehlerRahmen === 0, fehlerRahmen + " Fehlerseiten");
|
|
|
|
/* Bilder von genau den beiden Testelementen.
|
|
|
|
WICHTIG: Die Elemente werden dafür NICHT im Seitenbaum verschoben.
|
|
PayPal baut seine Knöpfe wieder ab, sobald ihr Rahmen umgehängt wird
|
|
— ein Bild davon zeigte dann fälschlich eine leere Kasse, obwohl sie
|
|
kurz zuvor sauber stand. */
|
|
for (const [wahl, datei] of [["#kassen-test", "bild-kasse-paypal.png"], ["#anmelde-test", "bild-kasse-google.png"]]) {
|
|
try {
|
|
await seite.locator(wahl).scrollIntoViewIfNeeded();
|
|
await seite.waitForTimeout(600);
|
|
await seite.locator(wahl).screenshot({ path: datei });
|
|
} catch { /* Bild ist Beiwerk, kein Prüfkriterium */ }
|
|
}
|
|
await browser.close();
|
|
clearTimeout(notbremse);
|
|
console.log("\n===== " + ok + " ok, " + fehl + " Fehler =====");
|
|
if (funde.length) { console.log("\nOffene Punkte:"); funde.forEach((f) => console.log(" - " + f)); }
|
|
beenden(fehl ? 1 : 0);
|
|
})().catch((e) => {
|
|
console.error("Testlauf abgebrochen:", e.message);
|
|
beenden(1);
|
|
});
|