Files
dogfather-universe/server/pruef-suchfeld.mjs
T
DogFatherGitandClaude Opus 5 7acd7a7674 Das Suchfeld nimmt wieder Eingaben, eigene Kanalnamen, Community in Kanälen
screen1, drei Teile.

=== 1. „bei suchen kann man nichts reinschreiben" ===

MEIN FEHLER VOM SELBEN TAG. Beim Popover-Umbau heute frueh hing die
Liste am <body>, damit sie nicht hinter dem modalen Dialog
verschwindet. Gemessen: Das Suchfeld war da, der Fokus landete nicht
darin, ein getipptes Zeichen kam nicht an.

DER GRUND IST DER FOKUS-KAEFIG. Ein Dialog aus showModal() sperrt den
Fokus auf seinen eigenen DOM-Baum ein. Die Liste lag im Top Layer --
sichtbar und anklickbar, aber ausserhalb des Kaefigs. Klicken braucht
keinen Fokus, Tippen schon. Deshalb fiel es niemandem auf: Die Liste
stand da, sie filterte auf Knopfdruck, nur eine Eingabe kam nicht an.

DIE LOESUNG WAR EINE KOMBINATION, die ich vorher fuer unmoeglich
hielt. Im Dialog wurde die Liste vom clip-path der abgeschraegten
Ecke abgeschnitten, draussen war sie nicht bedienbar. Ein POPOVER
wird aber in die Top Layer gehoben und dort gezeichnet -- der
Beschnitt des Elternteils erreicht es nicht mehr, waehrend der
DOM-Baum (und damit der Fokus) der des Dialogs bleibt. Jetzt haengt
sie wieder im Dialog UND ist ein Popover: bedienbar und unbeschnitten.

Gemessen beides gegeneinander: „comm" getippt -> kommt an, filtert auf
1 Treffer; und die LETZTE Zeile der vollen Liste ist wirklich
anklickbar (elementFromPoint trifft sie selbst, nicht den Dialog).
Daraus ist pruef-suchfeld geworden -- der Fehler war von aussen nicht
zu sehen, und gefunden hat ihn Filipe, nicht ich.

=== 2. „einen neuen namen erstellen den es noch nicht gibt" ===

`data-frei="ja"` war in wahl.js seit langem gebaut und wurde NIE
GESETZT -- dieselbe Sorte Lueue wie `grund_min` heute Morgen. Jetzt
gesetzt; der getippte Text erscheint als eigener Eintrag ganz oben.

