/* ===================================================================== DIE PERSONENKACHELN AUF DER ENTWICKLUNGSSEITE (17.09.2026) Filipe, mit zwei Bildschirmfotos nebeneinander: "wieso ist diese seite noch nicht im stil und perfektioniert wie die auf screen2 ... aber so dass ich die leute in einer kachel aussuchen kann alle einzel." Der Unterschied zur Checkliste lag nicht im Aufbau -- der stimmte -- sondern an zwei Stellen: 1. Die Zahlen standen als graue Textzeilen da, drueben als abgesetzte Kaesten mit grosser Ziffer (`bilanz-zahl`). 2. Eine angeklickte Person sah aus wie eine nicht angeklickte. Wer scrollte, wusste nicht mehr, wen er offen hat. --------------------------------------------------------------------- WAS HIER GEPRUEFT WIRD -- UND WARUM NICHT DAS AUSSEHEN Ob etwas "gleich aussieht", laesst sich nicht messen. Die drei Fragen darunter schon, und sie sind das, was das Aussehen traegt: - Benutzt die Kachel den HAUSBAUSTEIN (`bilanz-zahl`) oder einen Nachbau? Ein Nachbau laeuft auseinander, sobald jemand eins von beiden anfasst. - Stimmen die Zahlen? "angesehen" plus "noch offen" MUSS die Gesamtzahl ergeben. Zwei Zahlen, die nebeneinander stehen und nicht zusammenpassen, sind schlimmer als eine. - Sieht man, wer gewaehlt ist? Und zwar so, dass es auch ein Vorleseprogramm sagen kann. MIT GEGENPROBE: Vor dem ersten Klick darf KEINE Kachel gewaehlt sein. Sonst hiesse "eine ist gewaehlt" nur, dass immer eine gewaehlt ist. Aufrufen mit: node server/pruef-entwicklung-kacheln.mjs Bild: SCHIRM=1 node server/pruef-entwicklung-kacheln.mjs ===================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { portMussFreiSein } from "./helfer-port.mjs"; const PORT = await portMussFreiSein(4475, "pruef-entwicklung-kacheln"); const ordner = mkdtempSync(join(tmpdir(), "ws-ekach-")); 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-entwicklung-kacheln"); 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 { 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); return d.prepare("SELECT last_insert_rowid() AS id").get().id; } anlegen("Filipe", "admin", "CODE-DOGI-0001"); anlegen("VanVan", "hand", "CODE-HAND-0001"); anlegen("Diene", "modi", "CODE-MODI-0001"); d.close(); const an = await fetch(BASIS + "/workspace/api/anmelden", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" }), }); const keks = (an.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; "); 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: 1280, height: 900 } }); await kontext.addCookies(keks.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()); }); await seite.goto(`${BASIS}/workspace/entwicklung.html`, { waitUntil: "networkidle" }); await seite.waitForSelector("#personen .e-person", { timeout: 15_000 }); await seite.waitForTimeout(400); /* ===================================================================== 1. DIE REIHE UND IHRE KACHELN ===================================================================== */ melde(""); melde("=== 1. Die Reihe ==="); { const b = await seite.evaluate(() => { const reihe = document.getElementById("personen"); const kacheln = [...reihe.querySelectorAll(".e-person")]; return { schild: reihe.getAttribute("aria-label") || "", anzahl: kacheln.length, namen: kacheln.map((k) => k.querySelector(".e-person__name")?.textContent || ""), /* Der Hausbaustein, nicht ein Nachbau. */ bilanzJe: kacheln.map((k) => k.querySelectorAll(".bilanz-zahl").length), eigeneKlassen: kacheln.map((k) => k.querySelectorAll( "[class*='e-person__bilanz'] > :not(.bilanz-zahl)").length), }; }); ok(b.schild === "Person wählen", `die Reihe sagt, was sie ist ("${b.schild}")`); ok(b.anzahl === 2, `zwei Kacheln (${b.namen.join(", ")})`); /* DREI KAESTEN JE KACHEL -- dieselben drei wie auf der Checkliste. */ ok(b.bilanzJe.length > 0 && b.bilanzJe.every((n) => n === 3), `jede Kachel traegt drei Bilanz-Kaesten (${b.bilanzJe.join(", ")})`); /* UND NICHTS DANEBEN: Ein eigener Nachbau neben dem Hausbaustein waere genau die zweite Wahrheit, die hier vermieden wird. */ ok(b.eigeneKlassen.every((n) => n === 0), "und nichts Selbstgebautes daneben"); } /* ===================================================================== 2. STIMMEN DIE ZAHLEN? ===================================================================== */ melde(""); melde("=== 2. Die Zahlen ==="); { const z = await seite.evaluate(() => [...document.querySelectorAll("#personen .e-person")] .map((k) => { const werte = [...k.querySelectorAll(".bilanz-zahl")].map((b) => ({ n: Number(b.querySelector(".bilanz-zahl__n")?.textContent || "x"), text: b.querySelector(".bilanz-zahl__text")?.textContent || "", stufe: b.dataset.stufe || "", })); return { name: k.querySelector(".e-person__name")?.textContent || "", werte }; })); ok(z.every((k) => k.werte.every((w) => Number.isFinite(w.n))), "jede Ziffer ist eine Zahl"); ok(z.every((k) => k.werte[0].text === "angesehen" && k.werte[1].text === "noch offen" && k.werte[2].text === "verschieden gesehen"), `die drei heissen ueberall gleich (${z[0]?.werte.map((w) => w.text).join(" · ")})`); /* DIE ENTSCHEIDENDE RECHNUNG: Die ersten beiden teilen dasselbe Ganze. Gehen sie nicht auf, stehen zwei Zahlen nebeneinander, die einander widersprechen -- genau das, was auf der Startseite repariert wurde. Die Gesamtzahl kommt aus der Auskunft selbst, nicht aus einer festen Zahl hier. */ const gesamt = await seite.evaluate(async () => { const a = await fetch("/workspace/api/entwicklung/lage", { credentials: "same-origin" }); return a.ok ? (await a.json()).gesamt_fremd : -1; }); ok(gesamt > 0, `die Seite kennt ${gesamt} Punkte insgesamt`); const summenOk = z.every((k) => k.werte[0].n + k.werte[1].n === gesamt); ok(summenOk, `angesehen + noch offen = ${gesamt} bei jeder Kachel ` + `(${z.map((k) => `${k.name}: ${k.werte[0].n}+${k.werte[1].n}`).join(", ")})`); /* Der Ton gehoert zur Aussage: fertig ist gruen, halb ist es nicht. */ ok(z.every((k) => k.werte[0].stufe === (k.werte[1].n === 0 ? "gut" : "offen")), "fertig Angesehenes ist gruen, Halbes nicht"); } /* ===================================================================== 3. EINZELN AUSWAEHLBAR -- UND MAN SIEHT ES ===================================================================== */ melde(""); melde("=== 3. Auswaehlen ==="); { /* GEGENPROBE ZUERST: Vorher darf keine gewaehlt sein. */ const vorher = await seite.evaluate(() => [...document.querySelectorAll("#personen .e-person")] .map((k) => k.getAttribute("aria-pressed"))); ok(vorher.length > 0 && vorher.every((w) => w === "false"), `vor dem Klick ist keine gewaehlt (${vorher.join(", ")})`); await seite.click("#personen .e-person:nth-of-type(2)"); await seite.waitForTimeout(900); const nachher = await seite.evaluate(() => ({ gedrueckt: [...document.querySelectorAll("#personen .e-person")] .map((k) => k.getAttribute("aria-pressed")), wer: document.querySelector('#personen .e-person[aria-pressed="true"] .e-person__name') ?.textContent || "", karteOffen: !document.getElementById("karte")?.hidden, })); ok(nachher.gedrueckt.filter((w) => w === "true").length === 1, `genau eine ist gewaehlt (${nachher.gedrueckt.join(", ")})`); ok(nachher.wer === "Diene", `und zwar die angeklickte (${nachher.wer})`); ok(nachher.karteOffen, "ihre Karte steht offen"); /* UND DIE AUSWAHL WANDERT, statt sich zu sammeln. */ await seite.click("#personen .e-person:nth-of-type(1)"); await seite.waitForTimeout(900); const um = await seite.evaluate(() => document.querySelector('#personen .e-person[aria-pressed="true"] .e-person__name') ?.textContent || ""); const wieViele = await seite.evaluate(() => document.querySelectorAll('#personen .e-person[aria-pressed="true"]').length); ok(um === "VanVan" && wieViele === 1, `ein Klick weiter ist nur noch sie gewaehlt (${um}, ${wieViele} gewaehlt)`); } /* ===================================================================== 4. NICHTS KAPUTT ===================================================================== */ melde(""); melde("=== 4. Die Konsole ==="); ok(konsole.length === 0, konsole.length ? `Fehler in der Konsole: ${konsole[0]}` : "keine Fehler in der Konsole"); if (process.env.SCHIRM) { const pfad = fileURLToPath(new URL("pruef-entwicklung-kacheln.png", import.meta.url)); await seite.screenshot({ path: pfad, fullPage: false }); melde(" -- Bildschirmfoto: server/pruef-entwicklung-kacheln.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);