DIE VERZOGENE FORMULARZEILE hatte drei Ursachen, nicht eine -- und jede einzelne haette gereicht, damit es schief aussieht: 1. Die Rasterregel galt nur fuer Kinder mit der Klasse .feld. Der Kalender benutzt schlichte <div> ohne Klasse; die fielen hindurch, bekamen von zwoelf Spalten je EINE und wurden nur so breit, wie ihr Inhalt sie zwang. Daher "Art" schmal und "Beginn" breit. 2. "Dauer (Minuten)" brach in der schmalen Spalte auf zwei Zeilen um -- und schob das Feld darunter tiefer als seine Nachbarn. Die Klammer ist jetzt ein leiser Zusatz, die Beschriftung hat feste Hoehe. 3. Die letzten drei Pixel: Mit grid-template-rows: 1fr auto bestimmte jedes Element die Zeilenhoehe selbst. Ein <select> mass sich 3 px kleiner als ein <input> und sass dadurch hoeher. Gemessen: Container beider Felder identisch (652+71), Eingabe aber 676..720 gegen 679..723. Drei Pixel klingen nach nichts und sind genau das, was man als "verzogen" sieht. Jetzt hat die Zeile feste Hoehe und das Feld fuellt sie ganz. Ergebnis, gemessen statt betrachtet: alle Felder 44 px hoch, alle 362 px breit, alle Unterkanten auf einer Linie. Auf dem Handy untereinander -- zwei Felder auf 300 px sind zwei Streifen, in die nichts hineinpasst. NEUE PRUEFUNG pruef-formulare.mjs. Sie prueft nicht "sieht gut aus", sondern misst: gleiche Hoehe, Unterkanten auf einer Linie, kein Feld absurd schmal, keine Beschriftung mehrzeilig -- auf fuenf Seiten und zwei Geraetegroessen. Zwei Messfehler darin selbst gefunden und behoben (Teilpixel-Rundung, und eine Ausgabe ueber mehrere Zeilen, die die Datei zerschossen hat). DIE SCOUT-PIPELINE ERKLAERT SICH JETZT SELBST. Vorher stand im leeren Zustand ein Satz, der nur wiederholte, was man ohnehin sieht: dass nichts da ist. Wer die Seite zum ersten Mal oeffnet, wusste danach weiterhin nicht, wofuer es sie gibt. Der leere Zustand ist der EINZIGE Moment, in dem jemand garantiert liest, was dort steht -- spaeter ist die Flaeche von Daten belegt. Deshalb steht die Erklaerung genau dort und nicht in einer Hilfe, die niemand aufmacht. Erklaert wird der NUTZEN, nicht die Bedienung: nicht "hier klicken", sondern warum ein Scout ohne diese Liste Leute verliert -- naemlich die Interessierten, bei denen drei Wochen nichts passiert ist, und nicht die, die Nein sagen. Dazu die fuenf Stufen mit je einem Satz, was sie bedeuten. Co-Authored-By: Claude Opus 5 <[email protected]>
148 lines
7.0 KiB
JavaScript
148 lines
7.0 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");
|
||
process.env.PORT = "4203";
|
||
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 }); };
|
||
await import("./index.js");
|
||
await new Promise((r) => setTimeout(r, 700));
|
||
const BASIS = "http://127.0.0.1:4203";
|
||
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());
|
||
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());
|
||
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();
|
||
}
|
||
|
||
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);
|