Files
dogfather-universe/server/pruef-willkommen.mjs
T
DogFatherGit 077925ab25 Dreizehn rote Pruefungen, eine Ursache: openssl steht nicht im PATH
DER BEFUND

Im naechtlichen Lauf standen dreizehn Pruefungen mit derselben Zeile
rot:

    [uncaughtException] Error: spawnSync openssl ENOENT

Kein Programmfehler. Die Pruefungen brauchen einen https-Vorbau (der
Sitzungskeks des Hauses ist `secure` und kommt ueber http nicht an),
und dafuer erzeugen sie ein Wegwerf-Zertifikat mit openssl.

GEMESSEN, NICHT VERMUTET

  openssl liegt hier:   C:\Program Files\Git\mingw64\bin\openssl.exe
                        C:\Program Files\Git\usr\bin\openssl.exe
  im System-PATH steht: C:\Program Files\Git\cmd   (nur git.exe)
  im Benutzer-PATH:     nichts mit Git

Wer aus Git Bash startet, erbt die mingw-Pfade und merkt nie etwas.
Die Aufgabenplanung startet `node.exe` DIREKT, ohne Shell -- und
bekommt sie nicht. Bei mir gruen, nachts rot, und der Grund steht
nicht im Code.

Das Schlimmste daran ist nicht der Ausfall: Dreizehn dauerhaft rote
Zeilen in einer Notiz, die Filipe morgens liest, gewoehnen einem das
Hinsehen ab -- und decken dabei die echten Befunde zu.

DIE REPARATUR

`server/helfer-openssl.mjs` sucht openssl erst im PATH (ohne `where`
oder `which` -- beides sind selbst Programme und koennen genauso
fehlen), danach an den Orten, an denen es auf einem Windows-Rechner
mit Git wirklich liegt.

Dazu `zertifikatBauen(schluessel, zertifikat, host)`. Die acht
Aufrufformen im Haus waren identisch bis auf die Variablennamen
(schl/zert, schluesselDatei/zertDatei, zKey/zCrt, sk/zt, key/crt) --
eine Stelle traegt alle neunundzwanzig. Zwei Tage zuvor war eine
davon um ein `-addext` aermer als die anderen; gemerkt hat es
niemand, weil der Browser den fehlenden Alternativnamen erst bei
einer Weiterleitung anmahnt. Eine Stelle kann nicht von sich selbst
abweichen.

KEIN EINTRAG IN DEN SYSTEM-PATH. `mingw64\bin` enthaelt rund hundert
Programme mit Unix-Namen (`find`, `sort`, `link`), die gleichnamige
Windows-Befehle verdecken. Das fuer eine Pruefung zu aendern haette
an ganz anderen Stellen Fehler verursacht, die niemand hierher
zurueckverfolgt.

GEGENPROBE IN BEIDE RICHTUNGEN, mit dem PATH des Nachtlaufs:

  vorher (direkter Aufruf):  ABSTURZ: spawnSync openssl ENOENT
  nachher (ueber den Helfer): Zertifikat gebaut, 1236 Bytes

Und eine echte Pruefung unter denselben Bedingungen:

  PATH ohne mingw64 -> pruef-chat-neu-stelle
  63 Pruefungen, 0 Fehler, 0 Abstuerze

Genau diese Datei stand im Nachtlauf mit ENOENT rot.

EINE WACHE DAGEGEN

pruef-struktur prueft ab jetzt, dass niemand openssl wieder direkt
ruft -- der naechste merkt es dort und nicht erst in einem
Nachtlauf, den niemand liest. Drei Gegenproben.

Beim ersten Anlauf schlug sie auf ihre EIGENEN Probetexte an: Sie
liest alle Serverdateien, und dazu gehoert sie selbst. Die Texte
werden jetzt zusammengesetzt. Derselbe Selbsttreffer ist mir heute
schon zweimal passiert.

DER DRITTE AUSGANG BLEIBT, WO ER HINGEHOERT

