Auftrag vom 21.09.2026, Abschnitte 7 und 8. Aus der Kachel "Der Treff" wird die Willkommensseite. WARUM DAS NICHTS VERLIERT, gemessen am echten Bestand: Das Brett "treff" hatte NULL Eintraege, der Treff-Chat daneben 29 Nachrichten. Geredet wird im Chat; das Brett war eine Kachel ohne Inhalt. DIE SEITE ERKLAERT NICHT "DIE WEBSITE", SONDERN DEINE. Sie baut sich aus derselben Kachelliste wie die Startseite, durch denselben Filter (darfSeite). Wer morgen eine Kachel bekommt, bekommt automatisch auch ihre Erklaerung -- und wer eine nicht hat, liest nicht davon. Eine Liste von Dingen, die man nicht darf, ist keine Orientierung. Gemessen: DogFather 30 Kacheln, Modi 25, Community 11 -- und keine einzige davon ist fuer den Betreffenden gesperrt. DREI FEHLER, DIE ERST DAS BILDSCHIRMFOTO GEZEIGT HAT: body class="gate" ist die ANMELDEWAND (display:flex, zentriert). Kopfleiste und Inhalt standen dadurch NEBENEINANDER -- am Handy blieben dem Text 260 von 390 Pixeln. .huelle gibt es im Haus gar nicht. Der Inhalt stand linksbuendig statt mittig, ohne Sicherheitsrand an randlosen Geraeten. Jetzt .inhalt wie jede andere Seite. Der Text stand direkt auf dem Buehnenbild: Kontrast 1.68:1 am Rechner, 2.23:1 am Handy (noetig 4.5). Die Hausregel stand daneben in start.css -- "der Text steht auf eigenen Flaechen" -- und diese Seite hielt sich als einzige nicht daran. Jetzt 5.19 und 7.73. UND EIN LECK, ZUM DRITTEN MAL DIESELBE URSACHE: Die Kachel umzuleiten nahm dem Brett "treff" die Zugehoerigkeit -- TREFF_BRETTER wird aus Kachelzielen abgeleitet. Die Schranke laesst eine AGENTURROLLE unbesehen durch, wenn ein Brett in keiner Liste steht: Eine Creatorin konnte das Brett des Treffs lesen. Behoben, und diesmal dauerhaft laut gemacht: Nachgemessen ist die Trennung exakt und in beide Richtungen deckungsgleich -- die Bretter mit `fuerAlle` sind genau die des Treffs plus die Agenturablage. Das ist eine Eigenschaft des BRETTS und wandert nicht, wenn jemand eine Kachel umleitet. pruef-treff prueft das jetzt. Mein erster Entwurf der Regel war zu breit und meldete `content` und `schutz` -- beides Agenturbretter, die eine Agenturrolle zu Recht liest. Eine Pruefung, die zu viel meldet, wird abgeschaltet. AUSSERDEM: 16 Kennungen aus workspace-zuteilung.js hatten keinen deutschen Satz -- auf dem Bildschirm haette woertlich "nicht_zugeteilt" gestanden. Gefunden von pruef-meldungen am selben Tag. Und zwei Erwartungen in pruef-treff abgeleitet statt gezaehlt: die Zahl der Rollenkacheln auf der Wand (die Anmeldung sucht WHERE rolle=?, eine Rolle ohne Kachel ist unerreichbar) und "die breite Kachel steht vorne" ueber die Eigenschaft statt ueber den Namen. Geprueft: pruef-willkommen 66/0 (neu), pruef-treff 80/0 (war 59 ok / 13 FEHL), pruef-buehne fuer die neue Seite 10/0, pruef-meldungen 8/0, pruef-css-klassen 30/0, pruef-rechtetafel 19/0.
425 lines
19 KiB
JavaScript
425 lines
19 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";
|
|
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=== Die linke Hand ===");
|
|
{
|
|
const h = gesehen.linke?.daten?.hinweise || [];
|
|
ok(h.some((x) => /nicht/i.test(x.titel + x.text)),
|
|
"ihr wird gesagt, was sie nicht hat -- einmal, klar, ohne Liste");
|
|
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 Treff-Brett hat keine eigene Kachel mehr (${brett.length})`);
|
|
const chat = kacheln.filter((k) => String(k.ziel || "").includes("chat.html"));
|
|
ok(chat.length >= 1, `der Treff-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");
|
|
execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes",
|
|
"-keyout", schluessel, "-out", zertifikat, "-days", "2",
|
|
"-subj", `/CN=${HOST}`, "-addext", `subjectAltName=DNS:${HOST}`],
|
|
{ stdio: "ignore" });
|
|
|
|
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);
|