Filipe, mit einem Bildschirmfoto: "was ist das den fuer eine scheisse
verbesser das sofort." Darauf ein dreissig Pixel schmaler Streifen,
sechs Zeichen untereinander, ein Rollbalken daneben.
.chat-nachricht ist ein RASTER aus zwei Spalten: dreissig Pixel fuer
den Kreis mit den Initialen, der Rest fuer die Blase. Ein Kind ohne
Spaltenangabe wird automatisch in die naechste freie Zelle gesetzt --
und das ist die Spalte des Kreises. `width: min(330px, 100%)` machte
daraus brav 30 Pixel, und die Zeichen stapelten sich.
UND WARUM ES NIEMANDEM AUFGEFALLEN IST: Bei der EIGENEN Nachricht
entfaellt der Kreis, das Raster hat dann nur eine Spalte, die Tafel
bekam die volle Breite. Die Pruefung vom 14.09. hat genau diesen Fall
gemessen -- nachgemessen heute: selbst=ja, eine Spalte, 611 px. Sie
war gruen, waehrend es bei JEDER fremden Nachricht kaputt war.
Und sie hat ausserdem nur Anwesenheit geprueft ("gibt es die
Elemente", "sucht die Suche") -- nie Geometrie. Eine dreissig Pixel
breite Tafel besteht jede dieser Zeilen.
Jetzt gemessen, und zwar in BEIDEN Faellen: Breite, ob die sechs
Vorschlaege auf EINER Zeile stehen, und ob die Tafel neben dem Kreis
steht statt darunter. Dafuer schreibt der Testraum jetzt auch eine
Nachricht von jemand anderem -- ohne die gab es dort nur eigene.
GEGENPROBE (Reparatur kurz entfernt, gemessen, wieder eingesetzt):
ohne 30 px, 6 Zeilen, +0 px <- genau das Bildschirmfoto
mit 330 px, 1 Zeile, +39 px
server/pruef-chat-aufloesen.mjs 125 Pruefungen (vorher 119)
pruef-chat-optik, pruef-chat, pruef-css-klassen -- alle in Ordnung
Co-Authored-By: Claude Opus 5 <[email protected]>
982 lines
48 KiB
JavaScript
982 lines
48 KiB
JavaScript
/* EINE GRUPPE ODER EINEN KANAL AUFLÖSEN — FÜR ALLE (11.09.2026)
|
|
|
|
Filipe: "wenn ein gruppenchat oder kanal geöffnet wird, die person
|
|
die ihn öffnet soll auch das recht haben das zu löschen und so dass
|
|
es dan für jeden gelöscht ist. aber nur die person die es gründet."
|
|
|
|
---------------------------------------------------------------------
|
|
WARUM DIESE DATEI EXISTIERT
|
|
|
|
Es gibt jetzt ZWEI Löschknöpfe nebeneinander, und sie tun etwas sehr
|
|
Verschiedenes:
|
|
|
|
DELETE /raeume/:id -> Wegräumen. Nur bei mir. Umkehrbar in
|
|
dem Sinn, dass die anderen alles haben.
|
|
DELETE /raeume/:id/ganz -> Auflösen. Für alle. Endgültig.
|
|
|
|
Eine Verwechslung der beiden ist der teuerste Fehler, den dieser
|
|
Chat machen kann -- ein ganzer Verlauf ist weg, bei allen, ohne
|
|
Sicherung. Deshalb prüft diese Datei nicht nur, dass das Auflösen
|
|
funktioniert, sondern vor allem, was es NICHT tut und wer es NICHT
|
|
darf.
|
|
|
|
GEMESSEN WIRD AM BESTAND, NICHT AN DER ANTWORT. Ein "ok: true" sagt
|
|
nur, dass der Server nicht gestolpert ist. Ob der Raum wirklich weg
|
|
ist, steht in der Datenbank -- und ob der Anhang weg ist, auf der
|
|
Platte. Beides wird hier nachgesehen.
|
|
|
|
Aufruf: node server/pruef-chat-aufloesen.mjs
|
|
===================================================================== */
|
|
|
|
import { mkdtempSync, rmSync, existsSync, readdirSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
const HIER_BILD = dirname(fileURLToPath(import.meta.url));
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-aufloesen-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
|
|
const { portMussFreiSein } = await import("./helfer-port.mjs");
|
|
await portMussFreiSein(4391, "die Aufloesen-Pruefung");
|
|
|
|
process.env.PORT = "4391";
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
|
const BASIS = "http://127.0.0.1:4391";
|
|
|
|
await import("./index.js");
|
|
|
|
/* Eigene Zuhoerer neben denen aus index.js -- sonst wird ein Absturz
|
|
dieser Datei nur notiert und der Lauf haengt (siehe die
|
|
ausfuehrliche Begruendung in pruef-chat-kanaele.mjs). */
|
|
process.on("uncaughtException", (f) => {
|
|
console.error(""); console.error("ABBRUCH durch einen Absturz:", f?.stack || f);
|
|
process.exit(1);
|
|
});
|
|
process.on("unhandledRejection", (f) => {
|
|
console.error(""); console.error("ABBRUCH durch ein unbehandeltes Versprechen:", f?.stack || f);
|
|
process.exit(1);
|
|
});
|
|
const notbremse = setTimeout(() => {
|
|
console.error(""); console.error("ABBRUCH: Die Pruefung laeuft seit 120 s.");
|
|
process.exit(1);
|
|
}, 120000);
|
|
notbremse.unref?.();
|
|
|
|
let fehler = 0;
|
|
let 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();
|
|
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 idHand = anlegen("Vanessa", "hand", "CODE-HAND-0001");
|
|
const idEins = anlegen("Ayla", "modi", "CODE-TEAM-0001");
|
|
const idZwei = anlegen("Ben", "modi", "CODE-TEAM-0002");
|
|
const idDrei = anlegen("Cem", "modi", "CODE-TEAM-0003");
|
|
|
|
/* Anmeldung an der Crew-Wand: Modis und die rechte Hand kommen an der
|
|
Agenturwand nicht herein, und `fetch` laesst einen selbst gesetzten
|
|
Host-Kopf fallen -- deshalb node:http. */
|
|
const { request: httpAnfrage } = await import("node:http");
|
|
function anmelden(rolle, code, wand = "crew.dogfather-universe.com") {
|
|
return new Promise((fertig) => {
|
|
const rumpf = JSON.stringify({ rolle, code });
|
|
const a = httpAnfrage({
|
|
host: "127.0.0.1", port: 4391, path: "/workspace/api/anmelden", method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Content-Length": Buffer.byteLength(rumpf),
|
|
Host: wand,
|
|
},
|
|
}, (antwort) => {
|
|
antwort.resume();
|
|
antwort.on("end", () => fertig({
|
|
ok: antwort.statusCode === 200,
|
|
keks: (antwort.headers["set-cookie"] || []).map((k) => k.split(";")[0]).join("; "),
|
|
}));
|
|
});
|
|
a.on("error", () => fertig({ ok: false, keks: "" }));
|
|
a.end(rumpf);
|
|
});
|
|
}
|
|
const rufe = (keks, weg, koerper, methode = "GET") =>
|
|
fetch(BASIS + weg, {
|
|
method: methode,
|
|
headers: { "Content-Type": "application/json", Cookie: keks, Origin: BASIS },
|
|
body: koerper === undefined ? undefined : JSON.stringify(koerper),
|
|
});
|
|
const holen = async (keks, weg) => (await rufe(keks, weg)).json();
|
|
|
|
const dogi = await anmelden("admin", "CODE-DOGI-0001");
|
|
const hand = await anmelden("hand", "CODE-HAND-0001");
|
|
const eins = await anmelden("modi", "CODE-TEAM-0001");
|
|
const zwei = await anmelden("modi", "CODE-TEAM-0002");
|
|
const drei = await anmelden("modi", "CODE-TEAM-0003");
|
|
ok([dogi, hand, eins, zwei, drei].every((x) => x.ok && x.keks),
|
|
"alle fuenf sind angemeldet");
|
|
|
|
const raumVon = (keks, id) => holen(keks, "/workspace/api/chat/raeume")
|
|
.then((a) => (a.raeume || []).find((r) => r.id === id) || null);
|
|
const stehtDa = (id) => !!d.prepare("SELECT 1 FROM chat_raeume WHERE id = ?").get(id);
|
|
|
|
/* =======================================================================
|
|
1. Eine Gruppe, gegruendet von Ayla
|
|
======================================================================= */
|
|
console.log("");
|
|
console.log("=== Wer darf aufloesen? ===");
|
|
const gruppe = await (await rufe(eins.keks, "/workspace/api/chat/gruppe",
|
|
{ mit: [idZwei, idDrei], name: "Clipping Mo-Fr" }, "POST")).json();
|
|
ok(Number.isInteger(gruppe.raum_id), `Ayla legt eine Gruppe an (#${gruppe.raum_id})`);
|
|
|
|
/* Zwei Nachrichten hinein, damit es auch etwas zu verlieren gibt. */
|
|
for (const [k, t] of [[eins, "Moin"], [zwei, "Bin dabei"]]) {
|
|
await rufe(k.keks, `/workspace/api/chat/raeume/${gruppe.raum_id}/nachrichten`,
|
|
{ text: t }, "POST");
|
|
}
|
|
const wieViele = d.prepare(
|
|
"SELECT COUNT(*) AS n FROM chat_nachrichten WHERE raum_id = ?").get(gruppe.raum_id).n;
|
|
ok(wieViele === 2, `zwei Nachrichten stehen darin (${wieViele})`);
|
|
|
|
/* ---- Das Recht steht in der Liste, nicht im Browser ------------------- */
|
|
{
|
|
const beiAyla = await raumVon(eins.keks, gruppe.raum_id);
|
|
const beiBen = await raumVon(zwei.keks, gruppe.raum_id);
|
|
ok(beiAyla?.darf_aufloesen === true, "Ayla bekommt das Recht mitgeliefert");
|
|
ok(beiBen?.darf_aufloesen === false,
|
|
"Ben nicht -- er ist dabei, aber er hat sie nicht angelegt");
|
|
}
|
|
|
|
/* ---- Und wer es nicht hat, kommt auch nicht durch --------------------- */
|
|
{
|
|
const a = await rufe(zwei.keks, `/workspace/api/chat/raeume/${gruppe.raum_id}/ganz`,
|
|
undefined, "DELETE");
|
|
const d2 = await a.json().catch(() => ({}));
|
|
ok(a.status === 403 && d2.fehler === "nur_gruender",
|
|
`Ben wird abgewiesen (HTTP ${a.status}, ${d2.fehler})`);
|
|
ok(stehtDa(gruppe.raum_id), "und die Gruppe steht noch");
|
|
}
|
|
{
|
|
/* Die rechte Hand darf ALLES sehen -- aufloesen darf sie trotzdem
|
|
nicht. Das ist der Fall, den man beim Bauen am ehesten aufweicht
|
|
("sie ist ja die Leitung"), und genau deshalb steht er hier. */
|
|
const a = await rufe(hand.keks, `/workspace/api/chat/raeume/${gruppe.raum_id}/ganz`,
|
|
undefined, "DELETE");
|
|
ok(a.status === 403 || a.status === 404,
|
|
`auch die rechte Hand nicht (HTTP ${a.status})`);
|
|
ok(stehtDa(gruppe.raum_id), "und die Gruppe steht immer noch");
|
|
}
|
|
|
|
/* =======================================================================
|
|
2. Wegraeumen ist NICHT aufloesen
|
|
======================================================================= */
|
|
console.log("");
|
|
console.log("=== Der Unterschied zum Knopf daneben ===");
|
|
{
|
|
const a = await rufe(zwei.keks, `/workspace/api/chat/raeume/${gruppe.raum_id}`,
|
|
undefined, "DELETE");
|
|
ok(a.status === 200, `Ben raeumt sie bei sich weg (HTTP ${a.status})`);
|
|
ok(stehtDa(gruppe.raum_id), "der Raum bleibt in der Datenbank");
|
|
const beiBen = await raumVon(zwei.keks, gruppe.raum_id);
|
|
ok(beiBen === null, "bei Ben steht sie nicht mehr in der Liste");
|
|
const beiCem = await raumVon(drei.keks, gruppe.raum_id);
|
|
ok(beiCem !== null, "bei Cem dagegen schon -- niemand nimmt einem anderen etwas weg");
|
|
const uebrig = d.prepare(
|
|
"SELECT COUNT(*) AS n FROM chat_nachrichten WHERE raum_id = ?").get(gruppe.raum_id).n;
|
|
ok(uebrig === 2, `und beide Nachrichten sind noch da (${uebrig})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
3. Ein Zweier-Gespraech laesst sich gar nicht aufloesen
|
|
======================================================================= */
|
|
console.log("");
|
|
console.log("=== Zweier-Gespraeche bleiben unantastbar ===");
|
|
{
|
|
const direkt = await (await rufe(eins.keks, "/workspace/api/chat/direkt",
|
|
{ mit: idZwei }, "POST")).json();
|
|
ok(Number.isInteger(direkt.raum_id), `Ayla schreibt Ben direkt (#${direkt.raum_id})`);
|
|
const liste = await raumVon(eins.keks, direkt.raum_id);
|
|
ok(liste?.darf_aufloesen === false,
|
|
"auch die Anlegerin bekommt dort kein Aufloesen-Recht");
|
|
const a = await rufe(eins.keks, `/workspace/api/chat/raeume/${direkt.raum_id}/ganz`,
|
|
undefined, "DELETE");
|
|
const d2 = await a.json().catch(() => ({}));
|
|
ok(a.status === 403 && d2.fehler === "nur_gruppe_oder_kanal",
|
|
`und der Weg weist sie ab (HTTP ${a.status}, ${d2.fehler})`);
|
|
ok(stehtDa(direkt.raum_id), "das Gespraech steht weiterhin");
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. Fremde kommen nicht einmal in die Naehe
|
|
======================================================================= */
|
|
console.log("");
|
|
console.log("=== Wer nicht dabei ist ===");
|
|
{
|
|
/* Cem ist in der Gruppe, Ayla hat sie gegruendet. Ein VIERTER, der
|
|
nicht dabei ist, muss 404 bekommen -- nicht 403. 403 waere die
|
|
Auskunft, dass es diesen Raum gibt. */
|
|
const vier = anlegen("Dilan", "modi", "CODE-TEAM-0004");
|
|
const vierK = await anmelden("modi", "CODE-TEAM-0004");
|
|
ok(vierK.ok && Number.isInteger(vier), "ein Vierter ist angemeldet");
|
|
const a = await rufe(vierK.keks, `/workspace/api/chat/raeume/${gruppe.raum_id}/ganz`,
|
|
undefined, "DELETE");
|
|
ok(a.status === 404, `er bekommt 404, nicht 403 (HTTP ${a.status})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
5. Und jetzt wirklich: die Gruenderin loest auf
|
|
======================================================================= */
|
|
console.log("");
|
|
console.log("=== Aufloesen, gemessen am Bestand ===");
|
|
{
|
|
const vorher = {
|
|
raum: stehtDa(gruppe.raum_id),
|
|
nachrichten: d.prepare("SELECT COUNT(*) AS n FROM chat_nachrichten WHERE raum_id = ?")
|
|
.get(gruppe.raum_id).n,
|
|
leute: d.prepare("SELECT COUNT(*) AS n FROM chat_teilnehmer WHERE raum_id = ?")
|
|
.get(gruppe.raum_id).n,
|
|
};
|
|
ok(vorher.raum && vorher.nachrichten === 2 && vorher.leute === 3,
|
|
`vorher: Raum da, ${vorher.nachrichten} Nachrichten, ${vorher.leute} Beteiligte`);
|
|
|
|
const a = await rufe(eins.keks, `/workspace/api/chat/raeume/${gruppe.raum_id}/ganz`,
|
|
undefined, "DELETE");
|
|
const antwort = await a.json().catch(() => ({}));
|
|
ok(a.status === 200 && antwort.ok === true,
|
|
`Ayla loest auf (HTTP ${a.status})`);
|
|
ok(antwort.nachrichten === 2 && antwort.beteiligte === 3,
|
|
`die Antwort nennt, was wegfiel (${antwort.nachrichten} Nachrichten, `
|
|
+ `${antwort.beteiligte} Beteiligte)`);
|
|
|
|
ok(!stehtDa(gruppe.raum_id), "der Raum ist aus der Datenbank verschwunden");
|
|
const restN = d.prepare("SELECT COUNT(*) AS n FROM chat_nachrichten WHERE raum_id = ?")
|
|
.get(gruppe.raum_id).n;
|
|
const restT = d.prepare("SELECT COUNT(*) AS n FROM chat_teilnehmer WHERE raum_id = ?")
|
|
.get(gruppe.raum_id).n;
|
|
ok(restN === 0, `keine Nachricht blieb liegen (${restN})`);
|
|
ok(restT === 0, `keine Teilnehmerzeile blieb liegen (${restT})`);
|
|
|
|
/* UND BEI DEN ANDEREN. Das ist der eigentliche Satz aus Filipes
|
|
Wunsch: "so dass es dan fuer jeden geloescht ist." */
|
|
const beiCem = await raumVon(drei.keks, gruppe.raum_id);
|
|
ok(beiCem === null, "bei Cem ist sie ebenfalls weg -- fuer jeden");
|
|
|
|
/* GEGENPROBE: Ein zweiter Versuch findet nichts mehr. Ohne sie
|
|
koennte der Weg beim zweiten Aufruf stillschweigend "ok" sagen. */
|
|
const nochmal = await rufe(eins.keks, `/workspace/api/chat/raeume/${gruppe.raum_id}/ganz`,
|
|
undefined, "DELETE");
|
|
ok(nochmal.status === 404, `ein zweiter Versuch findet nichts (HTTP ${nochmal.status})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
6. Ein Kanal -- und seine Zustaendigkeit wird wieder frei
|
|
======================================================================= */
|
|
console.log("");
|
|
console.log("=== Kanaele ===");
|
|
{
|
|
const frei = await holen(hand.keks, "/workspace/api/chat/raeume");
|
|
const kat = (frei.kanal_frei || [])[0]?.wert;
|
|
ok(!!kat, `es gibt eine freie Zustaendigkeit (${kat || "keine"})`);
|
|
|
|
const kanal = await (await rufe(hand.keks, "/workspace/api/chat/kanal",
|
|
{ kategorie: kat }, "POST")).json();
|
|
ok(Number.isInteger(kanal.raum_id), `die rechte Hand legt den Kanal an (#${kanal.raum_id})`);
|
|
|
|
/* DogFather kommt in jeden Kanal -- aufloesen darf er trotzdem
|
|
nicht, denn angelegt hat ihn die rechte Hand. */
|
|
const beiDogi = await raumVon(dogi.keks, kanal.raum_id);
|
|
ok(beiDogi !== null, "DogFather sieht den Kanal");
|
|
ok(beiDogi?.darf_aufloesen === false,
|
|
"und darf ihn NICHT aufloesen -- er hat ihn nicht angelegt");
|
|
const versuch = await rufe(dogi.keks, `/workspace/api/chat/raeume/${kanal.raum_id}/ganz`,
|
|
undefined, "DELETE");
|
|
ok(versuch.status === 403, `der Weg bestaetigt das (HTTP ${versuch.status})`);
|
|
|
|
const beiHand = await raumVon(hand.keks, kanal.raum_id);
|
|
ok(beiHand?.darf_aufloesen === true, "die Gruenderin dagegen darf");
|
|
|
|
const weg = await rufe(hand.keks, `/workspace/api/chat/raeume/${kanal.raum_id}/ganz`,
|
|
undefined, "DELETE");
|
|
ok(weg.status === 200, `sie loest ihn auf (HTTP ${weg.status})`);
|
|
ok(!stehtDa(kanal.raum_id), "der Kanal ist weg");
|
|
|
|
/* Und die Zustaendigkeit steht wieder zur Verfuegung. Ohne diese
|
|
Zeile koennte der eindeutige Index sie dauerhaft blockieren -- ein
|
|
Kanal, den es nicht gibt und den man nicht anlegen kann. */
|
|
const danach = await holen(hand.keks, "/workspace/api/chat/raeume");
|
|
ok((danach.kanal_frei || []).some((k) => k.wert === kat),
|
|
`die Zustaendigkeit "${kat}" ist wieder frei`);
|
|
const neu = await (await rufe(hand.keks, "/workspace/api/chat/kanal",
|
|
{ kategorie: kat }, "POST")).json();
|
|
ok(Number.isInteger(neu.raum_id), `und laesst sich neu anlegen (#${neu.raum_id})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
7. Anhaenge verschwinden von der Platte
|
|
======================================================================= */
|
|
console.log("");
|
|
console.log("=== Anhaenge ===");
|
|
{
|
|
const g = await (await rufe(eins.keks, "/workspace/api/chat/gruppe",
|
|
{ mit: [idZwei], name: "Mit Anhang" }, "POST")).json();
|
|
ok(Number.isInteger(g.raum_id), `eine Gruppe mit Anhang (#${g.raum_id})`);
|
|
|
|
/* Ein winziges PNG, damit es wirklich durch die Bildpruefung geht. */
|
|
const png = Buffer.from(
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk"
|
|
+ "YPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", "base64");
|
|
/* ROH, NICHT ALS FORMULAR. Der Weg nimmt den Dateiinhalt direkt
|
|
entgegen (express.raw) und erkennt den Typ an den ersten Bytes --
|
|
ein multipart-Umschlag kommt dort als Unsinn an und wird mit 415
|
|
abgewiesen. Beim ersten Anlauf stand hier FormData, und die
|
|
Pruefung mass daraufhin ein Loeschen von nichts. */
|
|
const hoch = await fetch(BASIS + `/workspace/api/chat/raeume/${g.raum_id}/anhang`, {
|
|
method: "POST",
|
|
headers: {
|
|
Cookie: eins.keks, Origin: BASIS,
|
|
"Content-Type": "application/octet-stream",
|
|
"x-dateiname": "punkt.png",
|
|
},
|
|
body: png });
|
|
ok(hoch.status === 200 || hoch.status === 201,
|
|
`der Anhang ist oben (HTTP ${hoch.status})`);
|
|
|
|
const datei = d.prepare(
|
|
`SELECT anhang_datei FROM chat_nachrichten
|
|
WHERE raum_id = ? AND anhang_datei IS NOT NULL`).get(g.raum_id)?.anhang_datei;
|
|
ok(!!datei, `die Datei steht in der Tabelle (${datei || "keine"})`);
|
|
|
|
const { DATEN_ORDNER } = await import("./workspace.js");
|
|
const pfad = join(DATEN_ORDNER, "chat-anhaenge", datei || "-");
|
|
ok(existsSync(pfad), "und liegt auf der Platte");
|
|
|
|
const weg = await rufe(eins.keks, `/workspace/api/chat/raeume/${g.raum_id}/ganz`,
|
|
undefined, "DELETE");
|
|
ok(weg.status === 200, `die Gruppe wird aufgeloest (HTTP ${weg.status})`);
|
|
ok(!existsSync(pfad), "und die Datei ist von der Platte verschwunden");
|
|
|
|
/* GEGENPROBE ZUM PFAD: Der Ordner selbst bleibt stehen. Ein
|
|
`existsSync` auf einen Pfad, den es nie gab, waere sonst auch
|
|
"weg" -- und die Zeile darueber bewiese nichts. */
|
|
ok(existsSync(join(DATEN_ORDNER, "chat-anhaenge")),
|
|
"der Anhang-Ordner selbst steht noch (die Pruefung misst die Datei, nicht den Ordner)");
|
|
}
|
|
|
|
/* =======================================================================
|
|
7b. REAKTIONEN — auf die Nachricht, ohne zu antworten (14.09.2026)
|
|
=======================================================================
|
|
Filipe: "ich will dass man auf die nachrichten auch reagieren kann
|
|
mit einem emoji, die nachricht selbst ohne zu antworten."
|
|
|
|
GEMESSEN WIRD DER BESTAND, nicht die Antwort. Ein "ok" sagt nur,
|
|
dass der Server nicht gestolpert ist; ob die Reaktion wirklich steht
|
|
(und beim zweiten Klick wirklich weg ist), steht in der Datenbank.
|
|
======================================================================= */
|
|
console.log("");
|
|
console.log("=== Reaktionen ===");
|
|
{
|
|
const g = await (await rufe(eins.keks, "/workspace/api/chat/gruppe",
|
|
{ mit: [idZwei, idDrei], name: "Reaktionsrunde" }, "POST")).json();
|
|
const n = await (await rufe(eins.keks,
|
|
`/workspace/api/chat/raeume/${g.raum_id}/nachrichten`, { text: "Moin" }, "POST")).json();
|
|
const nid = n.nachricht?.id;
|
|
ok(Number.isInteger(nid), `eine Nachricht zum Reagieren (#${nid})`);
|
|
|
|
const auswahl = await holen(eins.keks, "/workspace/api/chat/reaktionen/auswahl");
|
|
ok(Array.isArray(auswahl.zeichen) && auswahl.zeichen.length >= 4,
|
|
`die Auswahl kommt vom Server (${(auswahl.zeichen || []).length} Zeichen)`);
|
|
const daumen = auswahl.zeichen[0];
|
|
const herz = auswahl.zeichen[1];
|
|
|
|
const zaehle = () => d.prepare(
|
|
"SELECT COUNT(*) AS n FROM chat_reaktionen WHERE nachricht_id = ?").get(nid).n;
|
|
|
|
/* ---- KATALOG UND FAVORITEN (14.09.2026) -------------------------
|
|
Filipe: "paar als vorschlaege mit der moeglichkeit andere
|
|
auszuwaehlen und als favoriten zu speichern."
|
|
|
|
DREI LISTEN, DREI FRAGEN -- und sie werden gern verwechselt:
|
|
Katalog (was gibt es?), Vorschlag (was steht vorn?), Favoriten
|
|
(was hat DIESER Mensch gewaehlt?). Geprueft wird, dass sie
|
|
zusammenhaengen, ohne dasselbe zu sein. */
|
|
const aus1 = await holen(eins.keks, "/workspace/api/chat/reaktionen/auswahl");
|
|
ok(Array.isArray(aus1.katalog) && aus1.katalog.length >= 4,
|
|
`der Katalog hat ${(aus1.katalog || []).length} Gruppen`);
|
|
const alleZeichen = (aus1.katalog || []).flatMap((g) => g.zeichen.map((z) => z[0]));
|
|
ok(alleZeichen.length >= 50, `und ${alleZeichen.length} Zeichen insgesamt`);
|
|
ok(new Set(alleZeichen).size === alleZeichen.length,
|
|
"keines davon doppelt -- ein doppeltes waere in der Tafel still");
|
|
ok((aus1.katalog || []).every((g) => g.zeichen.every(([, w]) => typeof w === "string" && w.length > 2)),
|
|
"jedes Zeichen hat deutsche Suchwoerter");
|
|
ok(aus1.eigene === false && aus1.zeichen.length === 6,
|
|
`ohne eigene Favoriten stehen die sechs Vorgaben vorn (${aus1.zeichen.length})`);
|
|
ok(aus1.zeichen.every((z) => alleZeichen.includes(z)),
|
|
"und jeder Vorschlag steht auch im Katalog");
|
|
|
|
/* ---- Einen Favoriten merken ------------------------------------- */
|
|
const exot = alleZeichen.find((z) => !aus1.zeichen.includes(z));
|
|
const f1 = await (await rufe(eins.keks, "/workspace/api/chat/reaktionen/favorit",
|
|
{ zeichen: exot }, "PUT")).json();
|
|
ok(f1.favoriten?.length === 1 && f1.favoriten[0] === exot,
|
|
`ein Zeichen wird gemerkt (${f1.favoriten?.join(" ")})`);
|
|
ok(f1.zeichen?.length === 1,
|
|
"und steht ab jetzt ALLEIN vorn -- Favoriten ersetzen die Vorgabe");
|
|
|
|
/* ---- Sie bleiben, auch nach dem Neuladen ------------------------- */
|
|
const aus2 = await holen(eins.keks, "/workspace/api/chat/reaktionen/auswahl");
|
|
ok(aus2.eigene === true && aus2.favoriten[0] === exot,
|
|
"beim naechsten Aufruf sind sie noch da");
|
|
|
|
/* ---- Und sie gehoeren EINEM Menschen ----------------------------- */
|
|
const ausBen = await holen(zwei.keks, "/workspace/api/chat/reaktionen/auswahl");
|
|
ok(ausBen.eigene === false,
|
|
"Ben hat davon nichts -- Favoriten sind persoenlich");
|
|
|
|
/* ---- Zweimal dasselbe nimmt es weg ------------------------------- */
|
|
const f2 = await (await rufe(eins.keks, "/workspace/api/chat/reaktionen/favorit",
|
|
{ zeichen: exot }, "PUT")).json();
|
|
ok(f2.favoriten?.length === 0 && f2.gesetzt === false,
|
|
"noch einmal darauf nimmt den Favoriten wieder weg");
|
|
const aus3 = await holen(eins.keks, "/workspace/api/chat/reaktionen/auswahl");
|
|
ok(aus3.eigene === false && aus3.zeichen.length === 6,
|
|
"und die Vorgabe steht wieder vorn");
|
|
|
|
/* ---- Die Grenze sagt Bescheid, statt still zu schlucken ---------- */
|
|
const acht = alleZeichen.slice(0, aus1.favoriten_max);
|
|
for (const z of acht) {
|
|
await rufe(eins.keks, "/workspace/api/chat/reaktionen/favorit", { zeichen: z }, "PUT");
|
|
}
|
|
const voll = await rufe(eins.keks, "/workspace/api/chat/reaktionen/favorit",
|
|
{ zeichen: alleZeichen[aus1.favoriten_max] }, "PUT");
|
|
const vollD = await voll.json();
|
|
ok(voll.status === 409 && /Favoriten/.test(vollD.fehler || ""),
|
|
`der neunte wird abgelehnt und begruendet (HTTP ${voll.status})`);
|
|
ok((vollD.favoriten || []).length === aus1.favoriten_max,
|
|
`die acht bleiben unangetastet (${(vollD.favoriten || []).length})`);
|
|
|
|
/* GEGENPROBE: Nach dem Wegnehmen eines Favoriten geht wieder einer
|
|
hinein. Ohne sie hiesse das oben nur, dass ab acht gar nichts mehr
|
|
geht. */
|
|
await rufe(eins.keks, "/workspace/api/chat/reaktionen/favorit",
|
|
{ zeichen: acht[0] }, "PUT");
|
|
const platzFrei = await rufe(eins.keks, "/workspace/api/chat/reaktionen/favorit",
|
|
{ zeichen: alleZeichen[aus1.favoriten_max] }, "PUT");
|
|
ok(platzFrei.status === 200,
|
|
`nach dem Wegnehmen passt wieder einer (HTTP ${platzFrei.status})`);
|
|
|
|
/* ---- Ein Favorit ist keine Erlaubnis ----------------------------- */
|
|
const erfunden = await rufe(eins.keks, "/workspace/api/chat/reaktionen/favorit",
|
|
{ zeichen: "nicht-im-katalog" }, "PUT");
|
|
ok(erfunden.status === 400,
|
|
`ein Zeichen ausserhalb des Katalogs wird nicht gemerkt (HTTP ${erfunden.status})`);
|
|
|
|
/* ---- Der ganze Katalog ist auch zum Reagieren erlaubt ------------ */
|
|
const weit = alleZeichen[alleZeichen.length - 1];
|
|
const gr = await (await rufe(eins.keks, "/workspace/api/chat/gruppe",
|
|
{ mit: [idZwei], name: "Katalogtest" }, "POST")).json();
|
|
const nn = await (await rufe(eins.keks,
|
|
`/workspace/api/chat/raeume/${gr.raum_id}/nachrichten`, { text: "x" }, "POST")).json();
|
|
const mitExot = await rufe(eins.keks,
|
|
`/workspace/api/chat/nachrichten/${nn.nachricht.id}/reaktion`, { zeichen: weit }, "PUT");
|
|
ok(mitExot.status === 200,
|
|
`auch das letzte Zeichen des Katalogs laesst sich geben (${weit})`);
|
|
|
|
/* ---- Setzen ---------------------------------------------------- */
|
|
const a1 = await (await rufe(zwei.keks,
|
|
`/workspace/api/chat/nachrichten/${nid}/reaktion`, { zeichen: daumen }, "PUT")).json();
|
|
ok(zaehle() === 1, `Ben setzt ein Zeichen -- es steht in der Datenbank (${zaehle()})`);
|
|
ok(a1.reaktionen?.[0]?.anzahl === 1 && a1.reaktionen?.[0]?.selbst === true,
|
|
"die Antwort nennt Anzahl und dass es seines ist");
|
|
|
|
/* ---- Derselbe Mensch, ZWEITES Zeichen -- das geht --------------- */
|
|
await rufe(zwei.keks, `/workspace/api/chat/nachrichten/${nid}/reaktion`,
|
|
{ zeichen: herz }, "PUT");
|
|
ok(zaehle() === 2, `dasselbe Mensch darf ein ZWEITES Zeichen geben (${zaehle()})`);
|
|
|
|
/* ---- Derselbe Mensch, DASSELBE Zeichen -- nimmt zurueck --------- */
|
|
const a3 = await (await rufe(zwei.keks,
|
|
`/workspace/api/chat/nachrichten/${nid}/reaktion`, { zeichen: daumen }, "PUT")).json();
|
|
ok(zaehle() === 1, `zweimal dasselbe nimmt es wieder weg (${zaehle()})`);
|
|
ok(a3.gesetzt === false, "und die Antwort sagt, dass es weg ist");
|
|
|
|
/* ---- Mehrere Menschen, dasselbe Zeichen ------------------------- */
|
|
await rufe(drei.keks, `/workspace/api/chat/nachrichten/${nid}/reaktion`,
|
|
{ zeichen: herz }, "PUT");
|
|
const stand = await (await rufe(eins.keks,
|
|
`/workspace/api/chat/nachrichten/${nid}/reaktion`, { zeichen: herz }, "PUT")).json();
|
|
const herzEintrag = (stand.reaktionen || []).find((r) => r.zeichen === herz);
|
|
ok(herzEintrag?.anzahl === 3, `drei Menschen, ein Zeichen, Anzahl 3 (${herzEintrag?.anzahl})`);
|
|
ok(Array.isArray(herzEintrag?.wer) && herzEintrag.wer.length === 3,
|
|
`und die Namen stehen dabei (${(herzEintrag?.wer || []).join(", ")})`);
|
|
|
|
/* ---- Was NICHT geht --------------------------------------------- */
|
|
const fremd = await rufe(hand.keks, `/workspace/api/chat/nachrichten/${nid}/reaktion`,
|
|
{ zeichen: daumen }, "PUT");
|
|
ok(fremd.status === 404,
|
|
`wer nicht im Raum ist, kommt nicht heran (HTTP ${fremd.status})`);
|
|
|
|
const unsinn = await rufe(zwei.keks, `/workspace/api/chat/nachrichten/${nid}/reaktion`,
|
|
{ zeichen: "<script>alert(1)</script>" }, "PUT");
|
|
ok(unsinn.status === 400,
|
|
`ein erfundenes Zeichen wird abgewiesen (HTTP ${unsinn.status})`);
|
|
ok(zaehle() === 3, `und aendert nichts am Bestand (${zaehle()})`);
|
|
|
|
/* ---- Eine zurueckgenommene Nachricht ----------------------------- */
|
|
const weg = await (await rufe(eins.keks,
|
|
`/workspace/api/chat/raeume/${g.raum_id}/nachrichten`, { text: "Weg gleich" }, "POST")).json();
|
|
await rufe(eins.keks, `/workspace/api/chat/nachrichten/${weg.nachricht.id}`,
|
|
undefined, "DELETE");
|
|
const drauf = await rufe(zwei.keks,
|
|
`/workspace/api/chat/nachrichten/${weg.nachricht.id}/reaktion`, { zeichen: daumen }, "PUT");
|
|
ok(drauf.status === 409,
|
|
`auf eine zurueckgenommene Nachricht geht es nicht (HTTP ${drauf.status})`);
|
|
|
|
/* ---- Und sie ueberleben das Neuladen ----------------------------- */
|
|
const verlauf = await holen(zwei.keks,
|
|
`/workspace/api/chat/raeume/${g.raum_id}/nachrichten`);
|
|
const wieder = (verlauf.nachrichten || []).find((x) => x.id === nid);
|
|
ok(Array.isArray(wieder?.reaktionen) && wieder.reaktionen.length === 1,
|
|
`beim Neuladen sind sie wieder da (${(wieder?.reaktionen || []).length} Sorte)`);
|
|
ok(wieder?.reaktionen?.[0]?.selbst === true,
|
|
"und Ben sieht, dass eines davon seines ist");
|
|
|
|
/* GEGENPROBE, DIE WIRKLICH UNTERSCHEIDET.
|
|
|
|
Der erste Entwurf pruefte "bei Cem ist ueberall selbst=true" -- und
|
|
das war trivial wahr, weil es zu dem Zeitpunkt nur EINE Sorte gab.
|
|
Eine Angabe, die nie `false` sein kann, sagt nichts.
|
|
|
|
Also bekommt Ayla ein drittes Zeichen, das Cem NICHT gegeben hat.
|
|
Jetzt muss bei Cem eines auf `true` und eines auf `false` stehen --
|
|
in derselben Antwort. */
|
|
const lachen = auswahl.zeichen[2];
|
|
await rufe(eins.keks, `/workspace/api/chat/nachrichten/${nid}/reaktion`,
|
|
{ zeichen: lachen }, "PUT");
|
|
const beiDrei = await holen(drei.keks,
|
|
`/workspace/api/chat/raeume/${g.raum_id}/nachrichten`);
|
|
const seins = (beiDrei.nachrichten || []).find((x) => x.id === nid);
|
|
const meins = (seins?.reaktionen || []).find((r) => r.zeichen === herz);
|
|
const fremdes = (seins?.reaktionen || []).find((r) => r.zeichen === lachen);
|
|
ok(meins?.selbst === true && fremdes?.selbst === false,
|
|
`bei Cem: sein Herz ist "selbst", Aylas Lachen nicht `
|
|
+ `(${meins?.selbst} / ${fremdes?.selbst})`);
|
|
|
|
/* ---- Die Nachricht loeschen nimmt die Reaktionen mit ------------- */
|
|
const vorherAnzahl = zaehle();
|
|
ok(vorherAnzahl > 0, `vorher stehen ${vorherAnzahl} Reaktionen da`);
|
|
d.prepare("DELETE FROM chat_nachrichten WHERE id = ?").run(nid);
|
|
ok(zaehle() === 0,
|
|
`mit der Nachricht verschwinden die Reaktionen (${zaehle()} uebrig)`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
8. Und wie es auf dem Bildschirm aussieht
|
|
=======================================================================
|
|
Filipe: "ich will eine bessere erklaerung dafuer bitte." -- gemeint
|
|
war der Umschalter "Gespraech | Kanal" im Neu-Fenster. Daneben stand
|
|
ein Satz ueber das ANTIPPEN von Personen, also ueber den naechsten
|
|
Schritt, nicht ueber die Wahl, die gerade ansteht.
|
|
|
|
EIN TEXT, DEN NIEMAND PRUEFT, VERSCHWINDET beim naechsten Umbau, und
|
|
es faellt keinem auf, weil ein fehlender Satz nichts kaputtmacht.
|
|
|
|
DIE ANMELDUNG LAEUFT UEBER DEN KEKS, NICHT UEBER DAS FORMULAR: Die
|
|
Crew-Rollen kommen an der Agenturwand nicht herein, und ein Browser
|
|
laesst den Host-Kopf nicht setzen (Chrome verbietet es). Der Keks
|
|
aus der Anmeldung oben gilt fuer 127.0.0.1 -- also wird er direkt
|
|
in den Browser gelegt. Das umgeht keine Regel, es umgeht ein
|
|
Werkzeug-Hindernis. */
|
|
console.log("");
|
|
console.log("=== Das Neu-Fenster ===");
|
|
{
|
|
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 [kname, kwert] = hand.keks.split("=");
|
|
await ctx.addCookies([{ name: kname, value: kwert, domain: "127.0.0.1", path: "/" }]);
|
|
const seite = await ctx.newPage();
|
|
const skriptfehler = [];
|
|
seite.on("pageerror", (e) => skriptfehler.push(String(e).slice(0, 140)));
|
|
await seite.goto(BASIS + "/workspace/chat.html", { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1400);
|
|
ok(skriptfehler.length === 0, `kein Skriptfehler (${skriptfehler.join(" | ") || "keiner"})`);
|
|
|
|
await seite.click("#neu-oeffnen");
|
|
await seite.waitForSelector("#neu-dialog[open]", { timeout: 5000 });
|
|
await seite.waitForTimeout(400);
|
|
|
|
const lage = await seite.evaluate(() => {
|
|
const e = document.getElementById("neu-erklaerung");
|
|
const u = document.getElementById("neu-unter");
|
|
return {
|
|
umschalter: document.getElementById("neu-art")?.hidden === false,
|
|
/* Ohne Leerraum-Muster: Ein regulaerer Ausdruck stand hier mit
|
|
einem verschluckten Backslash da ("/s+/g") und hat jedes "s"
|
|
durch ein Leerzeichen ersetzt -- aus "Gespraech" wurde
|
|
"Ge praech", und die Pruefung fand das Wort nicht mehr.
|
|
Zum Suchen von Woertern braucht es keine Normalisierung. */
|
|
erklaerung: e && !e.hidden ? e.textContent.trim() : "",
|
|
unter: u ? u.textContent.trim() : "",
|
|
};
|
|
});
|
|
ok(lage.umschalter === true, "die rechte Hand sieht den Umschalter");
|
|
ok(lage.erklaerung.length > 60,
|
|
`und darunter eine Erklaerung (${lage.erklaerung.length} Zeichen)`);
|
|
/* Sie muss BEIDE Woerter erklaeren -- eine Erklaerung, die nur eines
|
|
der beiden nennt, laesst genau die Frage offen, die Filipe hatte. */
|
|
ok(/Gespräch/.test(lage.erklaerung) && /Kanal/.test(lage.erklaerung),
|
|
"sie nennt beide Arten");
|
|
/* Und sie muss den UNTERSCHIED benennen, nicht zweimal dasselbe:
|
|
Leute gegen Thema. */
|
|
ok(/Leute/.test(lage.erklaerung) && /Thema/.test(lage.erklaerung),
|
|
"und sagt, worin sie sich unterscheiden (Leute / Thema)");
|
|
ok(/einmal/.test(lage.erklaerung),
|
|
"dass es einen Kanal je Zustaendigkeit nur einmal gibt");
|
|
ok(!/antippen/i.test(lage.erklaerung),
|
|
"sie erklaert die WAHL, nicht den naechsten Schritt");
|
|
|
|
/* Umschalten auf Kanal: Der Satz darunter wechselt mit. */
|
|
await seite.click('#neu-art [data-art="kanal"]');
|
|
await seite.waitForTimeout(250);
|
|
const unterKanal = await seite.$eval("#neu-unter", (e) => e.textContent.trim());
|
|
ok(unterKanal !== lage.unter && /Zuständigkeit/.test(unterKanal),
|
|
`im Kanal-Modus steht etwas anderes darunter ("${unterKanal.slice(0, 48)}…")`);
|
|
|
|
if (process.env.SCHIRM) {
|
|
await seite.screenshot({ path: join(HIER_BILD, "pruef-chat-neu.png") });
|
|
}
|
|
|
|
await seite.click("#neu-zu");
|
|
await seite.waitForTimeout(250);
|
|
|
|
/* ---- REAGIEREN AUF DEM BILDSCHIRM (14.09.2026) ------------------
|
|
Das Recht und der Weg sind oben geprueft. Ob die Oberflaeche
|
|
beides verbindet, ist eine eigene Frage -- genau daran ist am
|
|
11.09. der Aufloesen-Knopf gescheitert: Recht richtig, Knopf
|
|
unsichtbar. */
|
|
const rRaum = await (await rufe(hand.keks, "/workspace/api/chat/gruppe",
|
|
{ mit: [idEins], name: "Reagieren sichtbar" }, "POST")).json();
|
|
await rufe(hand.keks, `/workspace/api/chat/raeume/${rRaum.raum_id}/nachrichten`,
|
|
{ text: "Reagier mal drauf" }, "POST");
|
|
/* UND EINE VON JEMAND ANDEREM. Ohne sie gibt es in diesem Raum nur
|
|
eigene Nachrichten -- und genau daran ist die Prüfung am 15.09.
|
|
vorbeigelaufen: Bei einer eigenen Nachricht hat das Raster nur eine
|
|
Spalte, die Tafel bekam die volle Breite, alles war grün. Kaputt
|
|
war es bei jeder FREMDEN, wo der Kreis mit den Initialen eine
|
|
30-px-Spalte aufmacht. */
|
|
await rufe(eins.keks, `/workspace/api/chat/raeume/${rRaum.raum_id}/nachrichten`,
|
|
{ text: "Und hier steht was von Ayla" }, "POST");
|
|
await seite.goto(BASIS + `/workspace/chat.html?raum=${rRaum.raum_id}`,
|
|
{ waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1500);
|
|
|
|
const knopfDa = await seite.$$eval(".chat-nachricht__reag-knopf", (b) => b.length);
|
|
ok(knopfDa > 0, `der Knopf "reagieren" steht an der Nachricht (${knopfDa})`);
|
|
|
|
await seite.click(".chat-nachricht__reag-knopf");
|
|
await seite.waitForTimeout(600);
|
|
const tafel = await seite.$$eval(".reagieren-tafel__zeichen",
|
|
(b) => b.map((x) => x.textContent.trim()));
|
|
ok(tafel.length >= 4, `die Auswahl klappt auf (${tafel.length} Zeichen)`);
|
|
|
|
/* ---- DIE TAFEL: Vorschlaege, Suche, Katalog, Favoriten ----------
|
|
(14.09.2026) Filipe: "soll viel besser und geiler sein und man
|
|
soll auch auswaehlen koennen."
|
|
|
|
Gemessen wird, was auf dem Bildschirm PASSIERT -- nicht, dass es
|
|
Elemente gibt. Eine Tafel, die aufklappt und deren Suche nichts
|
|
tut, haette alle Anwesenheitspruefungen bestanden. */
|
|
const teile = await seite.evaluate(() => ({
|
|
schnell: document.querySelectorAll(".reagieren-tafel__schnell .reagieren-tafel__zeichen").length,
|
|
gruppen: document.querySelectorAll(".reagieren-tafel__gruppe").length,
|
|
katalog: document.querySelectorAll(".reagieren-tafel__katalog .reagieren-tafel__zeichen").length,
|
|
suche: !!document.querySelector(".reagieren-tafel__suche"),
|
|
stern: !!document.querySelector(".reagieren-tafel__stern"),
|
|
}));
|
|
ok(teile.schnell === 6, `sechs Vorschlaege stehen oben (${teile.schnell})`);
|
|
ok(teile.gruppen >= 4 && teile.katalog >= 50,
|
|
`darunter der Katalog: ${teile.gruppen} Gruppen, ${teile.katalog} Zeichen`);
|
|
ok(teile.suche && teile.stern, "Suchfeld und Favoriten-Schalter sind da");
|
|
|
|
/* ===== UND SIE MUSS AUCH AUSSEHEN WIE EINE TAFEL (15.09.2026) ======
|
|
|
|
Filipe, mit einem Bildschirmfoto: „was ist das den für eine
|
|
scheisse verbesser das sofort." Darauf: ein dreissig Pixel
|
|
schmaler Streifen, sechs Zeichen untereinander, ein Rollbalken
|
|
daneben.
|
|
|
|
ALLE PRÜFUNGEN OBEN WAREN DABEI GRÜN. Sie fragen, ob es die
|
|
Elemente gibt und ob die Suche sucht -- beides stimmte. Wie breit
|
|
die Tafel ist, hat nie jemand gemessen. Genau die Sorte Prüfung,
|
|
die bestätigt und nichts beweist.
|
|
|
|
Gemessen wird deshalb die GEOMETRIE: Ist sie so breit, wie eine
|
|
Karte sein muss? Und stehen die sechs Vorschläge NEBENeinander --
|
|
also auf derselben Höhe? */
|
|
const masse = await seite.evaluate(() => {
|
|
const t = document.querySelector(".reagieren-tafel");
|
|
if (!t) return null;
|
|
const k = t.getBoundingClientRect();
|
|
const z = [...document.querySelectorAll(
|
|
".reagieren-tafel__schnell .reagieren-tafel__zeichen")].map((x) => Math.round(
|
|
x.getBoundingClientRect().top));
|
|
const n = t.closest(".chat-nachricht");
|
|
return { breite: Math.round(k.width), hoehe: Math.round(k.height),
|
|
reihen: new Set(z).size, zeichen: z.length,
|
|
selbst: n?.dataset.selbst || "nein",
|
|
spalten: n ? getComputedStyle(n).gridTemplateColumns : "",
|
|
zelle: getComputedStyle(t).gridColumnStart };
|
|
});
|
|
ok(!!masse, "die Tafel liegt im Bild");
|
|
ok(masse.selbst === "ja", "hier haengt sie an einer EIGENEN Nachricht");
|
|
ok(masse.breite >= 280,
|
|
`sie ist eine Karte, kein Streifen (${masse.breite} px breit)`);
|
|
/* Sechs Zeichen auf EINER Zeile. Zwei Zeilen wären kein Fehler --
|
|
dreissig Pixel Breite sind einer, und dort stünden sechs. */
|
|
ok(masse.reihen === 1,
|
|
`die ${masse.zeichen} Vorschläge stehen nebeneinander (${masse.reihen} Zeile)`);
|
|
ok(masse.hoehe < masse.breite * 1.6,
|
|
`und sie ist breiter als hoch (${masse.breite} x ${masse.hoehe} px)`);
|
|
|
|
/* ===== UND JETZT DER FALL, DER WIRKLICH KAPUTT WAR ==================
|
|
|
|
Alles oben wurde an einer EIGENEN Nachricht gemessen. Dort hat das
|
|
Raster nur eine Spalte, und die Tafel bekam die volle Breite --
|
|
deshalb war diese Prüfung gruen, waehrend es bei jeder FREMDEN
|
|
Nachricht dreissig Pixel waren.
|
|
|
|
Eine Prüfung, die nur den Fall misst, in dem es ohnehin
|
|
funktioniert, ist keine Prüfung. */
|
|
await seite.evaluate(() => document.querySelector(".reagieren-tafel")?.remove());
|
|
const fremdeDa = await seite.evaluate(() => {
|
|
const n = [...document.querySelectorAll('.chat-nachricht:not([data-selbst="ja"])')]
|
|
.find((x) => x.querySelector(".chat-nachricht__reag-knopf"));
|
|
if (!n) return false;
|
|
n.querySelector(".chat-nachricht__reag-knopf").click();
|
|
return true;
|
|
});
|
|
/* KEIN ÜBERSPRINGEN: Findet sich keine fremde Nachricht, ist die
|
|
Prüfung nicht durchführbar -- und das ist ein Fehler, kein
|
|
stilles Weiterlaufen. */
|
|
ok(fremdeDa, "es gibt eine Nachricht von jemand anderem zum Messen");
|
|
await seite.waitForTimeout(500);
|
|
const fremdeTafel = await seite.evaluate(() => {
|
|
const t = document.querySelector(".reagieren-tafel");
|
|
if (!t) return null;
|
|
const k = t.getBoundingClientRect();
|
|
const z = [...document.querySelectorAll(
|
|
".reagieren-tafel__schnell .reagieren-tafel__zeichen")].map((x) => Math.round(
|
|
x.getBoundingClientRect().top));
|
|
const n = t.closest(".chat-nachricht");
|
|
const kreis = n?.querySelector(".chat-nachricht__zeichen")?.getBoundingClientRect();
|
|
return { breite: Math.round(k.width), reihen: new Set(z).size,
|
|
selbst: n?.dataset.selbst || "nein",
|
|
/* Steht sie NEBEN dem Kreis oder darunter in seiner Spalte? */
|
|
nebenDemKreis: kreis ? Math.round(k.left - kreis.left) : null };
|
|
});
|
|
ok(fremdeTafel?.selbst === "nein", "die Tafel haengt jetzt an einer fremden Nachricht");
|
|
ok(fremdeTafel.breite >= 280,
|
|
`auch dort ist sie eine Karte (${fremdeTafel.breite} px statt der 30 von vorher)`);
|
|
ok(fremdeTafel.reihen === 1,
|
|
`und die Vorschlaege stehen nebeneinander (${fremdeTafel.reihen} Zeile)`);
|
|
ok(fremdeTafel.nebenDemKreis >= 30,
|
|
`sie steht in der Spalte der Blase, nicht in der des Kreises (+${fremdeTafel.nebenDemKreis} px)`);
|
|
|
|
if (process.env.SCHIRM) {
|
|
await seite.locator(".chat-nachricht").last().screenshot(
|
|
{ path: join(HIER_BILD, "pruef-chat-tafel.png") });
|
|
}
|
|
|
|
/* ---- Die Suche muss SUCHEN --------------------------------------- */
|
|
await seite.fill(".reagieren-tafel__suche", "feuer");
|
|
await seite.waitForTimeout(400);
|
|
const gefunden = await seite.evaluate(() => ({
|
|
zahl: document.querySelectorAll(".reagieren-tafel__katalog .reagieren-tafel__zeichen").length,
|
|
erstes: document.querySelector(".reagieren-tafel__katalog .reagieren-tafel__zeichen")?.textContent,
|
|
}));
|
|
ok(gefunden.zahl >= 1 && gefunden.zahl < teile.katalog,
|
|
`"feuer" grenzt ein: ${gefunden.zahl} statt ${teile.katalog}`);
|
|
ok(gefunden.erstes === "\u{1F525}", `und findet das Richtige (${gefunden.erstes})`);
|
|
|
|
/* Ein Wort, das es nicht gibt, sagt das -- statt eine leere Flaeche
|
|
zu zeigen, die wie ein Fehler aussieht. */
|
|
await seite.fill(".reagieren-tafel__suche", "zwiebelkuchen");
|
|
await seite.waitForTimeout(400);
|
|
const leer = await seite.evaluate(() =>
|
|
document.querySelector(".reagieren-tafel__leer")?.textContent || "");
|
|
ok(leer.includes("Nichts zu"), `ein Fehlgriff wird gesagt ("${leer.slice(0, 40)}")`);
|
|
await seite.fill(".reagieren-tafel__suche", "");
|
|
await seite.waitForTimeout(400);
|
|
|
|
/* ---- Der Favoriten-Modus ----------------------------------------- */
|
|
await seite.click(".reagieren-tafel__stern");
|
|
await seite.waitForTimeout(300);
|
|
const anModus = await seite.evaluate(() => ({
|
|
merken: document.querySelector(".reagieren-tafel")?.dataset.merken,
|
|
hinweis: document.querySelector(".reagieren-tafel__hinweis")?.textContent || "",
|
|
}));
|
|
ok(anModus.merken === "ja", "der Stern schaltet den Merk-Modus ein");
|
|
ok(anModus.hinweis.includes("merkt"),
|
|
`und sagt, was ein Antippen jetzt tut ("${anModus.hinweis.slice(0, 44)}…")`);
|
|
|
|
/* Ein Zeichen im Merk-Modus antippen REAGIERT NICHT, es merkt. Das
|
|
ist der Unterschied, der schiefgehen kann -- und er wird hier an
|
|
der Zahl der Reaktionen gemessen, nicht an der Absicht. */
|
|
const vorher = await seite.$$eval(".reaktionen__stueck", (b) => b.length);
|
|
await seite.click(".reagieren-tafel__katalog .reagieren-tafel__zeichen");
|
|
await seite.waitForTimeout(700);
|
|
const nachher = await seite.evaluate(() => ({
|
|
reaktionen: document.querySelectorAll(".reaktionen__stueck").length,
|
|
gemerkt: document.querySelectorAll('.reagieren-tafel__zeichen[data-favorit="ja"]').length,
|
|
offen: !!document.querySelector(".reagieren-tafel"),
|
|
hinweis: document.querySelector(".reagieren-tafel__hinweis")?.textContent || "",
|
|
}));
|
|
ok(nachher.reaktionen === vorher,
|
|
`im Merk-Modus entsteht KEINE Reaktion (${vorher} -> ${nachher.reaktionen})`);
|
|
ok(nachher.gemerkt >= 1, `sondern ein Favorit (${nachher.gemerkt} markiert)`);
|
|
ok(nachher.offen, "und die Tafel bleibt offen -- sonst achtmal aufmachen");
|
|
ok(nachher.hinweis.includes("gemerkt"),
|
|
`der Hinweis zaehlt mit ("${nachher.hinweis.slice(0, 34)}…")`);
|
|
|
|
if (process.env.SCHIRM) {
|
|
await seite.locator(".chat-nachricht").last().screenshot(
|
|
{ path: join(HIER_BILD, "pruef-chat-favoriten.png") });
|
|
}
|
|
|
|
/* Und der Favorit steht danach VORN -- ohne die Tafel neu zu oeffnen. */
|
|
const vornDrin = await seite.evaluate(() => {
|
|
const merk = [...document.querySelectorAll('.reagieren-tafel__katalog .reagieren-tafel__zeichen[data-favorit="ja"]')]
|
|
.map((b) => b.dataset.zeichen);
|
|
const vorn = [...document.querySelectorAll(".reagieren-tafel__schnell .reagieren-tafel__zeichen")]
|
|
.map((b) => b.dataset.zeichen);
|
|
return merk.every((z) => vorn.includes(z)) && vorn.length > 0;
|
|
});
|
|
ok(vornDrin, "der frisch gemerkte steht sofort in der Vorschlagszeile");
|
|
|
|
/* Modus wieder aus, dann wirklich reagieren. */
|
|
await seite.click(".reagieren-tafel__stern");
|
|
await seite.waitForTimeout(250);
|
|
|
|
const tafelLage = await seite.evaluate(() => {
|
|
const t = document.querySelector(".reagieren-tafel");
|
|
const z = document.querySelector(".reagieren-tafel__zeichen");
|
|
const k = (e) => { if (!e) return null; const r = e.getBoundingClientRect();
|
|
const st = getComputedStyle(e);
|
|
return { b: Math.round(r.width), h: Math.round(r.height), d: st.display,
|
|
sicht: st.visibility, o: st.opacity, eltern: e.parentElement?.className }; };
|
|
return { tafel: k(t), zeichen: k(z) };
|
|
});
|
|
ok(tafelLage.zeichen && tafelLage.zeichen.b > 0 && tafelLage.zeichen.h > 0,
|
|
`die Zeichen haben eine Groesse (${JSON.stringify(tafelLage)})`);
|
|
|
|
if (process.env.SCHIRM) {
|
|
await seite.locator(".chat-nachricht").last().screenshot(
|
|
{ path: join(HIER_BILD, "pruef-chat-auswahl.png") });
|
|
}
|
|
|
|
await seite.click(".reagieren-tafel__zeichen");
|
|
await seite.waitForTimeout(900);
|
|
const stueck = await seite.evaluate(() => {
|
|
const b = document.querySelector(".reaktionen__stueck");
|
|
return b ? { text: b.textContent.trim(), selbst: b.dataset.selbst,
|
|
titel: b.title } : null;
|
|
});
|
|
ok(!!stueck, `unter der Nachricht steht die Reaktion ("${stueck?.text || "nichts"}")`);
|
|
ok(stueck?.selbst === "ja", "und sie ist als die eigene gekennzeichnet");
|
|
ok((stueck?.titel || "").includes("Vanessa"),
|
|
`im Titel steht, wer reagiert hat ("${(stueck?.titel || "").slice(0, 40)}…")`);
|
|
ok((await seite.$$(".reagieren-tafel")).length === 0,
|
|
"die Auswahl schliesst sich nach dem Klick von selbst");
|
|
|
|
if (process.env.SCHIRM) {
|
|
await seite.locator(".chat-nachricht").last().screenshot(
|
|
{ path: join(HIER_BILD, "pruef-chat-reaktion.png") });
|
|
}
|
|
|
|
/* ZWEITER KLICK NIMMT SIE ZURUECK -- an derselben Stelle, wo sie
|
|
steht. Ohne diese Zeile hiesse alles oben nur, dass man etwas
|
|
hinzufuegen kann. */
|
|
await seite.click(".reaktionen__stueck");
|
|
await seite.waitForTimeout(900);
|
|
ok((await seite.$$(".reaktionen__stueck")).length === 0,
|
|
"ein Klick darauf nimmt sie wieder weg");
|
|
|
|
/* ---- UND DER KNOPF SELBST -----------------------------------------
|
|
Bis hierher ist nur das RECHT geprueft (darf_aufloesen) und der
|
|
Weg dahinter. Ob die Oberflaeche beides verbindet, war unbewiesen
|
|
-- und genau dort sitzt der Fehler, den man am ehesten macht:
|
|
ein Knopf, der immer dasteht, oder einer, der nie erscheint.
|
|
|
|
Beide Faelle in einem Raum zu messen geht nicht; es braucht zwei.
|
|
Deshalb legt die rechte Hand hier eine eigene Gruppe an (sie darf)
|
|
und oeffnet danach ein Zweier-Gespraech (sie darf nicht). */
|
|
const eigene = await (await rufe(hand.keks, "/workspace/api/chat/gruppe",
|
|
{ mit: [idEins, idDrei], name: "Leitungsrunde" }, "POST")).json();
|
|
ok(Number.isInteger(eigene.raum_id),
|
|
`die rechte Hand legt eine eigene Gruppe an (#${eigene.raum_id})`);
|
|
|
|
await seite.goto(BASIS + `/workspace/chat.html?raum=${eigene.raum_id}`,
|
|
{ waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1500);
|
|
const beiEigener = await seite.evaluate(() => ({
|
|
aufloesen: document.getElementById("raum-aufloesen")?.hidden,
|
|
wegraeumen: document.getElementById("gespraech-weg")?.hidden,
|
|
titel: document.getElementById("verlauf-titel")?.textContent.trim(),
|
|
}));
|
|
ok(beiEigener.titel === "Leitungsrunde",
|
|
`die Gruppe ist offen ("${beiEigener.titel}")`);
|
|
ok(beiEigener.aufloesen === false,
|
|
"in ihrer eigenen Gruppe steht der Aufloesen-Knopf da");
|
|
ok(beiEigener.wegraeumen === false,
|
|
"und der Wegraeumen-Knopf ebenfalls -- beide Wege bleiben offen");
|
|
if (process.env.SCHIRM) {
|
|
await seite.locator(".chat__kopf--verlauf").screenshot(
|
|
{ path: join(HIER_BILD, "pruef-chat-kopf.png") });
|
|
}
|
|
|
|
const fremd = await (await rufe(hand.keks, "/workspace/api/chat/direkt",
|
|
{ mit: idEins }, "POST")).json();
|
|
await seite.goto(BASIS + `/workspace/chat.html?raum=${fremd.raum_id}`,
|
|
{ waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1500);
|
|
const beiDirekt = await seite.evaluate(() => ({
|
|
aufloesen: document.getElementById("raum-aufloesen")?.hidden,
|
|
wegraeumen: document.getElementById("gespraech-weg")?.hidden,
|
|
}));
|
|
ok(beiDirekt.aufloesen === true,
|
|
"im Zweier-Gespraech ist er weg");
|
|
ok(beiDirekt.wegraeumen === false,
|
|
"der Wegraeumen-Knopf bleibt aber -- die beiden sind nicht dasselbe");
|
|
|
|
await browser.close();
|
|
}
|
|
|
|
/* ---- Ende ------------------------------------------------------------- */
|
|
d.close();
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
console.log("");
|
|
console.log(`${geprueft} Pruefungen`);
|
|
console.log(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`);
|
|
process.exit(fehler ? 1 : 0);
|