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`. --> - - + + - +