`helfer-kachel-echtfarbe.mjs` faengt den Fehler weiterhin ab und
meldet `moeglich: false` mit Grund, statt abzubrechen. Eine
Sicherung abzuschaffen, weil ihr Anlass gerade behoben ist, ist der
Anfang des naechsten stillen Fehlschlags.

NEBENBEI: pruef-community-sicht

Sie war rot mit "ZU VIEL: reaktion.html" -- mein eigener Rueckstand
vom 28.09. Die Reaction steht als Kachel im Community-Bereich; die
Liste war nicht nachgezogen. Jetzt 10 / 0.

Die Liste bleibt bewusst von Hand gepflegt: Sie ist eine ABSICHT,
keine Ableitung. Aus rechte.js gelesen verglichen sich zwei Kopien
derselben Quelle -- immer gruen, nie ein Beweis.

GEPRUEFT
  pruef-struktur         ALLES IN ORDNUNG (mit der neuen Wache)
  pruef-community-sicht  10 / 0
  pruef-chat-neu-stelle  63 / 0  (mit dem PATH des Nachtlaufs)
  pruef-notizen, -teamlage-karten, -werdegang: EXIT 0, keine Abstuerze
  mess-quer              EXIT 0
  29 Dateien: node --check auf allen, kein direkter Aufruf mehr
2026-09-29 16:14:13 +02:00

461 lines
21 KiB
JavaScript

