diff --git a/server/pruef-chat-neu-stelle.mjs b/server/pruef-chat-neu-stelle.mjs
index f58945e9..8d680061 100644
--- a/server/pruef-chat-neu-stelle.mjs
+++ b/server/pruef-chat-neu-stelle.mjs
@@ -368,6 +368,161 @@ melde("=== 2. Gegenproben (jede MUSS anschlagen) ===");
+ "-- 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();
diff --git a/server/pruef-suchfeld.mjs b/server/pruef-suchfeld.mjs
index 559e7de8..72ceb21c 100644
--- a/server/pruef-suchfeld.mjs
+++ b/server/pruef-suchfeld.mjs
@@ -189,6 +189,96 @@ try {
ok(konsole.length === 0, konsole.length ? konsole[0] : "keine Abstuerze");
await seite.screenshot({ path: "pruef-suchfeld.png", fullPage: false });
await kontext.close();
+
+/* =====================================================================
+ EINE GROESSENAENDERUNG DARF DIE LISTE NICHT SCHLIESSEN (05.10.2026)
+
+ 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."
+
+ Auf dem Rechner faellt das nie auf: Dort aendert sich die
+ Fenstergroesse nur, wenn jemand sie aendert. Auf einem Handy aendert
+ sie sich beim Bedienen -- Adressleiste, Tastatur -- und `resize`
+ feuerte `schliessen()`.
+
+ Gemessen wird an GENAU IHREM Fall: Kalenderformular, die beiden
+ Auswahlfelder "Art" und "Wiederholung", und eine echte
+ Groessenaenderung dazwischen. Die Liste muss offen bleiben UND
+ danach an ihrem Knopf sitzen -- offen, aber verrutscht waere nur
+ die andere Haelfte.
+ ===================================================================== */
+{
+ const kontext = await browser.newContext({
+ viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true,
+ });
+ const seite = await kontext.newPage();
+ await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
+ await seite.click('.rolle[data-rolle="admin"]');
+ await seite.fill("#code", "CODE-DOGI-0001");
+ await seite.click("button[type=submit]");
+ await seite.waitForURL("**/start.html", { timeout: 15000 });
+ await seite.goto(BASIS + "/workspace/kalender.html", { waitUntil: "networkidle" });
+ await seite.waitForTimeout(1200);
+ await seite.evaluate(() => document.getElementById("neu-oeffnen")?.click());
+ await seite.waitForTimeout(600);
+
+ for (const [feldId, wie] of [["f-art", "Art"], ["f-takt", "Wiederholung"]]) {
+ const auf = await seite.evaluate((id) => {
+ const k = document.getElementById(id)?.closest(".wahl2")?.querySelector(".wahl2__knopf");
+ if (!k) return "kein Knopf";
+ k.click();
+ return !!document.querySelector(".wahl2__liste");
+ }, feldId);
+ ok(auf === true, `${wie}: die Liste geht auf (${auf})`);
+
+ /* Die Groessenaenderung, die auf dem Handy beim Bedienen entsteht:
+ Die Adressleiste faehrt ein, das Fenster wird hoeher. */
+ await seite.setViewportSize({ width: 390, height: 788 });
+ await seite.waitForTimeout(350);
+
+ const danach = await seite.evaluate(() => {
+ const l = document.querySelector(".wahl2__liste");
+ if (!l) return { offen: false };
+ const k = document.querySelector(".wahl2__knopf[aria-expanded='true']")
+ || document.querySelector(".wahl2 .wahl2__knopf");
+ const lr = l.getBoundingClientRect();
+ const kr = k ? k.getBoundingClientRect() : null;
+ return {
+ offen: true,
+ /* Sitzt sie noch an ihrem Knopf? Mehr als 40 px Abstand waere
+ "offen, aber verrutscht" -- und das ist kein Erfolg. */
+ abstand: kr ? Math.round(Math.min(Math.abs(lr.top - kr.bottom),
+ Math.abs(lr.bottom - kr.top))) : null,
+ imBild: lr.top >= 0 && lr.bottom <= innerHeight + 1,
+ };
+ });
+ ok(danach.offen === true,
+ `${wie}: sie bleibt nach der Groessenaenderung offen (${danach.offen})`);
+ ok(danach.offen && danach.abstand !== null && danach.abstand <= 40,
+ `${wie}: und sitzt weiter an ihrem Knopf (${danach.abstand} px Abstand)`);
+
+ await seite.setViewportSize({ width: 390, height: 844 });
+ await seite.keyboard.press("Escape");
+ await seite.waitForTimeout(250);
+ }
+
+ /* GEGENPROBE: Escape schliesst sie weiterhin. Sonst haette ich aus
+ "schliesst zu frueh" ein "schliesst nie" gemacht -- und das waere
+ schlimmer, weil man dann gar nicht mehr herauskommt. */
+ await seite.evaluate(() => document.getElementById("f-art")
+ ?.closest(".wahl2")?.querySelector(".wahl2__knopf")?.click());
+ await seite.waitForTimeout(300);
+ const vorEsc = await seite.evaluate(() => !!document.querySelector(".wahl2__liste"));
+ await seite.keyboard.press("Escape");
+ await seite.waitForTimeout(300);
+ const nachEsc = await seite.evaluate(() => !!document.querySelector(".wahl2__liste"));
+ ok(vorEsc && !nachEsc,
+ `Gegenprobe: Escape schliesst sie weiterhin (offen ${vorEsc} -> ${nachEsc})`);
+
+ await kontext.close();
+}
+
} finally {
await browser.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index be00c96e..4ef33eb5 100644
--- a/workspace/anruf-probe.html
+++ b/workspace/anruf-probe.html
@@ -16,8 +16,8 @@
verdeckt. Gefunden hat es pruef-struktur.
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
-
-
+
+
-
+