Damit sind alle acht Stufen des Plans gebaut. STUFE 6 -- DAS GESPRAECH "Der Treff -- Hallo sagen, fragen, loben" war eine Liste aus Karten. Der einzige Bereich, in dem Menschen MITEINANDER reden sollen, zwang sie, nebeneinander zu reden: Jeder schrieb eine eigene Karte, niemand antwortete jemandem. Jetzt: Antworten eingerueckt unter ihrem Beitrag, aelteste zuerst (so liest man ein Gespraech), mit Namen und Rolle. Das Feld steht immer da statt hinter einem "Antworten"-Knopf -- ein leeres Feld ist die deutlichste Einladung, die es gibt. Nur EINE Stufe eingerueckt: Auf 390 px ist bei der zweiten Schluss. Eine EIGENE Tabelle, kein Eintrag mit "antwort_auf": Eine Antwort hat keine Art, keinen Zustand, keine Frist und gehoert in keinen Filter. Als Eintrag gefuehrt haette sie zwanzig immer leere Spalten -- und tauchte in jeder Zaehlung auf, die Beitraege zaehlt. Und nicht ueberall: nur Treff und Wunschliste. Aus dem Plan, §9 -- ein Antwortfeld unter jedem Aushang hiesse, jeder Aushang muss betreut werden. ZWEI FEHLER, BEIDE VON DER PRUEFUNG GEFUNDEN 1. EIN PFAD, DER AUSSAH WIE EIN BEREICH. `/api/bereich/antwort/:id` lief in die Middleware auf `/api/bereich/:bereich` -- "antwort" ist kein Bereich. Fuer DogFather ging es (ein frueherer Zweig liess ihn durch), fuer einen Modi kam 404. Ein Weg, der je nach ROLLE an voellig anderer Stelle scheitert, sucht man lange. Liegt jetzt unter `/api/antwort/:id`. 2. UND EIN ECHTES LOCH. Die Loeschregel hiess `!darfSchreiben(req.person, "treff")` -- also "wer schreiben darf", und das duerfen Gaeste ab der Stufe "dabei". JEDER GAST HAETTE JEDE FREMDE ANTWORT LOESCHEN KOENNEN. Gefunden hat es eine Pruefung mit einer FALSCHEN Erwartung: Sie verlangte, dass ein Modi keine fremde Antwort loeschen kann. Das war falsch -- er moderiert ja --, aber der Weg dorthin hat das echte Problem freigelegt. Jetzt entscheidet TREFF_TEAM_ROLLEN. Der Gast-Fall ist ueber HTTP nicht messbar (er kommt nur ueber die crew-Adresse herein, und den Host-Kopf kann fetch nicht setzen). Statt stillschweigend zu ueberspringen prueft die Pruefung die REGEL selbst -- und dass die Route wirklich diese Menge benutzt. STUFE 7 -- JEDES BRETT BEKOMMT SEINE FORM - "Regeln & Hilfe" ist ein DOKUMENT: Sprungmarken oben, vier Abschnitte (Regel, Was passiert wenn, Hilfe, Haeufige Frage). Man schlaegt es im Streitfall auf und will FINDEN, nicht scrollen. scroll-margin-top, sonst verschwindet die angesprungene Ueberschrift unter der Kopfleiste und der Sprung sieht aus wie ins Leere. - "Anschlagbrett" zeigt grosse AUSHAENGE statt Zeilen -- gemessen 21 px Titel statt 17. Ein Aushang zwischen dreissig anderen ist kein Aushang. - "Mitmachen" gruppiert nach Art, die drei Schritte zuerst. UND EINE SCHRIFT, DIE ZU KLEIN WAR: Die Rollen-Marke an einer Antwort stand auf 11,2 px, die Hausgrenze liegt bei 11,5. Die Grenze anzuheben waere der bequeme Weg gewesen -- und ab da haette pruef-css-klassen nichts mehr gehalten. NEU: pruef-gespraech (27 Pruefungen) pruef-treff-start jetzt 34 (Stufe 7 mitgeprueft) Alle Community-Pruefungen gruen: gespraech 27, treff-start 34, kreislauf 23, countdown 22, wunschliste 30, galerie 26, bremse 16, treff 66, bereiche-lesend, css-klassen, deutsche-texte 12. Co-Authored-By: Claude Opus 5 <[email protected]>
413 lines
19 KiB
JavaScript
413 lines
19 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 { portMussFreiSein } from "./helfer-port.mjs";
|
|
import { ersatzschreibungen } from "./helfer-deutsch.mjs";
|
|
|
|
const PORT = await portMussFreiSein(4489, "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 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);
|