/* ===================================================================== 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 { 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", "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); });