/* ===================================================================== DIE KANALZEILE (17.09.2026) -- Schritt 3 aus dem Video-Plan Filipe: "fuer meinen Account, dogfather, dann dogfather clips und hasidog." Geprueft wird nicht nur, DASS die Zeile da ist: * Bei weniger als zwei Kanaelen darf sie NICHT erscheinen. Ein Filter, der nichts zu filtern hat, macht misstrauisch -- und wer einmal einen Knopf gedrueckt hat, bei dem nichts passiert, drueckt den naechsten nicht mehr. * Ein Klick muss wirklich weniger Karten uebrig lassen, und zwar die richtigen. * Ein zweiter Klick auf denselben Knopf hebt den Filter auf. * Kanal- und Art-Filter wirken ZUSAMMEN -- das war der Grund, den Kanal als zweite Dimension zu bauen statt als weiteren Wert in derselben Leiste. Aufrufen mit: node server/pruef-kanalzeile.mjs ===================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { createServer } from "node:http"; const ordner = mkdtempSync(join(tmpdir(), "ws-kz-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); const { eigenerPort } = await import("./helfer-port.mjs"); const PORT = await eigenerPort(import.meta, "das Bildwerkzeug"); const TIKTOK = await eigenerPort(import.meta, "nachgebautes TikTok", 1); process.env.PORT = `${PORT}`; process.env.SITE_ACCESS_SECRET = "lokaler-test"; process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; process.env.TIKTOK_OEMBED_BASIS = `http://127.0.0.1:${TIKTOK}/oembed`; /* DIE HANDLES WERDEN ABGELEITET, NICHT ABGESCHRIEBEN (21.09.2026). Hier standen sie als Text im Pruefcode -- und einer davon war falsch: "@hasidog00804" mit einer Null zu viel. Er stand genauso falsch in KANAELE, weshalb die Pruefung gruen war: Zwei Abschriften desselben Tippfehlers bestaetigen sich gegenseitig. Nachgemessen bei TikTok selbst gibt es nur "@hasidog0804" (65 Follower); die andere Adresse liefert "Couldn't find this account". Jetzt kommen sie aus KANAELE. Damit kann diese Datei den Wert gar nicht mehr eigenstaendig falsch haben -- sie misst den echten. */ const { KANAELE } = await import("./workspace.js"); const HANDLES = KANAELE.map((k) => k.handle.replace(/^@/, "")); const PNG = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", "base64"); const tiktok = createServer((q, a) => { const u = new URL(q.url, `http://127.0.0.1:${TIKTOK}`); if (u.pathname === "/oembed") { const ziel = u.searchParams.get("url") || ""; const wer = (ziel.match(/@([^/?]+)/) || [])[1] || ""; if (!wer) { a.writeHead(404); a.end("{}"); return; } const TITEL = ["Casper klaut die Fernbedienung 😄", "Casper schläft im Wäschekorb", "Der Chat entscheidet: Horror oder Koop?"]; const titel = TITEL[HANDLES.indexOf(wer)] || "Video"; a.writeHead(200, { "content-type": "application/json" }); a.end(JSON.stringify({ version: "1.0", type: "video", title: titel, author_url: `https://www.tiktok.com/@${wer}`, thumbnail_url: `http://127.0.0.1:${TIKTOK}/cover.jpg`, thumbnail_width: 576, thumbnail_height: 1024, })); return; } if (u.pathname === "/cover.jpg") { a.writeHead(200, { "content-type": "image/jpeg" }); a.end(PNG); return; } a.writeHead(404); a.end("{}"); }); await new Promise((r) => tiktok.listen(TIKTOK, "127.0.0.1", r)); const express = (await import("express")).default; const ec = express.response.cookie; express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; await import("./index.js"); await new Promise((r) => setTimeout(r, 1200)); const BASIS = `http://127.0.0.1:${PORT}`; const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes } = await import("node:crypto"); for (let i = 0; i < 60; i++) { try { const t = new DatabaseSync(process.env.WORKSPACE_DB); try { t.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { t.close(); } } catch { await new Promise((r) => setTimeout(r, 250)); } } const d = new DatabaseSync(process.env.WORKSPACE_DB); const j = new Date().toISOString(); const salt = randomBytes(16).toString("hex"); const hash = scryptSync("CODE-DOGI-0001", salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); d.prepare("INSERT INTO personen (name,rolle,code_hash,code_salt,code_n,aktiv,erstellt) VALUES (?,?,?,?,?,1,?)") .run("Filipe", "admin", hash, salt, 32768, j); d.close(); const an = await fetch(BASIS + "/workspace/api/anmelden", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" }), }); const keks = (an.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; "); async function video(handle, nr) { const a = await fetch(`${BASIS}/workspace/api/bereich/highlight/video`, { method: "POST", headers: { "Content-Type": "application/json", cookie: keks, origin: BASIS }, body: JSON.stringify({ url: `http://127.0.0.1:${TIKTOK}/@${handle}/video/${nr}` }), }); return a.status; } const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); const browser = await pw.chromium.launch(); const ctx = await browser.newContext({ viewport: { width: 1200, height: 900 }, deviceScaleFactor: 2 }); await ctx.addCookies(keks.split("; ").map((k) => { const [name, ...rest] = k.split("="); return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" }; })); const seite = await ctx.newPage(); const konsolenfehler = []; seite.on("console", (m) => { if (m.type() === "error") konsolenfehler.push(m.text()); }); seite.on("pageerror", (f) => konsolenfehler.push(String(f))); let fehler = 0, geprueft = 0; const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; const zeileDa = async () => !!(await seite.$(".filter__kanaele")); /* DER SELEKTOR HEISST .eintrag-karte -- nachgesehen in bereich.js (el("article", "eintrag-karte")), nicht geraten. Mein erster Versuch traf nichts, und die Messung meldete brav "0 -> 0": Das sah aus wie ein Ergebnis und war keins. BEIM BERICHTIGEN bin ich dann in die naechste Falle gelaufen und habe daraus "undefined -> undefined" gemacht: Das Patchskript benutzte String.replace, und dort bedeutet "$$" im ERSATZTEXT ein einzelnes "$". Aus seite.$$(...) wurde seite.$(...). Der Fehler steht woertlich in meinen eigenen Regeln -- und hat trotzdem zugeschlagen. Aufgefallen ist er nur, weil die Ausgabe gelesen wurde: "undefined" ist wenigstens ehrlich, "0" waere es nicht gewesen. */ const karten = async () => (await seite.$$(".eintrag-karte")).length; /* ---- 1. EIN Kanal: die Zeile darf NICHT da sein --------------------- */ console.log("\n=== 1. Mit nur einem Kanal ==="); console.log(" Video 1:", await video("dogfather0804", "101")); await seite.goto(BASIS + "/workspace/bereich.html?b=highlight", { waitUntil: "networkidle" }); await seite.waitForTimeout(1500); ok(!(await zeileDa()), "bei einem einzigen Kanal steht dort KEINE Filterzeile"); /* ---- 2. Drei Kanäle -------------------------------------------------- */ console.log("\n=== 2. Mit drei Kanaelen ==="); console.log(" Video 2:", await video(HANDLES[1], "102")); console.log(" Video 3:", await video(HANDLES[2], "103")); await seite.reload({ waitUntil: "networkidle" }); await seite.waitForTimeout(1800); ok(await zeileDa(), "sobald drei Kanaele da sind, erscheint sie"); const knoepfe = await seite.$$eval(".filter__kanal", (ns) => ns.map((n) => ({ text: n.textContent.trim().replace(/\s+/g, " "), kanal: n.dataset.kanal || "(alle)", gewaehlt: n.getAttribute("aria-pressed"), }))); for (const k of knoepfe) console.log(` ${k.gewaehlt === "true" ? "[x]" : "[ ]"} ${k.text}`); ok(knoepfe.length === 4, `vier Knoepfe: Alle Kanaele und die drei (${knoepfe.length})`); ok(knoepfe[0]?.gewaehlt === "true", "und „Alle Kanaele“ ist die Vorgabe"); const zeile = await seite.$(".filter__kanaele"); if (zeile) { const leiste = await seite.$("#filter"); await (leiste || zeile).scrollIntoViewIfNeeded(); await seite.waitForTimeout(400); await (leiste || zeile).screenshot({ path: "server/bild-kanalzeile.png" }); } /* ---- 3. Ein Klick filtert wirklich ----------------------------------- */ console.log("\n=== 3. Ein Klick ==="); const vorher = await karten(); ok(vorher === 3, `ungefiltert liegen drei Videos da (${vorher})`); const hasi = await seite.$('.filter__kanal[data-kanal="hasidog"]'); ok(!!hasi, "der HasiDog-Knopf ist da"); if (hasi) { await hasi.click(); await seite.waitForTimeout(1500); const nachher = await karten(); ok(nachher === 1, `nach einem Klick auf HasiDog bleibt eines uebrig (${nachher})`); /* UND ZWAR DAS RICHTIGE. "Weniger" allein waere auch erfuellt, wenn der Filter irgendetwas wegwirft -- das ist der Unterschied zwischen filtern und kaputt sein. */ const titel = await seite.$$eval(".eintrag-karte", /* /\s+/ und nicht /s+/ -- beim Erzeugen dieser Datei ist der Backslash durch eine Escaping-Ebene verloren gegangen, und der Ausdruck ersetzte danach JEDES "s" durch ein Leerzeichen. In der Ausgabe stand "Ca per chläft im Wä chekorb". Das war kein Fehler im Filter, sondern einer in der Pruefung -- und er hat sie rot gemacht, was genau richtig ist: Eine Pruefung, die ihren eigenen Text zerstoert, darf nicht gruen sein. */ (ns) => ns.map((n) => n.textContent.replace(/\s+/g, " ").trim())); for (const t of titel) console.log(" " + t.slice(0, 60)); ok(titel.length === 1 && /Wäschekorb/.test(titel[0]), "und es ist wirklich das HasiDog-Video"); const leiste2 = await seite.$("#filter"); if (leiste2) await leiste2.screenshot({ path: "server/bild-kanalzeile-gefiltert.png" }); /* Der gewaehlte Knopf sagt es auch. */ const gedrueckt = await seite.$$eval(".filter__kanal", (ns) => ns.filter((n) => n.getAttribute("aria-pressed") === "true") .map((n) => n.dataset.kanal || "(alle)")); ok(gedrueckt.length === 1 && gedrueckt[0] === "hasidog", `genau ein Knopf ist gewaehlt (${gedrueckt.join(", ")})`); /* ---- Und ein zweiter Klick hebt ihn auf --------------------------- Ohne das sucht man den Weg zurueck zu "Alle Kanaele" und findet ihn nicht, weil man nicht mehr weiss, dass man gefiltert hat. */ const nochmal = await seite.$('.filter__kanal[data-kanal="hasidog"]'); await nochmal.click(); await seite.waitForTimeout(1500); ok((await karten()) === vorher, `ein zweiter Klick hebt den Filter auf (wieder ${await karten()})`); } /* ---- 4. Kanal UND Art zusammen -------------------------------------- Der eigentliche Grund fuer zwei getrennte Reihen. Waere der Kanal nur ein weiterer Wert in derselben Leiste, koennte man das hier nicht fragen -- und die Pruefung waere trotzdem gruen geblieben. */ console.log("\n=== 4. Beide Filter zusammen ==="); { const df = await seite.$('.filter__kanal[data-kanal="dogfather"]'); await df.click(); await seite.waitForTimeout(1200); const nurKanal = await karten(); ok(nurKanal === 1, `nur DogFather: ${nurKanal}`); /* Jetzt zusaetzlich eine Art, die dieses Video NICHT hat. */ const arten = await seite.$$eval("#filter > .schritt", (ns) => ns.map((n) => n.textContent.trim())); console.log(" Arten in der ersten Reihe: " + arten.join(", ")); const bild = (await seite.$$("#filter > .schritt")) .find ? null : null; const alleKnoepfe = await seite.$$("#filter > .schritt"); let getroffen = false; for (const k of alleKnoepfe) { const t = (await k.textContent()).trim(); if (t !== "Bild") continue; await k.click(); await seite.waitForTimeout(1200); getroffen = true; ok((await karten()) === 0, `DogFather + Art "Bild" ergibt nichts (${await karten()}) -- beide Filter wirken`); break; } ok(getroffen, "die Art „Bild“ war zum Gegenprobieren da"); void bild; } console.log(""); ok(konsolenfehler.length === 0, konsolenfehler.length ? konsolenfehler.join(" | ") : "keine Fehler in der Browserkonsole"); console.log(""); console.log(`${geprueft} Pruefungen, ${fehler} Fehler`); console.log(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG"); await browser.close(); await new Promise((r) => tiktok.close(r)); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ } process.exit(fehler ? 1 : 0);