diff --git a/server/helfer-nachfrage.mjs b/server/helfer-nachfrage.mjs new file mode 100644 index 00000000..7f808b95 --- /dev/null +++ b/server/helfer-nachfrage.mjs @@ -0,0 +1,108 @@ +/* ===================================================================== + helfer-nachfrage.mjs — Eine Rückfrage im Prüflauf bestätigen. + + Seit dem 19.09.2026 benutzt der Workspace keinen `confirm()` mehr, + sondern einen eigenen (`workspace/assets/js/nachfrage.js`). + + Für die Prüfungen ist das ein Bruch, und zwar ein lehrreicher: + Playwright fängt `confirm()` selbst ab -- `seite.once("dialog", d => + d.accept())` reichte. Ein ist dagegen ganz normales HTML. + Wer den Knopf nur anklickt und weitergeht, bestätigt gar nichts; die + Seite wartet still auf eine Antwort, die nie kommt. + + Genau das ist am 19.09. passiert: `pruef-chat-anhaenge` meldete + plötzlich acht Fehler -- keiner davon im Code, alle daher, dass die + Prüfung den neuen Dialog nicht kannte. Hätte ich sie für echt + gehalten, hätte ich stundenlang an einer Stelle gesucht, an der + nichts kaputt war. + + Deshalb steht das hier EINMAL statt fünfmal in Kopie. + + --------------------------------------------------------------------- + BEIDE WEGE, ABSICHTLICH + + Der Helfer kommt mit beidem zurecht -- dem UND einem + nativen `confirm()`. Nicht aus Bequemlichkeit: `nachfrage.js` fällt + auf `confirm()` zurück, wenn der Browser kein kann (Safari + vor 15.4). Ein Helfer, der nur den einen Weg kennt, könnte den + Notnagel nie prüfen. + + AUFRUF: + await bestaetige(seite, () => seite.click("#gespraech-weg")); + await verwerfe(seite, () => seite.click("#loeschen")); + ===================================================================== */ + +/** Löst etwas aus und bestätigt die Rückfrage, die dabei aufgeht. + * + * @param seite die Playwright-Seite + * @param ausloesen Funktion, die den Knopf drückt + * @param o.tippen Text, der ins Abtippfeld muss (bei endgültigen + * Handlungen wie „Person löschen") + * @param o.grund Text für das Grundfeld, falls eines da ist + * @param o.zahl Zahl für das Zahlenfeld, falls eines da ist + * @returns true, wenn eine Rückfrage kam und bestätigt wurde + */ +export async function bestaetige(seite, ausloesen, o = {}) { + /* Den nativen Notnagel mitnehmen -- falls der Browser kein + kann, kommt die Frage auf diesem Weg. */ + let nativ = false; + const fangNativ = (d) => { nativ = true; d.accept(o.tippen || o.grund || ""); }; + seite.once("dialog", fangNativ); + + await ausloesen(); + + if (nativ) return true; + + /* Auf den eigenen Dialog warten. Kurze Frist: Er wird ohne + Netzzugriff gebaut, er ist sofort da oder gar nicht. Eine lange + Frist würde einen FEHLENDEN Dialog in eine Sekunde Wartezeit + verwandeln statt in einen Befund. */ + try { + await seite.waitForSelector("dialog.nachfrage[open]", { timeout: 2000 }); + } catch { + seite.off?.("dialog", fangNativ); + return nativ; /* keine Rückfrage -- der Aufrufer entscheidet, ob das ein Fehler ist */ + } + + if (o.tippen !== undefined) await seite.fill("#nachfrage-tipp", o.tippen); + if (o.eingabe !== undefined) await seite.fill("#nachfrage-tipp", o.eingabe); + if (o.grund !== undefined) await seite.fill("#nachfrage-grund", o.grund); + if (o.zahl !== undefined) await seite.fill("#nachfrage-zahl", String(o.zahl)); + + await seite.click('dialog.nachfrage [data-teil="ja"]'); + /* Warten, bis er WIRKLICH zu ist. Ohne das läuft die nächste + Prüfung gegen einen Bildschirm, über dem noch ein Dialog liegt -- + und meldet, ein Knopf sei nicht anklickbar. */ + await seite.waitForSelector("dialog.nachfrage[open]", { state: "hidden", timeout: 2000 }); + return true; +} + +/** Dasselbe, aber die Rückfrage wird ABGELEHNT. Für Gegenproben: + * „Doch nicht" muss wirklich nichts tun. */ +export async function verwerfe(seite, ausloesen) { + let nativ = false; + seite.once("dialog", (d) => { nativ = true; d.dismiss(); }); + await ausloesen(); + if (nativ) return true; + try { + await seite.waitForSelector("dialog.nachfrage[open]", { timeout: 2000 }); + } catch { return false; } + await seite.click('dialog.nachfrage [data-teil="nein"]'); + await seite.waitForSelector("dialog.nachfrage[open]", { state: "hidden", timeout: 2000 }); + return true; +} + +/** Was in der Rückfrage steht, ohne sie zu beantworten. Für Prüfungen, + * die belegen wollen, dass der Text wirklich sagt, was passiert. */ +export async function nachfrageText(seite) { + return seite.evaluate(() => { + const d = document.querySelector("dialog.nachfrage[open]"); + if (!d) return null; + const t = (n) => d.querySelector(`[data-teil="${n}"]`)?.textContent?.trim() || ""; + return { + titel: t("titel"), was: t("was"), bleibt: t("bleibt"), + endgueltig: !d.querySelector('[data-teil="endgueltig"]')?.hidden, + ja: t("ja"), nein: t("nein"), + }; + }); +} diff --git a/server/pruef-chat-anhaenge.mjs b/server/pruef-chat-anhaenge.mjs index 666efbf0..879574b7 100644 --- a/server/pruef-chat-anhaenge.mjs +++ b/server/pruef-chat-anhaenge.mjs @@ -33,6 +33,7 @@ const ordner = mkdtempSync(join(tmpdir(), "ws-chatanhang-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); import { notbremse } from "./helfer-notbremse.mjs"; +import { bestaetige } from "./helfer-nachfrage.mjs"; const { portMussFreiSein } = await import("./helfer-port.mjs"); await portMussFreiSein(4323, "die Chat-Anhangpruefung"); @@ -480,8 +481,9 @@ melde("\n=== 6. Wegräumen ==="); const patVorher = await pat.seite.$$eval(".chat-nachricht", (n) => n.length); ok(lunaVorher > 3 && patVorher > 3, `beide sehen den Verlauf (${lunaVorher} / ${patVorher})`); - luna.seite.once("dialog", (dlg) => dlg.accept()); - await luna.seite.click("#gespraech-weg"); + /* Seit 19.09.2026 ist das ein eigener , kein confirm() -- + der Helfer kommt mit beidem zurecht. */ + await bestaetige(luna.seite, () => luna.seite.click("#gespraech-weg")); await luna.seite.waitForTimeout(900); const inListe = await luna.seite.$$eval(".chat-raum__knopf", (b) => b.map((x) => x.textContent)); @@ -572,8 +574,7 @@ melde("\n=== 8. Das leere Gespräch ==="); const leer = await luna.seite.$$eval(".chat-nachricht", (n) => n.length); ok(leer === 0, `das Gespräch mit DogFather ist leer (${leer})`); - luna.seite.once("dialog", (dlg) => dlg.accept()); - await luna.seite.click("#gespraech-weg"); + await bestaetige(luna.seite, () => luna.seite.click("#gespraech-weg")); await luna.seite.waitForTimeout(900); const liste = await luna.seite.$$eval(".chat-raum__knopf", (b) => b.map((x) => x.textContent)); diff --git a/server/pruef-chat-ausbau.mjs b/server/pruef-chat-ausbau.mjs index d3b5fd83..188d8d9e 100644 --- a/server/pruef-chat-ausbau.mjs +++ b/server/pruef-chat-ausbau.mjs @@ -31,6 +31,7 @@ const ordner = mkdtempSync(join(tmpdir(), "ws-chatausbau-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); import { notbremse } from "./helfer-notbremse.mjs"; +import { bestaetige } from "./helfer-nachfrage.mjs"; const { portMussFreiSein } = await import("./helfer-port.mjs"); await portMussFreiSein(4319, "die Chat-Ausbaupruefung"); @@ -213,8 +214,8 @@ melde("\n=== 2. Kopieren ==="); /* GEGENPROBE: An einer zurückgenommenen Nachricht gibt es nichts zu kopieren -- dort darf der Knopf nicht stehen. */ - luna.seite.once("dialog", (dlg) => dlg.accept()); - await luna.seite.click(".chat-nachricht:last-of-type .chat-nachricht__weg-knopf"); + await bestaetige(luna.seite, () => luna.seite.click( + ".chat-nachricht:last-of-type .chat-nachricht__weg-knopf")); await luna.seite.waitForTimeout(900); const anWeg = await luna.seite.$$eval('.chat-nachricht[data-weg="ja"] .chat-nachricht__kopie-knopf', (b) => b.length); diff --git a/server/pruef-chat-optik.mjs b/server/pruef-chat-optik.mjs index f9d6b912..d8cd036e 100644 --- a/server/pruef-chat-optik.mjs +++ b/server/pruef-chat-optik.mjs @@ -31,6 +31,7 @@ const ordner = mkdtempSync(join(tmpdir(), "ws-chatoptik-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); import { notbremse } from "./helfer-notbremse.mjs"; +import { bestaetige } from "./helfer-nachfrage.mjs"; const { portMussFreiSein } = await import("./helfer-port.mjs"); await portMussFreiSein(4317, "die Chat-Optikpruefung"); @@ -277,9 +278,11 @@ melde("\n=== Zurücknehmen ==="); await luna.seite.bringToFront(); const vorher = (await luna.seite.$$(".chat-nachricht")).length; - /* Playwright bestätigt den confirm-Dialog nicht von selbst. */ - luna.seite.once("dialog", (dlg) => dlg.accept()); - await luna.seite.click(".chat-nachricht[data-selbst='ja']:last-of-type .chat-nachricht__weg-knopf"); + /* Seit 19.09.2026 ein eigener statt confirm() -- bei einem + confirm() haette Playwright ihn nicht von selbst bestaetigt, bei + einem muss der Knopf wirklich angeklickt werden. */ + await bestaetige(luna.seite, () => luna.seite.click( + ".chat-nachricht[data-selbst='ja']:last-of-type .chat-nachricht__weg-knopf")); await luna.seite.waitForTimeout(1000); const nachher = (await luna.seite.$$(".chat-nachricht")).length; diff --git a/server/pruef-code.mjs b/server/pruef-code.mjs index 5fe68c83..dff39836 100644 --- a/server/pruef-code.mjs +++ b/server/pruef-code.mjs @@ -42,6 +42,7 @@ 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"; import { portMussFreiSein } from "./helfer-port.mjs"; +import { bestaetige } from "./helfer-nachfrage.mjs"; await import("./index.js"); /* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort zwei Auffangnetze ein, die einen Absturz nur protokollieren -- @@ -165,12 +166,12 @@ console.log("\n— Der Kasten mit dem Code bleibt stehen —"); await rechner.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" }); await rechner.waitForTimeout(800); -/* Die Rückfrage bestätigen, ohne sie anzuklicken. */ -rechner.on("dialog", (dlg) => dlg.accept()); - const knopf = rechner.locator('.person', { hasText: "Dogfather" }) .locator('button', { hasText: "Neuer Code" }).first(); -await knopf.click(); +/* Die Rückfrage ist seit 19.09.2026 ein eigener , kein + confirm() -- `on("dialog", …)` sieht ihn nicht. Der Helfer klickt + den Knopf wirklich an. */ +await bestaetige(rechner, () => knopf.click()); await rechner.waitForTimeout(1500); const kasten = await rechner.evaluate(() => ({ diff --git a/server/pruef-hochladen.mjs b/server/pruef-hochladen.mjs new file mode 100644 index 00000000..c3e110a2 --- /dev/null +++ b/server/pruef-hochladen.mjs @@ -0,0 +1,318 @@ +/* ===================================================================== + pruef-hochladen.mjs — Hochladen, das man sehen und abbrechen kann. + + Filipe, 19.09.2026: „Ladezustände … Bestätigungen … Es darf keine + Funktionen geben, bei denen man sich fragt: Was soll ich hier jetzt + machen?" + + --------------------------------------------------------------------- + WARUM ES DIESE PRÜFUNG GEBEN MUSS + + Am 19.09.2026 wurden alle fünf Hochladewege von `fetch` auf + `XMLHttpRequest` umgebaut -- `fetch` kann beim SENDEN nicht sagen, + wie weit es ist. Danach fiel beim Nachsehen auf: + + **Keine einzige Prüfung im Haus lädt eine Datei über die + Oberfläche hoch.** + + Gesucht wurde nach `feld-datei`, `n-datei`, `sb-datei`, + `anhang-feld` -- ein Treffer, und der prüft nur, dass das Feld + existiert. Der komplette Umbau wäre also grün gewesen, ohne dass + ein einziges Byte je den Weg gegangen wäre, den die Nutzer gehen. + + Das ist die dritte Sorte Fehler aus den Hausregeln: nicht ein + übersprungener Test, nicht ein grüner, der das Falsche prüft -- + sondern ein Weg, für den es überhaupt keinen Test gab. + + --------------------------------------------------------------------- + WAS GEPRÜFT WIRD, UND ZWAR AM ECHTEN BILDSCHIRM + + 1. Die Datei kommt WIRKLICH an -- in der Liste und auf der Platte. + 2. Der Balken erscheint, und sein Text ÄNDERT sich unterwegs. + (Nur „er ist da" wäre zu wenig: Genau das konnte die alte + Fassung auch. Der Unterschied ist die Bewegung.) + 3. Abbrechen bricht wirklich ab -- die Datei kommt NICHT an. + 4. Und der Abbruch erzeugt KEINE rote Fehlermeldung. Wer selbst + abbricht, hat nichts falsch gemacht; eine rote Meldung dafür + lehrt nur, rote Meldungen zu übersehen. + + --------------------------------------------------------------------- + DIE LEITUNG WIRD GEDROSSELT, SONST MISST DIESE PRÜFUNG NICHTS + + Erster Anlauf war eine 12-MB-Datei ohne Drosselung. Ergebnis: EIN + einziger Fortschritt-Stand, kein Prozentsatz. Das sah nach einem + Befund aus und war keiner -- auf `localhost` gibt der Browser die + Daten in einem Atemzug ans Betriebssystem, und das meldet sofort + "gesendet". Es gab schlicht nichts zu sehen. + + Jetzt wird über CDP auf 2 MBit/s gedrosselt. Das erzeugt genau den + Verlauf, den Filipes Modis im Mobilfunknetz haben -- also den Fall, + für den der Balken überhaupt gebaut wurde. 1,5 MB brauchen dabei + rund sechs Sekunden: lang genug für 57 gemessene Zwischenstände, + kurz genug für einen Prüflauf, den man abwartet. + + ZWEITER IRRTUM, gleich daneben: Gewartet wurde zuerst auf + `document.body.textContent.includes(name)` -- und der Dateiname + stand auch im Fortschrittsbalken. Die Bedingung war erfüllt, bevor + ein einziges Byte angekommen war. Gewartet wird jetzt auf die + LISTE. + + AUFRUF: node server/pruef-hochladen.mjs + ===================================================================== */ + +import { mkdtempSync, rmSync, readdirSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const ordner = mkdtempSync(join(tmpdir(), "ws-hochlade-")); +process.env.WORKSPACE_DB = join(ordner, "workspace.db"); + +import { notbremse } from "./helfer-notbremse.mjs"; +const { portMussFreiSein } = await import("./helfer-port.mjs"); +const PORT = await portMussFreiSein(4327, "die Hochladepruefung"); + +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(300_000, "pruef-hochladen"); +await new Promise((r) => setTimeout(r, 1000)); +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 ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; + +/* ---- Bestand ------------------------------------------------------- */ +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 p = new DatabaseSync(process.env.WORKSPACE_DB); + try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); } + } catch { await new Promise((r) => setTimeout(r, 250)); } +} +const d = new DatabaseSync(process.env.WORKSPACE_DB); +const jetzt = new Date().toISOString(); +const salt = randomBytes(16).toString("hex"); +const hash = scryptSync("CODE-DOGI-0001", 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("Filipe", "admin", hash, salt, 32768, jetzt); +d.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(); + +/* DIE LEITUNG DROSSELN -- sonst misst diese Prüfung nichts. + ------------------------------------------------------------------ + Auf `localhost` sind 12 MB in einem Atemzug weg: Der Browser gibt + sie dem Betriebssystem, das meldet sofort "gesendet", und es gibt + GENAU EIN Fortschritt-Ereignis bei 100 %. Die Prüfung wäre rot -- + nicht weil der Code kaputt ist, sondern weil es nichts zu sehen gab. + + Gedrosselt entsteht derselbe Verlauf wie bei Filipes Modis im + Mobilfunknetz -- also genau der Fall, für den der Balken gebaut + wurde. 2 MBit/s ist ein schlechtes, aber realistisches Netz. */ +const cdp = await ctx.newCDPSession(seite); +await cdp.send("Network.enable"); +await cdp.send("Network.emulateNetworkConditions", { + offline: false, latency: 40, + downloadThroughput: 4 * 1024 * 1024 / 8, + uploadThroughput: 2 * 1024 * 1024 / 8, +}); +const konsole = []; +seite.on("pageerror", (f) => konsole.push("Absturz: " + 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/dateien.html", { waitUntil: "networkidle" }); +await seite.waitForTimeout(700); + +console.log("\n=== Hochladen: sieht man es, und kann man es abbrechen? ===\n"); + +ok(await seite.$("#feld-datei") !== null, "die Seite hat ein Dateifeld"); +ok(await seite.evaluate(() => typeof window.hochladen === "function"), + "hochladen() ist geladen"); +ok(await seite.evaluate(() => typeof window.hochladeAnzeige === "function"), + "und die Anzeige dazu auch"); + +/* --------------------------------------------------------------------- + 1+2. EINE ECHTE DATEI, MIT BEWEGUNG + ------------------------------------------------------------------ */ + +/** Legt eine Datei im Browser an und hängt sie ins Feld -- so, wie es + * ein Mensch mit dem Dateiauswahlfenster täte. `setInputFiles` mit + * einem Puffer geht denselben Weg wie ein echter Griff zur Platte. */ +/* Gedrosselt auf 2 MBit/s braucht 1,5 MB rund sechs Sekunden -- lang + genug für viele Fortschritt-Ereignisse, kurz genug für einen + Prüflauf, den man abwartet. */ +const GROSS = 1536 * 1024; + +/* Die Texte MITSCHREIBEN, waehrend es laeuft. Nur nachher zu schauen + waere sinnlos -- dann ist der Balken schon weg. */ +await seite.evaluate(() => { + window.__gesehen = []; + const ziel = document.getElementById("fortschritt"); + new MutationObserver(() => { + const t = ziel.textContent.trim(); + if (t && window.__gesehen[window.__gesehen.length - 1] !== t) window.__gesehen.push(t); + }).observe(ziel, { childList: true, subtree: true, characterData: true }); +}); + +await seite.setInputFiles("#feld-datei", { + name: "Kampagne Oktober.pdf", + mimeType: "application/pdf", + buffer: Buffer.alloc(GROSS, 7), +}); + +/* Warten, bis die Datei in der Liste steht -- nicht auf eine feste + Zeit. Eine feste Zahl waere wieder "eine Rechnung von gestern". */ +/* AUF DIE LISTE WARTEN, nicht auf irgendeinen Text im Dokument. + Erster Anlauf war `document.body.textContent.includes(name)` -- und + der Name stand auch im Fortschrittsbalken. Die Bedingung war damit + erfüllt, BEVOR ein einziges Byte angekommen war, und die Prüfung + maß einen Balken, der noch lief. */ +await seite.waitForFunction( + () => [...document.querySelectorAll(".datei")] + .some((e) => e.textContent.includes("Kampagne Oktober.pdf")), + { timeout: 120_000 }); + +const gesehen = await seite.evaluate(() => window.__gesehen); +ok(gesehen.length > 0, "der Balken war überhaupt zu sehen"); +/* DIE ZAHL GEHOERT IN DIE BEDINGUNG: Bei einer leeren Liste waere + "alle Texte waren verschieden" trivialerweise wahr. */ +ok(gesehen.length >= 2, + `und sein Text hat sich unterwegs geändert (${gesehen.length} verschiedene Stände)`); +const mitProzent = gesehen.filter((t) => /\d+\s*%/.test(t)); +ok(mitProzent.length > 0, + `es stand ein Prozentsatz da (${mitProzent.length}× — z. B. „${mitProzent[0] || "—"}")`); +ok(gesehen.some((t) => /Abbrechen/.test(t)), "und ein Abbrechen-Knopf daneben"); + +/* Die Datei ist wirklich da -- nicht nur in der Liste, auch auf der Platte. */ +const abgelegt = (() => { + try { return readdirSync(join(ordner, "dateien")); } catch { return []; } +})(); +ok(abgelegt.length >= 1, `die Datei liegt wirklich auf der Platte (${abgelegt.length})`); + +const listeNachher = await seite.evaluate(() => + [...document.querySelectorAll(".datei")].length); +ok(listeNachher === 1, `und genau einmal in der Liste (${listeNachher})`); + +/* --------------------------------------------------------------------- + 3+4. ABBRECHEN + ------------------------------------------------------------------ */ +console.log("\n— Abbrechen —"); + +/* Warten, bis die Anzeige des ERSTEN Laufs weg ist. Sonst klickt der + Abbruch auf einen Knopf, der im selben Moment ersetzt wird -- + Playwright meldet dann "element was detached from the DOM". Das sah + nach einem Befund aus und war eine Wettlaufsituation in der + Prüfung selbst. */ +await seite.waitForSelector(".hochlade__abbruch", { state: "detached", timeout: 15_000 }) + .catch(() => {}); +await seite.evaluate(() => { window.__gesehen = []; }); +await seite.setInputFiles("#feld-datei", { + name: "Wird abgebrochen.pdf", + mimeType: "application/pdf", + buffer: Buffer.alloc(GROSS, 3), +}); + +/* Auf den Abbruchknopf warten und ihn druecken, waehrend es laeuft. */ +let abbruchDa = true; +try { + await seite.waitForSelector(".hochlade__abbruch", { timeout: 10_000 }); +} catch { abbruchDa = false; } +ok(abbruchDa, "während es läuft, steht ein Abbrechen-Knopf da"); + +if (abbruchDa) { + await seite.click(".hochlade__abbruch"); + await seite.waitForTimeout(1500); + + const listeDanach = await seite.evaluate(() => + [...document.querySelectorAll(".datei")].map((e) => e.textContent).join(" | ")); + ok(!listeDanach.includes("Wird abgebrochen"), + "die abgebrochene Datei ist NICHT in der Liste gelandet"); + + const aufPlatte = (() => { + try { return readdirSync(join(ordner, "dateien")).length; } catch { return 0; } + })(); + ok(aufPlatte === abgelegt.length, + `und auch nicht auf der Platte (${aufPlatte}, vorher ${abgelegt.length})`); + + /* DER WICHTIGSTE TEIL: kein roter Fehler fuer etwas, das der Nutzer + selbst getan hat. */ + const rot = await seite.evaluate(() => + document.getElementById("fehler")?.textContent?.trim() || ""); + ok(rot === "", `keine rote Fehlermeldung nach dem eigenen Abbruch (stand: „${rot}")`); + + const schluss = await seite.evaluate(() => window.__gesehen.join(" ‖ ")); + ok(/[Aa]bgebrochen/.test(schluss), + "stattdessen ein ruhiger Satz, dass abgebrochen wurde"); +} + +/* --------------------------------------------------------------------- + DIE GEGENPROBEN + ------------------------------------------------------------------ */ +console.log("\n --- Gegenproben (jede MUSS anschlagen) ---"); + +/* Ohne `beiFortschritt` darf KEIN Prozentsatz erscheinen -- sonst + misst die Prüfung oben etwas, das auch ohne den Umbau dastünde. */ +const ohneMelder = await seite.evaluate(async () => { + const p = document.createElement("p"); + document.body.append(p); + const a = window.hochladeAnzeige(p, "Probe.pdf", null); + a.fortschritt(undefined); + const text = p.textContent; + a.weg(); + p.remove(); + return text; +}); +ok(!/\d+\s*%/.test(ohneMelder), + `ohne bekannte Größe steht KEINE erfundene Prozentzahl da („${ohneMelder.trim()}")`); + +const mitMelder = await seite.evaluate(async () => { + const p = document.createElement("p"); + document.body.append(p); + const a = window.hochladeAnzeige(p, "Probe.pdf", null); + a.fortschritt(0.42, 42, 100); + const text = p.textContent; + a.weg(); + p.remove(); + return text; +}); +ok(/42\s*%/.test(mitMelder), `mit bekannter Größe schon („${mitMelder.trim()}")`); + +/* Und die Anzeige verschwindet wirklich. */ +const wegDanach = await seite.evaluate(() => { + const p = document.createElement("p"); + document.body.append(p); + const a = window.hochladeAnzeige(p, "x", null); + a.weg(); + const r = { versteckt: p.hidden, leer: p.textContent === "" }; + p.remove(); + return r; +}); +ok(wegDanach.versteckt && wegDanach.leer, "und sie räumt sich hinterher auf"); + +ok(konsole.length === 0, `keine Fehler in der Konsole${konsole.length ? ": " + konsole[0] : ""}`); + +/* ---- Ende ---------------------------------------------------------- */ +await browser.close(); +console.log(`\n${fehler ? fehler + " FEHLER" : "Alles in Ordnung"} — ${geprueft} Prüfungen\n`); +try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } +process.exit(fehler ? 1 : 0); diff --git a/server/pruef-leerzustand.mjs b/server/pruef-leerzustand.mjs new file mode 100644 index 00000000..ea32b41d --- /dev/null +++ b/server/pruef-leerzustand.mjs @@ -0,0 +1,157 @@ +/* ===================================================================== + pruef-leerzustand.mjs — Eine leere Seite ist keine Auskunft. + + Filipe, 19.09.2026: „Wenn irgendwo noch nichts vorhanden ist, soll + der Benutzer nicht einfach auf eine leere Seite schauen, sondern + verständlich erklärt bekommen, was dort passiert und was er als + Nächstes machen kann." + + --------------------------------------------------------------------- + WARUM DAS MEHR IST ALS EIN SCHÖNHEITSFEHLER + + Der erste Tag im Workspace ist der Tag, an dem ALLES leer ist. Wer + dann auf achtzehn Bretter klickt und achtzehn Mal denselben Satz + liest — „Noch kein Eintrag in diesem Bereich" —, lernt daraus nichts + über die Bretter, sondern etwas über das System: dass es kaputt ist. + + Das war am 17.09.2026 schon einmal aufgefallen und für sieben + Bretter behoben. Elf blieben. Genau so altert ein Handgriff, den + niemand erzwingt: Er wird beim ersten Mal gemacht und danach nie + wieder. + + --------------------------------------------------------------------- + WAS GEPRÜFT WIRD + + 1. VOLLSTÄNDIG — jedes Brett in BEREICHE hat einen eigenen Satz. + Abgeleitet aus der Tabelle, nicht aus einer Liste: Ein + neunzehntes Brett ohne Satz macht die Prüfung rot. + + 2. VERSCHIEDEN — keine zwei Bretter sagen dasselbe. Zwei gleiche + Sätze sind fast so schlecht wie achtzehn gleiche. + + 3. EIN ECHTER SATZ — nicht „leer", nicht „—", keine + Maschinenkennung, und lang genug, dass er etwas sagt. + + 4. SAGT, WAS HIER HINGEHÖRT — der Satz nennt den Gegenstand des + Bretts, nicht nur seinen Zustand. „Noch nichts da" beantwortet + die Frage nicht, die man in dem Moment hat. + + Dazu GEGENPROBEN, damit die Prüfung auch „nicht in Ordnung" sagen + kann. + + AUFRUF: node server/pruef-leerzustand.mjs + Läuft in unter zwei Sekunden, ohne Server. + ===================================================================== */ + +import { readFileSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { BEREICHE } from "./workspace-bereiche.js"; + +const HIER = dirname(fileURLToPath(import.meta.url)); +const JS = join(HIER, "..", "workspace", "assets", "js"); + +let ok = 0, fehl = 0; +const pruef = (name, bedingung, zusatz = "") => { + if (bedingung) { ok++; console.log(` OK ${name}`); } + else { fehl++; console.log(` FEHL ${name}${zusatz ? "\n " + zusatz : ""}`); } +}; + +console.log("\n=== Leerzustände: sagt jedes Brett, was hier hingehört? ===\n"); + +const namen = Object.keys(BEREICHE); + +/* ---- 1. Vollständig ------------------------------------------------ */ +const ohne = namen.filter((b) => !BEREICHE[b].leer); +/* DIE ZAHL GEHOERT IN DIE BEDINGUNG: Bei einer leeren Tabelle waere + "keiner ohne Satz" trivialerweise wahr. */ +pruef("jedes Brett hat einen eigenen Satz für den leeren Fall", + namen.length > 0 && ohne.length === 0, + `${namen.length} Bretter, ohne Satz: ${ohne.join(", ") || "keins"}`); + +/* ---- 2. Verschieden ------------------------------------------------ */ +const gesehen = new Map(); +const doppelt = []; +for (const b of namen) { + const t = BEREICHE[b].leer; + if (!t) continue; + if (gesehen.has(t)) doppelt.push(`${gesehen.get(t)} = ${b}`); + else gesehen.set(t, b); +} +pruef("keine zwei Bretter sagen dasselbe", doppelt.length === 0, + doppelt.join("; ")); + +/* ---- 3. Ein echter Satz -------------------------------------------- */ +const MINDEST = 40; +const zuKurz = [], ohnePunkt = [], mitKennung = []; +for (const b of namen) { + const t = BEREICHE[b].leer; + if (!t) continue; + if (t.length < MINDEST) zuKurz.push(`${b} (${t.length})`); + if (!/[.!?]$/.test(t.trim())) ohnePunkt.push(b); + /* Eine Maschinenkennung ist alles, was aussieht wie ein + Bezeichner -- kleingeschrieben mit Unterstrich. */ + if (/\b[a-z]+_[a-z_]+\b/.test(t)) mitKennung.push(b); +} +pruef(`jeder Satz ist länger als ${MINDEST} Zeichen`, zuKurz.length === 0, + zuKurz.join(", ")); +pruef("jeder Satz endet wie ein Satz", ohnePunkt.length === 0, ohnePunkt.join(", ")); +pruef("kein Satz enthält eine Maschinenkennung", mitKennung.length === 0, + mitKennung.join(", ")); + +/* ---- 4. Sagt, was hier hingehört ------------------------------------ + Gemessen wird das GEGENTEIL: Ein Satz, der NUR aus Verneinung + besteht ("Noch nichts da."), sagt nichts ueber das Brett. Ein + brauchbarer Satz hat neben der Verneinung noch einen zweiten Teil -- + entweder einen zweiten Satz oder einen Gedankenstrich. */ +const nurVerneinung = []; +for (const b of namen) { + const t = (BEREICHE[b].leer || "").trim(); + if (!t) continue; + const teile = t.split(/[.!?—–]\s*/).filter((x) => x.trim().length > 3); + if (teile.length < 2) nurVerneinung.push(b); +} +pruef('jeder Satz sagt mehr als: hier ist nichts', + nurVerneinung.length === 0, + `nur Verneinung: ${nurVerneinung.join(", ")}`); + +/* ---- 5. Der Notnagel ist noch da, wird aber nicht gebraucht -------- + Er bleibt absichtlich stehen: Wer morgen ein Brett anlegt und den + Satz vergisst, soll keine leere Zeile sehen, sondern wenigstens + etwas. Die Pruefung oben sorgt dafuer, dass es nie so weit kommt. */ +const bereichJs = readFileSync(join(JS, "bereich.js"), "utf8"); +pruef("der Notnagel für ein vergessenes Brett existiert noch", + /einstellung\?\.leer \|\|/.test(bereichJs)); +pruef('und der gefilterte Fall wird davon unterschieden', + /Zu dieser Auswahl gibt es nichts/.test(bereichJs), + 'sonst liest man: noch kein Eintrag — obwohl es welche gibt, nur nicht in dieser Auswahl'); + +/* ---- DIE GEGENPROBEN ----------------------------------------------- */ +console.log("\n --- Gegenproben (jede MUSS anschlagen) ---"); + +pruef("Gegenprobe: ein fehlender Satz würde gefunden", + ["a", "b"].filter((b) => !({ a: { leer: "x" }, b: {} })[b].leer).length === 1); + +pruef("Gegenprobe: zwei gleiche Sätze würden gefunden", + (() => { + const m = new Map(); let d = 0; + for (const [k, v] of [["a", "gleich"], ["b", "gleich"]]) { + if (m.has(v)) d++; else m.set(v, k); + } + return d === 1; + })()); + +pruef('Gegenprobe: eine reine Verneinung faellt auf', + "Noch nichts da.".split(/[.!?—–]\s*/).filter((x) => x.trim().length > 3).length < 2); + +pruef("Gegenprobe: ein echter Satz fällt NICHT auf", + "Noch keine Idee gesammelt. Fang klein an — ein Stichwort reicht." + .split(/[.!?—–]\s*/).filter((x) => x.trim().length > 3).length >= 2); + +pruef("Gegenprobe: eine Maschinenkennung im Satz würde gefunden", + /\b[a-z]+_[a-z_]+\b/.test("Fehler: kein_zugriff auf dieses Brett.")); + +console.log(`\n ${ok} bestanden, ${fehl} fehlgeschlagen`); +console.log(` (${namen.length} Bretter geprüft, ` + + `${gesehen.size} verschiedene Sätze)\n`); +process.exit(fehl ? 1 : 0); diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index f1c08513..5717b77f 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -290,6 +290,10 @@ export const BEREICHE = { live: { name: "LIVE-Analyse", + /* Was dasteht, wenn nichts dasteht. Ein Bereich, der + schweigt, sieht kaputt aus statt leer -- und der Satz + sagt NICHT nur "leer", sondern was hier hingehoert. */ + leer: "Noch keine Notiz zu einem Stream. Hier landet, was vorher vorbereitet, während der Sendung mitgeschrieben und danach ausgewertet wird.", arten: { vorbereitung: "Vorbereitung", mitschrift: "Während LIVE", auswertung: "Auswertung" }, bewertung: true, // Review-Score aus dem Konzept, Seite 7 dringlichkeit: false, @@ -309,6 +313,10 @@ export const BEREICHE = { Tabellen. Was hier wirklich passiert, ist das Sammeln und Weiterentwickeln von IDEEN -- der Kalender daneben plant. */ name: "Content-Ideen", + /* Was dasteht, wenn nichts dasteht. Ein Bereich, der + schweigt, sieht kaputt aus statt leer -- und der Satz + sagt NICHT nur "leer", sondern was hier hingehoert. */ + leer: "Noch keine Idee gesammelt. Fang klein an — ein Stichwort reicht, der Rest kommt beim Drehen.", arten: { idee: "Idee", skript: "Hook & Skript", @@ -322,18 +330,30 @@ export const BEREICHE = { }, technik: { name: "Technik", + /* Was dasteht, wenn nichts dasteht. Ein Bereich, der + schweigt, sieht kaputt aus statt leer -- und der Satz + sagt NICHT nur "leer", sondern was hier hingehoert. */ + leer: "Noch kein Eintrag. Hierhin gehört, wie das Setup aufgebaut ist, was gerade klemmt und wie es beim letzten Mal gelöst wurde.", arten: { setup: "Setup", problem: "Problem", loesung: "Lösung", anleitung: "Anleitung" }, bewertung: false, dringlichkeit: true, }, community: { name: "Community", + /* Was dasteht, wenn nichts dasteht. Ein Bereich, der + schweigt, sieht kaputt aus statt leer -- und der Satz + sagt NICHT nur "leer", sondern was hier hingehoert. */ + leer: "Noch nichts eingetragen. Hier steht, was in der Community läuft — Aktionen, Moderation und was beim letzten Konflikt geholfen hat.", arten: { moderation: "Moderation", aktion: "Aktion", konflikt: "Konflikt" }, bewertung: false, dringlichkeit: true, }, schutz: { name: "Schutz & Regeln", + /* Was dasteht, wenn nichts dasteht. Ein Bereich, der + schweigt, sieht kaputt aus statt leer -- und der Satz + sagt NICHT nur "leer", sondern was hier hingehoert. */ + leer: "Noch kein Eintrag. Hierhin gehören die Regeln, Vorfälle und was daraus gelernt wurde — damit beim nächsten Mal niemand raten muss.", arten: { richtlinie: "Richtlinie", vorfall: "Vorfall", eskalation: "Eskalation", gelernt: "Gelernt" }, bewertung: false, dringlichkeit: true, @@ -473,6 +493,10 @@ export const BEREICHE = { ===================================================================== */ entwicklung: { name: "Entwicklung", + /* Was dasteht, wenn nichts dasteht. Ein Bereich, der + schweigt, sieht kaputt aus statt leer -- und der Satz + sagt NICHT nur "leer", sondern was hier hingehoert. */ + leer: "Noch keine Beobachtung eingetragen. Hier wird festgehalten, was jemand gut kann, woran er arbeitet und was verabredet ist.", ober: "Über die Zeit", arten: { staerke: "Das läuft gut", @@ -516,6 +540,10 @@ export const BEREICHE = { ===================================================================== */ talente: { name: "Talente", + /* Was dasteht, wenn nichts dasteht. Ein Bereich, der + schweigt, sieht kaputt aus statt leer -- und der Satz + sagt NICHT nur "leer", sondern was hier hingehoert. */ + leer: "Noch kein Name notiert. Wem im Chat etwas auffällt — jemand, der ruhig bleibt, wenn es hitzig wird, oder der anderen hilft — trägt ihn hier ein.", ober: "Nachwuchs", arten: { ruhig: "Bleibt ruhig, wenn es hitzig wird", @@ -535,6 +563,10 @@ export const BEREICHE = { rueckmeldung: { name: "Rückmeldung", + /* Was dasteht, wenn nichts dasteht. Ein Bereich, der + schweigt, sieht kaputt aus statt leer -- und der Satz + sagt NICHT nur "leer", sondern was hier hingehoert. */ + leer: "Noch keine Rückmeldung. Hier kann jeder sagen, was gut läuft und wo es hakt — auch über DogFather und über die Regeln.", /* Wie beim Ideen-Board: Das hier ist keine Akte ueber jemanden, sondern etwas, das allen gehoert. */ ober: "Gemeinsam besser werden", @@ -564,6 +596,10 @@ export const BEREICHE = { ideen: { name: "Ideen-Board", + /* Was dasteht, wenn nichts dasteht. Ein Bereich, der + schweigt, sieht kaputt aus statt leer -- und der Satz + sagt NICHT nur "leer", sondern was hier hingehoert. */ + leer: "Noch keine Idee gesammelt. Alles darf hier rein, auch halb gedacht — sortiert wird später.", /* DAS WORT UEBER DEM TITEL. Ueberall sonst steht dort "Betreuung", weil die Bereichsseiten Betreuungsakten sind -- ueber jemanden gefuehrt, von seinen Betreuern. Eine Ideensammlung ist das @@ -609,6 +645,10 @@ export const BEREICHE = { entscheidet dieselbe Regel wie ueberall. */ angebote: { name: "Angebote", + /* Was dasteht, wenn nichts dasteht. Ein Bereich, der + schweigt, sieht kaputt aus statt leer -- und der Satz + sagt NICHT nur "leer", sondern was hier hingehoert. */ + leer: "Noch kein Vorschlag. Wer eine Aktion, ein Format oder eine Regel vorschlagen will, schreibt es hier auf.", arten: { aktion: "Aktion", format: "Format", @@ -628,6 +668,10 @@ export const BEREICHE = { agentur: { name: "Agentur", + /* Was dasteht, wenn nichts dasteht. Ein Bereich, der + schweigt, sieht kaputt aus statt leer -- und der Satz + sagt NICHT nur "leer", sondern was hier hingehoert. */ + leer: "Noch kein Eintrag. Hier laufen Kampagnen, Schulungen und Anliegen zusammen, die zwischen Team und Agentur besprochen werden.", arten: { kampagne: "Agentur-Events", schulung: "Schulung", diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index 99df2007..e776fb07 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`. --> - - + + - +