Filipe, 22.09.2026, zum Bildschirmfoto: „wie scheisse sieht das aus,
verbesser das bitte." Zu sehen: „Anlegen" und „Abbrechen" lagen mitten
in der Personenliste.
GEMESSEN statt geraten, bei 1280 px mit aufgeklappter Liste:
Zelle Zeilen Hoehe Inhalt
feld-verant 24px 44px 68 107
feld-mehrere 43px 44px 87 261 <-- 174 zu viel
Jede Zelle des Formularrasters bekommt zwei Zeilen: Beschriftung oben,
Eingabe unten mit festen 44 px. Das ist richtig und der Grund, warum
alle Eingaben auf einer Linie sitzen (17.09.).
`feld-mehrere` ist aber keine Beschriftung mit Eingabe, sondern ein
Knopf mit einer Liste, die aufklappt. Ihr zweites Kind landet in der
44-Pixel-Zeile und laeuft heraus, sobald jemand sie oeffnet. Was
herauslaeuft, belegt keinen Platz -- also legt es sich ueber das
Naechste, und das Naechste sind die Knoepfe.
DIE MESSUNG HAT MICH VOR DEM NAHELIEGENDEN FEHLER BEWAHRT.
Erster Gedanke: „Zellen ohne eigene Eingabe brauchen die zwei Zeilen
nicht" -- `:has(> input, > select, > textarea)`. Die Messung sagt etwas
anderes: Von sieben Zellen haben SECHS ihre Eingabe nicht als direktes
Kind, weil der Auswahl-Baustein sie in ein <div> wickelt. Die Regel
haette fast das ganze Formular getroffen und genau die Ausrichtung
zerstoert, die sie schuetzen soll.
`aria-expanded` ist gemessen das einzige Merkmal, das nur bei dieser
Zelle steht -- und es ist das inhaltlich richtige: Es sagt „dieser
Bereich kann groesser werden". Etwas, das groesser werden kann, darf
keine feste Hoehe haben.
UND DIE PRUEFUNG, DIE ES HAETTE FINDEN MUESSEN
pruef-formulare war gruen -- sie klappt das Feld nie auf. Ein Zustand,
der nie hergestellt wird, kann nicht gemessen werden.
Neuer Abschnitt: Jedes Element mit `aria-expanded` im Formular wird
geoeffnet, danach darf keine Rasterzelle mehr Inhalt haben, als sie
hoch ist. Nicht diese eine Stelle, sondern die Eigenschaft -- damit
faellt auch die naechste auf, die es noch gar nicht gibt.
ZWEI ANLAEUFE DABEI WAREN FALSCH, und der zweite war der gefaehrliche:
1. `scrollHeight` der Zelle meldete elf Zellen als kaputt, die alle
in Ordnung sind: Der Hinweis unter einem Feld haengt seit dem
17.09. absichtlich absolut darunter. Eine Warnung, die bei
richtigem Verhalten anschlaegt, wird abgeschaltet.
2. Nur die direkten Kinder im Fluss -- das war gruen, AUCH MIT DEM
ECHTEN FEHLER. Nachgemessen mit zurueckgenommener Behebung:
immer noch gruen. Das Raster staucht das Kind auf die feste
Zeilenhoehe, das Kind bleibt also brav in der Zelle; was
herauslaeuft, ist der Inhalt darin.
Jetzt misst sie in die Tiefe (ohne Teilbaeume unter absolut gesetzten
Elementen und ohne eigene Rollbereiche) und ist beidseitig belegt:
mit Behebung -> gruen
ohne Behebung -> FEHL „feld-mehrere: Inhalt reicht bis 169 px,
Zelle ist 87 px hoch"
Dazu eine Gegenprobe, die eine Zelle kuenstlich einklemmt.
Ueberlappungen im Formular: von 12 auf 4 -- und die vier sind Absicht
(der echte <select> liegt unsichtbar ueber seinem Knopf).
pruef-formulare, pruef-aufgabenbrett, pruef-css-klassen gruen.
Co-Authored-By: Claude Opus 5 <[email protected]>
492 lines
24 KiB
JavaScript
492 lines
24 KiB
JavaScript
/* Prueft die Formularzeilen auf allen Seiten.
|
||
|
||
Beanstandet am 01.09.2026: "das sieht scheisse aus, so verzogen".
|
||
Gemeint war die Zeile im Kalender -- "Art" schmal, "Beginn" breit,
|
||
"Dauer (Minuten)" zweizeilig umgebrochen, alle Felder auf
|
||
verschiedenen Hoehen.
|
||
|
||
Ursache war eine Regel, die nur fuer Kinder mit der Klasse .feld galt.
|
||
Der Kalender benutzt schlichte <div> ohne Klasse; die fielen hindurch,
|
||
bekamen von zwoelf Rasterspalten je EINE und wurden nur so breit, wie
|
||
ihr Inhalt sie zwang.
|
||
|
||
Geprueft wird deshalb nicht "sieht gut aus", sondern messbar:
|
||
* alle Eingabefelder einer Zeile gleich hoch
|
||
* ihre Unterkanten auf einer Linie (sonst haengt eines tiefer)
|
||
* kein Feld absurd schmal
|
||
* keine Beschriftung mehrzeilig */
|
||
import { mkdtempSync, rmSync } from "node:fs";
|
||
import { tmpdir } from "node:os";
|
||
import { join } from "node:path";
|
||
|
||
const ordner = mkdtempSync(join(tmpdir(), "ws-form-"));
|
||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||
/* DER PORT MUSS MEINER SEIN (nachgeruestet 19.09.2026).
|
||
Ohne diese Zeile startet der eigene Server bei belegtem Port
|
||
STILL nicht -- und alles Folgende misst gegen einen fremden
|
||
Stand. Das ist der dritte Ausgang: belegt ist etwas anderes
|
||
als kaputt. */
|
||
const PORT = await eigenerPort(import.meta, "pruef-formulare");
|
||
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";
|
||
import { eigenerPort } from "./helfer-port.mjs";
|
||
await import("./index.js");
|
||
/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort
|
||
zwei Auffangnetze ein, die einen Absturz nur protokollieren --
|
||
richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese
|
||
Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der
|
||
Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */
|
||
notbremse(480_000, "pruef-formulare");
|
||
await new Promise((r) => setTimeout(r, 700));
|
||
const BASIS = `http://127.0.0.1:${PORT}`;
|
||
setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 250_000).unref?.();
|
||
|
||
let fehler = 0;
|
||
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||
|
||
await fetch(BASIS + "/workspace/api/anmelden", {
|
||
method: "POST", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
|
||
|
||
const { DatabaseSync } = await import("node:sqlite");
|
||
const { scryptSync, randomBytes } = await import("node:crypto");
|
||
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 salt = randomBytes(16).toString("hex");
|
||
const hash = scryptSync("CODE-DOGI-0001", 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("Filipe", "admin", hash, salt, 32768, new Date().toISOString());
|
||
/* EIN CREATOR MUSS DA SEIN. Ohne ihn bauen profil.html und
|
||
startcheck.html ihr Formular gar nicht erst auf -- die Pruefung findet
|
||
dort null Felder und meldet "in Ordnung", ohne ein einziges angesehen
|
||
zu haben. Genau deshalb steht die Anzahl in der Bedingung. */
|
||
const salt2 = randomBytes(16).toString("hex");
|
||
const hash2 = scryptSync("CODE-ANNA-0001", salt2, 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("Tili", "creator", hash2, salt2, 32768, new Date().toISOString());
|
||
d.close();
|
||
|
||
const { chromium } = await import(
|
||
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||
const browser = await chromium.launch();
|
||
|
||
/* Seite, Knopf zum Aufklappen des Formulars. */
|
||
const FORMULARE = [
|
||
["kalender.html", "#neu-oeffnen"],
|
||
["aufgaben.html", "#neu-oeffnen"],
|
||
["personen.html", "#neu-oeffnen"],
|
||
["bereich.html?b=live", "#neu-oeffnen"],
|
||
["scouting.html", "#neu-oeffnen"],
|
||
];
|
||
|
||
for (const [breite, wie] of [[1280, "Computer"], [390, "Handy"]]) {
|
||
console.log(`\n=== ${wie} (${breite}px)`);
|
||
const kontext = await browser.newContext({ viewport: { width: breite, height: 1000 } });
|
||
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 });
|
||
|
||
for (const [s, knopf] of FORMULARE) {
|
||
await seite.goto(BASIS + "/workspace/" + s, { waitUntil: "networkidle" });
|
||
await seite.waitForTimeout(700);
|
||
const da = await seite.locator(knopf).count();
|
||
if (!da) { console.log(` -- ${s}: kein Formularknopf`); continue; }
|
||
await seite.click(knopf).catch(() => {});
|
||
await seite.waitForTimeout(450);
|
||
|
||
const r = await seite.evaluate(() => {
|
||
const raster = document.querySelector(".neu__raster");
|
||
if (!raster) return null;
|
||
const felder = [...raster.children].filter((k) => k.offsetParent !== null);
|
||
const eingaben = felder
|
||
.map((f) => f.querySelector("input, select, textarea"))
|
||
.filter(Boolean)
|
||
.map((e) => e.getBoundingClientRect())
|
||
/* NUR WAS MAN WIRKLICH SIEHT (verschaerft 22.09.2026).
|
||
----------------------------------------------------------
|
||
Seit dem 21.09. steht im Aufgabenformular ein zugeklappter
|
||
Block ("Noch jemanden dazunehmen"). Sein erstes Eingabefeld
|
||
liegt in einem `hidden` fieldset und hat Hoehe und Breite 0 --
|
||
die Pruefung meldete es als "nicht buendig" und "zu schmal".
|
||
|
||
Ein Feld, das man nicht sieht, kann nicht schief stehen.
|
||
Diese Zeile misst die Buendigkeit dessen, was ein Mensch vor
|
||
sich hat -- und genau darum geht es. */
|
||
.filter((r2) => r2.width > 0 && r2.height > 0);
|
||
const schilder = felder
|
||
.map((f) => f.querySelector(".feld-schild"))
|
||
.filter(Boolean);
|
||
/* Mehrzeilige Beschriftung: tatsaechliche Hoehe gegen eine Zeile. */
|
||
const mehrzeilig = schilder.filter((s2) => {
|
||
const zeile = parseFloat(getComputedStyle(s2).lineHeight) || 16;
|
||
return s2.getBoundingClientRect().height > zeile * 1.6;
|
||
}).map((s2) => s2.textContent.trim());
|
||
return {
|
||
anzahl: eingaben.length,
|
||
hoehen: eingaben.map((e) => Math.round(e.height)),
|
||
/* Auf 3 px genau zusammengefasst: Unterkanten liegen durch
|
||
Teilpixel-Rundung selten exakt gleich, obwohl sie auf einer
|
||
Linie sitzen. Ein erster Anlauf meldete deshalb "3 Unterkanten"
|
||
bei drei Feldern derselben Reihe -- ein Messfehler, kein
|
||
Gestaltungsfehler. */
|
||
unterkanten: eingaben.map((e) => Math.round(e.bottom / 3) * 3),
|
||
breiten: eingaben.map((e) => Math.round(e.width)),
|
||
einzeln: felder.map((f) => { const e=f.querySelector("input, select, textarea"); if(!e) return null; const r2=e.getBoundingClientRect(); const fr=f.getBoundingClientRect(); return { id:e.id, tag:e.tagName, top:Math.round(r2.top), bottom:Math.round(r2.bottom), h:Math.round(r2.height), feldTop:Math.round(fr.top), feldH:Math.round(fr.height) }; }).filter(Boolean),
|
||
mehrzeilig,
|
||
};
|
||
});
|
||
|
||
if (!r || r.anzahl < 2) { console.log(` -- ${s}: kein Raster mit mehreren Feldern`); continue; }
|
||
|
||
const hoehenGleich = new Set(r.hoehen).size === 1;
|
||
/* Unterkanten auf einer Linie -- auf dem Handy stehen die Felder
|
||
untereinander, dort gilt das natuerlich nicht. */
|
||
const reihen = new Set(r.unterkanten).size;
|
||
const buendig = breite < 700 ? true : reihen <= Math.ceil(r.anzahl / 3);
|
||
const nichtWinzig = Math.min(...r.breiten) >= 90;
|
||
|
||
const gut = hoehenGleich && buendig && nichtWinzig && !r.mehrzeilig.length;
|
||
if (!gut) fehler++;
|
||
console.log(` ${gut ? "ok " : "FEHL"} ${String(s).padEnd(20)}`
|
||
+ `${r.anzahl} Felder Höhen=${[...new Set(r.hoehen)].join("/")} `
|
||
+ `Breiten=${Math.min(...r.breiten)}–${Math.max(...r.breiten)}`
|
||
+ (hoehenGleich ? "" : " UNGLEICH HOCH")
|
||
+ (buendig ? "" : ` NICHT BÜNDIG (Unterkanten ${r.unterkanten.join(",")})`)
|
||
+ (nichtWinzig ? "" : " ZU SCHMAL")
|
||
+ (r.mehrzeilig.length ? ` MEHRZEILIG: ${r.mehrzeilig.join(", ")}` : "")
|
||
+ (gut ? "" : " | " + r.einzeln.map((e) => `${e.id}(${e.tag}) Feld:${e.feldTop}+${e.feldH} Eingabe:${e.top}..${e.bottom}`).join(" ")));
|
||
}
|
||
await kontext.close();
|
||
}
|
||
|
||
/* =====================================================================
|
||
KEIN FELD OHNE GESTALTUNG (01.09.2026)
|
||
|
||
Der gemeldete Fehler: Das Feld "Ein Satz ueber dich" stand als grauer
|
||
Kasten in Monospace da, direkt neben gestalteten Feldern. Die Ursache
|
||
war ein Loch im Aufbau -- jede Seite gestaltete ihre Felder mit einem
|
||
EIGENEN Selektor (.gruppe textarea, .neu input, ...), und wer ein Feld
|
||
irgendwo anders hinsetzt, faellt durch alle Netze.
|
||
|
||
Ein Loch im Aufbau schliesst man nicht mit Hinsehen: Das naechste neue
|
||
Feld faellt an einer anderen Stelle durch. Diese Pruefung geht deshalb
|
||
ueber JEDES sichtbare Feld auf JEDER Seite und misst, was der Browser
|
||
tatsaechlich zeichnet.
|
||
|
||
Die Merkmale sind genau die, die im gemeldeten Fall fehlten:
|
||
RAHMEN ohne Rand ist ein Feld im dunklen Entwurf nicht zu sehen
|
||
ECKEN eckig heisst: der Browser hat gezeichnet, nicht wir
|
||
SCHRIFT <textarea> faellt ohne `font: inherit` auf Monospace
|
||
===================================================================== */
|
||
console.log("\n=== Kein Feld ohne Gestaltung");
|
||
{
|
||
const kontext = await browser.newContext({ viewport: { width: 1280, height: 1100 } });
|
||
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 });
|
||
|
||
let gesamt = 0;
|
||
for (const pfad of ["steckbrief.html", "profil.html", "aufgaben.html", "kalender.html",
|
||
"dateien.html", "bereich.html?b=live", "content.html",
|
||
"personen.html", "startcheck.html", "report.html"]) {
|
||
await seite.goto(BASIS + "/workspace/" + pfad, { waitUntil: "networkidle" });
|
||
await seite.waitForTimeout(900);
|
||
/* Zugeklappte Formulare aufmachen -- sonst pruefen wir genau die
|
||
Felder nicht, die selten jemand sieht und die deshalb ungestaltet
|
||
bleiben. */
|
||
for (const w of ["#neu-oeffnen", "#neu-auf", "#anlegen-auf"]) {
|
||
try { await seite.click(w, { timeout: 500 }); } catch { /* gibt es hier nicht */ }
|
||
}
|
||
await seite.waitForTimeout(400);
|
||
|
||
const r = await seite.evaluate(() => {
|
||
const felder = [...document.querySelectorAll(
|
||
'.inhalt input:not([type=file]):not([type=checkbox]):not([type=radio]), '
|
||
+ '.inhalt textarea, .inhalt select')]
|
||
/* NUR WAS MAN WIRKLICH SIEHT (verschaerft 22.09.2026).
|
||
----------------------------------------------------------
|
||
Hier stand nur `width > 0`. Seit dem 21.09. steht im
|
||
Aufgabenformular ein zugeklappter Block ("Noch jemanden
|
||
dazunehmen"); ein Feld darin hat Breite und Hoehe 0 und
|
||
keinen sichtbaren Platz -- die Pruefung meldete es trotzdem
|
||
als "nicht buendig" und "zu schmal".
|
||
|
||
Ein unsichtbares Feld kann nicht schief stehen. Gefragt wird
|
||
deshalb nach BEIDEN Massen und zusaetzlich danach, ob das
|
||
Element ueberhaupt im Bild haengt (`offsetParent`). */
|
||
.filter((e) => {
|
||
const r = e.getBoundingClientRect();
|
||
return r.width > 0 && r.height > 0 && e.offsetParent !== null;
|
||
});
|
||
const schlecht = [];
|
||
for (const e of felder) {
|
||
const st = getComputedStyle(e);
|
||
/* MANCHE FELDER HOLEN IHREN RAHMEN VOM UMSCHLAG, und das ist
|
||
richtig so: Bei den Kanaelen steht ein festes "@" vor dem Feld,
|
||
beide zusammen liegen in einem gemeinsamen Rahmen. Das Feld
|
||
selbst hat dort absichtlich keinen -- zwei Rahmen ineinander
|
||
saehen aus wie ein Fehler.
|
||
|
||
Der erste Anlauf dieser Pruefung meldete genau diese vier als
|
||
kaputt. Sie war damit eine Pruefung, die Richtiges anmahnt --
|
||
und die gewoehnt man sich ab zu lesen. Gemessen wird deshalb
|
||
der RAHMEN, der um das Feld herum tatsaechlich sichtbar ist,
|
||
egal ob er am Feld selbst oder am Umschlag haengt. */
|
||
const umschlag = e.parentElement;
|
||
const us = umschlag ? getComputedStyle(umschlag) : null;
|
||
const rahmenAmUmschlag = !!us && parseFloat(us.borderTopWidth) >= 1
|
||
&& parseFloat(us.borderTopLeftRadius) >= 4;
|
||
const grund = [];
|
||
if (!rahmenAmUmschlag) {
|
||
if (parseFloat(st.borderTopWidth) < 1) grund.push("kein Rand");
|
||
if (parseFloat(st.borderTopLeftRadius) < 4) grund.push("eckig");
|
||
if (parseFloat(st.paddingLeft) < 6) grund.push("keine Polsterung");
|
||
}
|
||
/* Die Schrift gilt IMMER -- ein Umschlag kann sie nicht liefern. */
|
||
if (/mono|courier/i.test(st.fontFamily)) grund.push("Monospace");
|
||
if (parseFloat(st.fontSize) < 12) grund.push("Schrift " + st.fontSize);
|
||
if (grund.length) schlecht.push((e.id || e.tagName) + " – " + grund.join(", "));
|
||
}
|
||
return { anzahl: felder.length, schlecht };
|
||
});
|
||
gesamt += r.anzahl;
|
||
/* DIE ANZAHL GEHOERT IN DIE BEDINGUNG, nicht nur in den Meldetext:
|
||
Eine Seite ohne gefundene Felder waere sonst "in Ordnung". */
|
||
const gut = r.anzahl > 0 && r.schlecht.length === 0;
|
||
if (!gut) fehler++;
|
||
console.log(` ${gut ? "ok " : "FEHL"} ${pfad.padEnd(22)}${r.anzahl} Felder`
|
||
+ (r.schlecht.length ? " -> " + r.schlecht.join(" | ")
|
||
: (r.anzahl ? "" : " KEIN FELD GEFUNDEN – die Pruefung hat nichts geprueft")));
|
||
}
|
||
ok(gesamt >= 40, `insgesamt ${gesamt} Felder gemessen`);
|
||
|
||
/* GEGENPROBE: Kann diese Pruefung ueberhaupt anschlagen? Ein Feld wird
|
||
absichtlich auf den Rohzustand des Browsers zurueckgesetzt -- sie MUSS
|
||
es melden. Ohne diesen Nachweis waere sie eine Pruefung, die immer
|
||
bestaetigt, und die bestaetigt nichts. */
|
||
await seite.goto(BASIS + "/workspace/steckbrief.html", { waitUntil: "networkidle" });
|
||
await seite.waitForTimeout(900);
|
||
const gegen = await seite.evaluate(() => {
|
||
const e = document.getElementById("sb-ueber");
|
||
const vorher = getComputedStyle(e);
|
||
const gut = parseFloat(vorher.borderTopLeftRadius) >= 4
|
||
&& !/mono|courier/i.test(vorher.fontFamily);
|
||
e.style.all = "revert";
|
||
const st = getComputedStyle(e);
|
||
return {
|
||
vorherGut: gut,
|
||
ecken: parseFloat(st.borderTopLeftRadius),
|
||
schrift: st.fontFamily,
|
||
erkannt: parseFloat(st.borderTopLeftRadius) < 4 || /mono|courier/i.test(st.fontFamily),
|
||
};
|
||
});
|
||
ok(gegen.vorherGut === true,
|
||
"das gemeldete Feld 'Ein Satz ueber dich' ist gestaltet (runde Ecken, keine Monospace)");
|
||
ok(gegen.erkannt === true,
|
||
`Gegenprobe: ein zurueckgesetztes Feld wird erkannt `
|
||
+ `(Ecken ${gegen.ecken}px, Schrift ${gegen.schrift})`);
|
||
|
||
await seite.goto(BASIS + "/workspace/steckbrief.html", { waitUntil: "networkidle" });
|
||
await seite.waitForTimeout(900);
|
||
await seite.screenshot({ path: "pruef-felder.png", fullPage: false });
|
||
await kontext.close();
|
||
}
|
||
|
||
/* =====================================================================
|
||
WAS AUFKLAPPT, MUSS AUCH PLATZ BEKOMMEN (22.09.2026)
|
||
=====================================================================
|
||
Filipe zum Bildschirmfoto: „wie scheisse sieht das aus, verbesser das
|
||
bitte." Zu sehen war das Aufgaben-Formular: „Anlegen" und
|
||
„Abbrechen" lagen mitten in der Personenliste.
|
||
|
||
WARUM DIE PRUEFUNG DARUEBER DAS NICHT FINDEN KONNTE: Sie oeffnet das
|
||
Formular und misst die Felder -- im zugeklappten Zustand. Der Fehler
|
||
entsteht erst beim AUFKLAPPEN. Ein Zustand, der nie hergestellt
|
||
wird, kann nicht gemessen werden; die Pruefung war gruen und hat
|
||
dabei nie hingesehen.
|
||
|
||
Gemessen war es dann eindeutig, bei 1280 px:
|
||
|
||
Zelle Zeilen Hoehe Inhalt
|
||
feld-verant 24px 44px 68 107
|
||
feld-mehrere 43px 44px 87 261 <-- 174 zu viel
|
||
|
||
Die Zelle steckte in einer festen 44-Pixel-Zeile. Was herauslaeuft,
|
||
belegt keinen Platz -- also legt es sich ueber das Naechste.
|
||
|
||
HIER WIRD NICHT DIESE EINE STELLE GEPRUEFT, sondern die Eigenschaft:
|
||
Jedes Element mit `aria-expanded` im Formular wird aufgeklappt, und
|
||
danach darf keine Rasterzelle mehr Inhalt haben, als sie hoch ist.
|
||
Damit faellt auch die naechste auf, die es noch gar nicht gibt.
|
||
===================================================================== */
|
||
console.log("\n=== Was aufklappt, bekommt auch Platz");
|
||
{
|
||
const kontext = await browser.newContext({ viewport: { width: 1280, height: 1000 } });
|
||
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.waitForTimeout(900);
|
||
|
||
let zellenGesamt = 0, aufgeklapptGesamt = 0;
|
||
const schlimm = [];
|
||
|
||
for (const [wohin, knopf] of FORMULARE) {
|
||
await seite.goto(`${BASIS}/workspace/${wohin}`, { waitUntil: "networkidle" });
|
||
await seite.waitForTimeout(900);
|
||
const auf = await seite.$(knopf);
|
||
if (!auf) continue;
|
||
await auf.click().catch(() => {});
|
||
await seite.waitForTimeout(700);
|
||
|
||
/* ALLES AUFKLAPPEN, was sich aufklappen laesst. */
|
||
const wieViele = await seite.evaluate(() => {
|
||
const f = document.querySelector(".neu__raster");
|
||
if (!f) return 0;
|
||
const knoepfe = [...f.querySelectorAll("[aria-expanded]")]
|
||
.filter((b) => b.offsetParent !== null);
|
||
for (const b of knoepfe) b.click();
|
||
return knoepfe.length;
|
||
});
|
||
await seite.waitForTimeout(600);
|
||
aufgeklapptGesamt += wieViele;
|
||
|
||
const befund = await seite.evaluate(() => {
|
||
const r = document.querySelector(".neu__raster");
|
||
if (!r) return { zellen: 0, zuEng: [] };
|
||
const zellen = [...r.children].filter((c) => !c.hidden && c.offsetParent !== null);
|
||
|
||
/* WIE TIEF REICHT DER INHALT WIRKLICH -- zwei Anlaeufe davor
|
||
waren falsch, und der zweite war der gefaehrlichere:
|
||
|
||
(1) `scrollHeight` der Zelle. Meldete elf Zellen als kaputt,
|
||
die alle in Ordnung sind: Der Hinweis unter einem Feld
|
||
haengt seit dem 17.09. ABSICHTLICH absolut unter der Zelle
|
||
(`top: 100%`). Er ragt hinaus, das ist seine Aufgabe. Eine
|
||
Warnung, die bei richtigem Verhalten anschlaegt, wird
|
||
abgeschaltet.
|
||
|
||
(2) Nur die DIREKTEN Kinder im Fluss. Das war gruen -- auch
|
||
mit dem echten Fehler. Nachgemessen mit
|
||
zurueckgenommener Behebung: immer noch gruen. Der Grund
|
||
ist das Raster selbst: Es staucht das Kind auf die feste
|
||
Zeilenhoehe, das Kind bleibt also brav in der Zelle. Was
|
||
herauslaeuft, ist der Inhalt DARIN. Eine Pruefung, die
|
||
nur eine Ebene tief sieht, sieht davon nichts.
|
||
|
||
Also in die Tiefe -- aber nicht blind: Teilbaeume unter
|
||
absolut gesetzten Elementen bleiben aussen vor (siehe 1), und
|
||
wer selbst scrollt (`overflow` nicht `visible`), darf Inhalt
|
||
unterhalb haben; das ist der Sinn eines Rollbereichs. */
|
||
const unten = (c) => {
|
||
const oben = c.getBoundingClientRect().top;
|
||
let tiefste = 0;
|
||
const geh = (el) => {
|
||
for (const k of el.children) {
|
||
const st = getComputedStyle(k);
|
||
if (st.position === "absolute" || st.position === "fixed") continue;
|
||
if (st.display === "none" || k.hidden) continue;
|
||
tiefste = Math.max(tiefste, k.getBoundingClientRect().bottom - oben);
|
||
if (st.overflow === "visible" && st.overflowY === "visible") geh(k);
|
||
}
|
||
};
|
||
geh(c);
|
||
return Math.round(tiefste);
|
||
};
|
||
const zuEng = zellen
|
||
/* 4 px Luft fuer Abrundungen im Rastermass. 174 px faellt
|
||
hier nicht durch. */
|
||
.filter((c) => unten(c) > Math.round(c.getBoundingClientRect().height) + 4)
|
||
.map((c) => `${c.id || "(ohne Kennung)"}: Inhalt reicht bis ${unten(c)} px, `
|
||
+ `Zelle ist ${Math.round(c.getBoundingClientRect().height)} px hoch`);
|
||
return { zellen: zellen.length, zuEng };
|
||
});
|
||
zellenGesamt += befund.zellen;
|
||
for (const z of befund.zuEng) schlimm.push(`${wohin} — ${z}`);
|
||
}
|
||
|
||
/* DIE ANZAHL GEHOERT IN DIE BEDINGUNG. Ohne sie waere „nichts laeuft
|
||
heraus" auch dann gruen, wenn gar nichts aufgeklappt wurde -- und
|
||
genau das war der Fehler der Pruefung darueber. */
|
||
ok(zellenGesamt > 10, `${zellenGesamt} Rasterzellen angesehen`);
|
||
ok(aufgeklapptGesamt > 0,
|
||
`${aufgeklapptGesamt} aufklappbare Bereiche wirklich geoeffnet`);
|
||
ok(schlimm.length === 0 && aufgeklapptGesamt > 0,
|
||
schlimm.length
|
||
? `${schlimm.length} Zelle(n) laufen aus ihrer Zeile heraus:\n `
|
||
+ schlimm.join("\n ")
|
||
: "keine Zelle laeuft aus ihrer Zeile heraus");
|
||
|
||
/* GEGENPROBE: Die Messung muss ein Herauslaufen auch SEHEN. Dafuer
|
||
wird eine Zelle kuenstlich eingeklemmt -- und danach wieder
|
||
freigegeben. Ohne diese Zeile waere „nichts laeuft heraus" die
|
||
Aussage einer Messung, von der niemand weiss, ob sie etwas
|
||
bemerken wuerde. */
|
||
/* Fuer die Gegenprobe zurueck auf ein Formular, das offen ist -- der
|
||
letzte Durchlauf oben kann auf einer Seite geendet haben, die gar
|
||
keines hat. Eine Gegenprobe, die mangels Gelegenheit ausfaellt,
|
||
ist keine. */
|
||
await seite.goto(`${BASIS}/workspace/aufgaben.html`, { waitUntil: "networkidle" });
|
||
await seite.waitForTimeout(900);
|
||
await seite.click("#neu-oeffnen").catch(() => {});
|
||
await seite.waitForTimeout(700);
|
||
|
||
const probe = await seite.evaluate(() => {
|
||
/* Dieselbe Rechnung wie oben -- sonst prueft die Gegenprobe eine
|
||
andere Messung als die, um die es geht. */
|
||
const unten = (c) => {
|
||
const oben = c.getBoundingClientRect().top;
|
||
let tiefste = 0;
|
||
const geh = (el) => {
|
||
for (const k of el.children) {
|
||
const st = getComputedStyle(k);
|
||
if (st.position === "absolute" || st.position === "fixed") continue;
|
||
if (st.display === "none" || k.hidden) continue;
|
||
tiefste = Math.max(tiefste, k.getBoundingClientRect().bottom - oben);
|
||
if (st.overflow === "visible" && st.overflowY === "visible") geh(k);
|
||
}
|
||
};
|
||
geh(c);
|
||
return Math.round(tiefste);
|
||
};
|
||
const r = document.querySelector(".neu__raster");
|
||
const c = r && [...r.children].find((x) => !x.hidden && x.offsetParent !== null && unten(x) > 40);
|
||
if (!c) return null;
|
||
const vorher = c.style.height;
|
||
c.style.height = "12px";
|
||
const erkannt = unten(c) > Math.round(c.getBoundingClientRect().height) + 4;
|
||
c.style.height = vorher;
|
||
const wiederGut = unten(c) <= Math.round(c.getBoundingClientRect().height) + 4;
|
||
return { erkannt, wiederGut };
|
||
});
|
||
ok(!!probe && probe.erkannt && probe.wiederGut,
|
||
probe ? "Gegenprobe: eine kuenstlich eingeklemmte Zelle wird erkannt"
|
||
: "KONNTE NICHT NACHSEHEN: keine Zelle fuer die Gegenprobe gefunden");
|
||
|
||
await kontext.close();
|
||
}
|
||
|
||
await browser.close();
|
||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||
console.log(fehler ? `\n${fehler} Formular(e) fehlerhaft.` : "\nAlles in Ordnung.");
|
||
process.exit(fehler ? 1 : 0);
|