Files
dogfather-universe/server/pruef-treff-start.mjs
T
DogFatherGitandClaude Opus 5 6e46a08543 Portnummern werden abgeleitet, nicht mehr vergeben
Gemessen: 165 Pruefdateien, 129 verschiedene Nummern -- NEUNZEHN
doppelt, vier davon dreifach. Niemand hatte das gewollt; jede neue
Pruefung wurde von einer vorhandenen abgeschrieben, und die Nummer kam
mit. Meine eigene Notiz sagte "sieben" -- auch eine Bestandsliste
altert.

Der Waechter faengt den Schaden ab, aber er kann nur melden, was schon
passiert ist: Zwei Pruefungen mit derselben Nummer koennen nie
gleichzeitig laufen, und ein liegengebliebener Prozess der einen laesst
die andere abbrechen mit einer Meldung, die wie ein Befund aussieht.
Genau das ist mir am 20.09. zweimal passiert.

Jetzt leitet jede Datei ihre Nummer aus ihrer STELLE IM ALPHABET ab
(eigenerPort in helfer-port.mjs), zwei je Datei. Nicht ueber eine
Pruefsumme: Bei 165 Namen in 4900 Nummern waeren nach dem
Geburtstagsproblem rund DREI Zusammenstoesse zu erwarten -- ein Hash
tauscht eine sichtbare Doppelung gegen eine unsichtbare. Die Stelle im
Alphabet ist eindeutig von der Bauart her.

--- ZWEI FEHLER AUF DEM WEG, BEIDE LEHRREICH -------------------------

