From e5eed8506c8590fc4039c1341aafdca56fd97cfc Mon Sep 17 00:00:00 2001 From: Dogfather Date: Sat, 19 Sep 2026 20:24:28 +0200 Subject: [PATCH] Der gefaehrlichste Handgriff im Haus wird jetzt am Bildschirm geprueft pruef-personen-loeschen gibt es seit Langem -- sie kennt aber nur die Schnittstelle: kein Browser, kein Knopf, kein Dialog. Der Weg, den ein Mensch geht, um eine Person zu loeschen, war nie geprueft. Und das ist der eine Handgriff, den man nicht zuruecknehmen kann. pruef-nachfrage hat jetzt einen Browserteil (33/0 statt 17/0): der Dialog geht auf und nennt den Namen im Titel er sagt, dass es endgueltig ist, was passiert und was bleibt ein FALSCH abgetippter Name schliesst ihn NICHT und sagt, warum der Abbruchknopf loescht nichts richtig abgetippt wird geloescht (Liste 2 -> 1) Abmelden fragt nach und nennt den Grund (Zugangscode) Esc bricht ab -- man bleibt angemeldet BEIM BAUEN ZWEIMAL DANEBENGEGRIFFEN, beide Male gemessen statt vermutet: Die Liste zeigte nur eine Person, obwohl vier in der Datenbank standen. Das sah nach einem Befund aus. Die Rollen-Abschnitte sind standardmaessig ZU -- nur der eigene ist offen, und das ist richtig so. Die Pruefung klappt ihn jetzt auf. Vorher wurde mit einer festen Wartezeit gearbeitet. Jetzt wird auf den Zustand gewartet, nicht auf die Uhr. Neu: server/helfer-nachfrage.mjs (bestaetige/verwerfe/nachfrageText) kennt beide Wege -- den und den confirm()-Notnagel fuer Safari vor 15.4. Ohne den zweiten liesse sich der Notnagel nie pruefen. --- server/pruef-nachfrage.mjs | 164 ++++++++++++++++++++++++++++++++ workspace/anruf-probe.html | 10 +- workspace/app.webmanifest | 8 +- workspace/aufgaben.html | 30 +++--- workspace/automation.html | 30 +++--- workspace/befinden.html | 28 +++--- workspace/bereich.html | 40 ++++---- workspace/bewerben.html | 34 +++---- workspace/bewerbungen.html | 32 +++---- workspace/calls.html | 32 +++---- workspace/chat.html | 34 +++---- workspace/content.html | 34 +++---- workspace/crew-index.html | 12 +-- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 32 +++---- workspace/entwicklung.html | 28 +++--- workspace/hilfe.html | 26 ++--- workspace/index.html | 10 +- workspace/kalender.html | 32 +++---- workspace/leistung.html | 30 +++--- workspace/personen.html | 30 +++--- workspace/profil.html | 34 +++---- workspace/rechte.html | 28 +++--- workspace/report.html | 32 +++---- workspace/scouting.html | 30 +++--- workspace/start.html | 28 +++--- workspace/startcheck.html | 32 +++---- workspace/steckbrief.html | 34 +++---- workspace/talente.html | 30 +++--- workspace/team.html | 30 +++--- workspace/teamlage.html | 28 +++--- workspace/teilen.html | 26 ++--- workspace/treff-moderation.html | 28 +++--- workspace/treff-regeln.html | 30 +++--- workspace/uebersicht.html | 30 +++--- workspace/unsere-seiten.html | 26 ++--- workspace/werdegang.html | 30 +++--- workspace/wissen.html | 32 +++---- 38 files changed, 678 insertions(+), 514 deletions(-) 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`. --> - - + + - +