Der Server nahm bisher nur die neun festen Zustaendigkeiten. Jetzt
auch einen eigenen Namen: Der SCHLUESSEL wird daraus abgeleitet
(„Technik & Ton" -> "eigen-technik-ton"), der ANGEZEIGTE Name bleibt
wie geschrieben. Das Praefix ist kein Schmuck -- ohne es entstuende
aus dem Namen „Community" derselbe Schluessel wie beim festen Thema,
und der eindeutige Index lehnte ihn ab, obwohl der Kanal nicht
existiert. Gemessen: genau dieser Fall antwortet jetzt mit 201.

Der alte Kommentar sprach sich gegen freie Namen aus („der sichere
Weg zu Clipping neben Clipping-Team"). Das Risiko bleibt und wird
begrenzt: Derselbe Name zweimal ergibt denselben Schluessel und damit
409.

=== 3. „kanäle mit den leuten mit der community rolle" ===

Seit dem 19.09. nimmt `ohneAussen()` die Community aus den Listen
aller anderen. Die Begruendung dort ist ausdruecklich: „Ein
Zweier-Gespraech ist kein Kanal. Es hat keine Nachtruhe, kein Modi
liest mit, und niemand koennte moderieren."

Genau diese Begruendung laesst den Kanal offen. Die Sperre bleibt
fuer GESPRAECHE und faellt fuer KANAELE -- `?fuer=kanal` an der
Partnerliste, und nur fuer den, der Kanaele ueberhaupt aufmachen
darf. Sonst waere der Parameter ein Weg, an `ohneAussen` vorbei Namen
zu erfahren.

Gemessen: DogFather sieht im Gespraech VanVan und Miss, im Kanal
zusaetzlich Kessi und Tom (beide Community). Ein Modi bekommt die
erweiterte Liste auch mit dem Parameter nicht.

Gruen: pruef-suchfeld (8, neu), pruef-chat-kanaele (81),
pruef-treffchat, pruef-chat-neu, pruef-freie-namen (32),
pruef-nachfrage (53), pruef-css-klassen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-24 11:37:03 +02:00

201 lines
9.0 KiB
JavaScript

/* KANN MAN IM SUCHFELD DER AUSWAHLLISTE TIPPEN?
Filipe: „bei suchen kann man nichts reinschreiben."
VERDACHT: Die Liste liegt seit dem Popover-Umbau von heute frueh im
Top-Layer -- und der Dialog darunter ist MODAL. Ein modaler Dialog
sperrt den Fokus auf sich selbst ein; ein Element ausserhalb ist
dann zwar sichtbar, aber nicht bedienbar.
BESTAETIGT, UND ES WAR MEIN FEHLER VOM SELBEN TAG: Die Liste hing
nach dem Popover-Umbau am <body>. Sichtbar (der Popover hebt sie in
die Top Layer), aber ausserhalb des Fokus-Kaefigs -- klicken ging,
tippen nicht.
WARUM DAS EINE DAUERPRUEFUNG IST: Der Fehler war von aussen nicht
zu sehen. Die Liste stand da, sie liess sich anklicken, sie
filterte auf Knopfdruck. Nur eine Eingabe kam nicht an, und das
faellt erst auf, wenn jemand wirklich tippt. Genau so hat Filipe
ihn gefunden -- nicht ich.
GEPRUEFT WIRD BEIDES GEGENEINANDER: Der Fokus muss ankommen UND
die Liste darf nicht abgeschnitten sein. Die beiden Anforderungen
haben sich an dieser Stelle schon zweimal gegenseitig kaputt
gemacht (draussen: nicht bedienbar, drinnen: beschnitten). */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const ordner = mkdtempSync(join(tmpdir(), "ws-sf-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
const { eigenerPort } = await import("./helfer-port.mjs");
const PORT = await eigenerPort(import.meta, "die Suchfeld-Pruefung");
process.env.PORT = `${PORT}`;
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
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";
await import("./index.js");
notbremse(150_000, "pruef-suchfeld");
await new Promise((r) => setTimeout(r, 900));
const BASIS = `http://127.0.0.1:${PORT}`;
let fehler = 0, geprueft = 0;
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 ks = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", ks, jetzt);
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,"
+ " code_kennung, aktiv, erstellt) VALUES (?,?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768,
createHmac("sha256", ks).update(code).digest("hex"), jetzt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
anlegen("Filipe", "admin", "CODE-DOGI-0001");
anlegen("VanVan", "hand", "CODE-HAND-0001");
d.close();
const { chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await chromium.launch();
try {
const kontext = await browser.newContext({ viewport: { width: 1100, height: 850 } });
const seite = await kontext.newPage();
const konsole = [];
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message));
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/chat.html", { waitUntil: "networkidle" });
await seite.waitForTimeout(1800);
await seite.evaluate(() => document.getElementById("neu-oeffnen")?.click());
await seite.waitForTimeout(700);
await seite.evaluate(() => {
[...document.querySelectorAll("#neu-art [data-art]")]
.find((x) => /kanal/i.test(x.dataset?.art || x.textContent))?.click();
});
await seite.waitForTimeout(500);
await seite.evaluate(() => document.querySelector("#kanal-zeile .wahl2__knopf")?.click());
await seite.waitForTimeout(800);
const lage = await seite.evaluate(() => {
const feld = document.querySelector(".wahl2__suche");
const liste = document.querySelector(".wahl2__liste");
const dlg = document.querySelector("dialog[open]");
return {
feldDa: !!feld,
listeDa: !!liste,
popover: liste ? liste.matches(":popover-open") : null,
/* DIE ENTSCHEIDENDE FRAGE: Liegt die Liste INNERHALB des
modalen Dialogs? Ein modaler Dialog sperrt den Fokus auf
seinen eigenen Baum ein. */
imDialog: dlg && liste ? dlg.contains(liste) : null,
dialogModal: dlg ? dlg.matches(":modal") : null,
frei: document.getElementById("kanal-kategorie")?.dataset.frei || "(nicht gesetzt)",
};
});
console.log(" " + JSON.stringify(lage));
ok(lage.feldDa, "das Suchfeld ist ueberhaupt da");
/* 1. LAESST SICH DER FOKUS SETZEN? */
const fokus = await seite.evaluate(() => {
const feld = document.querySelector(".wahl2__suche");
if (!feld) return null;
feld.focus();
return document.activeElement === feld
|| document.activeElement?.className === feld.className;
});
ok(fokus === true, `der Fokus landet im Suchfeld (${fokus})`);
/* 2. KOMMT EIN GETIPPTES ZEICHEN AN? Ueber die echte Tastatur --
`fill()` setzt den Wert direkt und wuerde auch bei gesperrtem
Fokus gelingen. */
await seite.keyboard.type("comm");
await seite.waitForTimeout(400);
const getippt = await seite.evaluate(() =>
document.querySelector(".wahl2__suche")?.value || "");
console.log(` nach dem Tippen steht im Feld: "${getippt}"`);
ok(getippt.length > 0, `ein getipptes Zeichen kommt an ("${getippt}")`);
/* 3. UND FILTERT ES WIRKLICH? */
const gefiltert = await seite.evaluate(() =>
document.querySelectorAll(".wahl2__zeilen .wahl2__eintrag").length);
console.log(` sichtbare Eintraege nach dem Tippen: ${gefiltert}`);
ok(getippt.length === 0 || gefiltert > 0, "und die Liste filtert danach");
/* 4. GIBT ES DEN WEG FUER EINEN EIGENEN NAMEN? */
ok(lage.frei === "ja",
`das Feld erlaubt freie Namen (data-frei="${lage.frei}")`);
/* 5. UND DAS ALTE PROBLEM DARF NICHT ZURUECK SEIN.
Die Liste hing frueher am <body>, WEIL sie als Kind des Dialogs
vom clip-path der abgeschraegten Ecke abgeschnitten wurde ("Die
Liste endete mitten im Wort Events"). Jetzt haengt sie wieder im
Dialog -- die Annahme ist, dass der Popover sie in die Top Layer
hebt und der Beschnitt sie dort nicht erreicht.
DAS IST EINE ANNAHME, ALSO WIRD SIE GEMESSEN: Ist der letzte
Eintrag der Liste wirklich sichtbar, oder liegt er unter dem
Beschnitt? Gefragt wird nicht das Rechteck (das stimmt auch bei
abgeschnittenem Inhalt), sondern was an seiner Stelle wirklich
zu sehen ist. */
await seite.evaluate(() => {
const f = document.querySelector(".wahl2__suche");
if (f) { f.value = ""; f.dispatchEvent(new Event("input", { bubbles: true })); }
});
await seite.waitForTimeout(400);
const beschnitt = await seite.evaluate(() => {
const zeilen = [...document.querySelectorAll(".wahl2__zeilen .wahl2__eintrag")];
if (!zeilen.length) return { keine: true };
const letzter = zeilen[zeilen.length - 1];
letzter.scrollIntoView({ block: "nearest" });
const r = letzter.getBoundingClientRect();
const mitte = document.elementFromPoint(
Math.round(r.left + r.width / 2), Math.round(r.top + r.height / 2));
return {
anzahl: zeilen.length,
hoehe: Math.round(r.height),
/* Trifft ein Klick auf die letzte Zeile wirklich sie selbst? */
trifft: !!(mitte && (mitte === letzter || letzter.contains(mitte))),
was: mitte ? (mitte.className || mitte.tagName) : "(nichts)",
/* Und liegt sie ueberhaupt im Bild? */
imBild: r.top >= 0 && r.bottom <= window.innerHeight + 1,
};
});
console.log(" letzte Zeile: " + JSON.stringify(beschnitt));
ok(!beschnitt.keine && beschnitt.hoehe > 0,
`die Liste hat sichtbare Zeilen (${beschnitt.anzahl})`);
ok(beschnitt.trifft === true,
`die LETZTE Zeile ist wirklich anklickbar -- kein Beschnitt (${beschnitt.was})`);
ok(konsole.length === 0, konsole.length ? konsole[0] : "keine Abstuerze");
await seite.screenshot({ path: "pruef-suchfeld.png", fullPage: false });
await kontext.close();
} finally {
await browser.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
}
console.log("");
console.log(`${geprueft} Pruefungen, ${fehler} Fehler`);
console.log(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
process.exit(fehler ? 1 : 0);