1. DER ERSTE VERSUCH WAR GRUEN UND KAPUTT. Ersetzt wurde mit einem
   Muster: "([^"]*4231[^"]*)". Das hielt

     { host: "127.0.0.1", port: 4231, path: "/404.html" }

   fuer eine Zeichenkette -- ein Muster kann eine oeffnende nicht von
   einer schliessenden Anfuehrung unterscheiden. Heraus kam

     { host: "127.0.0.1`, port: ${PORT}, path: `/404.html" }

   also GUELTIGER Code ohne port-Feld. `node --check` sagte gruen fuer
   alle 164 Dateien. Aufgefallen ist es erst, weil ich vier Vertreter
   gegen eine vorher gemessene Grundlinie laufen liess: pruef-schranke
   39/0 vorher, 38/1 nachher.

   Alles zurueckgenommen und mit einem Zerleger neu gemacht, der weiss,
   ob eine Stelle Code, Zeichenkette, Vorlage, Kommentar oder
   regulaerer Ausdruck ist. In pruef-ics stand die Nummer in einem
   regulaeren Ausdruck -- der wird jetzt gebaut statt hingeschrieben.

2. EIN MODUL, DAS BEIM IMPORTIEREN ARBEITET, IST EINE FALLE. Der
   zweite Durchgang importierte den ersten, um seine Mechanik zu
   benutzen -- und fuehrte dessen Hauptlauf gleich mit aus. Die
   zweiten Nummern wurden dadurch als erste behandelt, zwei Aufrufe
   bekamen dieselbe Nummer, und in pruef-content stand `const PORT`
   zweimal.

--- WAS DAS DAUERHAFT HAELT -----------------------------------------

pruef-portnummern.mjs (neu, 8 Pruefungen) fragt nicht "welche Nummern
sind doppelt", sondern "wer traegt ueberhaupt noch eine von Hand ein"
und "wer startet einen Server, ohne seine Nummer abzuleiten". Die
zweite Frage hat sofort etwas gefunden, das in KEINER Doppelungsliste
stand: pruef-push-weg belegte 4341 und 4342, rief den Waechter aber
gar nicht auf -- dieselbe Nummer wie pruef-agentur. Eine Liste zeigt
nur, was auf ihr steht.

Mit Gegenprobe: Eine unbekannte Datei bekommt keine geratene Nummer,
sondern einen Abbruch, und eine dritte Nummer je Datei gibt es nicht.

--- NACHGEMESSEN ----------------------------------------------------

Sechzehn Pruefungen gegen ihre vorher gemessene Grundlinie, je eine
Vertreterin jeder umgestellten Bauweise (eine Nummer, zwei Nummern,
Nummer in einer Zeichenkette, in einer Vorlage, in einem regulaeren
Ausdruck, dynamische Einfuhr, Nachtlauf mit zwei Laeufen):

  crew-adresse 132/0 · schranke 39/0 · content 45/0 · entwicklung 46/0
  anruf 127/0 · ics 37/0 · push-weg 20/0 · arten 28/0 · video 67/0
  kanalzeile 14/0 · agentur 62/0 · spicy 83/0 · creator-anlegen 50/0
  push-ziel 10/0 · portnummern 8/0

Alle exakt wie vorher. Zwei waren schon vorher rot und sind es
unveraendert geblieben (crew-wand-bild 41/4, teilen 14/3) -- per
`git stash` belegt, nicht angenommen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-20 22:51:21 +02:00

454 lines
21 KiB
JavaScript

/* =====================================================================
FERTIGE INHALTE STATT LEERER BRETTER (17.09.2026)
Filipe, mit drei Bildschirmfotos nebeneinander -- Der Treff, das
Anschlagbrett, Was ansteht, alle drei mit demselben grauen Kasten
"Noch kein Eintrag in diesem Bereich":
"ich meine alle kategorien sehen einfach so scheisse und leer aus,
ich will fertige sachen schon drin wo auch die community und alle
anderen benutzen koennen"
Er hatte recht, und es stand sogar im Plan (§5.1): "Acht leere Seiten
sind schlimmer als keine." Ein Bereich, der nur aus Filterknoepfen
und einem grauen Kasten besteht, sieht nicht neu aus, sondern
aufgegeben -- und dreimal derselbe Satz sieht aus wie ein Defekt.
---------------------------------------------------------------------
WAS HIER GEPRUEFT WIRD
1. Der Katalog selbst: Passt jede Art zu ihrem Bereich? Ist jeder
Text mehr als eine Ueberschrift?
2. Das Uebernehmen: einzeln, alle auf einmal -- und zweimal
uebernehmen darf nichts verdoppeln.
3. UND DER FALL, DEN ICH MIR SELBST GEBAUT HABE: Die Bremse aus
Stufe 4 zaehlt Beitraege pro Stunde. Wer fuenfzehn Regeln
uebernimmt, haette nach der zehnten eine halb gefuellte Seite
und die Meldung, er habe zu viel geschrieben.
4. Die Verborgenheit: Wer hier nicht schreiben darf, bekommt den
Katalog gar nicht.
Aufrufen mit: node server/pruef-treff-start.mjs
===================================================================== */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { eigenerPort } from "./helfer-port.mjs";
import { ersatzschreibungen } from "./helfer-deutsch.mjs";
const PORT = await eigenerPort(import.meta, "pruef-treff-start");
const ordner = mkdtempSync(join(tmpdir(), "ws-start-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = String(PORT);
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
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(180_000, "pruef-treff-start");
await new Promise((r) => setTimeout(r, 700));
const BASIS = `http://127.0.0.1:${PORT}`;
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", 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 { TREFF_START } = await import("./workspace-treff-start.js");
const { BEREICHE, BEITRAEGE_JE_STUNDE } = await import("./workspace-bereiche.js");
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
const schluessel = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", schluessel, jetzt);
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, code_kennung, aktiv, erstellt)"
+ " VALUES (?,?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768,
createHmac("sha256", schluessel).update(code).digest("hex"), jetzt);
}
anlegen("Filipe", "admin", "CODE-DOGI-0001");
anlegen("Tili", "creator", "CODE-CREA-0001");
d.close();
async function anmelden(kachel, code) {
const a = await fetch(BASIS + "/workspace/api/anmelden", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rolle: kachel, code }),
});
return (a.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
}
const dogi = await anmelden("admin", "CODE-DOGI-0001");
const crea = await anmelden("creator", "CODE-CREA-0001");
const kopf = (keks) => ({ "Content-Type": "application/json", cookie: keks, origin: BASIS });
/* =======================================================================
1. DER KATALOG SELBST
======================================================================= */
melde("");
melde("=== 1. Was bereitliegt ===");
{
const gesamt = Object.values(TREFF_START).reduce((n, l) => n + l.length, 0);
ok(gesamt >= 40, `${gesamt} fertige Eintraege in ${Object.keys(TREFF_START).length} Bereichen`);
/* JEDE ART MUSS ES IM BEREICH GEBEN. Sonst landet beim Uebernehmen
eine Art in der Datenbank, die das Formular nicht kennt -- und die
Zeile laesst sich hinterher nicht mehr aendern. */
const falsch = [];
for (const [bereich, liste] of Object.entries(TREFF_START)) {
const arten = BEREICHE[bereich]?.arten || {};
for (const v of liste) if (!Object.hasOwn(arten, v.art)) falsch.push(`${bereich}/${v.art}`);
}
ok(falsch.length === 0,
falsch.length ? `unbekannte Arten: ${falsch.join(", ")}` : "jede Art gibt es in ihrem Bereich");
/* EIN TEXT, DER NUR DEN TITEL WIEDERHOLT, ist keiner. Dieselbe Regel
wie beim Aufgabenkatalog der Modis. */
const duenn = [];
for (const [bereich, liste] of Object.entries(TREFF_START)) {
for (const v of liste) {
if (!v.text || v.text.length < v.titel.length + 25) duenn.push(`${bereich}/${v.titel}`);
}
}
ok(duenn.length === 0,
duenn.length ? `zu duenn: ${duenn[0]}` : "jeder Text sagt mehr als sein Titel");
/* KEINE DOPPELTEN TITEL innerhalb eines Bereichs -- das Uebernehmen
erkennt "schon da" am Titel. */
let doppelt = 0;
for (const liste of Object.values(TREFF_START)) {
doppelt += liste.length - new Set(liste.map((v) => v.titel)).size;
}
ok(doppelt === 0, `kein Titel doppelt (${doppelt})`);
/* UND SIE TRAGEN UMLAUTE. Dieselbe Falle wie bei den Kategorien und
den Bereichsnamen -- beide Male auf einem Bildschirmfoto gefunden. */
const ascii = [];
for (const [bereich, liste] of Object.entries(TREFF_START)) {
for (const v of liste) {
const t = ersatzschreibungen(`${v.titel} ${v.text}`);
if (t.length) ascii.push(`${bereich}/${v.titel} [${t.join(",")}]`);
}
}
ok(ascii.length === 0,
ascii.length ? `Ersatzschreibungen: ${ascii[0]}` : "alle Texte tragen echte Umlaute");
/* ---- DIE TEXTE SIND AUF DOGFATHER BEZOGEN (17.09.2026) ----------
Filipe: "die sachen sollen immer auf mich bezogen sein bitte. ich,
dogfather bin der mittelpunkt immer."
Der erste Entwurf war messbar anonym: 47 Eintraege, "DogFather"
kam fuenfmal vor, ein namenloses "wir" sechzehnmal. Das las sich
wie der Community-Bereich von irgendwem.
WARUM EINE QUOTE UND KEIN "JEDER TEXT": Bei der Telefonseelsorge
oder "Melden ist kein Petzen" waere ein eingebautes "DogFather"
aufgesetzt. Ein Text, der den Namen trotz allem traegt, ist
schlechter als einer ohne. */
const BEZUG = /DogFather|Casper|HasiDog|Clips/;
{
const alle = Object.values(TREFF_START).flat();
const mit = alle.filter((v) => BEZUG.test(`${v.titel} ${v.text}`)).length;
ok(mit / alle.length >= 0.6,
`${mit} von ${alle.length} Texten nennen DogFather, seine Kanaele oder Casper `
+ `(${Math.round(mit / alle.length * 100)} %)`);
/* UND DIE VIER BRETTER, DIE DIE COMMUNITY WIRKLICH SIEHT, ganz.
Auf dem Anschlagbrett, im Treff, in der Wunschliste und bei den
Highlights gibt es keinen Satz, der ohne ihn auskommt -- sie
handeln von seinen Streams. */
for (const b of ["treff", "anschlag", "wunsch", "highlight"]) {
const liste = TREFF_START[b] || [];
const ohne = liste.filter((v) => !BEZUG.test(`${v.titel} ${v.text}`));
ok(liste.length > 0 && ohne.length === 0,
ohne.length ? `${b}: ohne Bezug -- "${ohne[0].titel}"`
: `${b}: alle ${liste.length} Texte sind auf ihn bezogen`);
}
/* GEGENPROBE: Ein anonymer Text MUSS auffallen. Ohne sie hiesse
"alle bezogen" nur, dass das Muster auf alles passt. */
ok(!BEZUG.test("Wir machen demnaechst etwas Neues. Schreibt uns."),
"ein anonymer Satz wuerde erkannt");
ok(BEZUG.test("Was soll DogFather im naechsten Stream machen?"),
"und ein bezogener nicht faelschlich");
}
/* DIE BEIDEN NACHSCHLAGEWERKE muessen ordentlich gefuellt sein. Ein
leeres Regelwerk ist schlimmer als gar keins: Im Streitfall hat
dann niemand etwas in der Hand. */
ok((TREFF_START.regeln || []).length >= 12,
`Regeln & Hilfe hat ${(TREFF_START.regeln || []).length} Eintraege`);
ok((TREFF_START.mitmachen || []).length >= 8,
`Mitmachen hat ${(TREFF_START.mitmachen || []).length}`);
/* UND DIE HILFE NENNT EINE ECHTE NUMMER. Ein Community-Bereich fuer
junge Leute ohne Notrufnummer ist eine vertane Gelegenheit. */
ok((TREFF_START.regeln || []).some((v) => /0800 111 0 111|116 111/.test(v.text || "")),
"und die Hilfe nennt die Telefonseelsorge");
}
/* =======================================================================
2. UEBERNEHMEN
======================================================================= */
melde("");
melde("=== 2. Uebernehmen ===");
{
const a = await fetch(`${BASIS}/workspace/api/bereich/treff/start`, { headers: { cookie: dogi } });
const j = await a.json().catch(() => null);
ok(a.status === 200 && (j?.vorschlaege || []).length > 0,
`der Katalog kommt (${j?.offen} offen)`);
const e = await fetch(`${BASIS}/workspace/api/bereich/treff/start`, {
method: "POST", headers: kopf(dogi), body: JSON.stringify({ nr: 0 }),
});
const ej = await e.json().catch(() => null);
ok(e.status === 201 && ej?.angelegt === 1, `einer einzeln (${ej?.angelegt})`);
/* ZWEIMAL DARF NICHTS VERDOPPELN. Wer den Knopf zweimal drueckt --
oder zwei Leute gleichzeitig --, soll dieselbe Seite bekommen. */
const z = await fetch(`${BASIS}/workspace/api/bereich/treff/start`, {
method: "POST", headers: kopf(dogi), body: JSON.stringify({ nr: 0 }),
});
const zj = await z.json().catch(() => null);
ok(zj?.angelegt === 0 && zj?.schon === 1, `zweimal legt nichts nach (${zj?.angelegt})`);
}
/* =======================================================================
3. DIE BREMSE DARF DAS NICHT VERHINDERN
======================================================================= */
melde("");
melde("=== 3. Fuenfzehn Regeln auf einmal ===");
{
const wie_viele = (TREFF_START.regeln || []).length;
ok(wie_viele > BEITRAEGE_JE_STUNDE,
`es sind ${wie_viele} Regeln -- mehr als die Bremse je Stunde zulaesst (${BEITRAEGE_JE_STUNDE})`);
const a = await fetch(`${BASIS}/workspace/api/bereich/regeln/start`, {
method: "POST", headers: kopf(dogi), body: "{}",
});
const j = await a.json().catch(() => null);
/* DAS IST DER PUNKT. Ginge der Weg durch die Bremse, stuenden hier
zehn Regeln und eine Fehlermeldung -- eine halb gefuellte Seite
mit dem Vorwurf, man habe zu viel geschrieben. Genau die Sorte
Sicherung, die das richtige Verhalten bestraft. */
ok(a.status === 201 && j?.angelegt === wie_viele,
`alle ${wie_viele} auf einmal (${j?.angelegt})`);
const l = await fetch(`${BASIS}/workspace/api/bereich/regeln`, { headers: { cookie: dogi } });
const lj = await l.json().catch(() => null);
ok((lj?.eintraege || []).length === wie_viele,
`und sie stehen wirklich da (${(lj?.eintraege || []).length})`);
/* GEGENPROBE: Die Bremse ist deshalb nicht abgeschaltet. Ein
gewoehnlicher Beitrag danach muss sie spueren. */
let letzte = 0;
for (let i = 0; i < BEITRAEGE_JE_STUNDE + 2; i++) {
const r = await fetch(`${BASIS}/workspace/api/bereich/treff`, {
method: "POST", headers: kopf(dogi),
body: JSON.stringify({ art: "frage", titel: `Frage Nummer ${i}`,
datum: jetzt.slice(0, 10) }),
});
letzte = r.status;
if (r.status === 429) break;
}
ok(letzte === 429, `die Bremse greift trotzdem noch (${letzte})`);
}
/* =======================================================================
4. WER DEN KATALOG NICHT BEKOMMT
======================================================================= */
melde("");
melde("=== 4. Die Verborgenheit ===");
{
const a = await fetch(`${BASIS}/workspace/api/bereich/regeln/start`, { headers: { cookie: crea } });
ok(a.status === 404, `ein Creator bekommt ihn nicht (${a.status})`);
const b = await fetch(`${BASIS}/workspace/api/bereich/regeln/start`, {
method: "POST", headers: kopf(crea), body: "{}",
});
ok(b.status === 403 || b.status === 404,
`und kann auch nichts uebernehmen (${b.status})`);
}
/* =======================================================================
5. JEDER BEREICH SAGT ETWAS ANDERES, WENN ER LEER IST
======================================================================= */
melde("");
melde("=== 5. Die leeren Bretter ===");
{
/* DREI BILDSCHIRMFOTOS MIT DEMSELBEN SATZ sahen aus wie ein Defekt.
Jetzt sagt jeder Bereich in seinen eigenen Worten, was hier
hingehoert. */
const saetze = Object.entries(BEREICHE)
.filter(([k]) => TREFF_START[k])
.map(([k, e]) => [k, e.leer || ""]);
ok(saetze.every(([, t]) => t.length > 25),
`alle ${saetze.length} Community-Bereiche haben einen eigenen Leertext`);
ok(new Set(saetze.map(([, t]) => t)).size === saetze.length,
"und keiner davon ist derselbe");
ok(saetze.every(([, t]) => !/Noch kein Eintrag in diesem Bereich/.test(t)),
"der alte Einheitssatz kommt nicht mehr vor");
}
/* =======================================================================
6. IM BROWSER: VORHER UND NACHHER
======================================================================= */
melde("");
melde("=== 6. Wie es aussieht ===");
{
const { chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await chromium.launch();
try {
const kontext = await browser.newContext({ viewport: { width: 1100, height: 950 } });
await kontext.addCookies(dogi.split("; ").map((k) => {
const [name, ...rest] = k.split("=");
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
}));
const seite = await kontext.newPage();
const konsole = [];
seite.on("console", (e) => { if (e.type() === "error") konsole.push(e.text()); });
/* "mitmachen" ist noch unberuehrt -- dort ist der Katalog voll. */
await seite.goto(`${BASIS}/workspace/bereich.html?b=mitmachen`, { waitUntil: "networkidle" });
await seite.waitForSelector("#startkatalog:not([hidden])", { timeout: 15_000 });
await seite.waitForTimeout(400);
const vorher = await seite.evaluate(() => ({
karten: document.querySelectorAll(".startkatalog__karte").length,
alleKnopf: document.querySelector(".startkatalog__alle")?.textContent?.trim() || "",
leer: document.querySelector(".leer-hinweis")?.textContent?.trim() || "",
}));
ok(vorher.karten >= 8, `${vorher.karten} Vorschlaege stehen bereit`);
ok(/Alle \d+ übernehmen/.test(vorher.alleKnopf), `mit einem Knopf ("${vorher.alleKnopf}")`);
/* DER LEERTEXT IST DER DES BEREICHS, nicht der Einheitssatz. */
ok(vorher.leer.includes("dazugehören"),
`und der leere Bereich sagt seinen eigenen Satz ("${vorher.leer}")`);
if (process.env.SCHIRM) {
const { fileURLToPath } = await import("node:url");
await seite.screenshot({ path: fileURLToPath(new URL("pruef-treff-start-vorher.png",
import.meta.url)), fullPage: false });
}
await seite.click(".startkatalog__alle");
await seite.waitForTimeout(1500);
const nachher = await seite.evaluate(() => ({
katalogWeg: !!document.getElementById("startkatalog")?.hidden,
eintraege: document.querySelectorAll(".eintrag-karte").length,
leerWeg: !document.querySelector(".leer-hinweis"),
}));
ok(nachher.eintraege >= 8, `nach einem Klick stehen ${nachher.eintraege} Eintraege da`);
ok(nachher.leerWeg, "der leere Kasten ist weg");
/* UND DER KATALOG VERSCHWINDET, wenn nichts mehr zu holen ist --
ein Angebot, das leer weiterhin dasteht, ist eine Sackgasse. */
ok(nachher.katalogWeg, "und der Katalog verschwindet, weil nichts mehr offen ist");
ok(konsole.length === 0,
konsole.length ? `Konsole: ${konsole[0]}` : "keine Fehler in der Konsole");
if (process.env.SCHIRM) {
const { fileURLToPath } = await import("node:url");
await seite.screenshot({ path: fileURLToPath(new URL("pruef-treff-start-nachher.png",
import.meta.url)), fullPage: false });
melde(" -- Bilder: server/pruef-treff-start-vorher.png und -nachher.png");
}
await kontext.close();
} finally {
await browser.close();
}
}
/* =======================================================================
7. JEDES BRETT HAT SEINE FORM (Stufe 7)
======================================================================= */
melde("");
melde("=== 7. Die Form je Brett ===");
{
const { chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await chromium.launch();
try {
const kontext = await browser.newContext({ viewport: { width: 1000, height: 900 } });
await kontext.addCookies(dogi.split("; ").map((k) => {
const [name, ...rest] = k.split("=");
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
}));
const seite = await kontext.newPage();
const konsole = [];
seite.on("console", (e) => { if (e.type() === "error") konsole.push(e.text()); });
/* Regeln sind schon uebernommen (Abschnitt 3). */
await seite.goto(`${BASIS}/workspace/bereich.html?b=regeln`, { waitUntil: "networkidle" });
await seite.waitForSelector(".brettabschnitt", { timeout: 15_000 });
await seite.waitForTimeout(400);
const r = await seite.evaluate(() => ({
abschnitte: [...document.querySelectorAll(".brettabschnitt")]
.map((z) => z.firstChild?.textContent?.trim() || ""),
marken: document.querySelectorAll(".brettmarken__weg").length,
/* Springt die Marke wirklich zu einer Ueberschrift? Ein Verweis
ins Leere sieht aus wie ein kaputter Sprung. */
zieleDa: [...document.querySelectorAll(".brettmarken__weg")]
.every((a) => !!document.getElementById(a.getAttribute("href").slice(1))),
}));
ok(r.abschnitte.length >= 3,
`Regeln & Hilfe ist ein Dokument mit ${r.abschnitte.length} Abschnitten (${r.abschnitte.join(" · ")})`);
ok(r.marken >= 3, `mit ${r.marken} Sprungmarken oben`);
ok(r.zieleDa, "und jede Marke findet ihre Ueberschrift");
/* Das Anschlagbrett: Aushaenge statt Zeilen. */
await seite.goto(`${BASIS}/workspace/bereich.html?b=anschlag`, { waitUntil: "networkidle" });
await seite.waitForTimeout(600);
const gross = await seite.evaluate(async () => {
const knopf = document.querySelector(".startkatalog__alle");
if (knopf) { knopf.click(); await new Promise((r2) => setTimeout(r2, 1500)); }
const t = document.querySelector("#liste .eintrag-titel");
return {
aushang: document.getElementById("liste")?.dataset.aushang || "",
titelGroesse: t ? Math.round(parseFloat(getComputedStyle(t).fontSize)) : 0,
anzahl: document.querySelectorAll(".eintrag-karte").length,
};
});
ok(gross.aushang === "ja", "das Anschlagbrett ist als Aushangbrett gesetzt");
/* GROSS heisst messbar gross -- groesser als ein Titel auf einem
gewoehnlichen Brett (1.05rem = 17 px). */
ok(gross.titelGroesse >= 20,
`und seine Aushaenge sind gross (${gross.titelGroesse} px)`);
ok(gross.anzahl >= 4, `${gross.anzahl} Aushaenge haengen`);
ok(konsole.length === 0,
konsole.length ? `Konsole: ${konsole[0]}` : "keine Fehler in der Konsole");
if (process.env.SCHIRM) {
const { fileURLToPath } = await import("node:url");
await seite.goto(`${BASIS}/workspace/bereich.html?b=regeln`, { waitUntil: "networkidle" });
await seite.waitForTimeout(600);
await seite.screenshot({ path: fileURLToPath(new URL("pruef-regeln.png",
import.meta.url)), fullPage: false });
melde(" -- Bildschirmfoto: server/pruef-regeln.png");
}
await kontext.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 { /* Aufraeumrest */ }
process.exit(fehler ? 1 : 0);