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]>
291 lines
13 KiB
JavaScript
291 lines
13 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();
|
|
|
|
/* =====================================================================
|
|
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 */ }
|
|
}
|
|
|
|
console.log("");
|
|
console.log(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
|
console.log(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
|
process.exit(fehler ? 1 : 0);
|