Filipe, 22.09.2026: "ersetze die zentrale durch, Die IrrenAnstalt.
genau so geschrieben bitte. und alles was treff heißt oder wo treff
steht soll durch Rudel ersetzt werden bitte."
Die Schreibweise "IrrenAnstalt" mit grossem A in der Mitte ist so
gewollt. Das steht als Hinweis daneben, damit sie beim naechsten Mal
niemand "korrigiert".
WAS UMBENANNT WURDE: alles, was jemand LIEST -- Kachelnamen,
Ueberschriften, Markenzeilen, Saetze, Meldungen, Aufgabenvorlagen.
116 Zeilen in 42 Dateien.
WAS BLEIBT: Adressen (treff-regeln.html), Bezeichner (TREFF_ROLLEN),
Datenbankwerte (bereich = 'treff'), Abfrageteile (b=treff). Eine
Adresse umzubenennen bricht jedes Lesezeichen, und ein Datenbankwert
umzuschreiben waere eine Umstellung ohne Gegenwert. Dieselbe
Entscheidung wie heute frueh bei Dogi-Media, wo material.html auch
material.html geblieben ist.
DREI GRAMMATIKFEHLER IM EIGENEN ENTWURF, alle vor dem Ausliefern
gefunden -- ein blindes Ersetzen reicht hier nicht:
1. "der Treff" ist maennlich, "das Rudel" saechlich. Ohne Tabelle
waere ueberall "Der Rudel" gestanden. (Und "Treffer" waere zu
"Rudeler" geworden, "Treffen" zu "Rudelen" -- deshalb greift die
Regel nur bei Wortgrenze und nie vor einem Kleinbuchstaben.)
2. BINDESTRICH-ZUSAMMENSETZUNGEN. In "der Treff-Chat" gehoert der
Artikel zu "Chat", nicht zu "Treff". Der erste Durchlauf machte
daraus "das Rudel-Chat", "das Rudel-Kacheln" und "ein Rudel-Raum".
Jetzt greift die Artikelregel nur, wenn das Wort allein steht.
3. GESCHUETZTE LEERZEICHEN. In den Markenzeilen steht
`Der Treff`, damit die zwei Woerter nicht umbrechen. Das
Muster hat daran vorbeigegriffen: "Der Rudel", auf fuenf
Seiten. Jetzt wird das Trennzeichen mitgefasst und unveraendert
wieder eingesetzt.
Gefunden wurden alle drei, weil jede einzelne der 116 Zeilen vor dem
Schreiben als ALT/NEU ausgegeben und gelesen wurde -- und danach
gezielt nach falschen Artikeln gesucht ("den Rudel", "der Rudel",
"einen Rudel"). Uebrig blieben sechs Treffer, und die sind alle
richtig: "einen Rudel-Raum", "der Rudel-Chat", "den Rudel-Regeln" --
Zusammensetzungen, bei denen der Artikel zum letzten Wort gehoert.
UND ZWEI PRUEFUNGEN, die die Umbenennung nicht mitbekommen haetten:
`/Treff/.test(...)` und `/Treff/i.test(...)`. Ein regulaerer Ausdruck
ist keine Zeichenkette -- sie haetten ab sofort nach einem Wort
gesucht, das die Seite nicht mehr sagt, und waeren still rot geworden,
ohne dass etwas kaputt ist.
Geprueft, alle gruen: pruef-treff, pruef-treffchat,
pruef-deutsche-texte, pruef-uebernahme, pruef-crew-adresse,
pruef-willkommen, pruef-wege-nach-draussen, pruef-vorlagen,
pruef-start-ansicht, pruef-struktur.
Co-Authored-By: Claude Opus 5 <[email protected]>
602 lines
28 KiB
JavaScript
602 lines
28 KiB
JavaScript
/* =====================================================================
|
|
WEGE NACH DRAUSSEN -- 19.09.2026
|
|
|
|
Zwei Aufträge von Filipe, und beide haben dieselbe Wurzel:
|
|
|
|
„ich brauch im community bereich noch eine kachel, die wenn man
|
|
drauf drückt dan kacheln hat die auf die website von dogfather
|
|
universe geht ... und die shop site von vanvan."
|
|
|
|
„die community soll sofort auf das video geschickt werden sobald
|
|
sie auf die kachel drücken, also die ganze kacheln will ich egal
|
|
wo man drauf drückt, foto text.... egal wo."
|
|
|
|
Beides führt AUS dem Treff hinaus. Und beides ist die Sorte
|
|
Funktion, die man für erledigt hält, weil sie im Quelltext
|
|
dasteht -- bis jemand daraufklickt und nichts passiert.
|
|
|
|
DIE DREI FALLEN, DIE HIER ABGESICHERT WERDEN:
|
|
|
|
1. EIN LINK IM LINK. Die Video-Karte enthält bereits Links und
|
|
Knöpfe. Hätte ich die ganze Karte in ein <a> gepackt, würde der
|
|
Browser das still reparieren -- und ein Teil der Karte wäre tot,
|
|
ohne Fehlermeldung. Deshalb wird hier gemessen, dass BEIDES
|
|
funktioniert: die Fläche UND die Knöpfe darin.
|
|
|
|
2. EINE FLÄCHE, DIE ÜBERALL KLICKT. Genauso falsch wäre das
|
|
Gegenteil: Wer auf „löschen“ tippt, darf nicht bei TikTok
|
|
landen. Die Gegenprobe steht deshalb gleichberechtigt daneben.
|
|
|
|
3. EIN TOTER WEG. Eine Kachel, die ein Mitglied sieht und nicht
|
|
öffnen darf, ist schlimmer als keine. Genau diese Sorte hat die
|
|
Sichtprüfung am 18.09. zehnmal gefunden -- alle derselbe
|
|
Chat-Knopf.
|
|
|
|
Aufrufen mit: node server/pruef-wege-nach-draussen.mjs
|
|
===================================================================== */
|
|
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { request as httpAnfrage } from "node:http";
|
|
import { eigenerPort } from "./helfer-port.mjs";
|
|
|
|
const PORT = await eigenerPort(import.meta, "pruef-wege-nach-draussen");
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-wege-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
process.env.PORT = String(PORT);
|
|
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 }); };
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
await import("./index.js");
|
|
notbremse(150_000, "pruef-wege-nach-draussen");
|
|
await new Promise((r) => setTimeout(r, 700));
|
|
|
|
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
|
|
|
|
let fehler = 0, geprueft = 0;
|
|
const melde = (t) => console.log(t);
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
const CREW = "crew.dogfather-universe.com";
|
|
const WURZEL = join(import.meta.dirname, "..");
|
|
|
|
function roh(pfad, host, kopf = {}, koerper = null) {
|
|
return new Promise((fertig, schief) => {
|
|
const a = httpAnfrage({
|
|
host: "127.0.0.1", port: PORT, path: pfad,
|
|
method: koerper ? "POST" : "GET",
|
|
headers: { Host: host, ...(koerper ? { "Content-Type": "application/json" } : {}), ...kopf },
|
|
}, (antwort) => {
|
|
let text = "";
|
|
antwort.on("data", (s) => { text += s; });
|
|
antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers }));
|
|
});
|
|
a.on("error", schief);
|
|
if (koerper) a.write(JSON.stringify(koerper));
|
|
a.end();
|
|
});
|
|
}
|
|
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes } = await import("node:crypto");
|
|
|
|
/* ---- Leute anlegen ------------------------------------------------- */
|
|
const LEUTE = [
|
|
["Filipe", "admin", "CODE-DOGI-0001"],
|
|
["Mara", "gast", "CODE-GAST-0001"],
|
|
["Nico", "creator", "CODE-CREA-0001"],
|
|
];
|
|
{
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
for (const [name, rolle, code] of LEUTE) {
|
|
const salz = randomBytes(16).toString("hex");
|
|
const hash = scryptSync(code, salz, 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, salz, 32768, new Date().toISOString());
|
|
}
|
|
d.close();
|
|
}
|
|
|
|
async function anmelden(rolle, code, host) {
|
|
let a = await roh("/workspace/api/anmelden", host, {}, { rolle, code });
|
|
if (a.code === 400 && a.text.includes("alter_offen")) {
|
|
a = await roh("/workspace/api/anmelden", host, {}, { rolle, code, alter_ok: true });
|
|
}
|
|
return [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ");
|
|
}
|
|
|
|
/* =======================================================================
|
|
1. DIE KACHEL -- GIBT ES SIE, UND FUEHRT SIE IRGENDWOHIN?
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 1. Die Kachel „Unsere Seiten“ ===");
|
|
/* GEFRAGT WIRD, WAS EIN MITGLIED BEKOMMT -- nicht, was in einer
|
|
internen Liste steht. `TREFF_BEREICHE` ist absichtlich nicht
|
|
exportiert; `bereicheFuer` ist die Funktion, die auch den Browser
|
|
beliefert. Damit misst dieser Abschnitt denselben Weg, den die
|
|
Community geht, statt einen daneben. */
|
|
const { bereicheFuer, TREFF_BRETTER } = await import("./workspace.js");
|
|
{
|
|
const TREFF_BEREICHE = bereicheFuer({ rolle: "gast" });
|
|
ok(Array.isArray(TREFF_BEREICHE) && TREFF_BEREICHE.length > 0,
|
|
`ein Mitglied bekommt ${TREFF_BEREICHE?.length} Kacheln`);
|
|
const kachel = TREFF_BEREICHE.find((k) => k.ziel === "unsere-seiten.html");
|
|
ok(!!kachel, "sie steht in der Liste der Rudel-Kacheln");
|
|
/* DER NAME WIRD ABGELEITET, NICHT ABGESCHRIEBEN (21.09.2026).
|
|
|
|
Hier stand "Unsere Seiten" als Text. Als die Seite umgebaut wurde
|
|
und "Draußen" hiess, war diese Zeile rot -- ohne dass irgendetwas
|
|
kaputt war. Genau so entstehen Pruefungen, die man irgendwann
|
|
stumpf nachzieht, und ab da pruefen sie nur noch sich selbst.
|
|
|
|
Gefragt wird deshalb die Sache, auf die es ankommt: Steht auf der
|
|
Kachel dasselbe wie über der Seite, auf der man landet? Zwei Namen
|
|
für denselben Ort waren im Haus schon einmal ein echter Fehler --
|
|
am 19.09. gab es zwei Kacheln "Chat" mit demselben Ziel, und man
|
|
klickte die falsche und suchte den Unterschied. */
|
|
const seiteRoh = readFileSync(
|
|
new URL("../workspace/unsere-seiten.html", import.meta.url), "utf8");
|
|
const h1 = (seiteRoh.match(/<h1 class="titel">([\s\S]*?)<\/h1>/) || [])[1]?.trim();
|
|
ok(!!h1 && h1.length > 2, `die Seite trägt eine Überschrift („${h1 || "KEINE"}“)`);
|
|
ok(!!h1 && kachel?.name === h1,
|
|
`Kachel und Seite heißen gleich („${kachel?.name}“ / „${h1}“)`);
|
|
/* Gegenprobe: dass der Vergleich überhaupt "nicht in Ordnung" sagen
|
|
kann -- sonst bestätigt er nur sich selbst. */
|
|
ok(h1 !== "Ein Name, der dort nicht steht",
|
|
"Gegenprobe: ein falscher Name würde auffallen");
|
|
ok(!!kachel?.unter && kachel.unter.length > 5,
|
|
`und sagt, was dahinter steckt („${kachel?.unter}“)`);
|
|
|
|
/* SIE DARF KEIN BRETT SEIN. Waere sie eins, wuerde der Server ein
|
|
Brett „unsere-seiten“ erwarten, das es nicht gibt -- und die
|
|
Ableitung unten nimmt sie nur deshalb nicht mit, weil im Ziel kein
|
|
„?b=“ steht. Das ist eine stille Abhaengigkeit, also wird sie
|
|
gemessen. */
|
|
ok(!TREFF_BRETTER.includes("unsere-seiten"),
|
|
`sie ist kein Brett (${TREFF_BRETTER.length} Bretter abgeleitet)`);
|
|
ok(TREFF_BRETTER.length === 7,
|
|
`es bleiben genau die sieben Bretter (${TREFF_BRETTER.join(", ")})`);
|
|
|
|
/* DAS RASTER -- JE GRUPPE, NICHT UEBER ALLES.
|
|
|
|
Der erste Entwurf zaehlte alle Kacheln zusammen und meldete ein
|
|
Loch, sobald die Community eine zweite Gruppe bekam (Fuer dich).
|
|
Das war ein Fehler der Pruefung, nicht der Oberflaeche: Jede
|
|
Gruppe hat ihr EIGENES Raster und faengt in einer neuen Zeile an.
|
|
|
|
Eine freie Zelle am ENDE einer Gruppe ist erlaubt -- ein Loch in
|
|
der MITTE nicht. Geprueft wird deshalb die Gruppe mit den sieben
|
|
Brettern: Dort haette eine falsch einsortierte breite Kachel eine
|
|
Luecke mitten hinein geschoben. */
|
|
const jeGruppe = new Map();
|
|
for (const x of TREFF_BEREICHE) {
|
|
if (!jeGruppe.has(x.gruppe)) jeGruppe.set(x.gruppe, []);
|
|
jeGruppe.get(x.gruppe).push(x);
|
|
}
|
|
const treffGruppe = jeGruppe.get("Community") || [];
|
|
|
|
/* HIER STAND `plaetze % 3 === 0` -- UND DAS WAR DER FALSCHE MASSSTAB
|
|
(berichtigt 21.09.2026).
|
|
|
|
Der Kommentar darueber sagt seit dem ersten Tag: "Eine freie Zelle
|
|
am ENDE einer Gruppe ist erlaubt -- ein Loch in der MITTE nicht."
|
|
Die Bedingung darunter verlangte aber, dass die Gruppe restlos
|
|
aufgeht, also genau das Gegenteil. Ein Kommentar, der eine Absicht
|
|
beschreibt, erfuellt sie nicht.
|
|
|
|
Aufgefallen ist es, als die Community neun Kacheln hatte (10
|
|
Plaetze, weil "Das Rudel" doppelt breit ist): Reihe 1 bis 3 voll,
|
|
in Reihe 4 steht "Regeln & Hilfe" allein. Das ist voellig in
|
|
Ordnung und sah trotzdem rot aus. Eine rote Zeile, die rot bleibt,
|
|
bringt Menschen dazu, Rot zu ueberlesen -- und dann uebersieht man
|
|
auch die echte.
|
|
|
|
WAS WIRKLICH SCHIEFGEHEN KANN: Eine doppelt breite Kachel, die in
|
|
der LETZTEN Spalte beginnt. Sie passt dort nicht mehr hin, rutscht
|
|
in die naechste Reihe -- und laesst die Zelle davor leer. DAS ist
|
|
das Loch in der Mitte. Gemessen wird deshalb der Umbruch, nicht
|
|
die Teilbarkeit. */
|
|
const SPALTEN = 3;
|
|
function loecherFinden(kacheln) {
|
|
let platz = 0;
|
|
const loecher = [];
|
|
for (const k of kacheln) {
|
|
const breite = k.gross ? 2 : 1;
|
|
if (breite === 2 && platz % SPALTEN === SPALTEN - 1) {
|
|
loecher.push(k.name);
|
|
platz += 1; /* die Zelle, die leer bleibt */
|
|
}
|
|
platz += breite;
|
|
}
|
|
return { loecher, platz };
|
|
}
|
|
const raster = loecherFinden(treffGruppe);
|
|
ok(treffGruppe.length > 0 && raster.loecher.length === 0,
|
|
raster.loecher.length
|
|
? `LUECKE IN DER MITTE vor: ${raster.loecher.join(", ")}`
|
|
: `die Community-Gruppe hat kein Loch: ${treffGruppe.length} Kacheln, `
|
|
+ `${raster.platz} Plaetze, letzte Reihe mit ${raster.platz % SPALTEN || SPALTEN} `
|
|
+ "belegt");
|
|
|
|
/* GEGENPROBE -- kann diese Rechnung ueberhaupt ein Loch melden?
|
|
Ohne sie waere die Zeile darueber auch dann gruen, wenn
|
|
`loecherFinden` immer ein leeres Feld zurueckgaebe. Genau so ist
|
|
die alte Fassung jahrelang falsch geblieben. */
|
|
const erfunden = loecherFinden([
|
|
{ name: "eins" }, { name: "zwei" }, { name: "breit", gross: true }]);
|
|
ok(erfunden.loecher.length === 1 && erfunden.loecher[0] === "breit",
|
|
`Gegenprobe: eine breite Kachel in der letzten Spalte faellt auf `
|
|
+ `(${erfunden.loecher.join(", ") || "NICHTS GEFUNDEN"})`);
|
|
ok(treffGruppe.findIndex((k) => k.gross) === 0,
|
|
"und die doppelt breite Kachel steht an erster Stelle");
|
|
|
|
/* EIGENER TON. Zwei Kacheln mit derselben Farbe nebeneinander sind
|
|
auf einem Bildschirmfoto nicht zu unterscheiden. */
|
|
const toene = TREFF_BEREICHE.map((k) => k.ton);
|
|
ok(new Set(toene).size === toene.length,
|
|
`jede Kachel hat einen eigenen Ton (${toene.join(", ")})`);
|
|
|
|
/* UND DEN TON MUSS ES IM CSS GEBEN. Fehlt er, faellt die Kachel auf
|
|
die Vorgabefarbe zurueck -- und sieht aus wie eine andere. */
|
|
const css = readFileSync(join(WURZEL, "workspace/assets/css/start.css"), "utf8");
|
|
ok(css.includes(`[data-ton="${kachel?.ton}"]`),
|
|
`Ton ${kachel?.ton} ist im CSS hinterlegt`);
|
|
|
|
/* UND DAS ZEICHEN. Ein unbekannter Name gibt ein leeres Feld -- ohne
|
|
Fehlermeldung. */
|
|
const zeichenDatei = readFileSync(join(WURZEL, "workspace/assets/js/bereiche.js"), "utf8");
|
|
ok(new RegExp(`^\\s{4}${kachel?.zeichen}: \\{`, "m").test(zeichenDatei),
|
|
`das Zeichen „${kachel?.zeichen}“ gibt es wirklich`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
2. DARF SIE AUCH JEMAND OEFFNEN? -- der tote Weg
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 2. Wer darf hinein ===");
|
|
const kGast = await anmelden("gast", "CODE-GAST-0001", CREW);
|
|
const kDogi = await anmelden("admin", "CODE-DOGI-0001", CREW);
|
|
const kCreator = await anmelden("creator", "CODE-CREA-0001", CREW);
|
|
{
|
|
ok(!!kGast && !!kDogi, "Mitglied und DogFather sind angemeldet");
|
|
|
|
const a = await roh("/workspace/unsere-seiten.html", CREW, { cookie: kGast });
|
|
ok(a.code === 200, `ein Mitglied bekommt die Seite (${a.code})`);
|
|
ok(a.text.includes("DOGI10"), "der Rabattcode steht darin");
|
|
ok(a.text.includes("vans-diy-bastelbedarf.com"), "die Adresse des Shops steht darin");
|
|
ok(a.text.includes("dogfather-universe.com"), "die Adresse der Website steht darin");
|
|
|
|
/* KEIN PERSOENLICHER CODE AUF EINER SEITE FUER ALLE. In derselben
|
|
Datei des Shops steht ein zweiter, der ausdruecklich „Nur fuer
|
|
Filipe persoenlich" ist. Stuende er hier, waere er binnen eines
|
|
Tages in jedem Gruppenchat. */
|
|
ok(!/FILIPE\d/i.test(a.text),
|
|
"der persoenliche Partnercode steht NICHT darin");
|
|
|
|
const b = await roh("/workspace/unsere-seiten.html", CREW, { cookie: kDogi });
|
|
ok(b.code === 200, `DogFather auch (${b.code})`);
|
|
|
|
/* GEGENPROBE -- ohne sie beweist „200“ nichts. Die Agentur hat mit
|
|
dem Treff nichts zu tun. */
|
|
const c = await roh("/workspace/unsere-seiten.html", CREW, { cookie: kCreator });
|
|
ok(c.code === 403 || c.code === 302,
|
|
`ein Creator kommt NICHT hinein (${c.code})`);
|
|
|
|
const d = await roh("/workspace/unsere-seiten.html", CREW);
|
|
ok(d.code === 302 || d.code === 401 || d.code === 403,
|
|
`ohne Anmeldung auch nicht (${d.code})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
3. EIN HIGHLIGHT MIT VIDEO -- die Daten dafuer
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 3. Drei Karten zum Vergleich ===");
|
|
const VIDEO = "https://www.tiktok.com/@dogfather0804/video/7500000000000000000";
|
|
{
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const wer = d.prepare("SELECT id FROM personen WHERE name = 'Filipe'").get().id;
|
|
const jetzt = new Date().toISOString();
|
|
|
|
/* (a) mit Video -- muss klickbar sein */
|
|
d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von,"
|
|
+ " quelle_url, quelle_kanal) VALUES (?,?,?,?,?,?,?,?,?,?)")
|
|
.run("highlight", "clip", "Wer ist euer Komet", "Schreibt es in die Kommentare",
|
|
jetzt.slice(0, 10), "offen", jetzt, wer, VIDEO, "tiktok");
|
|
|
|
/* (b) ohne Video -- darf NICHT klickbar sein */
|
|
d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von)"
|
|
+ " VALUES (?,?,?,?,?,?,?,?)")
|
|
.run("highlight", "clip", "Nur ein Bild", "Ohne Quelle",
|
|
jetzt.slice(0, 10), "offen", jetzt, wer);
|
|
|
|
/* (c) Video bei TikTok geloescht -- darf NICHT klickbar sein, sonst
|
|
fuehrt der Klick auf eine Fehlerseite und WIR sehen kaputt aus */
|
|
d.prepare("INSERT INTO eintraege (bereich, art, titel, text, datum, status, erstellt, erstellt_von,"
|
|
+ " quelle_url, quelle_kanal, quelle_fehlt_seit) VALUES (?,?,?,?,?,?,?,?,?,?,?)")
|
|
.run("highlight", "clip", "Nicht mehr da", "Wurde geloescht",
|
|
jetzt.slice(0, 10), "offen", jetzt, wer, VIDEO, "tiktok", jetzt);
|
|
|
|
/* EIN ECHTES BILD AN DIE ERSTE KARTE.
|
|
|
|
Filipe hat „foto" ausdrücklich genannt -- es ist die größte Fläche
|
|
der Karte und das, worauf ein Daumen zuerst landet. Ohne
|
|
angehängtes Bild meldete dieser Abschnitt beim ersten Lauf
|
|
„Bildweg nicht gemessen", und damit wäre genau der Teil ungeprüft
|
|
geblieben, um den es geht.
|
|
|
|
ES MUSS EIN ECHTES PNG SEIN. Die Bildroute entscheidet an den
|
|
ersten Bytes der Datei, nicht am behaupteten Typ -- und schlägt
|
|
sie fehl, entfernt der Fehlerbehandler im Browser den Rahmen
|
|
wieder. Eine leere Datei hätte also dasselbe Loch hinterlassen,
|
|
nur unauffälliger. */
|
|
const EIN_PNG = Buffer.from(
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
|
|
"base64");
|
|
{
|
|
const { mkdirSync, writeFileSync } = await import("node:fs");
|
|
const bildOrdner = join(ordner, "dateien");
|
|
mkdirSync(bildOrdner, { recursive: true });
|
|
writeFileSync(join(bildOrdner, "probe-standbild.png"), EIN_PNG);
|
|
const mitVideoId = d.prepare(
|
|
"SELECT id FROM eintraege WHERE bereich = 'highlight' AND quelle_url IS NOT NULL"
|
|
+ " AND quelle_fehlt_seit IS NULL").get().id;
|
|
d.prepare("INSERT INTO dateien (name_original, name_datei, groesse, typ, status,"
|
|
+ " hochgeladen_von, erstellt, eintrag_id) VALUES (?,?,?,?,?,?,?,?)")
|
|
.run("standbild.png", "probe-standbild.png", EIN_PNG.length, "image/png",
|
|
"freigegeben", wer, jetzt, mitVideoId);
|
|
}
|
|
|
|
/* FREIGEBEN -- sonst sieht ein Mitglied gar nichts.
|
|
|
|
Im Treff ist ein Beitrag erst sichtbar, wenn jemand aus dem Team
|
|
ihn freigegeben hat (`treff_freigaben`). Beim ersten Lauf hat
|
|
genau das gefehlt: Die drei Karten standen in der Datenbank, das
|
|
Brett blieb leer, und vier Prüfungen meldeten „0 Karten" -- was wie
|
|
ein Fehler am Klickverhalten aussah und keiner war. Wer eine
|
|
Oberfläche prüft, muss den Zustand herstellen, in dem sie im
|
|
Alltag steht. */
|
|
const wann = new Date().toISOString();
|
|
for (const z of d.prepare("SELECT id FROM eintraege WHERE bereich = 'highlight'").all()) {
|
|
d.prepare("INSERT INTO treff_freigaben (eintrag_id, zeitpunkt, von) VALUES (?,?,?)")
|
|
.run(z.id, wann, wer);
|
|
}
|
|
const n = d.prepare("SELECT COUNT(*) AS n FROM eintraege WHERE bereich = 'highlight'").get().n;
|
|
const f = d.prepare("SELECT COUNT(*) AS n FROM treff_freigaben").get().n;
|
|
d.close();
|
|
ok(n === 3, `drei Karten angelegt: mit Video, ohne Video, geloescht (${n})`);
|
|
ok(f === 3, `und alle drei freigegeben, sonst sieht ein Mitglied nichts (${f})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. IM BROWSER -- klickt die Flaeche wirklich?
|
|
|
|
ALLES DAVOR IST VORARBEIT. Ob eine Karte anklickbar ist, entscheidet
|
|
sich nicht im Quelltext, sondern unter einem Finger.
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 4. Im Browser ===");
|
|
{
|
|
let pw = null;
|
|
try {
|
|
pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
} catch { /* dritter Ausgang unten */ }
|
|
|
|
if (!pw) {
|
|
/* DRITTER AUSGANG: nicht „bestanden“, sondern „konnte nicht
|
|
nachsehen" -- und die Zaehlung sagt das auch. */
|
|
melde(" -- Playwright ist nicht da -- im Browser wurde NICHT nachgesehen.");
|
|
melde(" Das ist kein Befund. Die Abschnitte 1-3 gelten weiter.");
|
|
} else {
|
|
const browser = await pw.chromium.launch();
|
|
try {
|
|
for (const breite of [1400, 412]) {
|
|
melde("");
|
|
melde(` --- Breite ${breite} px ---`);
|
|
const ctx = await browser.newContext({
|
|
viewport: { width: breite, height: 950 },
|
|
isMobile: breite < 700, hasTouch: breite < 700 });
|
|
/* ANMELDUNG ALS KEKS, NICHT ÜBER DAS FORMULAR. Der Weg über die
|
|
Oberfläche prüft hier nichts, was nicht anderswo schon
|
|
geprüft wird -- er bricht nur bei jeder Änderung an der
|
|
Anmeldeseite, und dann sieht diese Prüfung kaputt aus,
|
|
obwohl an den Wegen nach draußen nichts fehlt. */
|
|
await ctx.addCookies(kGast.split("; ").filter(Boolean).map((z) => {
|
|
const [name, ...rest] = z.split("=");
|
|
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
|
|
}));
|
|
const seite = await ctx.newPage();
|
|
const kaputt = [];
|
|
seite.on("pageerror", (e) => kaputt.push(String(e).slice(0, 140)));
|
|
|
|
await seite.goto(`http://127.0.0.1:${PORT}/workspace/start.html`,
|
|
{ waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(900);
|
|
|
|
/* ---- 4a. Steht die Kachel wirklich auf dem Bildschirm? ---- */
|
|
const kachel = await seite.evaluate(() => {
|
|
const a = [...document.querySelectorAll('a[href*="unsere-seiten"]')][0];
|
|
if (!a) return null;
|
|
const r = a.getBoundingClientRect();
|
|
return { text: a.textContent.trim().slice(0, 40), breit: Math.round(r.width),
|
|
hoch: Math.round(r.height), sichtbar: r.width > 0 && r.height > 0 };
|
|
});
|
|
ok(!!kachel && kachel.sichtbar,
|
|
`die Kachel ist da und sichtbar (${kachel?.breit}x${kachel?.hoch} px)`);
|
|
/* GROSS GENUG FUER EINEN DAUMEN. 44 px ist das Mass, unter dem
|
|
ein Ziel auf dem Handy regelmaessig verfehlt wird. */
|
|
ok((kachel?.hoch || 0) >= 44,
|
|
`und hoch genug zum Antippen (${kachel?.hoch} px, mindestens 44)`);
|
|
|
|
/* ---- 4b. Die Seite selbst ---- */
|
|
await seite.goto(`http://127.0.0.1:${PORT}/workspace/unsere-seiten.html`,
|
|
{ waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(500);
|
|
|
|
/* window.open abfangen, damit keine echten Fenster aufgehen --
|
|
und damit messbar wird, WOHIN es gegangen waere. */
|
|
await seite.evaluate(() => {
|
|
window.__wohin = [];
|
|
window.open = (u) => { window.__wohin.push(String(u)); return null; };
|
|
});
|
|
|
|
const karten = await seite.evaluate(() =>
|
|
[...document.querySelectorAll(".aussen[data-weg]")].map((k) => k.dataset.weg));
|
|
ok(karten.length === 2, `zwei Karten nach draussen (${karten.length})`);
|
|
ok(karten.some((w) => w.includes("dogfather-universe.com")), "eine zur Website");
|
|
ok(karten.some((w) => w.includes("vans-diy-bastelbedarf.com")), "eine zum Shop");
|
|
|
|
/* KLICK AUF DEN TEXT -- nicht auf den Knopf. Genau das war
|
|
Filipes Wunsch: „egal wo man drauf drückt“. */
|
|
await seite.click(".aussen--shop .aussen__satz");
|
|
await seite.waitForTimeout(250);
|
|
let wohin = await seite.evaluate(() => window.__wohin);
|
|
ok(wohin.length === 1 && wohin[0].includes("vans-diy-bastelbedarf"),
|
|
`ein Klick auf den TEXT fuehrt zum Shop (${wohin.join(", ") || "nichts"})`);
|
|
|
|
/* GEGENPROBE: Der Kopierknopf darf NICHT zusaetzlich die Karte
|
|
ausloesen. Ohne diese Zeile waere „die Flaeche klickt“ auch
|
|
dann gruen, wenn sie ueberall klickt -- auch dort, wo es
|
|
stoert. */
|
|
await seite.evaluate(() => { window.__wohin = []; });
|
|
await seite.click("#code-knopf");
|
|
await seite.waitForTimeout(250);
|
|
wohin = await seite.evaluate(() => window.__wohin);
|
|
ok(wohin.length === 0,
|
|
`der Kopierknopf oeffnet NICHTS (${wohin.length} Fenster)`);
|
|
|
|
const hinweis = await seite.textContent("#code-hinweis");
|
|
ok(/kopiert|markiert|abschreiben/.test(hinweis || ""),
|
|
`und sagt, was er getan hat: ${(hinweis || "").trim()}`);
|
|
|
|
/* GEGENPROBE 2: Der eigene Knopf oeffnet genau EINMAL -- nicht
|
|
zweimal, weil die Karte mitklickt. */
|
|
await seite.evaluate(() => {
|
|
window.__wohin = [];
|
|
/* Der Knopf ist ein echter Link; sein Standardverhalten
|
|
unterbinden, sonst navigiert die Prueferin weg. */
|
|
for (const a of document.querySelectorAll(".aussen__knopf")) {
|
|
a.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
window.__wohin.push(a.href);
|
|
});
|
|
}
|
|
});
|
|
await seite.click(".aussen--dogi .aussen__knopf");
|
|
await seite.waitForTimeout(250);
|
|
wohin = await seite.evaluate(() => window.__wohin);
|
|
ok(wohin.length === 1,
|
|
`der Knopf „Seite oeffnen“ wirkt genau einmal (${wohin.length}x)`);
|
|
|
|
/* ---- 4c. Die Highlight-Karten ---- */
|
|
await seite.goto(`http://127.0.0.1:${PORT}/workspace/bereich.html?b=highlight`,
|
|
{ waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1200);
|
|
await seite.evaluate(() => {
|
|
window.__wohin = [];
|
|
window.open = (u) => { window.__wohin.push(String(u)); return null; };
|
|
});
|
|
|
|
const stand = await seite.evaluate(() =>
|
|
[...document.querySelectorAll(".eintrag-karte")].map((k) => ({
|
|
titel: k.querySelector(".eintrag-titel")?.textContent?.trim() || "",
|
|
klickbar: k.dataset.klickbar === "ja",
|
|
})));
|
|
ok(stand.length === 3, `drei Karten stehen da (${stand.length})`);
|
|
|
|
const mitVideo = stand.find((s) => s.titel.includes("Komet"));
|
|
const ohneVideo = stand.find((s) => s.titel.includes("Nur ein Bild"));
|
|
const fort = stand.find((s) => s.titel.includes("Nicht mehr da"));
|
|
|
|
ok(mitVideo?.klickbar === true, "die Karte MIT Video ist klickbar");
|
|
/* DIE BEIDEN GEGENPROBEN. Ohne sie waere „klickbar“ auch dann
|
|
gruen, wenn ALLE Karten klicken -- und ein Klick auf ein
|
|
geloeschtes Video fuehrt auf eine Fehlerseite. */
|
|
ok(ohneVideo?.klickbar === false, "die Karte OHNE Video ist es nicht");
|
|
ok(fort?.klickbar === false,
|
|
"und die mit geloeschtem Video auch nicht (sonst Fehlerseite)");
|
|
|
|
/* Klick auf die Ueberschrift -- „egal wo“. */
|
|
const titelWahl = ".eintrag-karte[data-klickbar='ja'] .eintrag-titel";
|
|
await seite.click(titelWahl);
|
|
await seite.waitForTimeout(250);
|
|
wohin = await seite.evaluate(() => window.__wohin);
|
|
ok(wohin.length === 1 && wohin[0] === VIDEO,
|
|
`ein Klick auf die UEBERSCHRIFT fuehrt zum Video (${wohin[0] || "nichts"})`);
|
|
|
|
/* Und auf den Fliesstext. */
|
|
await seite.evaluate(() => { window.__wohin = []; });
|
|
const textWahl = ".eintrag-karte[data-klickbar='ja'] .eintrag-text";
|
|
if (await seite.$(textWahl)) {
|
|
await seite.click(textWahl);
|
|
await seite.waitForTimeout(250);
|
|
wohin = await seite.evaluate(() => window.__wohin);
|
|
ok(wohin.length === 1 && wohin[0] === VIDEO,
|
|
`und ein Klick auf den TEXT ebenso (${wohin[0] || "nichts"})`);
|
|
} else {
|
|
ok(false, "der Fliesstext der Karte liess sich nicht finden");
|
|
}
|
|
|
|
/* GEGENPROBE: Auf der NICHT klickbaren Karte passiert nichts. */
|
|
await seite.evaluate(() => { window.__wohin = []; });
|
|
const stiller = ".eintrag-karte:not([data-klickbar='ja']) .eintrag-titel";
|
|
await seite.click(stiller);
|
|
await seite.waitForTimeout(250);
|
|
wohin = await seite.evaluate(() => window.__wohin);
|
|
ok(wohin.length === 0,
|
|
`auf einer Karte ohne Video passiert NICHTS (${wohin.length})`);
|
|
|
|
/* DER TIKTOK-KNOPF MUSS WEITER EIN LINK SEIN. Waere er von
|
|
einem <a> umschlossen worden, haette der Browser ihn still
|
|
aufgeloest -- die haeufigste Art, eine Karte kaputtzumachen. */
|
|
const knopf = await seite.evaluate(() => {
|
|
const a = document.querySelector(".eintrag-karte[data-klickbar='ja'] .videozeile__weg");
|
|
return a ? { tag: a.tagName, href: a.getAttribute("href"),
|
|
imLink: !!a.closest("a a") } : null;
|
|
});
|
|
ok(knopf?.tag === "A" && knopf.href === VIDEO,
|
|
`der TikTok-Knopf ist noch ein echter Link (${knopf?.tag})`);
|
|
ok(knopf?.imLink === false, "und steckt in keinem zweiten Link");
|
|
|
|
/* DAS BILD FUEHRT ZUM VIDEO, nicht mehr ins Vollbild -- das war
|
|
Filipes „foto“. Ohne Bilddatei gibt es keinen Bildstreifen;
|
|
dann ist hier nichts zu messen, und das muss man sehen. */
|
|
const bild = await seite.evaluate(() => {
|
|
const a = document.querySelector(".eintrag-karte[data-klickbar='ja'] .bildstreifen__feld");
|
|
return a ? { href: a.getAttribute("href"), label: a.getAttribute("aria-label") } : null;
|
|
});
|
|
if (bild) {
|
|
ok(bild.href === VIDEO, `das Bild fuehrt zum Video (${bild.href})`);
|
|
ok(/Video ansehen/.test(bild.label || ""),
|
|
`und sagt das auch vorlesbar („${bild.label}“)`);
|
|
} else {
|
|
/* KEIN STILLES UEBERSPRINGEN. Das Bild ist angelegt; fehlt der
|
|
Streifen trotzdem, ist etwas kaputt -- entweder lud das
|
|
Bild nicht (dann entfernt der Fehlerbehandler den Rahmen)
|
|
oder die Karte zeigt gar keine Anhaenge mehr. Beides ist
|
|
ein Befund, kein Grund zu schweigen. */
|
|
ok(false, "der Bildstreifen fehlt, obwohl ein Bild angehaengt ist");
|
|
}
|
|
|
|
ok(kaputt.length === 0,
|
|
`kein Skriptfehler auf dem Weg${kaputt.length ? ": " + kaputt.join(" | ") : ""}`);
|
|
await ctx.close();
|
|
}
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
melde("");
|
|
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
|
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Rest */ }
|
|
process.exit(fehler ? 1 : 0);
|