Files
dogfather-universe/server/pruef-chat-neu-stelle.mjs
T
DogFatherGitandClaude Opus 5 c5e3ab1c44 Zwei Meldungen aus dem Support: Auswahlmenues und die Stelle im Chat
DIENE: "Beim Kalender laesst sich die Art noch nicht einstellen, das
Menue ploppt nur ganz kurz auf und verschwindet direkt wieder. Das
selbe bei den Wiederholungen."

In wahl.js stand `addEventListener('resize', schliessen)`. Auf dem
Rechner ist das harmlos -- dort aendert sich die Fenstergroesse nur,
wenn jemand sie aendert. Auf einem Handy aendert sie sich BEIM
BEDIENEN: Die Adressleiste faehrt beim kleinsten Scrollen ein und
aus, die Tastatur kommt und geht, und jedes Mal feuert `resize`. Die
Liste ging auf, der Browser meldete eine neue Hoehe, und sie war
wieder zu.

Es ist derselbe Fehler wie am 04.09. beim Scrollen ("wenn ich da
scollen will geht das immer zu"), nur eine Zeile tiefer: Ein
Ereignis, das beim BEDIENEN entsteht, wird als Grund zum Abbrechen
genommen. Jetzt wird die Liste neu ausgerichtet statt geschlossen --
`stelle()` kann das ohnehin, und nach einer Groessenaenderung muss
sie es sowieso.

pruef-suchfeld 8 -> 15, an Dienes genauem Fall: Kalenderformular,
beide Felder, echte Groessenaenderung dazwischen. Gemessen wird nicht
nur "offen", sondern auch "sitzt noch am Knopf" (6 px) -- offen, aber
verrutscht waere nur die halbe Antwort. Gegenprobe: mit der alten
Zeile 4 Fehler.

----------------------------------------------------------------------

MISS: "Wenn ich auf den Chat gehen, komme ich zuerst auf die erste
neue Nachricht, aber kurze Zeit spaeter springt er auf die zuletzt
geschrieben Nachricht."

In chat.js stand `unten = true; neuUnten = 0;` AUSSERHALB von
`if (!sanft)` -- es lief also bei jedem Nachladen, auch beim sanften,
das staendig passiert (Strom verbindet, jemand heftet etwas an, eine
Nachricht kommt).

Solange die Neu-Linie steht, faellt das nicht auf: Der Zweig darueber
springt dorthin und kehrt zurueck, bevor `unten` gelesen wird. Die
zweite Haelfte ist `wache` -- sie loescht den Sprung-Merker beim
ersten FINGERTIPP auf den Verlauf, und das ist richtig so. Ab da ist
der Weg frei, und das naechste sanfte Nachladen findet `unten ===
true` vor und reisst die Ansicht ans Ende. Genau ihre "kurze Zeit
spaeter".

Ein sanftes Nachladen ist kein Oeffnen. Wo jemand steht, weiss ab
jetzt allein der Scroll-Horcher -- und der misst es, statt es
anzunehmen.

DREI FEHLVERSUCHE BIS ZUR MESSUNG, und sie gehoeren ins Protokoll:
Erst sechs Sekunden Nichtstun, dann ein Fingertipp auf Koordinaten,
dann ein Verbindungsabriss -- alle drei waren mit dem ALTEN Code
gruen. Eine Pruefung, die den Fehler nicht herstellt, misst nichts,
und ich haette sie beinahe als Beweis genommen. Was fehlte: Der Griff
muss den Verlauf WIRKLICH treffen (Koordinaten gehen daneben), und es
braucht einen echten sanften Nachlauf -- hier das Anheften, das ein
`pin`-Ereignis an alle im Raum schickt.

Erst damit flippt die Gegenprobe: ohne Korrektur 500 -> 7054 px und
`amEnde: true`, mit Korrektur bleibt es bei 500.

pruef-chat-neu-stelle 62 -> 69.

Unterwegs wieder entfernt: ein Merker `schonGesprungen`, den ich
zuerst eingebaut hatte. Nach der echten Korrektur ist er
unerreichbar, und ich konnte ihn mit keiner Messung zum Greifen
bringen. Ein Zweig, den nichts erreicht, sieht beim Lesen aus wie ein
Fall, den es gibt.

Gegengemessen: pruef-chat 80, pruef-chat-optik 84, pruef-freie-namen
32 -- 0 Fehler.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-05 22:41:18 +02:00

534 lines
25 KiB
JavaScript

/* =====================================================================
DER CHAT OEFFNET DORT, WO DAS NEUE ANFAENGT (25.09.2026)
=====================================================================
Filipe: „wenn ich in den chat rein gehe und es neue kommentare gibt,
will ich dass mein chat sich da öffnet wo die neuen nachrichten
anfangen die ich noch nicht gesehen hab bitte und nicht immer ganz
unten. sonst muss man immer hoch scrollen um die neuen zu lesen und
das ist scheisse. perfektionier das bei allen rollen."
---------------------------------------------------------------------
WARUM ES DIESE PRUEFUNG GIBT, OBWOHL DIE FUNKTION SEIT DEM 09.09. DA IST
Sie war da und sass falsch. Der Sprung rechnete mit `offsetTop` --
dem Abstand zum `offsetParent`, und das ist nur dann der Verlauf
selbst, wenn dieser `position: relative` traegt. Tut er nicht.
Gemessen auf 390 px landete die Linie „Ab hier neu" 23 px OBERHALB
des sichtbaren Bereichs: Man musste genau das tun, was Filipe nicht
mehr tun wollte. Am Rechner stimmte es zufaellig, weil dort weniger
dazwischenliegt -- deshalb ist es nie aufgefallen.
EIN FEHLER, DEN NUR EINE MESSUNG IN PIXELN FINDET. „Die Linie ist
da" war die ganze Zeit wahr. Gemessen wird hier deshalb, ob sie IM
BILD steht und wie weit sie vom oberen Rand entfernt ist.
---------------------------------------------------------------------
BEI ALLEN ROLLEN, UND ZWAR IN BEIDEN HAEUSERN
Die Mechanik ist dieselbe Datei fuer alle -- aber „dieselbe Datei"
war schon oefter ein Trugschluss: Die Kopfleiste ist je nach Rolle
verschieden hoch, der Treff hat eigene Regeln, und die Agentur laedt
ihre eigene Wand. Ein Verlauf, der bei DogFather richtig sitzt und
beim Modi 40 px daneben, ist fuer den Modi kaputt.
Aufrufen mit: node server/pruef-chat-neu-stelle.mjs
===================================================================== */
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { execFileSync } from "node:child_process";
import { request as httpAnfrage } from "node:http";
import { createServer as httpsServer } from "node:https";
const ordner = mkdtempSync(join(tmpdir(), "ws-neustelle-p-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
const { eigenerPort } = await import("./helfer-port.mjs");
const PORT = await eigenerPort(import.meta, "die Pruefung der Oeffnungsstelle");
const HP = PORT + 1;
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 });
};
import { notbremse } from "./helfer-notbremse.mjs";
import { zertifikatBauen } from "./helfer-openssl.mjs";
await import("./index.js");
notbremse(300_000, "pruef-chat-neu-stelle");
await new Promise((r) => setTimeout(r, 1000));
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
const CREW = "crew.dogfather-universe.com";
const AGENTUR = "workspace.dogfather-universe.com";
let fehler = 0, geprueft = 0;
const melde = (t) => console.log(t);
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
const sch = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", sch, jetzt);
const anlegen = (name, rolle, code) => {
const s = randomBytes(16).toString("hex");
const h = scryptSync(code, s, 64,
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
return d.prepare("INSERT INTO personen"
+ " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
+ " VALUES (?,?,?,?,?,?,1,?) RETURNING id")
.get(name, rolle, h, s, 32768,
createHmac("sha256", sch).update(code).digest("hex"), jetzt).id;
};
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
const idHand = anlegen("Rieke", "hand", "CODE-HAND-0001");
const idLinke = anlegen("Mara", "linke", "CODE-LINK-0001");
const idModi = anlegen("Kessi", "modi", "CODE-MODI-0001");
const idManager = anlegen("Cigdem", "manager", "CODE-MANA-0001");
const idCreator = anlegen("Luna", "creator", "CODE-CREA-0001");
/* DIE BETREUUNG GEHOERT DAZU. Im Agenturhaus darf ein Manager nur mit
SEINEN Creators schreiben -- ohne diese Zeile antwortet die Route
mit 403, und die Pruefung meldet einen Fehler, den es nicht gibt.
Im ersten Anlauf genau so passiert: „Manager: ein Gespraech entsteht
(403)". Der Server hatte recht; die Lage war unvollstaendig. */
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit, gesetzt_von)"
+ " VALUES (?,?,?,?) ON CONFLICT(creator_id) DO UPDATE SET betreuer_id = excluded.betreuer_id")
.run(idCreator, idManager, jetzt, idDogi);
d.close();
const roh = (host, keks, pfad, methode = "GET", koerper = null) =>
new Promise((fertig, schief) => {
const daten = koerper === null ? null : JSON.stringify(koerper);
const a = httpAnfrage({
host: "127.0.0.1", port: PORT, path: pfad, method: methode,
headers: {
Host: host, ...(keks ? { Cookie: keks } : {}),
Origin: `https://${host}`, Referer: `https://${host}/workspace/chat.html`,
...(daten ? { "Content-Type": "application/json",
"Content-Length": Buffer.byteLength(daten) } : {}),
},
}, (r) => {
let t = ""; r.on("data", (x) => { t += x; });
r.on("end", () => fertig({ status: r.statusCode, kopf: r.headers,
json: () => { try { return JSON.parse(t); } catch { return {}; } } }));
});
a.on("error", schief);
if (daten) a.write(daten);
a.end();
});
const anmelden = async (host, rolle, code) => {
const a = await roh(host, null, "/workspace/api/anmelden", "POST", { rolle, code });
if (a.status !== 200) { console.log(`ABBRUCH: ${rolle} ${a.status}`); process.exit(2); }
return [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ");
};
/* =======================================================================
DIE LAGE: je Rolle ein Gespraech mit 20 alten und 12 neuen Zeilen
======================================================================= */
const LEUTE = [
{ wer: "DogFather", host: CREW, rolle: "admin", code: "CODE-DOGI-0001", id: idDogi,
partner: idModi, partnerCode: "CODE-MODI-0001", partnerRolle: "modi", partnerHost: CREW },
{ wer: "rechte Hand", host: CREW, rolle: "hand", code: "CODE-HAND-0001", id: idHand,
partner: idModi, partnerCode: "CODE-MODI-0001", partnerRolle: "modi", partnerHost: CREW },
{ wer: "linke Hand", host: CREW, rolle: "linke", code: "CODE-LINK-0001", id: idLinke,
partner: idModi, partnerCode: "CODE-MODI-0001", partnerRolle: "modi", partnerHost: CREW },
{ wer: "Modi", host: CREW, rolle: "modi", code: "CODE-MODI-0001", id: idModi,
partner: idHand, partnerCode: "CODE-HAND-0001", partnerRolle: "hand", partnerHost: CREW },
{ wer: "Manager", host: AGENTUR, rolle: "manager", code: "CODE-MANA-0001", id: idManager,
partner: idCreator, partnerCode: "CODE-CREA-0001", partnerRolle: "creator",
partnerHost: AGENTUR },
{ wer: "Creator", host: AGENTUR, rolle: "creator", code: "CODE-CREA-0001", id: idCreator,
partner: idManager, partnerCode: "CODE-MANA-0001", partnerRolle: "manager",
partnerHost: AGENTUR },
];
/* ---- Zertifikat und Vorbau (eine Adresse je Haus) ------------------- */
const schl = join(ordner, "b.key"), zert = join(ordner, "b.crt");
zertifikatBauen(schl, zert, CREW);
const vorbau = httpsServer({ key: readFileSync(schl), cert: readFileSync(zert) },
(von, nach) => {
const w = httpAnfrage({
host: "127.0.0.1", port: PORT, path: von.url, method: von.method,
headers: { ...von.headers },
}, (a) => { nach.writeHead(a.statusCode || 502, a.headers); a.pipe(nach); });
von.pipe(w);
});
await new Promise((r) => vorbau.listen(HP, "127.0.0.1", r));
const pw = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await pw.chromium.launch({
args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1,MAP ${AGENTUR} 127.0.0.1`,
"--ignore-certificate-errors"],
});
/** Wo steht der Verlauf, nachdem die Seite geoeffnet wurde? */
async function blick(person, breite, hoehe, keks, raumId) {
const kontext = await browser.newContext({
viewport: { width: breite, height: hoehe },
deviceScaleFactor: breite < 700 ? 2 : 1,
isMobile: breite < 700, hasTouch: breite < 700,
});
await kontext.addCookies(keks.split("; ").map((k) => {
const [x, ...r] = k.split("=");
return { name: x, value: r.join("="), domain: person.host, path: "/" };
}));
const seite = await kontext.newPage();
const konsole = [];
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message.slice(0, 120)));
await seite.goto(`https://${person.host}:${HP}/workspace/chat.html?raum=${raumId}`,
{ waitUntil: "networkidle" });
await seite.waitForTimeout(2200);
const lage = await seite.evaluate(() => {
const v = document.getElementById("verlauf");
if (!v) return { da: false };
const linie = v.querySelector(".chat-neu");
const sicht = v.getBoundingClientRect();
return {
da: true,
hoehe: Math.round(v.clientHeight),
amEnde: v.scrollHeight - v.scrollTop - v.clientHeight < 40,
linieDa: !!linie,
/* DIE EIGENTLICHE FRAGE: Steht sie im Bild, und wie weit unter
dem oberen Rand? Ein negativer Wert heisst: darueber, also
unsichtbar -- genau der Fehler, um den es hier geht. */
abstand: linie
? Math.round(linie.getBoundingClientRect().top - sicht.top) : null,
sichtbar: linie
? (linie.getBoundingClientRect().top >= sicht.top - 2
&& linie.getBoundingClientRect().bottom <= sicht.bottom + 2)
: false,
/* Was steht ganz oben im Bild? Ueber der Linie soll Zusammenhang
stehen, nicht schon das Neue. */
obenImBild: (() => {
const z = [...v.querySelectorAll(".chat-nachricht")]
.find((x) => x.getBoundingClientRect().bottom > sicht.top + 4);
return z ? (z.textContent || "").trim().slice(0, 24) : "-";
})(),
};
});
await kontext.close();
return { ...lage, konsole };
}
melde("");
melde("=== 1. Bei jeder Rolle oeffnet der Chat an der neuen Stelle ===");
/* ==== JEDER BLICK BEKOMMT SEINEN EIGENEN RAUM ======================
Der erste Anlauf benutzte einen Raum je Rolle und setzte den
Lesestand zwischen Handy und Rechner zurueck. Das Ergebnis
FLATTERTE: Mal war DogFather rot, mal die rechte Hand, mal der Modi
-- bei unveraendertem Code.
Der Grund ist ein Wettlauf: Die Seite meldet „gelesen bis hier" beim
Oeffnen, und zwar nicht sofort. Wird der Stand danach
zurueckgesetzt, gewinnt mal das eine, mal das andere. Eine Pause
davor macht es wahrscheinlicher und nicht sicher -- und eine
Pruefung, die von Wahrscheinlichkeit lebt, ist keine.
MIT EINEM EIGENEN RAUM JE BLICK gibt es nichts zurueckzusetzen. Das
ist mehr Aufbau und dafuer eine Messung, die immer dasselbe sagt. */
for (const person of LEUTE) {
const keks = await anmelden(person.host, person.rolle, person.code);
const kPartner = await anmelden(person.partnerHost, person.partnerRolle, person.partnerCode);
for (const [wie, breite, hoehe] of [["Handy", 390, 844], ["Rechner", 1280, 900]]) {
const gr = await roh(person.host, keks, "/workspace/api/chat/gruppe", "POST",
{ name: `${person.wer} ${wie}`, mit: [person.partner] });
const raumId = gr.json().raum_id;
if (!raumId) {
ok(false, `${person.wer} / ${wie}: ein Raum entsteht (${gr.status})`);
continue;
}
let letzteAlte = 0;
for (let i = 1; i <= 20; i++) {
const a = await roh(person.partnerHost, kPartner,
`/workspace/api/chat/raeume/${raumId}/nachrichten`, "POST",
{ text: `Alte Nachricht ${i} -- lange genug, damit sie auf einem Handy umbricht.` });
letzteAlte = a.json().nachricht?.id || a.json().id || letzteAlte;
}
await roh(person.host, keks, `/workspace/api/chat/raeume/${raumId}/gelesen`,
"PUT", { bis: letzteAlte });
for (let i = 1; i <= 12; i++) {
await roh(person.partnerHost, kPartner,
`/workspace/api/chat/raeume/${raumId}/nachrichten`, "POST",
{ text: `NEUE Nachricht ${i} -- auch diese ist lang genug zum Umbrechen.` });
}
/* Was sagt der Server unmittelbar vor dem Blick? Ohne diese Zeile
steht bei einem Fehlschlag nur „die Linie fehlt" da -- und man
weiss nicht, ob die Oberflaeche sie nicht zeichnet oder der
Server schon alles als gelesen fuehrt. */
const vorher = await roh(person.host, keks,
`/workspace/api/chat/raeume/${raumId}/nachrichten`);
const standVor = vorher.json().gelesen_bis;
const l = await blick(person, breite, hoehe, keks, raumId);
const wo = `${person.wer} / ${wie}`;
ok(l.da && l.linieDa, `${wo}: die Linie „Ab hier neu" steht im Verlauf `
+ `(gelesen_bis ${standVor}, letzte alte #${letzteAlte})`);
ok(l.sichtbar === true,
`${wo}: und sie ist zu SEHEN (${l.abstand} px unter dem oberen Rand)`);
/* Nicht am Rand kleben: Ueber der Linie soll Zusammenhang stehen.
Ein Viertel der Hoehe ist die Vorgabe; gemessen wird grosszuegig
(mindestens 24 px), damit eine andere Kopfhoehe nicht sofort rot
wird -- die Aussage ist „es steht etwas darueber", nicht „genau
ein Viertel". */
ok((l.abstand || 0) >= 24,
`${wo}: mit Zusammenhang darueber ("${l.obenImBild}")`);
ok(l.amEnde === false, `${wo}: und NICHT ganz unten`);
ok(l.konsole.length === 0, `${wo}: keine Abstuerze (${l.konsole.join(" | ") || "keine"})`);
}
}
/* =======================================================================
2. GEGENPROBEN
======================================================================= */
melde("");
melde("=== 2. Gegenproben (jede MUSS anschlagen) ===");
{
const person = LEUTE[0];
const keks = await anmelden(person.host, person.rolle, person.code);
const kPartner = await anmelden(CREW, "modi", "CODE-MODI-0001");
const gr = await roh(person.host, keks, "/workspace/api/chat/direkt", "POST",
{ mit: idModi });
const raumId = gr.json().raum_id;
/* (a) NICHTS NEUES -> ans Ende. Ohne diese Zeile koennte der Sprung
immer stattfinden, und wer nichts verpasst hat, laendete
grundlos mitten im Verlauf. */
let letzte = 0;
for (let i = 1; i <= 8; i++) {
const a = await roh(CREW, kPartner, `/workspace/api/chat/raeume/${raumId}/nachrichten`,
"POST", { text: `Alles gelesen ${i}.` });
letzte = a.json().nachricht?.id || a.json().id || letzte;
}
await roh(person.host, keks, `/workspace/api/chat/raeume/${raumId}/gelesen`,
"PUT", { bis: letzte });
await new Promise((r) => setTimeout(r, 400));
const ohneNeues = await blick(person, 390, 844, keks, raumId);
ok(ohneNeues.linieDa === false,
`ohne Ungelesenes gibt es keine Linie (${ohneNeues.linieDa})`);
ok(ohneNeues.amEnde === true,
"und der Verlauf steht am Ende -- da, wo man weiterschreibt");
/* (b) Kann die Messung „nicht sichtbar" ueberhaupt erkennen? Sonst
waere jede gruene Zeile oben wertlos. Gemessen wird an einem
Verlauf, der absichtlich nach unten gescrollt wird. */
for (let i = 1; i <= 12; i++) {
await roh(CREW, kPartner, `/workspace/api/chat/raeume/${raumId}/nachrichten`,
"POST", { text: `Danach noch etwas ${i}.` });
}
{
const dd = new DatabaseSync(process.env.WORKSPACE_DB);
dd.prepare("UPDATE chat_teilnehmer SET gelesen_bis = ? WHERE raum_id = ? AND person_id = ?")
.run(letzte, raumId, person.id);
dd.close();
}
const kontext = await browser.newContext({ viewport: { width: 390, height: 844 },
deviceScaleFactor: 2, isMobile: true, hasTouch: true });
await kontext.addCookies(keks.split("; ").map((k) => {
const [x, ...r] = k.split("=");
return { name: x, value: r.join("="), domain: person.host, path: "/" };
}));
const seite = await kontext.newPage();
await seite.goto(`https://${person.host}:${HP}/workspace/chat.html?raum=${raumId}`,
{ waitUntil: "networkidle" });
await seite.waitForTimeout(2000);
const kuenstlich = await seite.evaluate(() => {
const v = document.getElementById("verlauf");
v.scrollTop = v.scrollHeight; /* mit Absicht ans Ende */
const linie = v.querySelector(".chat-neu");
const sicht = v.getBoundingClientRect();
if (!linie) return null;
return {
abstand: Math.round(linie.getBoundingClientRect().top - sicht.top),
sichtbar: linie.getBoundingClientRect().top >= sicht.top - 2
&& linie.getBoundingClientRect().bottom <= sicht.bottom + 2,
};
});
await kontext.close();
ok(kuenstlich && kuenstlich.sichtbar === false && kuenstlich.abstand < 0,
`am Ende ist die Linie NICHT sichtbar (${kuenstlich?.abstand} px) `
+ "-- die Messung kann also auch nein sagen");
}
/* =====================================================================
DIE STELLE HAELT AUCH, WENN DIE LINIE VERSCHWINDET (05.10.2026)
Miss: "Wenn ich auf den Chat gehen, komme ich zuerst auf die erste
neue Nachricht, aber kurze Zeit spaeter springt er auf die zuletzt
geschrieben Nachricht."
Die Pruefungen oben messen den Zustand nach 2,2 Sekunden -- und der
war immer richtig. Ihr Fehler passiert DANACH: Beim Oeffnen meldet
die Seite "gelesen bis hier". Beim naechsten sanften Nachladen
steht die letzte Nachricht schon in `gelesen_bis`, die Linie
entsteht nicht mehr, und dieselbe Bedingung bedeutete ploetzlich
das Gegenteil -- nicht "das Neue ist jetzt gelesen", sondern
"nichts Neues, also ans Ende".
Deshalb wird hier ZWEIMAL gemessen: einmal wie oben, und einmal
nach weiteren sechs Sekunden, in denen niemand etwas anfasst. Eine
Messung, die nur den ersten Moment ansieht, haette diesen Fehler
nie gefunden -- sie hat ihn wochenlang nicht gefunden.
===================================================================== */
melde("");
melde("=== 4. Ein sanftes Nachladen reisst niemanden ans Ende ===");
{
const person = LEUTE[0];
const keks = await anmelden(person.host, person.rolle, person.code);
const kPartner = await anmelden(person.partnerHost, person.partnerRolle, person.partnerCode);
const gr = await roh(person.host, keks, "/workspace/api/chat/gruppe", "POST",
{ name: `${person.wer} Miss-Fall`, mit: [person.partner] });
const raumId = gr.json().raum_id;
let letzteAlte = 0;
for (let i = 1; i <= 20; i++) {
const a = await roh(person.partnerHost, kPartner,
`/workspace/api/chat/raeume/${raumId}/nachrichten`, "POST",
{ text: `Alte Nachricht ${i} -- lange genug, damit sie auf einem Handy umbricht.` });
letzteAlte = a.json().nachricht?.id || a.json().id || letzteAlte;
}
await roh(person.host, keks, `/workspace/api/chat/raeume/${raumId}/gelesen`,
"PUT", { bis: letzteAlte });
for (let i = 1; i <= 12; i++) {
await roh(person.partnerHost, kPartner,
`/workspace/api/chat/raeume/${raumId}/nachrichten`, "POST",
{ text: `NEUE Nachricht ${i} -- auch diese ist lang genug zum Umbrechen.` });
}
const kontext = await browser.newContext({
viewport: { width: 390, height: 844 },
deviceScaleFactor: 2, isMobile: true, hasTouch: true,
});
await kontext.addCookies(keks.split("; ").map((k) => {
const [x, ...r] = k.split("=");
return { name: x, value: r.join("="), domain: person.host, path: "/" };
}));
const seite = await kontext.newPage();
const konsole = [];
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message.slice(0, 120)));
await seite.goto(`https://${person.host}:${HP}/workspace/chat.html?raum=${raumId}`,
{ waitUntil: "networkidle" });
const messen = () => seite.evaluate(() => {
const v = document.getElementById("verlauf");
if (!v) return { da: false };
const l = v.querySelector(".chat-neu");
return {
da: true,
stelle: Math.round(v.scrollTop),
amEnde: v.scrollHeight - v.scrollTop - v.clientHeight < 40,
linieDa: !!l,
};
});
await seite.waitForTimeout(2200);
const erst = await messen();
ok(erst.da && erst.linieDa && !erst.amEnde,
`beim Oeffnen steht sie an der neuen Stelle (Linie ${erst.linieDa}, am Ende ${erst.amEnde})`);
/* ==== DER FINGERTIPP IST DER AUSLOESER ========================
Der erste Anlauf liess die Seite einfach sechs Sekunden in Ruhe
-- und war mit dem ALTEN Code genauso gruen. Die Messung hat
Miss' Fall gar nicht hergestellt.
Es fehlte das, was ein Mensch beim Lesen zwangslaeufig tut: Er
tippt einmal auf den Verlauf. `wache` nimmt das (zu Recht) als
"er hat die Stelle gesehen" und loescht den Sprung-Merker. Erst
DANACH ist der Weg zu `if (unten)` frei -- und `unten` stand bei
jedem sanften Nachladen wieder auf true.
Getippt wird an den linken Rand, nicht auf eine Nachricht: Es
geht um die Beruehrung, nicht um einen Knopf. */
/* Der Griff muss den Verlauf WIRKLICH treffen -- ein Tipp auf
Koordinaten kann danebengehen, und dann bleibt der Merker
stehen (gemessen: die Ansicht sprang danach zur Linie zurueck,
nicht ans Ende, weil `wache` nie lief). */
await seite.locator("#verlauf").dispatchEvent("touchstart");
await seite.waitForTimeout(400);
/* ==== UND JETZT NACH OBEN LESEN ===============================
Hier wird der Defekt selbst gemessen statt Miss' Ablauf geraten
-- drei Versuche, ihn nachzustellen, waren mit dem ALTEN Code
gruen, und eine Pruefung, die den Fehler nicht herstellt, misst
nichts.
Der Defekt ist eine Zeile: Beim Laden eines Raums wurde `unten`
auf true gesetzt, und zwar auch beim SANFTEN Nachladen. Wer also
irgendwo im Verlauf steht und dessen Strom neu verbindet, wird
ans Ende gerissen -- unabhaengig davon, wie er dorthin kam.
Das ist allgemeiner als Miss' Fall und schliesst ihn ein. */
await seite.evaluate(() => { document.getElementById("verlauf").scrollTop = 500; });
await seite.waitForTimeout(400);
const obenGelesen = await messen();
ok(obenGelesen.stelle <= 600 && !obenGelesen.amEnde,
`sie liest weiter oben (${obenGelesen.stelle} px, am Ende ${obenGelesen.amEnde})`);
/* ==== UND DER ZWEITE AUSLOESER: EIN SANFTER NACHLAUF ===========
Auch mit dem Fingertipp blieb die Messung mit dem alten Code
gruen -- es fehlte das, was `unten` ueberhaupt wieder auf true
setzt: ein erneutes Laden des Raums. Das passiert nicht von
selbst, sondern wenn der Ereignisstrom neu verbindet.
Erst versucht mit einem Verbindungsabriss (offline/online) -- der
loeste in dieser Umgebung keinen aus, und die Messung blieb
gruen, ohne etwas hergestellt zu haben. Jetzt heftet jemand
anderes eine Nachricht an: Das schickt ein `pin`-Ereignis an
alle im Raum, und die Seite laedt sanft nach. Im "Rudel" mit zehn
Leuten ist das Alltag. */
const angeheftet = await roh(person.host, keks,
`/workspace/api/chat/nachrichten/${letzteAlte}/anheften`, "PUT", { an: true });
ok(angeheftet.status === 200,
`jemand heftet etwas an -- das loest den sanften Nachlauf aus (${angeheftet.status})`);
/* Und jetzt sechs Sekunden nichts weiter tun -- genau das, was Miss
beschreibt: Sie liest, und "kurze Zeit spaeter" springt es. */
await seite.waitForTimeout(6000);
const spaeter = await messen();
ok(spaeter.amEnde === false,
`nach dem Nachladen steht sie nicht am Ende (amEnde ${spaeter.amEnde})`);
/* Die eigentliche Zahl: Die Bildlaufposition darf sich nicht
nennenswert bewegt haben. "Nicht am Ende" allein koennte auch
heissen, dass sie irgendwohin anders gesprungen ist. */
ok(Math.abs(spaeter.stelle - obenGelesen.stelle) <= 60,
`und zwar da, wo sie gelesen hat (${obenGelesen.stelle} -> ${spaeter.stelle} px)`);
/* Dass die Linie inzwischen WEG sein darf, ist kein Fehler --
gelesen ist gelesen. Nur springen darf es deswegen nicht. */
melde(` (die Linie ist jetzt ${spaeter.linieDa ? "noch da" : "weg -- gelesen"},`
+ " das ist in Ordnung)");
ok(konsole.length === 0, `keine Abstuerze (${konsole.join(" | ") || "keine"})`);
await kontext.close();
}
await browser.close();
vorbau.close();
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
melde(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG");
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
process.exit(fehler === 0 ? 0 : 1);