Die Kanalzeile -- und der Kasten, der auf dem Chili-Motiv stand

SCHRITT 3 AUS DEM VIDEO-PLAN: DIE KANALZEILE
Filipe: "fuer meinen Account, dogfather, dann dogfather clips und
hasidog." Sobald Videos aus drei Kanaelen nebeneinander liegen, ist die
erste Frage nicht "welche Art", sondern "von welchem Kanal".

ALS ZWEITE DIMENSION, nicht als weiterer Wert in derselben Leiste. Der
vorhandene Filter ist EIN Wert (Alle / Nur offene / die Arten); wer den
Kanal dort hineinpresst, kann "nur offene Clips von HasiDog" nicht mehr
fragen. Zwei getrennte Reihen zeigen ausserdem, dass es zwei Fragen
sind -- in einer Zeile klickt man das zweite und wundert sich, warum
das erste wieder aus ist.

SIE ERSCHEINT NUR AB ZWEI KANAELEN, und die Kanaele werden aus den
vorhandenen Eintraegen abgeleitet statt aufgezaehlt. Auf einem Brett
ohne Videos steht keine leere Leiste; bei einem Kanal steht kein
Filter, der nichts tut. Ein Knopf, bei dem nichts passiert, kostet das
Vertrauen in den naechsten.

Die drei Toene sind dieselben wie am Zeichen auf der Karte -- wer
"DogFather Clips" in Tuerkis filtert, findet darunter Karten mit
tuerkisem Zeichen. Ungewaehlt bleiben sie gedeckt: drei bunte Knoepfe
nebeneinander waeren eine Ampel, und eine Ampel sagt "gut/schlecht",
wo nur "welcher Kanal" gemeint ist.

DER KASTEN, DER AUF DEM CHILI-MOTIV STAND
Der leere Zustand der Talente-Seite von heute Vormittag hatte nur einen
gestrichelten Rand. Solange dort EIN Satz stand, ging das; jetzt stehen
dort drei Absaetze, fuenf Merkmale und zwei Knoepfe -- und das
Spicy-Media-Motiv mit Chilischoten und Neonlicht schien voll durch den
Text. Lesbar war das nicht, augenschonend erst recht nicht.

DIE WARNUNG STAND 70 ZEILEN WEITER UNTEN in derselben Datei, bei
.t-fuss, und beschreibt exakt diesen Fehler ("Sie stand als einziger
Text der Seite direkt auf der Buehne"). Ich habe sie gelesen und bin
trotzdem hineingelaufen. Ein Kommentar, der vor einem Fehler warnt,
verhindert ihn nicht -- gefunden hat es erst das Bildschirmfoto.

GESEHEN STATT GEZAEHLT (server/bild-stufen.mjs)
Stufenleiter und Talente-Seite waren heute Vormittag nur an Zahlen
geprueft. Das Bild zeigte: die Leiste sitzt sauber in einer Zeile
(1400 px) und in vier auf 412 px, nichts laeuft ueber den Rand, keine
Konsolenfehler -- und den Kasten ohne Grund.

DIE PRUEFUNG (server/pruef-kanalzeile.mjs, 14 Pruefungen)
Sie prueft nicht nur, DASS gefiltert wird, sondern dass das Richtige
uebrig bleibt: "weniger" allein waere auch erfuellt, wenn der Filter
irgendetwas wegwirft. Dazu der zweite Klick (hebt auf) und Abschnitt 4:
Kanal UND Art zusammen -- der eigentliche Grund fuer zwei Reihen.

ZWEI GEGENPROBEN:
  a) Kanalfilter ausgebaut       -> 3 Pruefungen rot
  b) Zeile immer anzeigen        -> 1 rot, genau der Ein-Kanal-Fall

UND ZWEI FEHLER IN DER PRUEFUNG SELBST, beide von mir:
  * Der Kartenselektor traf nichts, und die Messung meldete "0 -> 0".
    Das sah aus wie ein Ergebnis und war keins.
  * Beim Berichtigen machte String.replace aus "$$" ein "$" --
    seite.$$() wurde zu seite.$(). Der Fehler steht woertlich in den
    Hausregeln und hat trotzdem zugeschlagen. Aufgefallen ist er nur,
    weil "undefined" in der Ausgabe stand; "0" waere durchgegangen.
  * Und eine verlorene Escaping-Ebene machte aus /\s+/ ein /s+/ --
    die Pruefung ersetzte jedes "s" durch ein Leerzeichen und las
    "Ca per chläft im Wä chekorb". Sie wurde rot, und das war richtig.

