Drei Bildschirmfotos, drei Befunde -- und alle drei waren messbar. --- SCREEN 2: RING UND UHR STANDEN SCHIEF --- Filipe: "ich will dass auf dem handy die uhr und der kreis ganz oben mit den erledigten aufgaben, schoen mittig, zentriert sind. die stehen sehr verzogen von der mitte. das soll bei jedem und jeder rolle verbessert werden." Gemessen bei 390 und 412 px, in allen fuenf Rollen, immer dieselbe Zahl: Ring 31 px nach links, Uhr 31 px nach rechts. Gleicher Betrag, entgegengesetzte Richtung -- eine Ursache, nicht zwei. `justify-content: center` zentriert den INHALT der Reihe, und in der Reihe steht neben dem Instrument noch ein Knopf (Glocke rechts vom Ring, Wecker links von der Uhr). Zentriert wurde also [Knopf + Abstand + Instrument] als Block; das Instrument rutschte um die halbe Knopfbreite zur Seite. Die Knopfbreite steht seit jeher als `--instrument-knoepfe: 62px` in derselben Datei -- 62 / 2 = 31. Die gemessene Zahl war von Anfang an aufgeschrieben, nur an anderer Stelle. Der Knopf zaehlt jetzt nicht mehr mit: aus dem Fluss genommen, neben das zentrierte Instrument gehaengt. NICHT um 31 px verschoben -- das waere dieselbe Zahl ein zweites Mal, und beim naechsten Knopf waere sie falsch. Jetzt folgt seine Lage dem Instrument, wie gross das auch ist. Nachher: 0 px Abweichung, alle fuenf Rollen, beide Breiten. --- SCREEN 3: DAS BABYBLAU WAR STAHLGRAU --- Filipe: "die rolle dogfather soll eine uebertrieben geile babyblau haben." Zum dritten Mal -- und die ersten beiden Male habe ich das Falsche vergroessert. Beide Male hatte ich den Abstand zwischen Rot- und Blaukanal im HEXWERT erhoeht (45 Stufen, dann 86). Diesmal zuerst gemessen, was am Bildschirm ankommt: Die DogFather-Zeile traegt 98,7 % farbige Bildpunkte -- MEHR als jede andere Rolle. An der Menge lag es also nie. Ihr Mittelwert war rgb(43,62,78): 35 Stufen zwischen Rot und Blau. Das ist Stahlgrau. Die richtige Groesse ist die SAETTIGUNG. In OKLab hatte #9ed3f4 eine Buntheit von 0,0718 -- der blasseste Wert aller fuenf Rollen. #5fbdff hat 0,1303, also 81 Prozent mehr. Die Zeile kommt damit auf rgb(36,60,79), und der mittlere Kanalabstand steigt von 35,2 auf 43,0. Nachgerechnet, was NICHT verlorengeht: Kontrast auf dem Grund 9,75:1 (vorher 12,49) -- weit ueber den 7 fuer AAA. Abstand zur naechsten Rollenfarbe 0,1608 in OKLab, vorher 0,1466; die Hausgrenze liegt bei 0,0973. DogFather ist also SICHERER unterscheidbar als vorher. Gemessen an der echten Flaeche: 8,84:1 und 9,60:1. --- SCREEN 4: SPICY FEHLTE IM UMSCHALTER --- Filipe: "ich will da auch noch die spicy rolle sehen und die leute in der rolle wie die anderen." Der Sicht-Umschalter fuehrte eine EIGENE Rollenliste -- vier Eintraege, Spicy Media fehlte. Zum vierten Mal derselbe Fehler: eine zweite Fassung einer Liste, die es zentral schon gibt. Vorher traf es die Leitungsliste (ein Set), die Rollennamen (ein Objekt in fuenf Skripten -- im Chat stand "UNDEFINED" ueber einem Namen) und die Personenliste. Jetzt steht dort keine Liste mehr, sondern eine Ableitung aus `ROLLENFOLGE` und `ROLLEN_GRUPPE`. Kommt eine sechste Rolle, ist sie ohne eine Zeile Arbeit dabei. --- Pruefung --- pruef-handy: zehn neue Messungen (fuenf Rollen x zwei Breiten), alle 0 px. Die beiden fehlenden Rollen sind dafuer in den Testdaten ergaenzt -- eine Pruefung an drei von fuenf haette "bei jeder Rolle" nicht belegen koennen. pruef-sicht: Cigdem als Spicy-Media-Person ergaenzt. Ohne sie waere es nie aufgefallen -- eine Rolle ohne Leute wird ohnehin weggelassen, und die alte Zusicherung "Manager, Scouts, Creator" waere weiter gruen gewesen. Dazu eine Gegenprobe, dass die Reihenfolge WIRKLICH aus der zentralen Liste kommt und nicht wieder abgeschrieben ist. pruef-personen-kachel: DogFather-Kontrast neu dabei. Ausserdem gruen: pruef-css-klassen, pruef-lesbarkeit. Co-Authored-By: Claude Opus 5 <[email protected]>
513 lines
25 KiB
JavaScript
513 lines
25 KiB
JavaScript
/* DIE PERSONENKACHEL — Aufbau, Farben, Kontrast (09.09.2026)
|
|
|
|
Filipe, mit Bildschirmfoto: "mach die ganze kachel perfekter
|
|
detailliert schönes. der hintergrund soll auch bissl dunkler sein
|
|
von der kachel."
|
|
|
|
Auf seinem Bild stand unter jeder zugeklappten Rolle ein leerer
|
|
Streifen, und die fünf Zeilen sahen fünfmal gleich aus. Gemessen
|
|
waren es 29 px Nichts je Zeile, fünfmal untereinander.
|
|
|
|
WAS HIER GEMESSEN WIRD UND WARUM SO:
|
|
|
|
Der Rollenstreifen wird an ECHTEN BILDPUNKTEN geprüft, nicht am
|
|
Stylesheet. Der Grund steht im Code: Im ersten Anlauf sagte
|
|
getComputedStyle "3 px breit, sichtbar, Deckkraft 0,55" -- und zu
|
|
sehen war nichts, weil das Kantenlicht der Kachel (module.css,
|
|
z-index 2) genau darüber lag. Eine Prüfung, die den berechneten Stil
|
|
liest, hätte grün gemeldet.
|
|
|
|
Der dunklere Grund wird als Helligkeit gemessen UND gegen die Karten
|
|
darin gehalten: Eine Vitrine ist dunkler als das, was darin steht.
|
|
Eine feste Zahl allein wäre eine Rechnung von gestern.
|
|
|
|
DREI AUSGÄNGE: 0 in Ordnung · 1 Befunde · 2 konnte nicht arbeiten. */
|
|
|
|
import { mkdtempSync, rmSync, appendFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-perskachel-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
const { portMussFreiSein } = await import("./helfer-port.mjs");
|
|
await portMussFreiSein(4329, "die Personenkachel-Pruefung");
|
|
|
|
process.env.PORT = "4329";
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
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");
|
|
notbremse(420_000, "pruef-personen-kachel");
|
|
await new Promise((r) => setTimeout(r, 900));
|
|
const BASIS = "http://127.0.0.1:4329";
|
|
|
|
const PROTOKOLL = join(process.cwd(), "pruef-personen-kachel-lauf.txt");
|
|
try { rmSync(PROTOKOLL, { force: true }); } catch { /* gab es noch nicht */ }
|
|
const melde = (z) => { console.log(z); try { appendFileSync(PROTOKOLL, z + "\n"); } catch { /* Zugabe */ } };
|
|
let fehler = 0, geprueft = 0;
|
|
const ok = (b, t) => { geprueft++; melde((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
/* ---- Bestand ------------------------------------------------------------ */
|
|
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 jetzt = new Date().toISOString();
|
|
function anlegen(name, rolle, code, aktiv = 1) {
|
|
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, aktiv, erstellt) VALUES (?,?,?,?,?,?,?)")
|
|
.run(name, rolle, hash, salt, 32768, aktiv, jetzt);
|
|
}
|
|
anlegen("Dogfather", "admin", "CODE-DOGI-0001");
|
|
anlegen("Spicy Media", "spicy", "CODE-SPIC-0001");
|
|
anlegen("Modis", "manager", "CODE-MODI-0001");
|
|
anlegen("BanaStift", "manager", "CODE-BSTF-0001", 0);
|
|
anlegen("BananaStift", "scout", "CODE-BANA-0001");
|
|
anlegen("Patrick", "scout", "CODE-PATR-0001");
|
|
/* SECHS Creator -- damit die Namenszeichen wirklich abgeschnitten
|
|
werden. Mit vier waere der "+n"-Fall nie geprueft. */
|
|
for (const n of ["Tili", "SpongBob", "Luna", "Mika", "Noa", "Robin"]) {
|
|
anlegen(n, "creator", "CODE-" + n.slice(0, 4).toUpperCase().padEnd(4, "X") + "-0001");
|
|
}
|
|
d.close();
|
|
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
|
|
const konsole = [];
|
|
async function alsDogi(breite = 1440) {
|
|
const ctx = await browser.newContext({ viewport: { width: breite, height: 1100 } });
|
|
const s = await ctx.newPage();
|
|
s.on("pageerror", (f) => konsole.push("Absturz: " + f.message));
|
|
s.on("console", (m) => { if (m.type() === "error") konsole.push("Konsole: " + m.text()); });
|
|
await s.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
|
await s.click('.rolle[data-rolle="admin"]');
|
|
await s.fill("#code", "CODE-DOGI-0001");
|
|
await s.click("button[type=submit]");
|
|
await s.waitForURL("**/start.html", { timeout: 20000 });
|
|
await s.evaluate(() => localStorage.setItem("dfw_personen_offen", "[]"));
|
|
await s.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" });
|
|
await s.waitForSelector(".gruppe", { timeout: 10000 });
|
|
await s.waitForTimeout(700);
|
|
return { ctx, s };
|
|
}
|
|
const gross = await alsDogi();
|
|
const seite = gross.s;
|
|
|
|
/* Punkte aus einem Bildschirmfoto lesen -- derselbe Weg wie in
|
|
pruef-lesbarkeit: das Bild in ein Canvas, dort jeden Punkt. */
|
|
async function mittelFarbe(clip) {
|
|
const bild = await seite.screenshot({ clip });
|
|
return seite.evaluate(async (b64) => {
|
|
const bild = new Image();
|
|
await new Promise((r) => { bild.onload = r; bild.src = "data:image/png;base64," + b64; });
|
|
const c = document.createElement("canvas");
|
|
c.width = bild.width; c.height = bild.height;
|
|
const g = c.getContext("2d", { willReadFrequently: true });
|
|
g.drawImage(bild, 0, 0);
|
|
const dd = g.getImageData(0, 0, c.width, c.height).data;
|
|
let r = 0, gr = 0, bl = 0, n = 0;
|
|
for (let i = 0; i < dd.length; i += 4) { r += dd[i]; gr += dd[i + 1]; bl += dd[i + 2]; n++; }
|
|
return [Math.round(r / n), Math.round(gr / n), Math.round(bl / n)];
|
|
}, bild.toString("base64"));
|
|
}
|
|
const leuchtkraft = ([r, g, b]) => {
|
|
const f = (w) => { const v = w / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
|
|
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
|
|
};
|
|
const kontrast = (a, b) => {
|
|
const l1 = leuchtkraft(a), l2 = leuchtkraft(b);
|
|
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
|
};
|
|
|
|
/* =======================================================================
|
|
1. AUFBAU: fünf Rollen, jede mit ihrem Merkmal
|
|
======================================================================= */
|
|
melde("\n=== 1. Aufbau ===");
|
|
{
|
|
const g = await seite.$$eval(".liste-kachel > .gruppe", (b) => b.map((x) => ({
|
|
rolle: x.dataset.rolle, auf: x.dataset.auf,
|
|
name: x.querySelector(".gruppe__name")?.textContent,
|
|
zahl: x.querySelector(".gruppe__zahl")?.textContent,
|
|
})));
|
|
ok(g.length === 5, `fünf Rollenabschnitte (${g.length})`);
|
|
ok(g.every((x) => x.rolle), `jeder trägt seine Rolle (${g.map((x) => x.rolle).join(", ")})`);
|
|
ok(g.every((x) => x.auf === "ja" || x.auf === "nein"),
|
|
"und seinen Zustand als Merkmal — nicht nur im aria-expanded");
|
|
|
|
/* GEGENPROBE: Zustand am Merkmal und im aria-expanded müssen
|
|
dasselbe sagen. Zwei Wahrheiten wären schlimmer als eine. */
|
|
const einig = await seite.$$eval(".liste-kachel > .gruppe", (b) => b.every((x) =>
|
|
(x.dataset.auf === "ja") === (x.querySelector(".gruppe__kopf").getAttribute("aria-expanded") === "true")));
|
|
ok(einig, "Merkmal und aria-expanded stimmen überein");
|
|
}
|
|
|
|
/* =======================================================================
|
|
2. KEIN LEERES BAND MEHR
|
|
|
|
Das war der sichtbare Mangel auf Filipes Bild: 74 px Abschnitt,
|
|
45 px Zeile, 29 px Nichts.
|
|
======================================================================= */
|
|
melde("\n=== 2. Kein leeres Band ===");
|
|
{
|
|
const zu = await seite.$$eval('.liste-kachel > .gruppe[data-auf="nein"]', (b) => b.map((x) => {
|
|
const r = x.getBoundingClientRect();
|
|
const k = x.querySelector(".gruppe__kopf").getBoundingClientRect();
|
|
return { name: x.dataset.rolle, rest: Math.round(r.height - k.height), hoehe: Math.round(r.height) };
|
|
}));
|
|
ok(zu.length >= 4, `${zu.length} zugeklappte Abschnitte zum Messen`);
|
|
const schlimm = zu.filter((x) => x.rest > 3);
|
|
ok(schlimm.length === 0,
|
|
schlimm.length ? `leeres Band unter: ${schlimm.map((x) => `${x.name} ${x.rest} px`).join(", ")}`
|
|
: `unter keiner zugeklappten Zeile steht Leerraum (höchstens ${Math.max(...zu.map((x) => x.rest))} px)`);
|
|
ok(zu.every((x) => x.hoehe >= 46 && x.hoehe <= 66),
|
|
`die Zeilen sind gleich hoch und gut greifbar (${zu.map((x) => x.hoehe).join("/")} px)`);
|
|
|
|
/* GEGENPROBE: Ein OFFENER Abschnitt braucht Luft -- sonst hätte ich
|
|
das eine Übel gegen das andere getauscht. */
|
|
await seite.click('.gruppe[data-rolle="manager"] .gruppe__kopf');
|
|
await seite.waitForTimeout(400);
|
|
const auf = await seite.$eval('.gruppe[data-rolle="manager"]', (x) => {
|
|
const r = x.getBoundingClientRect();
|
|
const k = x.querySelector(".gruppe__kopf").getBoundingClientRect();
|
|
return { rest: Math.round(r.height - k.height), personen: x.querySelectorAll(".person").length };
|
|
});
|
|
ok(auf.personen === 2 && auf.rest > 100,
|
|
`offen stehen die Karten mit Abstand da (${auf.personen} Karten, ${auf.rest} px)`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
3. DER ROLLENSTREIFEN — an echten Bildpunkten
|
|
|
|
getComputedStyle hilft hier nicht: Beim ersten Anlauf meldete es
|
|
tadellose Werte für einen Streifen, den das Kantenlicht der Kachel
|
|
vollständig verdeckte.
|
|
======================================================================= */
|
|
melde("\n=== 3. Der Rollenstreifen ===");
|
|
{
|
|
/* Erwartet wird der KANAL, der bei dieser Rollenfarbe führt --
|
|
nicht ein genauer Wert. Ein genauer Wert wäre eine Rechnung von
|
|
gestern: Sobald jemand die Rollenfarbe leicht verschiebt, wäre die
|
|
Prüfung rot, ohne dass etwas kaputt ist. */
|
|
const ERWARTET = {
|
|
spicy: "r", // #ef5f57
|
|
admin: "b", // #9ed3f4
|
|
manager: "b", // #8a76ff
|
|
scout: "g", // #5fc99a
|
|
creator: "r", // #c79a6d
|
|
};
|
|
/* DIESELBE STELLE, EINMAL MIT UND EINMAL OHNE STREIFEN.
|
|
|
|
Der erste Anlauf verglich den Streifen mit einem Punkt 40 px weiter
|
|
rechts -- und traf dort die Schrift. Der Unterschied war riesig und
|
|
sagte nichts über den Streifen. Zwei Stellen sind eben zwei
|
|
Stellen; es gibt keinen Grund, warum sie gleich aussehen sollten.
|
|
|
|
Richtig ist der Vergleich DERSELBEN Stelle mit sich selbst: einmal
|
|
wie sie ist, einmal mit ausgeschaltetem Streifen. Was sich dabei
|
|
ändert, IST der Streifen -- und was sich nicht ändert, ist die
|
|
Gegenprobe, ohne dass man sie eigens erfinden müsste. */
|
|
const AUS = "personen-kachel-probe";
|
|
const streifenAus = async (an) => seite.evaluate(([id, an]) => {
|
|
let s = document.getElementById(id);
|
|
if (!s) { s = document.createElement("style"); s.id = id; document.head.append(s); }
|
|
s.textContent = an ? ".liste-kachel > .gruppe::after { opacity: 0 !important; }" : "";
|
|
}, [AUS, an]);
|
|
|
|
const stellen = {};
|
|
for (const rolle of Object.keys(ERWARTET)) {
|
|
stellen[rolle] = await seite.$eval(`.gruppe[data-rolle="${rolle}"]`, (x) => {
|
|
const r = x.getBoundingClientRect();
|
|
return { x: Math.round(r.x) + 3, y: Math.round(r.y) + 18, width: 3, height: 10 };
|
|
});
|
|
}
|
|
const mit = {};
|
|
for (const [rolle, clip] of Object.entries(stellen)) mit[rolle] = await mittelFarbe(clip);
|
|
await streifenAus(true);
|
|
await seite.waitForTimeout(200);
|
|
const ohne = {};
|
|
for (const [rolle, clip] of Object.entries(stellen)) ohne[rolle] = await mittelFarbe(clip);
|
|
await streifenAus(false);
|
|
await seite.waitForTimeout(200);
|
|
const nochmal = await mittelFarbe(stellen.scout);
|
|
|
|
for (const [rolle, kanal] of Object.entries(ERWARTET)) {
|
|
const [r, g, b] = mit[rolle];
|
|
const o = ohne[rolle];
|
|
/* Welcher Kanal hat durch den Streifen am meisten GEWONNEN? Der
|
|
absolute Wert allein wäre irreführend: Auf einem bläulichen Grund
|
|
führt Blau auch dann, wenn der Streifen rot ist. */
|
|
const zu = [r - o[0], g - o[1], b - o[2]];
|
|
const fuehrt = zu[0] >= zu[1] && zu[0] >= zu[2] ? "r"
|
|
: zu[1] >= zu[0] && zu[1] >= zu[2] ? "g" : "b";
|
|
const abstand = zu.reduce((s, x) => s + Math.abs(x), 0);
|
|
ok(abstand > 25 && fuehrt === kanal,
|
|
`${rolle}: mit rgb(${mit[rolle]}) / ohne rgb(${o}) — Zuwachs ${JSON.stringify(zu)}, führend ${fuehrt}`);
|
|
}
|
|
|
|
/* GEGENPROBE: Ausgeschaltet und wieder eingeschaltet muss dieselbe
|
|
Stelle wieder aussehen wie am Anfang. Täte sie das nicht, hätte die
|
|
Messung etwas anderes gemessen als den Streifen. */
|
|
const zurueck = nochmal.reduce((s, x, i) => s + Math.abs(x - mit.scout[i]), 0);
|
|
ok(zurueck <= 6, `nach dem Wiedereinschalten sieht es aus wie vorher (Abweichung ${zurueck})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. DIE KACHEL IST DUNKLER — UND DUNKLER ALS IHR INHALT
|
|
======================================================================= */
|
|
melde("\n=== 4. Der Grund ===");
|
|
{
|
|
const lage = await seite.$eval('.gruppe[data-rolle="scout"]', (x) => {
|
|
const r = x.getBoundingClientRect();
|
|
return { x: Math.round(r.x), y: Math.round(r.y) };
|
|
});
|
|
const kachel = await mittelFarbe({ x: lage.x + 300, y: lage.y + 6, width: 120, height: 12 });
|
|
const hell = Math.round(leuchtkraft(kachel) * 1000) / 10;
|
|
ok(hell < 2.0, `der Kachelgrund ist dunkel (rgb(${kachel}), Leuchtkraft ${hell} %)`);
|
|
|
|
/* Die Vitrine muss dunkler sein als das, was darin steht -- sonst
|
|
sieht man die Vitrine statt des Inhalts. Manager ist offen. */
|
|
const karte = await seite.$eval('.gruppe[data-rolle="manager"] .person', (x) => {
|
|
const r = x.getBoundingClientRect();
|
|
return { x: Math.round(r.x) + 200, y: Math.round(r.y) + 6 };
|
|
});
|
|
const kartenFarbe = await mittelFarbe({ x: karte.x, y: karte.y, width: 120, height: 10 });
|
|
ok(leuchtkraft(kartenFarbe) > leuchtkraft(kachel),
|
|
`die Karten darin sind heller als die Kachel (Karte rgb(${kartenFarbe}) gegen Kachel rgb(${kachel}))`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
5. LESBARKEIT — jede neue Textstelle gemessen
|
|
======================================================================= */
|
|
melde("\n=== 5. Lesbarkeit ===");
|
|
{
|
|
async function messen(waehler) {
|
|
const stelle = await seite.evaluate((w) => {
|
|
const e = document.querySelector(w);
|
|
if (!e) return null;
|
|
e.scrollIntoView({ block: "center" });
|
|
const r = e.getBoundingClientRect();
|
|
if (r.width < 8 || r.height < 6) return null;
|
|
/* DIE FARBE WIRD GEZEICHNET, NICHT GELESEN.
|
|
------------------------------------------------------------------
|
|
Hier stand `.color.match(/\d+/g).slice(0,3)`. Das geht gut,
|
|
solange getComputedStyle "rgb(148, 165, 187)" liefert. Sobald
|
|
eine Farbe aus `color-mix()` kommt -- und genau das tun hier
|
|
alle neuen Stellen --, antwortet Chromium mit
|
|
"color(srgb 0.36 0.51 0.42)". Aus dem Muster fielen dann die
|
|
Ziffernfolgen "0", "510588", "0" heraus, und die Prüfung meldete
|
|
775929299:1. Grün, und vollkommen wertlos.
|
|
|
|
Ein Canvas nimmt JEDE Schreibweise entgegen, die der Browser
|
|
versteht, und gibt drei Zahlen von 0 bis 255 zurück. Was der
|
|
Browser malt, ist per Definition das, was man sieht. */
|
|
const c = document.createElement("canvas");
|
|
c.width = 1; c.height = 1;
|
|
const gg = c.getContext("2d", { willReadFrequently: true });
|
|
gg.fillStyle = "#000000";
|
|
gg.fillStyle = getComputedStyle(e).color;
|
|
gg.fillRect(0, 0, 1, 1);
|
|
const px = gg.getImageData(0, 0, 1, 1).data;
|
|
const farbe = [px[0], px[1], px[2]];
|
|
e.dataset.messen = "ja";
|
|
e.style.color = "transparent";
|
|
return { x: Math.round(r.x), y: Math.round(r.y),
|
|
w: Math.round(r.width), h: Math.round(Math.min(r.height, 40)), farbe };
|
|
}, waehler);
|
|
if (!stelle) return null;
|
|
const hinten = await mittelFarbe({ x: stelle.x, y: stelle.y, width: stelle.w, height: stelle.h });
|
|
await seite.evaluate(() => {
|
|
const e = document.querySelector('[data-messen="ja"]');
|
|
if (e) { e.style.color = ""; delete e.dataset.messen; }
|
|
});
|
|
return { farbe: stelle.farbe, hinten, wert: kontrast(stelle.farbe, hinten) };
|
|
}
|
|
|
|
/* GEGENPROBE ZUM MESSGERÄT, VOR DER ERSTEN MESSUNG.
|
|
|
|
Der Umweg über das Canvas ist neu, weil der alte Weg falsche Zahlen
|
|
lieferte. Bevor ich ihm glaube: zwei Farben mit bekanntem Ergebnis
|
|
hindurchschicken -- eine in gewöhnlicher Schreibweise, eine aus
|
|
color-mix(). Kommt dabei nicht heraus, was herauskommen muss, ist
|
|
die ganze Lesbarkeitsprüfung darunter wertlos. */
|
|
{
|
|
const probe = await seite.evaluate(() => {
|
|
const c = document.createElement("canvas");
|
|
c.width = 1; c.height = 1;
|
|
const g = c.getContext("2d", { willReadFrequently: true });
|
|
const lies = (farbe) => {
|
|
g.fillStyle = "#000000";
|
|
g.fillStyle = farbe;
|
|
g.fillRect(0, 0, 1, 1);
|
|
const p = g.getImageData(0, 0, 1, 1).data;
|
|
return [p[0], p[1], p[2]];
|
|
};
|
|
return { klar: lies("rgb(148, 165, 187)"),
|
|
gemischt: lies("color-mix(in srgb, #ffffff 50%, #000000)") };
|
|
});
|
|
const nah = (a, b) => a.every((x, i) => Math.abs(x - b[i]) <= 3);
|
|
ok(nah(probe.klar, [148, 165, 187]) && nah(probe.gemischt, [128, 128, 128]),
|
|
`das Messgerät stimmt: rgb-Schreibweise ${probe.klar}, color-mix ${probe.gemischt}`);
|
|
}
|
|
|
|
for (const [name, waehler] of [
|
|
["Rollenname", '.gruppe[data-rolle="scout"] .gruppe__name'],
|
|
["Zusatztext", '.gruppe[data-rolle="scout"] .gruppe__unter'],
|
|
["Anzahl", '.gruppe[data-rolle="scout"] .gruppe__zahl'],
|
|
["Namenszeichen", '.gruppe[data-rolle="scout"] .gruppe__kopf-zeichen'],
|
|
["Namenszeichen (Creator, hellste Rolle)", '.gruppe[data-rolle="creator"] .gruppe__kopf-zeichen'],
|
|
/* DogFather steht ausdruecklich dabei: Sein Babyblau wurde am
|
|
09.09.2026 saettiger gemacht (#9ed3f4 -> #5fbdff) und dabei
|
|
dunkler. Von 12,49:1 auf 9,75:1 gegen den Grund gerechnet -- eine
|
|
Rechnung ist aber keine Messung, und gemessen wird hier gegen die
|
|
Flaeche, die wirklich dahinter liegt. */
|
|
["Anzahl (DogFather)", '.gruppe[data-rolle="admin"] .gruppe__zahl'],
|
|
["Namenszeichen (DogFather)", '.gruppe[data-rolle="admin"] .gruppe__kopf-zeichen'],
|
|
["Anzahl (Creator)", '.gruppe[data-rolle="creator"] .gruppe__zahl'],
|
|
["Name offen", '.gruppe[data-rolle="manager"] .gruppe__name'],
|
|
]) {
|
|
const m = await messen(waehler);
|
|
ok(m !== null && m.wert >= 4.5,
|
|
`${name}: ${m ? m.wert.toFixed(2) + ":1 (" + m.farbe.join(",") + " auf " + m.hinten.join(",") + ")" : "nicht gefunden"}`);
|
|
}
|
|
|
|
/* GEGENPROBE: ein absichtlich zu dunkler Text MUSS durchfallen. */
|
|
await seite.evaluate(() => {
|
|
document.querySelector('.gruppe[data-rolle="scout"] .gruppe__name').style.color = "#141a22";
|
|
});
|
|
const schlecht = await messen('.gruppe[data-rolle="scout"] .gruppe__name');
|
|
ok(schlecht !== null && schlecht.wert < 4.5,
|
|
`absichtlich zu dunkler Text fällt durch (${schlecht?.wert.toFixed(2)}:1)`);
|
|
await seite.evaluate(() => {
|
|
document.querySelector('.gruppe[data-rolle="scout"] .gruppe__name').style.color = "";
|
|
});
|
|
}
|
|
|
|
/* =======================================================================
|
|
6. WER DRINSTEHT — die Namenszeichen
|
|
======================================================================= */
|
|
melde("\n=== 6. Namenszeichen ===");
|
|
{
|
|
const creator = await seite.$eval('.gruppe[data-rolle="creator"]', (x) => ({
|
|
zahl: x.querySelector(".gruppe__zahl").textContent,
|
|
zeichen: [...x.querySelectorAll(".gruppe__kopf-zeichen")].map((z) => ({
|
|
text: z.textContent, titel: z.title, mehr: z.dataset.mehr })),
|
|
versteckt: x.querySelector(".gruppe__leute").getAttribute("aria-hidden"),
|
|
}));
|
|
ok(creator.zahl === "6", `sechs Creator sind angelegt (${creator.zahl})`);
|
|
ok(creator.zeichen.length === 5,
|
|
`vier Zeichen plus eines für den Rest (${creator.zeichen.map((z) => z.text).join(" ")})`);
|
|
ok(creator.zeichen[4]?.mehr === "ja" && creator.zeichen[4]?.text === "+2",
|
|
`das letzte sagt, wie viele fehlen ("${creator.zeichen[4]?.text}")`);
|
|
/* WELCHE zwei fehlen, hängt an der Sortierung des Servers -- die
|
|
hier abzuschreiben hiesse, sie an zwei Stellen zu pflegen. Geprüft
|
|
wird die Eigenschaft: genau zwei Namen, und keiner davon steht
|
|
schon als eigenes Zeichen davor. */
|
|
const genannt = (creator.zeichen[4]?.titel || "").split(", ").filter(Boolean);
|
|
const vorne = creator.zeichen.slice(0, 4).map((z) => z.titel);
|
|
ok(genannt.length === 2 && genannt.every((n) => !vorne.includes(n)),
|
|
`und nennt beim Darüberfahren genau die zwei, die fehlen ("${creator.zeichen[4]?.titel}")`);
|
|
ok(creator.versteckt === "true",
|
|
"für das Vorleseprogramm sind sie ausgeblendet — die Anzahl steht als Zahl daneben");
|
|
|
|
/* Zwei Wörter geben zwei Buchstaben, ein Wort nur einen. */
|
|
const spicy = await seite.$eval('.gruppe[data-rolle="spicy"] .gruppe__kopf-zeichen',
|
|
(z) => ({ text: z.textContent, titel: z.title }));
|
|
ok(spicy.text === "SM", `"Spicy Media" wird SM (${spicy.text})`);
|
|
const scout = await seite.$$eval('.gruppe[data-rolle="scout"] .gruppe__kopf-zeichen',
|
|
(b) => b.map((z) => z.textContent));
|
|
ok(scout.join("") === "BP", `"BananaStift" und "Patrick" geben B und P (${scout.join(" ")})`);
|
|
|
|
/* Gesperrt wird gekennzeichnet -- und zwar leise. */
|
|
const manager = await seite.$$eval('.gruppe[data-rolle="manager"] .gruppe__kopf-zeichen',
|
|
(b) => b.map((z) => ({ text: z.textContent, gesperrt: z.dataset.gesperrt, titel: z.title })));
|
|
const dieGesperrte = manager.find((z) => z.gesperrt === "ja");
|
|
ok(!!dieGesperrte && /gesperrt/.test(dieGesperrte.titel),
|
|
`die gesperrte Person ist gekennzeichnet ("${dieGesperrte?.titel}")`);
|
|
ok(manager.filter((z) => z.gesperrt === "ja").length === 1,
|
|
"und nur sie — die aktive nicht");
|
|
}
|
|
|
|
/* =======================================================================
|
|
7. AM HANDY
|
|
======================================================================= */
|
|
melde("\n=== 7. Am Handy (390 px) ===");
|
|
{
|
|
const handy = await alsDogi(390);
|
|
const m = await handy.s.evaluate(() => {
|
|
const k = document.getElementById("liste");
|
|
const leute = document.querySelector(".gruppe__leute");
|
|
return {
|
|
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
|
leuteSichtbar: leute ? getComputedStyle(leute).display !== "none" : null,
|
|
breiter: [...k.querySelectorAll(".gruppe__kopf")]
|
|
.filter((x) => x.getBoundingClientRect().right > 391).length,
|
|
zeilen: [...k.querySelectorAll(".gruppe__kopf")].map((x) => Math.round(x.getBoundingClientRect().height)),
|
|
};
|
|
});
|
|
ok(m.ueber <= 1, `kein seitlicher Überstand (${m.ueber} px)`);
|
|
ok(m.leuteSichtbar === false,
|
|
"die Namenszeichen treten am Handy zurück — die Zahl sagt es dort");
|
|
ok(m.breiter === 0, `keine Zeile ragt hinaus (${m.breiter})`);
|
|
ok(m.zeilen.every((h) => h >= 44), `die Zeilen bleiben greifbar (${m.zeilen.join("/")} px)`);
|
|
|
|
/* ZWEI ZEILEN, NICHT DREI.
|
|
|
|
Der Zusatztext bekommt am Handy eine eigene Zeile -- richtig, er
|
|
passt sonst nicht. Der PFEIL bekam dadurch eine dritte und stand
|
|
mittig unter dem Text wie ein vergessenes Zeichen: 106 px je Rolle,
|
|
fünfmal untereinander. Gemessen wird deshalb beides: die Höhe und
|
|
die Lage des Pfeils. */
|
|
ok(m.zeilen.every((h) => h <= 82),
|
|
`und werden nicht dreizeilig (${m.zeilen.join("/")} px)`);
|
|
const pfeil = await handy.s.evaluate(() => {
|
|
const k = document.querySelector(".gruppe__kopf");
|
|
const p = k.querySelector(".klapp-pfeil").getBoundingClientRect();
|
|
const n = k.querySelector(".gruppe__name").getBoundingClientRect();
|
|
const kr = k.getBoundingClientRect();
|
|
return { rechts: Math.round(kr.right - p.right),
|
|
mitte: Math.round((p.top + p.bottom) / 2 - (kr.top + kr.bottom) / 2),
|
|
nebenNamen: p.top < n.bottom + 4 };
|
|
});
|
|
ok(pfeil.rechts >= 0 && pfeil.rechts < 30,
|
|
`der Pfeil steht rechts (${pfeil.rechts} px vom Rand)`);
|
|
ok(Math.abs(pfeil.mitte) < 8,
|
|
`und mittig zwischen beiden Zeilen (${pfeil.mitte} px von der Mitte)`);
|
|
await handy.ctx.close();
|
|
}
|
|
|
|
/* =======================================================================
|
|
8. KEINE STILLEN FEHLER
|
|
======================================================================= */
|
|
melde("\n=== 8. Stille Fehler ===");
|
|
{
|
|
ok(konsole.length === 0, konsole.length ? "Fehler: " + konsole.slice(0, 3).join(" | ")
|
|
: "keine Skript- oder Konsolenfehler");
|
|
await seite.evaluate(() => { setTimeout(() => { throw new Error("Absicht"); }, 0); });
|
|
await seite.waitForTimeout(400);
|
|
ok(konsole.some((z) => /Absicht/.test(z)), `ein absichtlicher Fehler wird bemerkt (${konsole.length})`);
|
|
}
|
|
|
|
await gross.ctx.close();
|
|
await browser.close();
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
|
|
melde(`\n${geprueft} Prüfungen`);
|
|
melde(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`);
|
|
process.exit(fehler ? 1 : 0);
|