diff --git a/pruef-musik.mjs b/pruef-musik.mjs new file mode 100644 index 00000000..dddac3d7 --- /dev/null +++ b/pruef-musik.mjs @@ -0,0 +1,154 @@ +/* ===================================================================== + Musik-Knopf unten rechts — läuft gegen den ECHTEN Server + + WARUM NICHT GEGEN EINEN SIMPLEN DATEI-SERVER + + Der Fehler, um den es hier geht, stand in keiner HTML-Datei und in + keiner Zeile JavaScript: Die Seite war fehlerfrei, aber der Server + schickte "frame-src 'none'" mit — und der Browser warf die beiden + Spotify-Player still weg (gemeldet 27.08.2026). Ein Test, der die + Dateien nur ausliefert, hätte das nie gesehen, weil er die Kopfzeile + gar nicht erst erzeugt. + + Deshalb startet dieser Test server/index.js selbst auf einem eigenen + Port und prüft im echten Browser, ob die Player wirklich ankommen. + + Geprüft wird: + 1. Der Knopf ist da und groß genug zum Antippen. + 2. Klick klappt das Feld auf, beide Player werden nachgeladen. + 3. Die Rahmen laden WIRKLICH von open.spotify.com (nicht Fehlerseite). + 4. Kein einziger Verstoß gegen die Inhaltsrichtlinie in der Konsole. + 5. Die Richtlinie bleibt eng: nur open.spotify.com, kein Sternchen. + 6. Schließen über ✕ und über Escape. + 7. Am Handy passt das Feld in den Bildschirm. + ===================================================================== */ +import { chromium } from "playwright"; +import { spawn } from "node:child_process"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const HIER = dirname(fileURLToPath(import.meta.url)); +const PORT = 4433; +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", "pipe", "pipe"], +}); + +async function warteAufServer() { + for (let i = 0; i < 40; i++) { + try { + const res = await fetch(BASIS + "/index.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") || ""; + + console.log("\n== Inhaltsrichtlinie =="); + const frameRegel = (richtlinie.split(";").find((t) => t.trim().startsWith("frame-src")) || "").trim(); + pruefe("frame-src erlaubt Spotify", /https:\/\/open\.spotify\.com/.test(frameRegel), frameRegel || "(keine frame-src-Regel)"); + pruefe("frame-src bleibt eng (kein *)", !/[\s:]\*/.test(frameRegel) && !/'unsafe/.test(frameRegel), frameRegel); + + const browser = await chromium.launch(); + + /* ---------- Computer ---------- */ + console.log("\n== Computer =="); + const seite = await browser.newPage({ viewport: { width: 1366, height: 800 } }); + const verstoesse = new Set(); + seite.on("console", (m) => { + const text = m.text(); + if (/Content Security Policy|violates the following/i.test(text)) verstoesse.add(text.replace(/\s+/g, " ").slice(0, 160)); + }); + await seite.goto(BASIS + "/index.html", { waitUntil: "domcontentloaded" }); + await seite.waitForSelector("#music-toggle", { timeout: 10000 }); + + const knopf = await seite.evaluate(() => { + const r = document.getElementById("music-toggle").getBoundingClientRect(); + return { breite: Math.round(r.width), hoehe: Math.round(r.height), rechts: Math.round(innerWidth - r.right), unten: Math.round(innerHeight - r.bottom) }; + }); + pruefe("Knopf ist groß genug zum Antippen", knopf.breite >= 44 && knopf.hoehe >= 44, knopf.breite + "x" + knopf.hoehe + "px"); + pruefe("Knopf sitzt unten rechts", knopf.rechts >= 0 && knopf.rechts < 80 && knopf.unten >= 0 && knopf.unten < 80, "Abstand rechts " + knopf.rechts + "px, unten " + knopf.unten + "px"); + + pruefe("Feld ist zu Beginn zu", await seite.evaluate(() => document.getElementById("music-panel").hidden), ""); + + await seite.click("#music-toggle"); + await seite.waitForTimeout(500); + const nachKlick = await seite.evaluate(() => { + const p = document.getElementById("music-panel"); + return { + offen: !p.hidden, + quellen: [...p.querySelectorAll("iframe")].map((f) => f.getAttribute("src") || ""), + nochUngeladen: p.querySelectorAll("iframe[data-src]").length, + }; + }); + pruefe("Feld klappt auf", nachKlick.offen, ""); + pruefe("Beide Player werden nachgeladen", nachKlick.quellen.length === 2 && nachKlick.quellen.every((q) => q.startsWith("https://open.spotify.com/embed/artist/")), nachKlick.quellen.length + " Player"); + pruefe("Kein Player bleibt ungeladen", nachKlick.nochUngeladen === 0, nachKlick.nochUngeladen + " ohne src"); + + /* Der eigentliche Punkt: Kommt der Rahmen wirklich an? Wird er von der + Richtlinie geblockt, steht in seiner Adresse chrome-error://. */ + await seite.waitForTimeout(6000); + const rahmen = seite.frames().map((f) => f.url()); + const spotifyRahmen = rahmen.filter((u) => u.startsWith("https://open.spotify.com/")); + const kaputteRahmen = rahmen.filter((u) => u.startsWith("chrome-error://")); + pruefe("Beide Spotify-Rahmen sind wirklich geladen", spotifyRahmen.length === 2, spotifyRahmen.length + " geladen: " + spotifyRahmen.map((u) => u.slice(38, 60)).join(", ")); + pruefe("Kein Rahmen ist eine Fehlerseite", kaputteRahmen.length === 0, kaputteRahmen.length + " Fehlerseiten"); + pruefe("Kein Verstoß gegen die Inhaltsrichtlinie", verstoesse.size === 0, [...verstoesse].join(" | ") || "keine"); + + await seite.screenshot({ path: "bild-musik-computer.png" }); + + /* ---------- Schließen ---------- */ + console.log("\n== Schließen =="); + await seite.click("#music-close"); + await seite.waitForTimeout(300); + pruefe("✕ schließt das Feld", await seite.evaluate(() => document.getElementById("music-panel").hidden), ""); + await seite.click("#music-toggle"); + await seite.waitForTimeout(300); + await seite.keyboard.press("Escape"); + await seite.waitForTimeout(300); + pruefe("Escape schließt das Feld", await seite.evaluate(() => document.getElementById("music-panel").hidden), ""); + + /* ---------- Handy ---------- */ + console.log("\n== Handy (390x844) =="); + const handy = await browser.newPage({ viewport: { width: 390, height: 844 } }); + await handy.goto(BASIS + "/index.html", { waitUntil: "domcontentloaded" }); + await handy.waitForSelector("#music-toggle", { timeout: 10000 }); + await handy.click("#music-toggle"); + await handy.waitForTimeout(4000); + const h = await handy.evaluate(() => { + const r = document.getElementById("music-panel").getBoundingClientRect(); + return { links: Math.round(r.left), rechts: Math.round(innerWidth - r.right), oben: Math.round(r.top), unten: Math.round(r.bottom), + fensterHoehe: innerHeight, ueberlauf: document.documentElement.scrollWidth - document.documentElement.clientWidth }; + }); + pruefe("Feld passt seitlich in den Bildschirm", h.links >= 0 && h.rechts >= 0, "links " + h.links + "px, rechts " + h.rechts + "px"); + pruefe("Feld ragt nicht oben heraus", h.oben >= 0, "oben " + h.oben + "px"); + pruefe("Seite läuft nicht seitlich über", h.ueberlauf <= 1, h.ueberlauf + "px"); + const handyRahmen = handy.frames().filter((f) => f.url().startsWith("https://open.spotify.com/")).length; + pruefe("Am Handy laden die Player auch", handyRahmen === 2, handyRahmen + " geladen"); + await handy.screenshot({ path: "bild-musik-handy.png" }); + + await browser.close(); + server.kill(); + console.log("\n===== " + ok + " ok, " + fehl + " Fehler ====="); + if (funde.length) { console.log("\nOffene Punkte:"); funde.forEach((f) => console.log(" - " + f)); } + process.exit(fehl ? 1 : 0); +})().catch((e) => { + console.error("Testlauf abgebrochen:", e.message); + server.kill(); + process.exit(1); +}); diff --git a/server/inhaltsrichtlinie.js b/server/inhaltsrichtlinie.js index 69f60230..9979ca51 100644 --- a/server/inhaltsrichtlinie.js +++ b/server/inhaltsrichtlinie.js @@ -186,7 +186,31 @@ export function inhaltsrichtlinie(wurzel) { "form-action 'self'", /* Ersetzt X-Frame-Options; wirkt gegen Klickfallen. */ "frame-ancestors 'self'", - "frame-src 'none'", + /* ⚠️ HIER STAND "frame-src 'none'" — DAS HAT DEN MUSIK-KNOPF + STILLGELEGT (gemeldet 27.08.2026). + + Der Knopf unten rechts klappt ein Feld mit zwei Spotify-Playern + auf (Hasidog und Van-Van, siehe main.js). Genau die sind +