Stempel 202609171407.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-17 14:08:15 +02:00
co-authored by Claude Opus 5
parent 8f0a184b9f
commit d69eb34448
35 changed files with 903 additions and 362 deletions
+153
View File
@@ -0,0 +1,153 @@
/* Bild der Stufenleiter und der Talente-Seite (17.09.2026)
Beides wurde heute gebaut und bisher nur an Zahlen geprueft. Die
Hausregel dazu ist eindeutig: Ein gruener Haken sagt nicht, wie es
aussieht. Der Startkatalog hat am selben Tag den Countdown von
458 px auf 1488 px geschoben, ohne dass eine einzige Pruefung rot
wurde -- gesehen habe ich es im Bildschirmfoto.
Aufrufen mit: node server/bild-stufen.mjs
*/
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const ordner = mkdtempSync(join(tmpdir(), "ws-bild-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
const { portMussFreiSein } = await import("./helfer-port.mjs");
await portMussFreiSein(4331, "das Bildwerkzeug");
process.env.PORT = "4331";
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
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:4331";
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes } = await import("node:crypto");
for (let i = 0; i < 60; i++) {
try {
const p = new DatabaseSync(process.env.WORKSPACE_DB);
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
} catch { await new Promise((r) => setTimeout(r, 250)); }
}
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const j = new Date().toISOString();
function anlegen(name, rolle, code) {
const salt = randomBytes(16).toString("hex");
const hash = scryptSync(code, 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(name, rolle, hash, salt, 32768, j);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
anlegen("Filipe", "admin", "CODE-DOGI-0001");
anlegen("Diene", "modi", "CODE-MODI-0001");
d.close();
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: 1400, height: 1000 }, deviceScaleFactor: 2 });
const seite = await ctx.newPage();
const fehlerAufSeite = [];
seite.on("console", (m) => { if (m.type() === "error") fehlerAufSeite.push(m.text()); });
seite.on("pageerror", (f) => fehlerAufSeite.push(String(f)));
await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
await seite.click('.rolle[data-rolle="admin"]');
await seite.fill("#code", "CODE-DOGI-0001");
await seite.click("button[type=submit]");
await seite.waitForURL("**/start.html", { timeout: 25000 });
/* ---- 1. Die Entwicklungsseite mit der Stufenleiter ------------------- */
await seite.goto(BASIS + "/workspace/entwicklung.html", { waitUntil: "networkidle" });
await seite.waitForTimeout(1800);
/* Eine Person waehlen, damit die Karte mit den Bloecken erscheint. */
const person = await seite.$(".e-person, .p-kachel, [data-person-id]");
if (person) { await person.click(); await seite.waitForTimeout(1500); }
const leiste = await seite.$(".e-stufen");
console.log("\n=== 1. Die Stufenleiter ===");
console.log(" vorhanden:", !!leiste);
if (leiste) {
const knoepfe = await seite.$$eval(".e-stufen__knopf", (ns) => ns.map((n) => ({
text: n.textContent.trim().replace(/\s+/g, " "),
breite: Math.round(n.getBoundingClientRect().width),
gewaehlt: n.getAttribute("aria-pressed"),
})));
for (const k of knoepfe) console.log(` ${String(k.breite).padStart(4)} px ${k.gewaehlt === "true" ? "[x]" : "[ ]"} ${k.text}`);
/* WIE VIELE ZEILEN BRAUCHT DIE LEISTE? Bei sechs Knoepfen in einer
Leiste, die schon einen "Alle aufklappen"-Knopf enthaelt, ist das
die Frage. Gezaehlt ueber die verschiedenen Oberkanten -- nicht
ueber die Hoehe geteilt durch etwas. */
const zeilen = await seite.$$eval(".e-stufen__knopf",
(ns) => new Set(ns.map((n) => Math.round(n.getBoundingClientRect().top))).size);
console.log(" Zeilen:", zeilen);
await leiste.scrollIntoViewIfNeeded();
await seite.waitForTimeout(400);
const kasten = await seite.$(".e-bloecke__leiste");
await (kasten || leiste).screenshot({ path: "server/bild-stufenleiste.png" });
/* ---- Und nach einem Klick auf "Fortgeschritten" ------------------- */
const fort = await seite.$('.e-stufen__knopf[data-erwartung="fort"]');
if (fort) {
await fort.click();
await seite.waitForTimeout(1200);
const koepfe = await seite.$$eval(".e-block", (ns) => ns.map((n) => ({
name: n.querySelector(".e-block__name")?.textContent?.trim(),
stand: n.querySelector(".e-block__stand")?.textContent?.trim(),
})));
console.log("\n Nach Klick auf „Fortgeschritten“:");
for (const k of koepfe) console.log(` ${String(k.name).padEnd(28)} ${k.stand}`);
await seite.screenshot({ path: "server/bild-stufen-gefiltert.png", fullPage: false });
}
}
/* ---- 2. Die Talente-Seite, leer -------------------------------------- */
await seite.goto(BASIS + "/workspace/talente.html", { waitUntil: "networkidle" });
await seite.waitForTimeout(1800);
console.log("\n=== 2. Die Talente-Seite ===");
const leer = await seite.$(".t-leer");
console.log(" leerer Zustand:", !!leer);
if (leer) {
const merkmale = await seite.$$eval(".t-leer__merkmal",
(ns) => ns.map((n) => n.textContent.trim().replace(/\s+/g, " ")));
console.log(" Merkmalsgruppen:", merkmale.length);
for (const m of merkmale) console.log(" " + m);
const knoepfe = await seite.$$eval(".t-leer__knoepfe a",
(ns) => ns.map((n) => n.textContent.trim()));
console.log(" Wege:", knoepfe.join(" | "));
await leer.scrollIntoViewIfNeeded();
await seite.waitForTimeout(400);
await leer.screenshot({ path: "server/bild-talente-leer.png" });
}
/* ---- 3. Auf dem Handy ------------------------------------------------ */
const handy = await ctx.newPage();
await handy.goto(BASIS + "/workspace/entwicklung.html", { waitUntil: "networkidle" });
await handy.setViewportSize({ width: 412, height: 915 });
await handy.waitForTimeout(1500);
const p2 = await handy.$(".e-person, .p-kachel, [data-person-id]");
if (p2) { await p2.click(); await handy.waitForTimeout(1500); }
console.log("\n=== 3. Auf 412 px (haeufigste Android-Breite) ===");
const l2 = await handy.$(".e-stufen");
if (l2) {
const zeilen = await handy.$$eval(".e-stufen__knopf",
(ns) => new Set(ns.map((n) => Math.round(n.getBoundingClientRect().top))).size);
const ueber = await handy.$$eval(".e-stufen__knopf",
(ns) => ns.filter((n) => n.getBoundingClientRect().right > window.innerWidth).length);
console.log(" Zeilen:", zeilen, " ueber den Rand hinaus:", ueber);
const kasten2 = await handy.$(".e-bloecke__leiste");
await (kasten2 || l2).screenshot({ path: "server/bild-stufen-handy.png" });
} else { console.log(" keine Leiste gefunden"); }
console.log("\nFehler auf der Seite:", fehlerAufSeite.length ? fehlerAufSeite.join(" | ") : "keine");
await browser.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
process.exit(0);
+259
View File
@@ -0,0 +1,259 @@
/* =====================================================================
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 { portMussFreiSein } = await import("./helfer-port.mjs");
await portMussFreiSein(4332, "das Bildwerkzeug");
const TIKTOK = await portMussFreiSein(4502, "nachgebautes TikTok");
process.env.PORT = "4332";
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`;
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 = { "dogfather0804": "Casper klaut die Fernbedienung 😄",
"hasidog00804": "Casper schläft im Wäschekorb",
"dogis.modi.gang": "Der Chat entscheidet: Horror oder Koop?" }[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:4332";
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("hasidog00804", "102"));
console.log(" Video 3:", await video("dogis.modi.gang", "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);