diff --git a/server/pruef-nachfrage.mjs b/server/pruef-nachfrage.mjs
index e31c5142..40898a7f 100644
--- a/server/pruef-nachfrage.mjs
+++ b/server/pruef-nachfrage.mjs
@@ -58,6 +58,7 @@
import { readFileSync, readdirSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
+import { bestaetige, nachfrageText } from "./helfer-nachfrage.mjs";
const HIER = dirname(fileURLToPath(import.meta.url));
const WURZEL = join(HIER, "..");
@@ -268,6 +269,169 @@ pruef("Gegenprobe: der Klammerzähler findet das Ende auch bei Verschachtelung",
aufrufe("frageNach({ titel: f({a:1}), was: 'x' }) ; danach")[0]
=== "{ titel: f({a:1}), was: 'x' }");
+/* =====================================================================
+ UND JETZT AM ECHTEN BILDSCHIRM
+
+ Alles oben ist Textsuche. Sie beweist, dass der Aufruf dasteht --
+ nicht, dass der Dialog aufgeht, dass "Doch nicht" wirklich nichts
+ tut und dass das Abtippen wirklich sperrt.
+
+ Geprüft wird ausgerechnet der gefährlichste Handgriff im Haus: eine
+ Person löschen. `pruef-personen-loeschen` gibt es zwar, aber sie
+ kennt nur die Schnittstelle -- kein Browser, kein Knopf, kein
+ Dialog. Der Weg, den ein Mensch geht, war nie geprüft.
+ ===================================================================== */
+if (process.env.OHNE_BROWSER === "ja") {
+ console.log("\n (Browserteil übersprungen — OHNE_BROWSER=ja)");
+} else {
+ const { mkdtempSync, rmSync } = await import("node:fs");
+ const { tmpdir } = await import("node:os");
+ const { portMussFreiSein } = await import("./helfer-port.mjs");
+ const { notbremse } = await import("./helfer-notbremse.mjs");
+
+ const ordner = mkdtempSync(join(tmpdir(), "ws-nachfrage-"));
+ process.env.WORKSPACE_DB = join(ordner, "workspace.db");
+ const PORT = await portMussFreiSein(4329, "die Nachfragepruefung");
+ process.env.PORT = String(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(240_000, "pruef-nachfrage");
+ await new Promise((r) => setTimeout(r, 1000));
+ const BASIS = `http://127.0.0.1:${PORT}`;
+
+ const { DatabaseSync } = await import("node:sqlite");
+ const { scryptSync, randomBytes } = await import("node:crypto");
+ await fetch(BASIS + "/workspace/api/anmelden", {
+ method: "POST", headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
+ for (let i = 0; i < 60; i++) {
+ try {
+ const pb = new DatabaseSync(process.env.WORKSPACE_DB);
+ try { pb.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { pb.close(); }
+ } catch { await new Promise((r) => setTimeout(r, 250)); }
+ }
+ const db = new DatabaseSync(process.env.WORKSPACE_DB);
+ const jetzt = new Date().toISOString();
+ const anlegen = (name, rolle, code) => {
+ const salt = randomBytes(16).toString("hex");
+ const h = scryptSync(code, salt, 64,
+ { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
+ db.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt)"
+ + " VALUES (?,?,?,?,?,1,?)").run(name, rolle, h, salt, 32768, jetzt);
+ };
+ anlegen("Filipe", "admin", "CODE-DOGI-0001");
+ anlegen("Testolina", "creator", "CODE-CREA-0001");
+ db.close();
+
+ const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
+ const browser = await pw.chromium.launch();
+ const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
+ const seite = await ctx.newPage();
+ const abstuerze = [];
+ seite.on("pageerror", (f) => abstuerze.push(f.message));
+
+ await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
+ await seite.click('.rolle[data-rolle="admin"]');
+ await seite.fill("#code", "CODE-DOGI-0001");
+ await seite.click("button[type=submit]");
+ await seite.waitForURL("**/start.html", { timeout: 25000 });
+ await seite.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" });
+ await seite.waitForTimeout(900);
+
+ console.log("\n --- Am echten Bildschirm: eine Person löschen ---");
+
+ const wieViele = () => seite.evaluate(() =>
+ [...document.querySelectorAll(".person")].length);
+ /* DEN ABSCHNITT AUFKLAPPEN. Die Rollen-Abschnitte sind
+ standardmäßig ZU -- nur der eigene ist offen. Das ist so gewollt
+ (eine Liste mit fünf offenen Abschnitten ist unlesbar), und eine
+ Prüfung, die das nicht weiß, misst eine leere Liste und hält das
+ für einen Befund. Genau das ist beim Bauen passiert. */
+ await seite.waitForSelector('.gruppe[data-rolle="creator"] .gruppe__kopf',
+ { timeout: 15_000 });
+ await seite.click('.gruppe[data-rolle="creator"] .gruppe__kopf');
+ await seite.waitForFunction(
+ () => document.querySelectorAll(".person").length >= 2,
+ { timeout: 15_000 }).catch(() => {});
+ const vorher = await wieViele();
+ const namen2 = await seite.evaluate(() =>
+ [...document.querySelectorAll(".person")]
+ .map((e) => e.textContent.trim().slice(0, 30)).join(" | "));
+ pruef("die Liste steht da", vorher >= 2, `${vorher} Personen: ${namen2}`);
+
+ const loeschKnopf = seite.locator(".person", { hasText: "Testolina" })
+ .locator("button", { hasText: "Löschen" }).first();
+ await loeschKnopf.click();
+
+ let dialogDa = true;
+ try { await seite.waitForSelector("dialog.nachfrage[open]", { timeout: 4000 }); }
+ catch { dialogDa = false; }
+ pruef("der Dialog geht auf", dialogDa);
+
+ if (dialogDa) {
+ const inhalt = await nachfrageText(seite);
+ pruef("er nennt den Namen im Titel", /Testolina/.test(inhalt.titel), inhalt.titel);
+ pruef("er sagt, dass es endgültig ist", inhalt.endgueltig);
+ pruef("er sagt, WAS passiert", inhalt.was.length > 10, inhalt.was);
+ pruef("und was BLEIBT", inhalt.bleibt.length > 10, inhalt.bleibt);
+
+ /* DER RIEGEL: Ein falsch abgetippter Name darf NICHTS loeschen. */
+ await seite.fill("#nachfrage-tipp", "Testolinaa");
+ await seite.click('dialog.nachfrage [data-teil="ja"]');
+ await seite.waitForTimeout(500);
+ const nochOffen = await seite.evaluate(() =>
+ !!document.querySelector("dialog.nachfrage[open]"));
+ pruef("ein falsch abgetippter Name schließt den Dialog NICHT", nochOffen);
+ const fehlertext = await seite.evaluate(() =>
+ document.querySelector('dialog.nachfrage [data-teil="fehler"]')?.textContent?.trim() || "");
+ pruef("und sagt, was erwartet wird", /Testolina/.test(fehlertext), fehlertext);
+ pruef("die Person ist noch da", await wieViele() === vorher);
+
+ /* Der Knopf daneben muss wirklich nichts tun. */
+ await seite.click('dialog.nachfrage [data-teil="nein"]');
+ await seite.waitForTimeout(400);
+ pruef("der Abbruchknopf schließt den Dialog",
+ !await seite.evaluate(() => !!document.querySelector("dialog.nachfrage[open]")));
+ pruef("und löscht nichts", await wieViele() === vorher);
+
+ /* Und jetzt richtig. */
+ await bestaetige(seite, () => loeschKnopf.click(), { tippen: "Testolina" });
+ await seite.waitForTimeout(1400);
+ const nachher = await wieViele();
+ pruef("mit richtig abgetipptem Namen wird gelöscht",
+ nachher === vorher - 1, `${vorher} → ${nachher}`);
+ }
+
+ /* Und Esc bricht ab -- auf einem Dialog OHNE Abtippen. */
+ await seite.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" });
+ await seite.waitForTimeout(700);
+ await seite.click("#abmelden");
+ let abmeldeDialog = true;
+ try { await seite.waitForSelector("dialog.nachfrage[open]", { timeout: 4000 }); }
+ catch { abmeldeDialog = false; }
+ pruef("auch Abmelden fragt nach", abmeldeDialog);
+ if (abmeldeDialog) {
+ const t = await nachfrageText(seite);
+ pruef("und nennt den Grund: der Zugangscode", /Zugangscode/.test(t.was), t.was);
+ await seite.keyboard.press("Escape");
+ await seite.waitForTimeout(800);
+ pruef("Esc bricht ab – man bleibt angemeldet",
+ seite.url().includes("start.html"), seite.url());
+ }
+
+ pruef("keine Abstürze im Browser", abstuerze.length === 0, abstuerze[0] || "");
+
+ await browser.close();
+ try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
+}
+
console.log(`\n ${ok} bestanden, ${fehl} fehlgeschlagen`);
console.log(` (${gezaehlt} Rückfragen in ${nutzer.size} Dateien, `
+ `${mitNutzung} Seiten)\n`);
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index e776fb07..eac9bbc4 100644
--- a/workspace/anruf-probe.html
+++ b/workspace/anruf-probe.html
@@ -16,15 +16,15 @@
verdeckt. Gefunden hat es pruef-struktur.
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
-
-
+
+
-
+