/* =====================================================================
pruef-willkommen.mjs — DIE SEITE, DIE ERKLAERT, WAS ES HIER GIBT
=====================================================================
Auftrag vom 21.09.2026, Abschnitte 7 und 8.
WAS HIER GEMESSEN WIRD — und warum gerade das:
(1) DASS DIE ERKLAERUNG ZU DEN RECHTEN PASST. Abschnitt 8 verlangt
woertlich, die Erklaerung solle sich "automatisch an die
tatsaechlich vorhandenen Rechte" anpassen. Der Fehler, den man
dabei macht, ist eine gepflegte Liste: Sie stimmt am ersten Tag
und erzaehlt am zweiten von einer Seite, die es fuer diesen
Menschen nie gab. Geprueft wird deshalb nicht "steht da etwas",
sondern: JEDE erklaerte Kachel muss diese Person auch oeffnen
duerfen — und zwar gemessen an derselben Rechtetafel, die der
Server durchsetzt.
(2) DIE GEGENPROBE DAZU. Eine Pruefung, die nur "alles erlaubt"
bestaetigt, bestaetigt nichts. Also wird ausdruecklich gezaehlt,
dass ein Modi die Seiten, die er NICHT hat (Personen, Hilfe,
Bewerbungen), auch nicht erklaert bekommt — und dass DogFather
mehr erklaert bekommt als er.
(3) DASS DIE ROLLEN WIRKLICH VERSCHIEDENE TEXTE BEKOMMEN. "Sie muss
vollstaendig rollenbasiert aufgebaut werden" ist nur erfuellt,
wenn sich die Texte auch unterscheiden. Vier gleiche Absaetze
mit vier verschiedenen Namen davor waeren keine Rollenseite.
(4) DASS DIE AUSGELIEFERTE DATEI KEINE ROLLENNAMEN TRAEGT. Sie geht
an jeden Browser. Steht die Aufteilung dort, ist sie lesbar und
veraltet ausserdem. Dieselbe Regel wie bei pruef-modi-wortleck.
(5) UND AM ECHTEN BILDSCHIRM, dass ein Mensch es lesen und antippen
kann. Ein richtiger Datensatz, den niemand sieht, ist kein
Erfolg.
===================================================================== */
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import http from "node:http";
const ordner = mkdtempSync(join(tmpdir(), "ws-willk-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
import { notbremse } from "./helfer-notbremse.mjs";
import { zertifikatBauen } from "./helfer-openssl.mjs";
const { eigenerPort } = await import("./helfer-port.mjs");
const PORT = await eigenerPort(import.meta, "die Willkommenspruefung");
process.env.PORT = `${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 }); };
await import("./index.js");
notbremse(300_000, "pruef-willkommen");
await new Promise((r) => setTimeout(r, 1200));
let fehler = 0;
const melde = (t) => console.log(t);
const ok = (b, t) => { melde((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
const HOST = "crew.dogfather-universe.com";
function rufe(pfad, wie = {}) {
return new Promise((fertig) => {
const daten = wie.body === undefined ? null : JSON.stringify(wie.body);
const a = http.request({
host: "127.0.0.1", port: PORT, path: pfad, method: wie.method || "GET",
headers: {
Host: HOST,
...(daten ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(daten) } : {}),
...(wie.keks ? { Cookie: wie.keks } : {}),
Origin: `https://${HOST}`, Referer: `https://${HOST}/workspace/`,
},
}, (a2) => {
let t = "";
a2.on("data", (c) => (t += c));
a2.on("end", () => fertig({
code: a2.statusCode, kopf: a2.headers, text: t,
json: (() => { try { return JSON.parse(t); } catch { return null; } })(),
}));
});
a.on("error", (e) => fertig({ code: 0, text: e.message, json: null }));
if (daten) a.write(daten);
a.end();
});
}
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes } = await import("node:crypto");
await rufe("/workspace/api/anmelden", { method: "POST", body: { rolle: "admin", code: "x" } });
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 jetzt = 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, jetzt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
anlegen("Filipe", "admin", "CODE-DOGI-0001");
anlegen("Funny", "hand", "CODE-HAND-0001");
anlegen("Lino", "linke", "CODE-LINK-0001");
anlegen("Kessi", "modi", "CODE-MOD1-0001");
/* Ein Gast muss sein Alter bestaetigt haben, sonst kommt er nicht durch. */
const idGast = anlegen("Robin", "gast", "CODE-GAST-0001");
d.prepare("UPDATE personen SET alter_bestaetigt_am = ? WHERE id = ?").run(jetzt, idGast);
d.close();
async function alsWer(rolle, code) {
const a = await rufe("/workspace/api/anmelden", { method: "POST", body: { rolle, code } });
return (a.kopf?.["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ");
}
/* ---------------------------------------------------------------------
1. DIE SCHRANKE
--------------------------------------------------------------------- */
melde("\n=== Die Schranke ===");
{
const ohne = await rufe("/workspace/api/willkommen");
ok(ohne.code === 401, `unangemeldet gibt es nichts (${ohne.code})`);
}
/* ---------------------------------------------------------------------
2. JEDE ROLLE BEKOMMT IHRE EIGENE SEITE
--------------------------------------------------------------------- */
melde("\n=== Jede Rolle bekommt ihre eigene Seite ===");
const LEUTE = [
{ rolle: "admin", code: "CODE-DOGI-0001", name: "Filipe" },
{ rolle: "hand", code: "CODE-HAND-0001", name: "Funny" },
{ rolle: "linke", code: "CODE-LINK-0001", name: "Lino" },
{ rolle: "modi", code: "CODE-MOD1-0001", name: "Kessi" },
{ rolle: "gast", code: "CODE-GAST-0001", name: "Robin" },
];
const gesehen = {};
for (const p of LEUTE) {
const keks = await alsWer(p.rolle, p.code);
const a = await rufe("/workspace/api/willkommen", { keks });
gesehen[p.rolle] = { keks, daten: a.json };
ok(a.code === 200, `${p.rolle}: die Seite antwortet (${a.code})`);
ok(a.json?.gruss === `Willkommen, ${p.name}!`,
` und begruesst mit Namen ("${a.json?.gruss}")`);
ok(!!a.json?.einleitung?.was && !!a.json?.einleitung?.dazu,
" mit einer Einleitung, die aus zwei Teilen besteht");
}
/* DIE EINLEITUNGEN MUESSEN SICH UNTERSCHEIDEN. Sonst ist es keine
rollenbasierte Seite, sondern eine mit Namensfeld. */
{
const saetze = LEUTE.map((p) => gesehen[p.rolle]?.daten?.einleitung?.was);
const verschieden = new Set(saetze).size;
ok(verschieden === LEUTE.length,
`${verschieden} von ${LEUTE.length} Einleitungen sind verschieden`);
}
/* ---------------------------------------------------------------------
3. DER KERN: ERKLAERT WIRD NUR, WAS MAN AUCH DARF
---------------------------------------------------------------------
Gemessen gegen die Rechtetafel selbst, nicht gegen eine Liste in
dieser Datei -- sonst pruefe ich meine Abschrift gegen seine. */
melde("\n=== Erklaert wird nur, was man auch oeffnen darf ===");
const { darfSeite } = await import("./rechte.js");
const { bereicheFuer, zusatzBereicheFuer } = await import("./workspace.js");
const { istCrewAdresse } = await import("./crew-adresse.js");
/* DAS HAUS GEHOERT DAZU, und es wird ABGELEITET wie beim Server.
Gemessen am 21.09.: DogFather hat auf der Crew-Adresse 30 Kacheln
und auf der Agenturadresse 1 (die Tuer hinueber) -- er ist die
einzige Rolle, die in beiden Haeusern lebt. Eine Vergleichsrechnung
ohne Haus misst deshalb ausgerechnet bei ihm das falsche Haus und
meldet einen Fehler, den es nicht gibt. */
const HAUS = istCrewAdresse(HOST) ? "crew" : "agentur";
const alsPerson = (rolle) => ({ rolle, haus: HAUS });
for (const p of LEUTE) {
const daten = gesehen[p.rolle]?.daten;
const kacheln = (daten?.gruppen || []).flatMap((g) => g.kacheln || []);
ok(kacheln.length > 0, `${p.rolle}: es wird ueberhaupt etwas erklaert (${kacheln.length})`);
const verboten = kacheln.filter((k) => {
const ziel = String(k.ziel || "").split("?")[0];
if (!ziel || /^https?:/.test(ziel)) return false;
return !darfSeite(alsPerson(p.rolle), "/workspace/" + ziel);
});
ok(verboten.length === 0, verboten.length === 0
? " keine einzige davon ist fuer ihn gesperrt"
: ` ERKLAERT GESPERRTE SEITEN: ${verboten.map((v) => v.ziel).join(", ")}`);
/* Und die andere Richtung: die Zahl muss zu seinen echten Kacheln
passen. Zu WENIG waere genauso falsch -- dann verschweigt die
Seite etwas, das er hat. */
const roh = [
...(bereicheFuer(alsPerson(p.rolle)) || []),
...(zusatzBereicheFuer(alsPerson(p.rolle)) || []),
];
const sollte = roh.filter((k) => {
if (k?.aussen) return true;
const ziel = String(k?.ziel || "");
if (!ziel) return false;
return darfSeite(alsPerson(p.rolle), "/workspace/" + ziel.split("?")[0]);
}).length;
ok(kacheln.length === sollte,
` und es sind genau seine ${sollte} Kacheln (erklaert: ${kacheln.length})`);
}
/* GEGENPROBE MIT ZAHLEN: Kann die Pruefung ueberhaupt Unterschiede
sehen? Wenn alle gleich viel bekaemen, waere oben nichts bewiesen. */
{
const zahl = (r) => (gesehen[r]?.daten?.gruppen || []).flatMap((g) => g.kacheln || []).length;
const chef = zahl("admin"), modi = zahl("modi"), gast = zahl("gast");
ok(chef > modi && modi > gast,
`DogFather ${chef} > Modi ${modi} > Community ${gast} -- die Seite unterscheidet wirklich`);
/* DIE DREI, DIE EIN MODI NICHT HAT. Namentlich, damit ein stiller
Durchrutscher auffaellt. */
const modiZiele = (gesehen.modi?.daten?.gruppen || [])
.flatMap((g) => g.kacheln || []).map((k) => String(k.ziel).split("?")[0]);
for (const seite of ["personen.html", "bewerbungen.html", "rechte.html"]) {
ok(!modiZiele.includes(seite), `einem Modi wird "${seite}" nicht erklaert`);
}
/* Und der Gegenbeweis, dass diese Namen ueberhaupt vorkommen koennen: */
const chefZiele = (gesehen.admin?.daten?.gruppen || [])
.flatMap((g) => g.kacheln || []).map((k) => String(k.ziel).split("?")[0]);
ok(chefZiele.includes("personen.html"),
"DogFather dagegen schon (sonst waere die Probe oben wertlos)");
}
/* ---------------------------------------------------------------------
4. DIE LINKE HAND BEKOMMT IHRE GRENZEN GESAGT
--------------------------------------------------------------------- */
melde("\n=== Niemand erfaehrt, was er NICHT hat ===");
{
/* UMGEDREHT AM 22.09.2026. Hier stand: "ihr wird gesagt, was sie
nicht hat -- einmal, klar, ohne Liste."
Filipe: "keiner soll wissen was er nicht sieht das geht keinen was
an. sie sehen die sachen ja nicht weil es sie nichts angeht und das
muss auch nicht erwaehnt werden. kontrollier das ueberall bitte."
Das "ueberall" ist der Grund, warum hier nicht eine Zeile
umgedreht, sondern JEDE ROLLE gemessen wird. Ein Satz, der
aufzaehlt, was fehlt, hebt ausserdem die Sorgfalt wieder auf, mit
der der Server ueberall 404 statt 403 antwortet -- damit ein "das
darfst du nicht" gar nicht erst verraet, dass es etwas gibt. */
const VERRAETERISCH = [
/nicht f(u|ue|\u00fc)r dich freigeschaltet/i,
/was du nicht siehst/i,
/was du nicht hast/i,
/hast du nicht/i,
/siehst du nicht/i,
/darfst du nicht/i,
/bleiben seine/i,
/kein zugriff/i,
];
const gefunden = [];
for (const pp of LEUTE) {
const d = gesehen[pp.rolle]?.daten;
const text = [
d?.einleitung?.was, d?.einleitung?.dazu,
...(d?.hinweise || []).flatMap((h) => [h.titel, h.text]),
].filter(Boolean).join(" ");
for (const m of VERRAETERISCH) {
if (m.test(text)) gefunden.push(pp.rolle + ": " + m.source);
}
}
ok(gefunden.length === 0, gefunden.length === 0
? `keine der ${LEUTE.length} Rollen bekommt gesagt, was ihr fehlt`
: `ZAEHLT AUF, WAS FEHLT: ${gefunden.join(" | ")}`);
/* GEGENPROBE: Wuerde dieses Muster so einen Satz ueberhaupt finden?
Ohne sie waere die Zeile oben gruen, sobald die Liste kaputt ist. */
ok(VERRAETERISCH.some((m) => m.test("Das ist nicht fuer dich freigeschaltet.")),
"und ein solcher Satz wuerde hier wirklich auffallen");
const ziele = (gesehen.linke?.daten?.gruppen || [])
.flatMap((g) => g.kacheln || []).map((k) => String(k.ziel).split("?")[0]);
ok(!ziele.includes("personen.html") && !ziele.includes("hilfe.html"),
"und Personen/Meldeweg stehen nicht in ihrer Erklaerung");
ok(ziele.includes("aufgaben.html"),
"die Aufgaben dagegen schon");
}
/* ---------------------------------------------------------------------
5. DIE AUSGELIEFERTE DATEI TRAEGT KEINE ROLLENAUFTEILUNG
--------------------------------------------------------------------- */
melde("\n=== Was im Browser landet ===");
{
const js = readFileSync(new URL("../workspace/assets/js/willkommen.js", import.meta.url), "utf8");
const html = readFileSync(new URL("../workspace/willkommen.html", import.meta.url), "utf8");
const ohneKommentar = js
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/(^|[^:])\/\/.*$/gm, "$1");
for (const wort of ["admin", "linke", "gast"]) {
const treffer = new RegExp("[\"'`]" + wort + "[\"'`]").test(ohneKommentar);
ok(!treffer, `die Datei kennt "${wort}" nicht (sie kommt vom Server)`);
}
ok(!/aufgaben\.html|personen\.html/.test(ohneKommentar),
"und fuehrt auch keine eigene Kachelliste");
ok(html.includes("willkommen.css"), "die Seite laedt ihre Gestaltung");
ok(html.includes("meldung.js"), "und den gemeinsamen Meldeweg");
/* GEGENPROBE: Findet dieses Muster ueberhaupt etwas? Sonst waere die
Aussage oben nur die Bestaetigung eines kaputten Suchmusters. */
const probe = 'const x = "admin";';
ok(new RegExp("[\"'`]admin[\"'`]").test(probe),
"das Suchmuster wuerde einen Rollennamen finden");
}
/* ---------------------------------------------------------------------
6. DIE KACHEL FUEHRT HIN
--------------------------------------------------------------------- */
melde("\n=== Der Weg zur Seite ===");
{
const kacheln = [
...(bereicheFuer(alsPerson("gast")) || []),
...(zusatzBereicheFuer(alsPerson("gast")) || []),
];
const willk = kacheln.filter((k) => String(k.ziel || "").startsWith("willkommen.html"));
ok(willk.length === 1, `die Community hat genau eine Kachel dorthin (${willk.length})`);
ok(willk[0]?.name === "Willkommen", `und sie heisst so (${willk[0]?.name})`);
/* DAS TREFF-BRETT DARF NICHT STILL VERSCHWINDEN. Gemessen am
21.09.: null Eintraege, 29 Chatnachrichten daneben -- geredet wird
im Chat. Falls das Brett je wieder eine Kachel bekommt, soll diese
Zeile daran erinnern, dass hier einmal eine war. */
const brett = kacheln.filter((k) => String(k.ziel || "").includes("b=treff"));
ok(brett.length === 0,
`das leere Rudel-Brett hat keine eigene Kachel mehr (${brett.length})`);
const chat = kacheln.filter((k) => String(k.ziel || "").includes("chat.html"));
ok(chat.length >= 1, `der Rudel-Chat dagegen schon (${chat.length})`);
}
/* ---------------------------------------------------------------------
7. AM ECHTEN BILDSCHIRM
--------------------------------------------------------------------- */
melde("\n=== Am echten Bildschirm ===");
{
const { execFileSync } = await import("node:child_process");
const { createServer: httpsServer } = await import("node:https");
const { request: httpAnfrage } = await import("node:http");
const schluessel = join(ordner, "pruef.key");
const zertifikat = join(ordner, "pruef.crt");
zertifikatBauen(schluessel, zertifikat, HOST);
const HTTPS_PORT = PORT + 1;
const vorbau = httpsServer({
key: readFileSync(schluessel), cert: readFileSync(zertifikat),
}, (von, nach) => {
const weiter = httpAnfrage({
host: "127.0.0.1", port: PORT, path: von.url, method: von.method,
headers: { ...von.headers, host: `${HOST}:${HTTPS_PORT}` },
}, (antwort) => {
nach.writeHead(antwort.statusCode || 502, antwort.headers);
antwort.pipe(nach);
});
von.pipe(weiter);
});
await new Promise((r) => vorbau.listen(HTTPS_PORT, "127.0.0.1", r));
const SEITE = `https://${HOST}:${HTTPS_PORT}`;
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await pw.chromium.launch({
args: [`--host-resolver-rules=MAP ${HOST} 127.0.0.1`, "--ignore-certificate-errors"],
});
const seitenFehler = [];
async function alsSeite(rolle, code, breite = 1280) {
const ctx = await browser.newContext({ viewport: { width: breite, height: 950 } });
const s = await ctx.newPage();
s.on("pageerror", (e) => seitenFehler.push(`${rolle}: ${String(e).slice(0, 110)}`));
await s.goto(`${SEITE}/workspace/`, { waitUntil: "networkidle" });
await s.click(`.rolle[data-rolle="${rolle}"]`);
await s.fill("#code", code);
await s.click("button[type=submit]");
await s.waitForTimeout(800);
await s.goto(`${SEITE}/workspace/willkommen.html`, { waitUntil: "networkidle" });
await s.waitForTimeout(900);
return { ctx, seite: s };
}
/* ---- Ein Modi, am Handy ---- */
const pModi = await alsSeite("modi", "CODE-MOD1-0001", 390);
try {
const bild = await pModi.seite.evaluate(() => {
const kacheln = [...document.querySelectorAll(".w-kachel")];
const breite = document.documentElement.scrollWidth;
return {
gruss: document.getElementById("gruss")?.textContent || "",
rolle: document.getElementById("rollentext")?.textContent || "",
was: document.getElementById("einleitung-was")?.textContent || "",
kacheln: kacheln.length,
mitLang: kacheln.filter((k) => k.querySelector(".w-kachel__lang")).length,
laedtWeg: document.getElementById("laedt")?.hidden === true,
busy: document.getElementById("haupt")?.getAttribute("aria-busy"),
breite,
ueberlauf: kacheln.filter((k) => k.getBoundingClientRect().right > breite + 1).length,
zuKlein: kacheln.filter((k) => k.getBoundingClientRect().height < 44).length,
ersteZiel: kacheln[0]?.getAttribute("href") || "",
};
});
ok(bild.gruss.includes("Kessi"), `der Name steht oben ("${bild.gruss}")`);
ok(bild.was.length > 20, "darunter ein Satz, der etwas sagt");
ok(bild.kacheln > 0, `${bild.kacheln} Kacheln werden erklaert`);
ok(bild.mitLang > 0, `davon ${bild.mitLang} mit ausfuehrlichem Satz`);
ok(bild.laedtWeg, "der Ladehinweis ist weg");
ok(bild.busy !== "true", "und die Seite meldet sich nicht mehr als ladend");
ok(bild.breite <= 390, `nichts sprengt das Handy (${bild.breite}px)`);
ok(bild.ueberlauf === 0, `keine Kachel ragt hinaus (${bild.ueberlauf})`);
ok(bild.zuKlein === 0, `jede ist mindestens 44px hoch (${bild.zuKlein} zu klein)`);
/* SIE MUSS AUCH HINFUEHREN. Eine Erklaerung ohne Weg ist ein
Prospekt. */
await pModi.seite.click(".w-kachel");
await pModi.seite.waitForTimeout(700);
const wohin = pModi.seite.url();
ok(!wohin.includes("willkommen.html"),
`ein Antippen fuehrt wirklich hin (${wohin.split("/").pop()})`);
} finally { await pModi.ctx.close(); }
/* ---- DogFather, am Rechner ---- */
const pChef = await alsSeite("admin", "CODE-DOGI-0001");
try {
const bild = await pChef.seite.evaluate(() => ({
gruss: document.getElementById("gruss")?.textContent || "",
kacheln: document.querySelectorAll(".w-kachel").length,
gruppen: [...document.querySelectorAll(".w-gruppe__titel")].map((t) => t.textContent),
hinweise: document.querySelectorAll(".w-hinweis").length,
}));
ok(bild.gruss.includes("Filipe"), `sein Name steht oben ("${bild.gruss}")`);
ok(bild.gruppen.length >= 2,
`nach Gruppen geordnet (${bild.gruppen.join(" · ")})`);
ok(bild.hinweise > 0, `mit ${bild.hinweise} Hinweisen zu seiner Rolle`);
ok(bild.kacheln > 10, `und allem, was er hat (${bild.kacheln})`);
} finally { await pChef.ctx.close(); }
ok(seitenFehler.length === 0, seitenFehler.length === 0
? "kein Skript ist unterwegs gestolpert"
: `Skriptfehler: ${seitenFehler.join(" | ")}`);
await browser.close();
await new Promise((r) => vorbau.close(r));
}
melde(fehler === 0
? "\nAlles in Ordnung.\n"
: `\n${fehler} FEHLER.\n`);
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
process.exit(fehler ? 1 : 0);