Filipe: "die sachen von screen3 verbinde die mit dem auf screen4. so dass wenn man drauf drueckt man die anderen zu sehen bekommt. damit es nicht zu viel platz nimmt. und dan screen5, passe die reihenfolge der sachen danach an und dan perfektionierst du das alles auf dem handy auch bitte." F, K, U und S standen als EIGENE ZEILE ueber dem Schreibfeld -- dauerhaft, auf jedem Geraet. Gemessen auf 390 px: Die Konsole brauchte dafuer drei Reihen statt zwei. Nachgemessen ist es jetzt genau 73 px Hoehe, jeden Tag, fuer vier Zeichen, die man in den seltensten Nachrichten braucht. DER KNOPF HEISST "Aa" UND STEHT ALS LETZTES WERKZEUG, direkt vor dem Schreibfeld -- das ist die neue Reihenfolge, um die Filipe gebeten hat: Bueroklammer, Mikrofon, GIF und Emoji fuegen etwas EIN; das hier veraendert, was schon dasteht. Die Reihe geht damit von "dazu" nach "daran", und das Letzte liegt dem Text am naechsten. "Aa" und kein Symbol: Ein Stift heisst "bearbeiten", ein Pinsel "malen". Fuer Fett und Kursiv gibt es seit jeher genau ein Zeichen, das jeder liest, ohne es zu lernen. AUF DEM HANDY IST DAS DER GANZE PUNKT: Statt drei Reihen (Zeichen / Werkzeuge / Feld+Senden) sind es zwei. Der Aa-Knopf faellt dabei nicht ins Gewicht, weil er IN der Werkzeuggruppe sitzt -- die bricht als Block um, und ein Knopf mehr laesst das Schreibfeld nicht schrumpfen. Das ist dieselbe Ueberlegung, die die Gruppe am 23.09. ueberhaupt entstehen liess. DIE WAHL WIRD GEMERKT. Wer viel gestaltet, gestaltet weiter -- die Leiste bleibt dann auch nach dem Neuladen offen. Die TASTENKUERZEL laufen unabhaengig davon: Strg+B und Strg+I haengen am Schreibfeld, nicht an der Leiste. Zugeklappt wird sie versteckt, nicht abgebaut. ZWEI SACHEN AN DEN PRUEFUNGEN, und die erste ist ein Fund: pruef-chat-optik verlangte "die vier Werkzeuge stehen in einer Gruppe" -- mit einer festen 4. Diese Zahl war vom ersten Tag an eine Rechnung von gestern: Kommt ein Werkzeug dazu, wird die Zeile rot, obwohl nichts kaputt ist, und wer sie dann auf 5 setzt, macht denselben Fehler mit einer anderen Zahl. Dieses Haus ist an festen Zahlen schon dreimal hereingefallen (Kopfleiste 06.09., Schriftgroessen 14.09., Tippziele 20.09.). Gefragt wird jetzt, was gemeint ist: Liegt KEINES der Werkzeuge ausserhalb der Gruppe? Das misst, statt zu rechnen -- und der naechste Knopf bringt es nicht zu Fall. Die Anzahl steht trotzdem in der Bedingung, sonst waeren "0 von 0" gruen. Die Gegenprobe wollte im ersten Anlauf Strg+B tippen. Das lief in eine Zeitbombe: Der Treff hat eine Nachtruhe, und zwischen 22 und 6 Uhr ist das Schreibfeld `disabled` -- die Pruefung waere sechs Stunden am Tag rot gewesen, ohne dass an der Sache etwas ist. Genau die Sorte Fehlalarm, die am 06.09.2026 schon einmal notiert wurde. Gemessen wird jetzt die Sache selbst: Die vier Knoepfe sind weiterhin im Dokument und haben nur keine Flaeche mehr. pruef-tippziele meldete sofort "1 ungedeckt: .chat__schriftknopf (40h 40w)" -- am Daumen fehlten vier Pixel. Nachgezogen, wie bei den vier Werkzeugen daneben. Geprueft: pruef-chat-optik (mit 6 neuen Punkten, alle gruen), pruef-tippziele (11), pruef-css-klassen, pruef-chat-ausbau (64). Co-Authored-By: Claude Opus 5 <[email protected]>
977 lines
46 KiB
JavaScript
977 lines
46 KiB
JavaScript
/* CHAT — SCHREIBEN ZWEI LEUTE WIRKLICH MITEINANDER?
|
||
|
||
pruef-chat.mjs beweist, dass der Server richtig entscheidet, WER mit
|
||
wem darf. Diese Prüfung fragt das, was der Wunsch eigentlich meinte:
|
||
*"jeder kann dann eine nachricht reinschreiben, die andere person
|
||
bekommt eine benachrichtigung und kann dann antworten."*
|
||
|
||
Dafür laufen ZWEI Browser gleichzeitig. Luna schreibt, und ohne dass
|
||
irgendjemand etwas neu lädt, muss die Nachricht bei Patrick
|
||
erscheinen. Das ist der Kern -- alles andere ist Ausstattung.
|
||
|
||
WARUM ZWEI ECHTE BROWSER und nicht zwei Anfragen: Die sofortige
|
||
Zustellung hängt an einer offenen Verbindung (SSE), an einem
|
||
EventSource im Browser und daran, dass die Seite darauf richtig
|
||
reagiert. Mit fetch allein prüfte man den Server, nicht den Chat.
|
||
|
||
GEMESSEN WIRD AUSSERDEM:
|
||
* Die Zahl in der Kopfleiste -- auf einer ANDEREN Seite, denn dort
|
||
muss sie ankommen, damit jemand von der Nachricht erfährt.
|
||
* Dass die eigene Nachricht sofort dasteht, nicht erst nach der
|
||
Antwort des Servers.
|
||
* Dass "gelesen" die Zahl wieder wegnimmt.
|
||
|
||
GEGENPROBEN durchgehend. */
|
||
|
||
import { mkdtempSync, rmSync, appendFileSync } from "node:fs";
|
||
import { tmpdir } from "node:os";
|
||
import { join } from "node:path";
|
||
|
||
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 { eigenerPort } = await import("./helfer-port.mjs");
|
||
const PORT = await eigenerPort(import.meta, "die Chat-Optikpruefung");
|
||
|
||
process.env.PORT = `${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");
|
||
/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort
|
||
zwei Auffangnetze ein, die einen Absturz nur protokollieren --
|
||
richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese
|
||
Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der
|
||
Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */
|
||
notbremse(480_000, "pruef-chat-optik");
|
||
await new Promise((r) => setTimeout(r, 1000));
|
||
const BASIS = `http://127.0.0.1:${PORT}`;
|
||
|
||
const PROTOKOLL = join(process.cwd(), "pruef-chat-optik-lauf.txt");
|
||
try { rmSync(PROTOKOLL, { force: true }); } catch { /* gab es noch nicht */ }
|
||
const melde = (z) => {
|
||
console.log(z);
|
||
try { appendFileSync(PROTOKOLL, z + "\n"); } catch { /* Zugabe */ }
|
||
};
|
||
let fehler = 0;
|
||
const ok = (b, t) => { melde((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();
|
||
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, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
|
||
.run(name, rolle, hash, salt, 32768, jetzt);
|
||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||
}
|
||
anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
||
const idPat = anlegen("Patrick", "scout", "CODE-SCOU-0001");
|
||
const idLuna = anlegen("Luna", "creator", "CODE-CREA-0001");
|
||
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idLuna, idPat, 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();
|
||
|
||
async function alsRolle(rolle, code, wohin = "chat.html", breite = 1280) {
|
||
/* MIT FINGER, WENN ES SCHMAL IST. Playwright schaltet
|
||
`pointer: coarse` nur ein, wenn der Zusammenhang Beruehrung kennt
|
||
-- ohne das misst man den Rechner und glaubt, es sei das Handy.
|
||
Genau daran ist mir am 23.09. eine Messung danebengegangen. */
|
||
const ctx = await browser.newContext({
|
||
viewport: { width: breite, height: 900 },
|
||
hasTouch: breite < 900, isMobile: breite < 900 });
|
||
const seite = await ctx.newPage();
|
||
await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
||
await seite.click(`.rolle[data-rolle="${rolle}"]`);
|
||
await seite.fill("#code", code);
|
||
await seite.click("button[type=submit]");
|
||
await seite.waitForURL("**/start.html", { timeout: 25000 });
|
||
await seite.goto(`${BASIS}/workspace/${wohin}`, { waitUntil: "networkidle" });
|
||
await seite.waitForTimeout(900);
|
||
return { ctx, seite };
|
||
}
|
||
|
||
/* =======================================================================
|
||
1. Ein Gespräch anfangen
|
||
======================================================================= */
|
||
melde("\n=== Ein Gespräch anfangen ===");
|
||
const luna = await alsRolle("creator", "CODE-CREA-0001");
|
||
{
|
||
const leer = await luna.seite.isVisible("#liste-leer");
|
||
ok(leer, "am Anfang steht da, dass es noch keine Gespräche gibt");
|
||
|
||
await luna.seite.click("#neu-oeffnen");
|
||
await luna.seite.waitForSelector("#neu-dialog[open]", { timeout: 5000 });
|
||
|
||
const knoepfe = await luna.seite.$$eval(".chat-wahl__knopf",
|
||
(b) => b.map((x) => x.textContent.trim()));
|
||
/* Luna erreicht ihren Scout und DogFather -- die Regel aus
|
||
pruef-chat, hier an der Oberfläche nachgewiesen. */
|
||
ok(knoepfe.length === 2, `${knoepfe.length} Personen zur Auswahl: ${knoepfe.join(", ")}`);
|
||
|
||
const los = await luna.seite.$eval("#neu-los", (b) => b.disabled);
|
||
ok(los, "der Knopf ist gesperrt, solange niemand gewählt ist");
|
||
|
||
await luna.seite.click('.chat-wahl__knopf[data-rolle="scout"]');
|
||
await luna.seite.waitForTimeout(200);
|
||
const frei = await luna.seite.$eval("#neu-los", (b) => !b.disabled);
|
||
const beschriftung = await luna.seite.$eval("#neu-los", (b) => b.textContent.trim());
|
||
ok(frei && beschriftung === "Schreiben",
|
||
`nach der Wahl ist er frei und heißt "${beschriftung}"`);
|
||
|
||
/* Der Gruppenname erscheint erst ab zwei -- vorher wäre er eine
|
||
Frage ohne Anlass. */
|
||
const nameVersteckt = await luna.seite.$eval("#gruppenname-zeile", (e) => e.hidden);
|
||
ok(nameVersteckt, "bei einer Person wird nicht nach einem Gruppennamen gefragt");
|
||
|
||
await luna.seite.click("#neu-los");
|
||
await luna.seite.waitForSelector("#schreiben:not([hidden])", { timeout: 8000 });
|
||
const titel = await luna.seite.$eval("#verlauf-titel", (e) => e.textContent);
|
||
ok(titel === "Patrick", `das Gespräch ist offen: "${titel}"`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
2. DER KERN: schreiben, und es kommt an — ohne Neuladen
|
||
======================================================================= */
|
||
melde("\n=== Zwei Browser, eine Nachricht ===");
|
||
const pat = await alsRolle("scout", "CODE-SCOU-0001");
|
||
{
|
||
/* Patrick öffnet dasselbe Gespräch. */
|
||
await pat.seite.waitForSelector(".chat-raum__knopf", { timeout: 8000 });
|
||
await pat.seite.click(".chat-raum__knopf");
|
||
await pat.seite.waitForSelector("#schreiben:not([hidden])", { timeout: 8000 });
|
||
await pat.seite.waitForTimeout(600);
|
||
|
||
const vorher = (await pat.seite.$$(".chat-nachricht")).length;
|
||
|
||
/* Luna schreibt. */
|
||
await luna.seite.fill("#text", "Hey Patrick, kurze Frage zum Start-Check!");
|
||
await luna.seite.click("#senden");
|
||
|
||
/* SOFORT bei Luna sichtbar -- vor jeder Serverantwort. Deshalb wird
|
||
hier bewusst nur ganz kurz gewartet. */
|
||
await luna.seite.waitForTimeout(120);
|
||
const beiLuna = await luna.seite.$$eval(".chat-nachricht__text", (t) => t.map((x) => x.textContent));
|
||
ok(beiLuna.some((t) => /Start-Check/.test(t)),
|
||
"die eigene Nachricht steht sofort da, ohne auf den Server zu warten");
|
||
|
||
/* UND SIE KOMMT BEI PATRICK AN, ohne dass er etwas tut. */
|
||
const angekommen = await pat.seite.waitForFunction(
|
||
() => [...document.querySelectorAll(".chat-nachricht__text")]
|
||
.some((t) => /Start-Check/.test(t.textContent)),
|
||
{ timeout: 8000 }).then(() => true).catch(() => false);
|
||
ok(angekommen, angekommen
|
||
? "sie erscheint bei Patrick von selbst — kein Neuladen nötig"
|
||
: "sie kam bei Patrick NICHT an (der Strom liefert nicht)");
|
||
|
||
const nachher = (await pat.seite.$$(".chat-nachricht")).length;
|
||
ok(nachher === vorher + 1, `Patrick sieht ${nachher} statt ${vorher} Nachrichten`);
|
||
|
||
/* Und die Seiten sind sich einig, WER sie geschrieben hat. */
|
||
const beiPatSelbst = await pat.seite.$eval(".chat-nachricht:last-of-type",
|
||
(e) => e.dataset.selbst === "ja");
|
||
const beiLunaSelbst = await luna.seite.$eval(".chat-nachricht:last-of-type",
|
||
(e) => e.dataset.selbst === "ja");
|
||
ok(!beiPatSelbst && beiLunaSelbst,
|
||
"bei Luna steht sie rechts (eigene), bei Patrick links (fremde)");
|
||
}
|
||
|
||
/* =======================================================================
|
||
3. Und zurück
|
||
======================================================================= */
|
||
melde("\n=== Patrick antwortet ===");
|
||
{
|
||
await pat.seite.fill("#text", "Klar, schieß los!");
|
||
await pat.seite.click("#senden");
|
||
|
||
const zurueck = await luna.seite.waitForFunction(
|
||
() => [...document.querySelectorAll(".chat-nachricht__text")]
|
||
.some((t) => /schieß los/.test(t.textContent)),
|
||
{ timeout: 8000 }).then(() => true).catch(() => false);
|
||
ok(zurueck, "die Antwort erscheint bei Luna von selbst");
|
||
|
||
/* Der Verlauf steht in der richtigen Reihenfolge -- eine
|
||
Unterhaltung, die durcheinandergerät, ist unbrauchbar. */
|
||
const texte = await luna.seite.$$eval(".chat-nachricht__text", (t) => t.map((x) => x.textContent));
|
||
ok(/Start-Check/.test(texte[0]) && /schieß los/.test(texte[1]),
|
||
`Reihenfolge stimmt: "${texte.map((t) => t.slice(0, 18)).join('" → "')}"`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
4. Die Zahl in der Kopfleiste — auf einer ANDEREN Seite
|
||
======================================================================= */
|
||
melde("\n=== Erfährt man davon, ohne im Chat zu sein? ===");
|
||
{
|
||
/* Patrick geht woandershin. Genau dort muss die Zahl erscheinen --
|
||
sonst merkt er von einer Nachricht nichts. */
|
||
await pat.seite.goto(`${BASIS}/workspace/aufgaben.html`, { waitUntil: "networkidle" });
|
||
await pat.seite.waitForTimeout(900);
|
||
|
||
const knopfDa = await pat.seite.$("#chat-knopf, .chat-knopf");
|
||
ok(!!knopfDa, "auf der Aufgabenseite steht ein Chat-Knopf in der Kopfleiste");
|
||
|
||
const vorher = await pat.seite.$eval(".chat-knopf__zahl",
|
||
(e) => (e.hidden ? "0" : e.textContent));
|
||
ok(vorher === "0", `vorher zeigt er nichts an (${vorher})`);
|
||
|
||
await luna.seite.fill("#text", "Wann hast du Zeit für einen Call?");
|
||
await luna.seite.click("#senden");
|
||
|
||
const erschienen = await pat.seite.waitForFunction(() => {
|
||
const z = document.querySelector(".chat-knopf__zahl");
|
||
return z && !z.hidden && z.textContent === "1";
|
||
}, { timeout: 8000 }).then(() => true).catch(() => false);
|
||
ok(erschienen, erschienen
|
||
? "die Zahl 1 erscheint von selbst in der Kopfleiste"
|
||
: "die Zahl erscheint NICHT — man erfährt nichts von der Nachricht");
|
||
|
||
/* Und der Knopf selbst trägt Farbe -- eine kleine Zahl allein
|
||
übersieht man beim Überfliegen. */
|
||
const markiert = await pat.seite.$eval(".chat-knopf", (e) => e.dataset.neu === "ja");
|
||
ok(markiert, "der Knopf ist zusätzlich hervorgehoben, nicht nur die Zahl");
|
||
}
|
||
|
||
/* =======================================================================
|
||
5. Lesen nimmt die Zahl wieder weg
|
||
======================================================================= */
|
||
melde("\n=== Gelesen ===");
|
||
{
|
||
await pat.seite.goto(`${BASIS}/workspace/chat.html`, { waitUntil: "networkidle" });
|
||
await pat.seite.waitForSelector(".chat-raum__knopf", { timeout: 8000 });
|
||
|
||
const zahlInListe = await pat.seite.$eval(".chat-raum__zahl", (e) => e.textContent)
|
||
.catch(() => null);
|
||
ok(zahlInListe === "1", `in der Gesprächsliste steht die Zahl (${zahlInListe})`);
|
||
|
||
await pat.seite.click(".chat-raum__knopf");
|
||
await pat.seite.waitForTimeout(1200);
|
||
|
||
const weg = await pat.seite.$(".chat-raum__zahl");
|
||
ok(!weg, "nach dem Öffnen ist sie weg");
|
||
|
||
/* Und sie bleibt weg -- auch nach einem Neuladen. Wäre "gelesen" nur
|
||
eine Anzeige im Browser, käme die Zahl beim nächsten Aufruf zurück. */
|
||
await pat.seite.reload({ waitUntil: "networkidle" });
|
||
await pat.seite.waitForTimeout(1000);
|
||
const wiederDa = await pat.seite.$(".chat-raum__zahl");
|
||
ok(!wiederDa, "auch nach dem Neuladen bleibt sie weg — es ist wirklich gespeichert");
|
||
}
|
||
|
||
/* =======================================================================
|
||
6. Zurücknehmen, im Bild
|
||
======================================================================= */
|
||
melde("\n=== Zurücknehmen ===");
|
||
{
|
||
await luna.seite.bringToFront();
|
||
const vorher = (await luna.seite.$$(".chat-nachricht")).length;
|
||
|
||
/* Seit 19.09.2026 ein eigener <dialog> statt confirm() -- bei einem
|
||
confirm() haette Playwright ihn nicht von selbst bestaetigt, bei
|
||
einem <dialog> 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);
|
||
|
||
/* UMGEDREHT AM 22.09.2026 ABENDS.
|
||
------------------------------------------------------------------
|
||
Hier stand: „die Zeile bleibt stehen -- kein Loch im Verlauf", und
|
||
daneben „an ihrer Stelle steht 'Nachricht zurueckgenommen'".
|
||
|
||
Filipe: „Geloeschte Nachrichten verschwinden vollstaendig -- keine
|
||
Spur, kein ‚wurde geloescht'-Hinweis, bei niemandem, auch nicht
|
||
bei DogFather."
|
||
|
||
Die Zahl davor und danach ist der Beleg: Sie muss um genau eins
|
||
sinken. „Irgendwie weniger" waere auch dann gruen, wenn der ganze
|
||
Verlauf verschwaende. */
|
||
const nachher = (await luna.seite.$$(".chat-nachricht")).length;
|
||
ok(vorher > 0 && nachher === vorher - 1,
|
||
`die Zeile ist weg (${vorher} -> ${nachher}) — und nur sie`);
|
||
|
||
const hinweis = await luna.seite.$(".chat-nachricht__weg");
|
||
ok(!hinweis, "und es steht kein Hinweis an ihrer Stelle");
|
||
|
||
/* UND BEIM GEGENUEBER EBENFALLS -- von selbst, ohne Neuladen. Das
|
||
ist der Teil, der ueber den Ereignisstrom laeuft; ohne ihn saehe
|
||
Patrick die Nachricht weiter, bis er die Seite neu laedt. */
|
||
const beiPat = await pat.seite.waitForFunction(
|
||
(wieViele) => document.querySelectorAll(".chat-nachricht").length === wieViele,
|
||
vorher - 1, { timeout: 8000 }).then(() => true).catch(() => false);
|
||
ok(beiPat, "auch bei Patrick verschwindet sie, ohne Neuladen");
|
||
}
|
||
|
||
/* =======================================================================
|
||
GEGENPROBE — misst die Prüfung überhaupt etwas?
|
||
======================================================================= */
|
||
melde("\n=== Gegenprobe ===");
|
||
{
|
||
/* Träfe der Selektor nichts, wären alle Haken oben wertlos. */
|
||
const vorher = (await luna.seite.$$(".chat-nachricht")).length;
|
||
await luna.seite.evaluate(() => {
|
||
for (const n of document.querySelectorAll(".chat-nachricht")) n.remove();
|
||
});
|
||
const nachher = (await luna.seite.$$(".chat-nachricht")).length;
|
||
ok(vorher >= 2 && nachher === 0,
|
||
`der Selektor trifft (${vorher} gefunden, nach dem Entfernen ${nachher})`);
|
||
|
||
/* Und ein Text, der nie geschrieben wurde, darf NICHT gefunden
|
||
werden -- sonst prüfte "kam an" gar nichts. */
|
||
await luna.seite.reload({ waitUntil: "networkidle" });
|
||
await luna.seite.waitForTimeout(1200);
|
||
const erfunden = await luna.seite.$$eval(".chat-nachricht__text",
|
||
(t) => t.some((x) => /Diesen Satz gab es nie/.test(x.textContent)));
|
||
ok(!erfunden, "ein nie geschriebener Satz wird nicht gefunden");
|
||
}
|
||
|
||
/* =======================================================================
|
||
DER FUSS EINER NACHRICHT BLEIBT IN SEINER BLASE (10.09.2026)
|
||
|
||
Am Fuss jeder Nachricht stehen die Handgriffe: Zeit, antworten,
|
||
zuruecknehmen, kopieren -- und seit Kapitel 7.2 auch anheften. Mit
|
||
drei Eintraegen ging die Rechnung auf, mit vier nicht mehr:
|
||
Gemessen stand "kopieren" bei 390 px, der haeufigsten Handybreite,
|
||
18 px UEBER der Blase; bei 320 px waren es 75.
|
||
|
||
Gefunden hat das kein Testlauf, sondern ein Blick auf ein
|
||
Bildschirmfoto. Damit das beim FUENFTEN Handgriff nicht wieder
|
||
passiert, wird es ab jetzt gemessen -- und zwar an echten
|
||
Bildpunkten, nicht an der Rechnung, die zu dem Fehler gefuehrt hat.
|
||
|
||
GEPRUEFT WIRD NICHT DIE ZAHL DER ZEILEN. Ob der Fuss ein- oder
|
||
zweizeilig ist, darf sich aendern; dass etwas darin ueber den Rand
|
||
haengt, nicht. Ein `flex-wrap: wrap` ist deshalb die Loesung und
|
||
keine Schwelle: Es bricht genau dann um, wenn der Platz nicht reicht.
|
||
======================================================================= */
|
||
melde("\n=== Der Fuss bleibt in der Blase ===");
|
||
{
|
||
let gemessen = 0;
|
||
let heraus = [];
|
||
for (const breite of [320, 360, 390, 430]) {
|
||
await luna.seite.setViewportSize({ width: breite, height: 844 });
|
||
await luna.seite.waitForTimeout(350);
|
||
const m = await luna.seite.evaluate(() => {
|
||
const raus = [];
|
||
let stuecke = 0;
|
||
for (const f of document.querySelectorAll(".chat-nachricht__fuss")) {
|
||
const blase = f.closest(".chat-nachricht__blase")?.getBoundingClientRect();
|
||
if (!blase) continue;
|
||
for (const k of f.children) {
|
||
stuecke++;
|
||
const r = k.getBoundingClientRect();
|
||
if (r.width === 0) continue; /* ausgeblendet: kein Fall */
|
||
if (r.right > blase.right + 0.5 || r.left < blase.left - 0.5) {
|
||
raus.push((k.textContent || "?").trim() + " +" + Math.round(r.right - blase.right));
|
||
}
|
||
}
|
||
}
|
||
return { raus, stuecke };
|
||
});
|
||
gemessen += m.stuecke;
|
||
if (m.raus.length) heraus.push(`${breite}px: ${m.raus.join(", ")}`);
|
||
}
|
||
/* DIE ZAHL GEHOERT IN DIE BEDINGUNG, nicht nur in den Meldetext.
|
||
Findet der Selektor nichts, waere `heraus.length === 0` gruen --
|
||
und haette nichts angesehen. Genau der Fehler vom 01.09. */
|
||
ok(gemessen >= 12 && heraus.length === 0,
|
||
`${gemessen} Fusselemente auf vier Breiten gemessen, nichts haengt heraus`
|
||
+ (heraus.length ? ` -- ${heraus.join(" | ")}` : ""));
|
||
|
||
/* GEGENPROBE: Kann die Messung ueberhaupt "haengt heraus" sagen?
|
||
Ein Element wird absichtlich hinausgeschoben. Sagt sie dann
|
||
weiterhin "alles gut", misst sie gar nichts. */
|
||
const erkannt = await luna.seite.evaluate(() => {
|
||
const f = document.querySelector(".chat-nachricht__fuss");
|
||
const k = f?.lastElementChild;
|
||
if (!k) return null;
|
||
const alt = k.style.marginLeft;
|
||
k.style.marginLeft = "400px";
|
||
const blase = f.closest(".chat-nachricht__blase").getBoundingClientRect();
|
||
const draussen = k.getBoundingClientRect().right > blase.right + 0.5;
|
||
k.style.marginLeft = alt;
|
||
return draussen;
|
||
});
|
||
ok(erkannt === true, "und sie erkennt ein absichtlich hinausgeschobenes Element");
|
||
await luna.seite.setViewportSize({ width: 1280, height: 900 });
|
||
await luna.seite.waitForTimeout(300);
|
||
}
|
||
|
||
/* =====================================================================
|
||
EINE NACHRICHT, DIE NICHT ANKOMMT (20.09.2026)
|
||
|
||
Der wichtigste Satz im Code daneben lautete: „NICHT STILL
|
||
VERSCHWINDEN LASSEN. Wer etwas schreibt und es sieht, glaubt, es
|
||
sei angekommen." Gemessen am 20.09.2026 wurde er nicht eingehalten:
|
||
`markiereAlsGescheitert` setzte `n.gescheitert = true` -- und
|
||
gelesen hat das Merkmal NIEMAND, weder das Skript noch das CSS.
|
||
Eine gescheiterte Nachricht sah exakt aus wie eine zugestellte.
|
||
|
||
Ein Kommentar, der eine Absicht beschreibt, erfüllt sie nicht.
|
||
|
||
Und der Satz in der Meldezeile bat den Nutzer, die Arbeit der Seite
|
||
zu machen: „kopiere sie und versuch es erneut" -- markieren,
|
||
kopieren, Feld leeren, einfügen, senden. Fünf Handgriffe für etwas,
|
||
das die Seite in einem tun kann; den Text hat sie ja noch.
|
||
|
||
Geprüft wird deshalb der GANZE Weg, und zwar an einem echten
|
||
Fehlschlag: Die Anfrage wird unterwegs abgefangen, die Nachricht
|
||
muss sichtbar scheitern, und der Knopf daneben muss sie wirklich
|
||
zustellen.
|
||
===================================================================== */
|
||
{
|
||
melde("\n— Eine Nachricht, die nicht ankommt —");
|
||
|
||
/* Den Sendeweg EINMAL scheitern lassen. Nicht den Server abschalten:
|
||
Dann wäre auch der Ereignisstrom weg, und die Prüfung misst einen
|
||
anderen Fehler als den gemeinten. */
|
||
let abgefangen = 0;
|
||
await luna.seite.route("**/api/chat/raeume/*/nachrichten", (weg) => {
|
||
if (abgefangen === 0) { abgefangen++; return weg.abort("failed"); }
|
||
return weg.continue();
|
||
});
|
||
|
||
const TEXT = "Diese Nachricht scheitert beim ersten Versuch.";
|
||
await luna.seite.fill("#text", TEXT);
|
||
await luna.seite.press("#text", "Enter");
|
||
await luna.seite.waitForTimeout(900);
|
||
|
||
ok(abgefangen === 1, `die Anfrage wurde wirklich abgefangen (${abgefangen}x)`);
|
||
|
||
const gescheitert = await luna.seite.evaluate((t) => {
|
||
const alle = [...document.querySelectorAll(".chat-nachricht")];
|
||
const z = alle.find((e) => e.textContent.includes(t));
|
||
if (!z) return null;
|
||
return {
|
||
markiert: z.dataset.gescheitert === "ja",
|
||
knopf: !!z.querySelector(".chat-nachricht__nochmal"),
|
||
/* Sieht man den Unterschied WIRKLICH -- oder steht er nur im
|
||
Merkmal? Gemessen an echten Bildpunkten. */
|
||
rand: getComputedStyle(z.querySelector(".chat-nachricht__blase")).borderTopWidth,
|
||
};
|
||
}, TEXT);
|
||
|
||
ok(gescheitert !== null, "die Nachricht steht noch im Verlauf");
|
||
ok(gescheitert?.markiert === true, "und ist sichtbar als gescheitert markiert");
|
||
ok(gescheitert?.rand !== "0px",
|
||
`der Unterschied ist auch zu SEHEN, nicht nur im Merkmal (Rand ${gescheitert?.rand})`);
|
||
ok(gescheitert?.knopf === true, "und daneben steht „Nochmal senden“");
|
||
|
||
/* Der Satz in der Meldezeile verlangt nichts mehr vom Nutzer. */
|
||
const satz = await luna.seite.evaluate(() =>
|
||
document.getElementById("fehler")?.textContent || "");
|
||
ok(!/kopiere/i.test(satz),
|
||
`und die Meldung bittet nicht mehr ums Kopieren („${satz.slice(0, 70)}“)`);
|
||
|
||
/* ---- Und jetzt der Knopf ---------------------------------------- */
|
||
const vorher = await luna.seite.evaluate(() =>
|
||
document.querySelectorAll(".chat-nachricht").length);
|
||
await luna.seite.click(".chat-nachricht__nochmal");
|
||
await luna.seite.waitForTimeout(1200);
|
||
|
||
const danach = await luna.seite.evaluate((t) => {
|
||
const alle = [...document.querySelectorAll(".chat-nachricht")];
|
||
const treffer = alle.filter((e) => e.textContent.includes(t));
|
||
return {
|
||
anzahl: alle.length,
|
||
mitText: treffer.length,
|
||
nochGescheitert: treffer.some((e) => e.dataset.gescheitert === "ja"),
|
||
};
|
||
}, TEXT);
|
||
|
||
ok(danach.mitText === 1,
|
||
`die Nachricht steht danach GENAU EINMAL da (${danach.mitText}x) `
|
||
+ `— nicht doppelt, wie es bei einem zweiten Sendeweg passieren würde`);
|
||
ok(danach.nochGescheitert === false, "und gilt nicht mehr als gescheitert");
|
||
ok(danach.anzahl === vorher, `die Zahl der Nachrichten blieb gleich (${vorher} → ${danach.anzahl})`);
|
||
|
||
/* Und sie ist WIRKLICH beim Gegenüber angekommen -- nicht nur im
|
||
eigenen Bild. Das ist der Unterschied zwischen „sieht gut aus"
|
||
und „funktioniert". */
|
||
await pat.seite.waitForTimeout(800);
|
||
const beimAnderen = await pat.seite.evaluate((t) =>
|
||
document.body.textContent.includes(t), TEXT);
|
||
ok(beimAnderen === true, "und sie ist beim Gegenüber angekommen");
|
||
|
||
await luna.seite.unroute("**/api/chat/raeume/*/nachrichten");
|
||
}
|
||
|
||
|
||
/* =======================================================================
|
||
MAN MUSS SEHEN, MIT WEM MAN SCHREIBT (20.09.2026)
|
||
|
||
Gemessen am Handy, Gespraech "Team-Runde":
|
||
|
||
360 px -> 0 px sichtbar (98 noetig)
|
||
390 px -> 12 px
|
||
412 px -> 33 px
|
||
|
||
Auf JEDEM Telefon stand also kein Name in der Kopfzeile. Man oeffnet
|
||
ein Gespraech und weiss nicht, mit wem -- und weil die Zeile nicht
|
||
leer aussieht (Zurueck-Pfeil, fuenf Knoepfe), wirkt sie nicht kaputt,
|
||
sondern nur eng.
|
||
|
||
WARUM DER GROSSE HANDY-RUNDGANG ES NICHT GEFUNDEN HAT -- zwei eigene
|
||
Entscheidungen, die zusammen ein Loch ergaben:
|
||
|
||
1. Er ueberspringt, was `width > 0` nicht erfuellt. Ein auf null
|
||
gequetschtes Element ist unsichtbar, und "unsichtbar" hiess
|
||
dort "nichts zu pruefen".
|
||
2. Er verzeiht `text-overflow: ellipsis` als ABSICHT. Das ist
|
||
richtig -- ein langer Name soll gekuerzt werden. Es ist nur
|
||
dann falsch, wenn gar nichts mehr uebrig ist.
|
||
|
||
Beides ist einzeln vernuenftig. Deshalb steht die Pruefung hier, wo
|
||
sie genau eine Sache misst, statt die Regeln dort aufzuweichen.
|
||
======================================================================= */
|
||
melde("\n=== Man sieht, mit wem man schreibt ===");
|
||
{
|
||
let gemessen = 0;
|
||
const zuEng = [];
|
||
for (const breite of [320, 360, 390, 430]) {
|
||
await luna.seite.setViewportSize({ width: breite, height: 844 });
|
||
await luna.seite.waitForTimeout(400);
|
||
const m = await luna.seite.evaluate(() => {
|
||
const t = document.getElementById("verlauf-titel");
|
||
if (!t) return null;
|
||
const r = t.getBoundingClientRect();
|
||
return { text: (t.textContent || "").trim(), sicht: Math.round(r.width),
|
||
noetig: Math.round(t.scrollWidth) };
|
||
});
|
||
if (!m || !m.text) continue;
|
||
gemessen++;
|
||
/* SICHTBAR MUSS SEIN, WAS GEBRAUCHT WIRD -- oder wenigstens so
|
||
viel, dass man den Namen erkennt. Ein Drittel eines Namens ist
|
||
kein Name. */
|
||
if (m.sicht < Math.min(m.noetig, 90)) {
|
||
zuEng.push(`${breite}px: ${m.sicht} von ${m.noetig} px`);
|
||
}
|
||
}
|
||
/* DIE ZAHL GEHOERT IN DIE BEDINGUNG: Faende die Schleife gar keinen
|
||
Titel, waere "nichts zu eng" trivialerweise wahr. */
|
||
ok(gemessen === 4 && zuEng.length === 0,
|
||
`der Gespraechsname ist auf allen ${gemessen} Breiten lesbar`
|
||
+ (zuEng.length ? " — ZU ENG: " + zuEng.join(", ") : ""));
|
||
|
||
/* ---- GEGENPROBE ------------------------------------------------
|
||
|
||
ERST DEN GEMESSENEN FALL NACHSTELLEN. Der erste Anlauf drehte nur
|
||
die Angabe zurueck und blieb gruen: In DIESEM Gespraech stehen
|
||
drei Knoepfe, da reicht der Platz auch ohne die Reparatur. Der
|
||
Fehler trat in einer GRUPPE auf, wo fuenf Knoepfe stehen --
|
||
Anrufen, Video, Aufloesen, Wegraeumen, Wer-ist-drin.
|
||
|
||
Eine Gegenprobe, die den Fall nicht herstellt, den sie pruefen
|
||
soll, ist keine. Deshalb bekommt die Knopfreihe hier die Breite,
|
||
die fuenf Knoepfe wirklich brauchen (5 x 44 + 4 x 6 Abstand).
|
||
Damit muss die alte Angabe den Namen zerdruecken -- und die neue
|
||
nicht. Beide Richtungen werden gemessen. */
|
||
await luna.seite.setViewportSize({ width: 390, height: 844 });
|
||
await luna.seite.waitForTimeout(300);
|
||
const ab = await luna.seite.evaluate(() => {
|
||
const eng = document.createElement("style");
|
||
eng.textContent = ".chat__kopf-knoepfe { min-width: 244px !important; }";
|
||
document.head.append(eng);
|
||
|
||
const t = document.getElementById("verlauf-titel");
|
||
const mitNeu = Math.round(t.getBoundingClientRect().width);
|
||
|
||
const alt = document.createElement("style");
|
||
alt.textContent = ".chat__kopf--verlauf .chat__kopf-text { flex: 1 1 0 !important; }";
|
||
document.head.append(alt);
|
||
const mitAlt = Math.round(t.getBoundingClientRect().width);
|
||
|
||
alt.remove(); eng.remove();
|
||
return { mitNeu, mitAlt };
|
||
});
|
||
ok(ab.mitAlt < 40,
|
||
`Gegenprobe: mit der alten Angabe bleiben ${ab.mitAlt} px — die Pruefung kann auch NEIN sagen`);
|
||
ok(ab.mitNeu >= 90,
|
||
` und mit der neuen ${ab.mitNeu} px, im selben Aufbau — die Reparatur ist der Unterschied`);
|
||
await luna.seite.setViewportSize({ width: 1280, height: 900 });
|
||
await luna.seite.waitForTimeout(300);
|
||
}
|
||
|
||
await luna.ctx.close();
|
||
await pat.ctx.close();
|
||
/* =======================================================================
|
||
DAS SCHREIBFELD BLEIBT BREIT GENUG -- AUCH BEIM ANTWORTEN
|
||
=======================================================================
|
||
|
||
Filipes Bildschirmfoto vom 21.09.2026: Die Antwortleiste nimmt die
|
||
ganze Breite, und daneben steht ein Schreibfeld von der Breite eines
|
||
einzigen Buchstabens. Der Text laeuft senkrecht: "defi / niti / v /
|
||
hah / a".
|
||
|
||
URSACHE: `.chat__eingabe` ist eine Reihe OHNE Umbruch, und die
|
||
Antwortleiste ist darin ein gleichberechtigtes Element. Das
|
||
Schreibfeld hatte `min-width: 0` -- also woertlich die Erlaubnis,
|
||
auf nichts zu schrumpfen.
|
||
|
||
Das `margin-bottom` an der Leiste sagte seit jeher, dass sie
|
||
DARUEBER gehoert. Durchgesetzt wurde es nie. Wieder ein Fall von:
|
||
Die Absicht stand da, die Regel nicht.
|
||
|
||
GEMESSEN WIRD AN DER SCHMALSTEN BREITE, die das Haus kennt -- wenn
|
||
es dort passt, passt es ueberall.
|
||
======================================================================= */
|
||
melde("\n=== Antworten quetscht das Schreibfeld nicht ===");
|
||
{
|
||
for (const breite of [320, 430]) {
|
||
const p2 = await alsRolle("admin", "CODE-DOGI-0001", "chat.html?raum=treff", breite);
|
||
try {
|
||
await p2.seite.waitForTimeout(900);
|
||
const mass = await p2.seite.evaluate(() => {
|
||
const l = document.getElementById("antwort-leiste");
|
||
const t = document.getElementById("text");
|
||
if (!l || !t) return null;
|
||
/* Die Leiste wird eingeblendet wie beim Antworten -- mit einem
|
||
langen Text, denn genau der hat sie breit gemacht. */
|
||
l.hidden = false;
|
||
const von = document.getElementById("antwort-von");
|
||
const txt = document.getElementById("antwort-text");
|
||
if (von) von.textContent = "Diene";
|
||
if (txt) {
|
||
txt.textContent = "Ohaaa, da bin ich voll dabei. Das waere ein "
|
||
+ "Unterschied wie Tag und Nacht, wirklich jedes Mal wieder.";
|
||
}
|
||
const r = t.getBoundingClientRect();
|
||
const rl = l.getBoundingClientRect();
|
||
return { feld: Math.round(r.width), leiste: Math.round(rl.width),
|
||
fenster: window.innerWidth };
|
||
});
|
||
ok(!!mass, `${breite} px: Schreibfeld und Antwortleiste sind da`);
|
||
if (mass) {
|
||
/* 8rem = 128 px ist die Untergrenze aus der Stilvorlage. Ein
|
||
Feld darunter ist kein Schreibfeld mehr, sondern ein Spalt. */
|
||
ok(mass.feld >= 128,
|
||
`${breite} px: das Schreibfeld ist ${mass.feld} px breit (mindestens 128)`);
|
||
/* UND DIE LEISTE HAT IHRE EIGENE ZEILE. Waere sie schmaler als
|
||
das Fenster, saesse sie wieder neben dem Feld -- dann waere
|
||
die Zeile darueber nur zufaellig gruen, weil der Text kurz
|
||
genug war. */
|
||
ok(mass.leiste > mass.feld,
|
||
`${breite} px: die Antwortleiste nimmt die ganze Zeile `
|
||
+ `(${mass.leiste} gegen ${mass.feld} px)`);
|
||
}
|
||
} finally {
|
||
await p2.ctx.close();
|
||
}
|
||
}
|
||
}
|
||
|
||
/* =======================================================================
|
||
DIE SCHREIBZEILE AUF DEM HANDY (23.09.2026)
|
||
|
||
Filipe schreibt vom Handy. In dieser einen Zeile stehen inzwischen
|
||
SECHS Dinge: Bueroklammer, Mikrofon, GIF, Emoji, Schreibfeld,
|
||
Senden -- die letzten drei sind in der Nacht zum 23.09. dazu-
|
||
bzw. umgekommen.
|
||
|
||
WARUM DAS GEFAEHRLICH IST, steht in den Hausregeln: Am 06.09.2026
|
||
kam ein sechstes Element in die Kopfleiste, und bei 412 px lag ein
|
||
Knopf ueber dem anderen -- wer auf "Meine Sicht" tippte, landete im
|
||
Chat, auf zwoelf Seiten.
|
||
|
||
GEMESSEN AM 23.09., VOR DEM UMBAU:
|
||
320 px Feld 164
|
||
390 px Feld 130 -- und "Senden" ALLEIN auf einer eigenen Zeile
|
||
412 px Feld 151 -- dito
|
||
|
||
Das Feld lag auf seiner Untergrenze, und der Knopf, der die
|
||
Nachricht wegschickt, war weiter vom Text entfernt als die
|
||
Bueroklammer. Kaputt war nichts; benutzbar war es auch nicht.
|
||
|
||
NACH DEM UMBAU (zwei Gruppen: Werkzeuge / Schreiben):
|
||
320 px Feld 214 390 px Feld 278
|
||
412 px Feld 299 1280 px Feld 560
|
||
|
||
Diese Pruefung haelt beides fest: dass das Feld breit genug bleibt,
|
||
und dass Senden dabei steht.
|
||
======================================================================= */
|
||
melde("\n=== Die Schreibzeile auf dem Handy ===");
|
||
{
|
||
/* ==== MIT JEMANDEM, DER ALLE VIER WERKZEUGE HAT =================
|
||
|
||
Der erste Anlauf mass mit einem Scout -- und der bekommt Mikrofon
|
||
und GIF gar nicht zu sehen (nur Team Dogi und DogFather duerfen
|
||
Sprachnachrichten und GIFs schicken). Gemessen wurde damit eine
|
||
Zeile mit ZWEI Dingen darin, waehrend der gefaehrliche Fall der
|
||
mit VIER ist.
|
||
|
||
Aufgefallen ist es erst in der Gegenprobe: Mit der alten Fassung
|
||
kam das Feld auf 178 px statt der 130, die ich am selben Tag von
|
||
Hand gemessen hatte. Zwei verschiedene Zahlen fuer dieselbe
|
||
Sache -- das war der Hinweis.
|
||
|
||
Jetzt DogFather im Treff: vier Werkzeuge, und der Raum ist
|
||
sicher da. */
|
||
const handy = await alsRolle("admin", "CODE-DOGI-0001", "chat.html?raum=treff", 390);
|
||
try {
|
||
await handy.seite.waitForSelector("#schreiben:not([hidden])", { timeout: 8000 });
|
||
await handy.seite.waitForTimeout(900);
|
||
const zeile = await handy.seite.evaluate(() => {
|
||
const f = document.getElementById("text");
|
||
const senden = document.getElementById("senden");
|
||
const werk = document.querySelector(".chat__werkzeuge");
|
||
const r = (e) => { const x = e?.getBoundingClientRect();
|
||
return x ? { b: Math.round(x.width), h: Math.round(x.height),
|
||
y: Math.round(x.top), x: Math.round(x.left), re: Math.round(x.right) } : null; };
|
||
/* Ueberlappt irgendetwas in der Zeile? Genau das ist am 06.09.
|
||
passiert -- und man merkt es erst, wenn man danebentippt. */
|
||
const stuecke = [...document.querySelectorAll(
|
||
"#schreiben button, #schreiben textarea")]
|
||
.filter((e) => { const x = e.getBoundingClientRect(); return x.width > 1 && x.height > 1; })
|
||
.map((e) => ({ was: e.id || e.className.toString().split(" ")[0], ...r(e) }));
|
||
const drueber = [];
|
||
for (let i = 0; i < stuecke.length; i++) {
|
||
for (let j = i + 1; j < stuecke.length; j++) {
|
||
const a2 = stuecke[i], b2 = stuecke[j];
|
||
const ux = Math.min(a2.re, b2.re) - Math.max(a2.x, b2.x);
|
||
const uy = Math.min(a2.y + a2.h, b2.y + b2.h) - Math.max(a2.y, b2.y);
|
||
if (ux > 2 && uy > 2) drueber.push(`${a2.was} / ${b2.was}`);
|
||
}
|
||
}
|
||
return {
|
||
feld: r(f), senden: r(senden), werkzeuge: r(werk),
|
||
klein: stuecke.filter((t) => t.b < 38 || t.h < 38).map((t) => `${t.was} ${t.b}x${t.h}`),
|
||
drueber,
|
||
quer: document.documentElement.scrollWidth - window.innerWidth,
|
||
werkzeugZahl: werk ? werk.querySelectorAll("button").length : 0,
|
||
/* WIE VIELE WERKZEUGE GIBT ES UEBERHAUPT -- unabhaengig davon,
|
||
wo sie stehen. Die Zahl daneben sagt, wie viele IN der
|
||
Gruppe sitzen; erst der Vergleich beider sagt, ob eines
|
||
danebenliegt. Eine feste Zahl kann das nicht: Sie wird
|
||
falsch, sobald ein Werkzeug dazukommt -- und genau das ist
|
||
am 25.09.2026 passiert (der Schriftknopf „Aa"). */
|
||
werkzeugAlle: document.querySelectorAll(
|
||
"#schreiben :is(.chat__anhang, .chat__ton, .chat__gif,"
|
||
+ " .chat__emoji, .chat__schriftknopf)").length,
|
||
};
|
||
});
|
||
|
||
/* DIE ZAHL GEHOERT IN DIE BEDINGUNG -- ohne Feld waere alles
|
||
darunter gruen, ohne etwas angesehen zu haben. */
|
||
ok(!!zeile.feld && !!zeile.senden, "Schreibfeld und Senden sind da");
|
||
/* 220 px, und die Zahl ist der gemessene Ist-Wert minus Luft:
|
||
Gemessen sind 278. Vor dem Umbau waren es 130 -- die Pruefung
|
||
waere damals rot geworden, und genau das soll sie wieder, wenn
|
||
jemand einen siebten Knopf dazustellt. */
|
||
ok((zeile.feld?.b || 0) >= 220,
|
||
`das Schreibfeld ist ${zeile.feld?.b} px breit (mindestens 220)`);
|
||
/* SENDEN STEHT BEIM FELD. Vorher lag es eine Zeile tiefer, unten
|
||
links -- weiter vom Text entfernt als die Bueroklammer. */
|
||
/* ==== SIE VERGLICH DIE OBERKANTEN (behoben 25.09.2026) =========
|
||
|
||
Hier stand `Math.abs(senden.y - feld.y) <= 6`. Die Absicht ist
|
||
richtig, die Messung war es nicht: Die Zeile ist `align-items:
|
||
flex-end` -- beide stehen UNTEN buendig. Das Schreibfeld ist
|
||
zwei Zeilen hoch (66 px), der Knopf 42. Die Oberkanten liegen
|
||
damit zwangslaeufig 24 px auseinander, OBWOHL beide in
|
||
derselben Zeile stehen.
|
||
|
||
Die Pruefung war dadurch dauerhaft rot, ohne dass etwas kaputt
|
||
war -- und eine Warnung, die immer kommt, wird ueberlesen.
|
||
Gefunden habe ich es erst, als ich zweimal an der falschen
|
||
Stelle repariert hatte (Knopfbreite, Abstand): Beide Male
|
||
aenderte sich nichts an der Zahl. Eine Zahl, die sich durch die
|
||
Reparatur nicht bewegt, misst etwas anderes, als man denkt.
|
||
|
||
GEFRAGT WIRD JETZT, WAS GEMEINT IST: Teilen sich die beiden
|
||
eine Zeile? Das heisst, ihre senkrechten Bereiche UEBERLAPPEN
|
||
sich -- und zwar deutlich, nicht nur um einen Pixel. Rutscht
|
||
"Senden" wirklich eine Zeile tiefer, ist die Ueberlappung null
|
||
und die Zeile wird rot. Sie prueft damit mehr als vorher, nicht
|
||
weniger: Die alte Fassung haette zwei gleich hohe Elemente auch
|
||
dann durchgelassen, wenn sie in verschiedenen Spalten stehen. */
|
||
const fO = zeile.feld?.y || 0, fU = fO + (zeile.feld?.h || 0);
|
||
const sO = zeile.senden?.y || 0, sU = sO + (zeile.senden?.h || 0);
|
||
const gemeinsam = Math.min(fU, sU) - Math.max(fO, sO);
|
||
const kleinste = Math.min(zeile.feld?.h || 0, zeile.senden?.h || 0);
|
||
ok(kleinste > 0 && gemeinsam >= kleinste * 0.8,
|
||
`und "Senden" steht daneben, nicht darunter `
|
||
+ `(${gemeinsam} von ${kleinste} px gemeinsame Hoehe)`);
|
||
ok(zeile.drueber.length === 0,
|
||
`nichts liegt uebereinander (${zeile.drueber.join(", ") || "nichts"})`);
|
||
ok(zeile.klein.length === 0,
|
||
`alle Bedienelemente sind am Daumen treffbar (${zeile.klein.join(", ") || "alle gross genug"})`);
|
||
ok(zeile.quer <= 1, `die Seite scrollt nicht waagerecht (${zeile.quer})`);
|
||
|
||
/* GEGENPROBE ZUR MESSUNG: Die Werkzeuge muessen auf einem Handy
|
||
in einer ANDEREN Zeile stehen als das Feld -- sonst hiesse
|
||
"Senden steht daneben" nur, dass alles in einer Zeile liegt
|
||
und die Messung gar nicht unterscheiden kann. */
|
||
/* HIER STAND EINE 4 (nachgebessert 25.09.2026).
|
||
|
||
Sie war vom ersten Tag an eine Rechnung von gestern: Kommt ein
|
||
Werkzeug dazu, wird die Zeile rot, obwohl nichts kaputt ist --
|
||
und wer sie dann einfach auf 5 hochsetzt, macht denselben Fehler
|
||
mit einer anderen Zahl. Dieses Haus ist an festen Zahlen schon
|
||
dreimal hereingefallen (Kopfleiste 06.09., Schriftgroessen
|
||
14.09., Tippziele 20.09.).
|
||
|
||
GEFRAGT WIRD JETZT, WAS GEMEINT IST: Liegt KEINES der Werkzeuge
|
||
ausserhalb der Gruppe? Das ist die Sache -- sie sollen als Block
|
||
umbrechen, damit nicht eines allein auf einer Zeile landet. Die
|
||
Regel misst damit, statt zu rechnen, und der naechste Knopf
|
||
bringt sie nicht zu Fall.
|
||
|
||
DIE ANZAHL STEHT TROTZDEM IN DER BEDINGUNG: Waere die Gruppe
|
||
leer, weil jemand die Klasse umbenennt, waeren „0 von 0" sonst
|
||
gruen -- ein Haken ohne Gegenstand. */
|
||
ok(zeile.werkzeugAlle >= 4 && zeile.werkzeugZahl === zeile.werkzeugAlle,
|
||
`alle ${zeile.werkzeugAlle} Werkzeuge stehen in EINER Gruppe `
|
||
+ `(${zeile.werkzeugZahl} davon drin)`);
|
||
|
||
/* ==== DIE SCHRIFTLEISTE KLAPPT AUF UND ZU (25.09.2026) =========
|
||
|
||
Filipe: „die sachen von screen3 verbinde die mit dem auf
|
||
screen4. so dass wenn man drauf drückt man die anderen zu sehen
|
||
bekommt. damit es nicht zu viel platz nimmt."
|
||
|
||
GEMESSEN WIRD DER PLATZ, nicht nur der Zustand: Die Konsole muss
|
||
zugeklappt SPUERBAR niedriger sein. Ein Knopf, der ein Merkmal
|
||
umstellt, ohne dass Platz frei wird, haette den Auftrag nicht
|
||
erfuellt -- und ein `aria-expanded`, das sich aendert, waere
|
||
trotzdem gruen.
|
||
|
||
UND DIE TASTENKUERZEL: Sie haengen am Schreibfeld, nicht an der
|
||
Leiste. Waere das anders, haette das Zuklappen Strg+B
|
||
mitgenommen -- eine Funktion, die verschwindet, weil man Platz
|
||
sparen wollte. */
|
||
const vorher = await handy.seite.evaluate(() => ({
|
||
hoehe: Math.round(document.getElementById("schreiben").getBoundingClientRect().height),
|
||
leiste: !!document.querySelector(".textform")?.getBoundingClientRect().height,
|
||
auf: document.getElementById("schrift-auf")?.getAttribute("aria-expanded"),
|
||
}));
|
||
ok(vorher.auf === "false" && vorher.leiste === false,
|
||
`die Schriftleiste ist von sich aus zu (aria-expanded=${vorher.auf})`);
|
||
|
||
await handy.seite.click("#schrift-auf");
|
||
await handy.seite.waitForTimeout(350);
|
||
const offen = await handy.seite.evaluate(() => ({
|
||
hoehe: Math.round(document.getElementById("schreiben").getBoundingClientRect().height),
|
||
knoepfe: [...document.querySelectorAll(".textform__knopf")]
|
||
.filter((e) => e.getBoundingClientRect().height > 1).length,
|
||
auf: document.getElementById("schrift-auf")?.getAttribute("aria-expanded"),
|
||
}));
|
||
ok(offen.auf === "true" && offen.knoepfe === 4,
|
||
`ein Druck macht sie auf -- vier Zeichen (${offen.knoepfe})`);
|
||
ok(offen.hoehe > vorher.hoehe + 20,
|
||
`und zugeklappt spart das wirklich Platz `
|
||
+ `(${vorher.hoehe} px zu ${offen.hoehe} px, ${offen.hoehe - vorher.hoehe} px)`);
|
||
|
||
/* DIE WAHL UEBERLEBT DAS NEULADEN -- wer viel gestaltet, gestaltet
|
||
weiter. Ohne diese Zeile waere „sie klappt auf" ein Zustand, den
|
||
man bei jeder Nachricht neu herstellen muesste. */
|
||
await handy.seite.reload({ waitUntil: "networkidle" });
|
||
await handy.seite.waitForSelector("#schreiben:not([hidden])", { timeout: 8000 });
|
||
await handy.seite.waitForTimeout(700);
|
||
const danach = await handy.seite.evaluate(() => ({
|
||
auf: document.getElementById("schrift-auf")?.getAttribute("aria-expanded"),
|
||
knoepfe: [...document.querySelectorAll(".textform__knopf")]
|
||
.filter((e) => e.getBoundingClientRect().height > 1).length,
|
||
}));
|
||
ok(danach.auf === "true" && danach.knoepfe === 4,
|
||
`und bleibt nach dem Neuladen offen (${danach.auf}, ${danach.knoepfe} Zeichen)`);
|
||
|
||
/* GEGENPROBE: ZUGEKLAPPT IST NICHT ABGEBAUT.
|
||
|
||
Der erste Anlauf wollte hier Strg+B tippen und pruefen, ob "**"
|
||
im Feld landet. Das lief in eine Zeitbombe: Der Treff hat eine
|
||
NACHTRUHE, und zwischen 22 und 6 Uhr ist das Schreibfeld
|
||
`disabled`. Die Pruefung waere sechs Stunden am Tag rot gewesen,
|
||
ohne dass an der Sache etwas ist -- genau die Sorte Fehlalarm,
|
||
die dieses Haus am 06.09.2026 schon einmal notiert hat („ein
|
||
Test, der die Wanduhr als Annahme benutzt").
|
||
|
||
GEMESSEN WIRD STATTDESSEN DIE SACHE SELBST: Die vier Knoepfe
|
||
sind weiterhin IM DOKUMENT, sie haben nur keine Flaeche mehr.
|
||
Das ist der Unterschied zwischen „versteckt" und „abgebaut" --
|
||
und daran haengt, dass die Tastenkuerzel weiterlaufen: Sie
|
||
haengen am Schreibfeld (textform.js), und die Leiste wird nicht
|
||
neu gebaut, wenn man sie wieder aufmacht. Waere sie beim
|
||
Zuklappen entfernt worden, waere der naechste Aufbau ein zweiter
|
||
Satz Zuhoerer -- und Strg+B haette doppelt zugeschlagen. */
|
||
await handy.seite.click("#schrift-auf");
|
||
await handy.seite.waitForTimeout(300);
|
||
const zu = await handy.seite.evaluate(() => ({
|
||
imDokument: document.querySelectorAll(".textform__knopf").length,
|
||
sichtbar: [...document.querySelectorAll(".textform__knopf")]
|
||
.filter((e) => e.getBoundingClientRect().height > 1).length,
|
||
hoehe: Math.round(document.getElementById("schreiben").getBoundingClientRect().height),
|
||
merker: document.getElementById("text")?.dataset.textform,
|
||
}));
|
||
ok(zu.imDokument === 4 && zu.sichtbar === 0,
|
||
`Gegenprobe: zugeklappt sind sie versteckt, nicht abgebaut `
|
||
+ `(${zu.imDokument} im Dokument, ${zu.sichtbar} sichtbar)`);
|
||
ok(zu.merker === "ja" && zu.hoehe === vorher.hoehe,
|
||
`und die Konsole ist wieder so hoch wie am Anfang (${zu.hoehe} px)`);
|
||
/* `!!zeile.werkzeuge` GEHOERT IN DIE BEDINGUNG, und das ist kein
|
||
Zierrat: In der Gegenprobe gab es die Gruppe nicht, `y` war
|
||
`undefined`, und `(undefined || 0) < 834` ist wahr -- die Zeile
|
||
stand gruen da, ohne etwas angesehen zu haben. Genau die Sorte
|
||
gruener Haken, vor der die Hausregeln warnen, und sie ist mir
|
||
hier selbst passiert. */
|
||
ok(!!zeile.werkzeuge && !!zeile.feld
|
||
&& zeile.werkzeuge.y < zeile.feld.y - 10,
|
||
`und auf dem Handy in einer eigenen Zeile darueber (y ${zeile.werkzeuge?.y} zu ${zeile.feld?.y})`);
|
||
} finally { await handy.ctx.close(); }
|
||
|
||
/* UND AM RECHNER PASST ALLES IN EINE ZEILE. Ohne diese Zeile waere
|
||
"die Werkzeuge stehen darueber" eine Regel, die auch dort gaelte,
|
||
wo reichlich Platz ist -- und die Zeile waere unnoetig hoch. */
|
||
const laptop = await alsRolle("admin", "CODE-DOGI-0001", "chat.html?raum=treff", 1280);
|
||
try {
|
||
await laptop.seite.waitForSelector("#schreiben:not([hidden])", { timeout: 8000 });
|
||
await laptop.seite.waitForTimeout(800);
|
||
const weit = await laptop.seite.evaluate(() => {
|
||
const f = document.getElementById("text")?.getBoundingClientRect();
|
||
const w = document.querySelector(".chat__werkzeuge")?.getBoundingClientRect();
|
||
const se = document.getElementById("senden")?.getBoundingClientRect();
|
||
return { feld: Math.round(f?.width || 0),
|
||
gleicheZeile: !!f && !!w && Math.abs(f.top - w.top) <= 12,
|
||
sendenDaneben: !!f && !!se && Math.abs(f.top - se.top) <= 6 };
|
||
});
|
||
ok(weit.feld >= 400, `am Rechner ist das Feld ${weit.feld} px breit`);
|
||
ok(weit.gleicheZeile,
|
||
"und die Werkzeuge stehen daneben statt darueber — dort ist Platz");
|
||
ok(weit.sendenDaneben, "Senden steht auch dort beim Feld");
|
||
} finally { await laptop.ctx.close(); }
|
||
}
|
||
|
||
await browser.close();
|
||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||
|
||
melde(`\n${fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`}`);
|
||
/* HIER IST process.exit() RICHTIG -- anders als in pruef-push-weg.
|
||
|
||
Diese Pruefung startet einen Express-Server, und der laeuft von
|
||
selbst weiter: offene Verbindungen, ein Zeitgeber fuer die
|
||
Sicherung, bei dieser Pruefung zusaetzlich die offenen
|
||
Ereignisstroeme des Chats. Mit `exitCode` allein wartet Node ewig
|
||
darauf, dass all das zu Ende geht -- der Lauf lief in die
|
||
Zeitueberschreitung, obwohl alle Pruefungen schon durch waren.
|
||
|
||
Der Absturz, der in pruef-push-weg gegen exit() sprach, kam von
|
||
fetch-Verbindungen mitten im Schliessen. Die gibt es hier nicht
|
||
mehr: Die Browser sind zu, bevor diese Zeile laeuft. */
|
||
process.exit(fehler ? 1 : 0);
|