pruef-kachel-universum war rot mit drei Meldungen. Die zweite rote
Pruefung von dreien.
ERST DIE FRAGE, OB ES DRIFT IST ODER VON ANFANG AN SO WAR: Die Pruefung
und das Sternenfeld kamen im selben Commit (ad47fc2). Dort laufen
lassen -- alles gruen, Kachelzeile 0,017 % auf 11610 Bildpunkten. Heute
2,372 % auf 6282. Die gemessene Flaeche hat sich HALBIERT.
Das war der Hinweis: Es wird eine andere Kachel gemessen. Der Waehler
nimmt die erste (`.kachel .kachel__unter`), und die war damals die
grosse Dashboard-Kachel (645 px breit), heute ist es "Team-Lage" (349
px). Die Reihenfolge hat sich zwischendurch geaendert.
WARUM DAS EINEN UNTERSCHIED MACHT: Die beiden Nebel sind auf
`100% 100%` skaliert, sie wachsen also mit der Kachel. Der babyblaue
sass bei `at 12% 88%` -- unten links. Auf einer schmalen Kachel liegt
genau dort die Unterzeile; auf der doppelt so breiten verteilt sich
derselbe Nebel ueber die ganze Breite und faellt nicht auf.
Der Kern ist jetzt tiefer in die Ecke geschoben (`at 10% 104%`, Ellipse
etwas flacher). Der Verlauf laeuft weiterhin quer durch die Kachel, die
Sterne sind unveraendert -- nachgesehen im Bild, nicht nur gemessen.
Kachelzeile 2,372 % -> 0,000 % (Mittel 6.93 -> 7.28)
Personenzeile 0,253 % -> 0,253 % (unveraendert, nichts kaputt)
UND EINE PRUEFUNG, DIE SEIT WOCHEN GAR NICHT LIEF. Die Zeile "dort
steht kein Stern" hing an `if (zwischenraum)`, und `zwischenraum` war
`null`, seit die grosse Kachel an Platz zwei steht: Sie faengt eine
Zeile tiefer wieder bei x=140 an, also gab es keinen Spalt zwischen
den ersten beiden. Ein uebersprungener Test beweist nichts -- und
gemeldet hat er nur, dass er nichts messen konnte.
Beim Wiederbeleben stellte sich heraus, dass er die falsche Frage
stellte: Er verlangte, dass der Grund zwischen den Kacheln "ruhig" ist
(Spanne <= 12). Das ist eine Aussage ueber das HINTERGRUNDFOTO der
Seite, und ein Foto darf helle Stellen haben -- gemessen 27, ohne dass
etwas kaputt war. Die eigentliche Frage ist eine andere: Liegt das
Sternenfeld wirklich nur auf den Kacheln?
Jetzt wird derselbe Fleck zweimal gemessen, mit und ohne Sternenfeld:
Mittel 42,7,12 -> 42,7,12 hellster 69,14,17 -> 69,14,17
Bildpunkt fuer Bildpunkt identisch. Das beantwortet die Frage
unabhaengig davon, was auf dem Foto zu sehen ist.
pruef-kachel-universum 37 Pruefungen, 0 Fehler (vorher 3)
pruef-css-klassen, pruef-neue-seiten 97 -- 0 Fehler
Co-Authored-By: Claude Opus 5 <[email protected]>
672 lines
31 KiB
JavaScript
672 lines
31 KiB
JavaScript
/* =====================================================================
|
|
DER GRUND JEDER KACHEL IM HAUS VON TEAM DOGI (11.09.2026)
|
|
|
|
Filipe: "veraender die kacheln und den hintergrund, ich will dass
|
|
die hintergrunde von den kacheln immer unviersum artig ist, es muss
|
|
richtig geil sein aber immer so dass man alles noch gut erkkent.
|
|
und das IN DER GANZEN WEBSITE VON TEAM DOGI. nur die kacheln. [...]
|
|
wichtig ist die form der kacheln soll gleich bleiben."
|
|
|
|
Daraus werden vier pruefbare Saetze, und jeder einzelne kann
|
|
scheitern:
|
|
|
|
1. UEBERALL. Nicht "auf der Startseite", sondern auf jeder Kachel
|
|
des Hauses -- auch auf denen, die es heute noch nicht gibt.
|
|
Deshalb wird geprueft, dass crew-haus.css KEINE Kachelklasse
|
|
nennt: Sie setzt vier Werte um, aus denen module.css den Grund
|
|
jeder Kachel baut. Eine Datei, die stattdessen Kachel fuer
|
|
Kachel aufzaehlt, waere bei der naechsten neuen unvollstaendig
|
|
-- lautlos.
|
|
2. NUR DIE KACHELN. Der Grund der SEITE bleibt, wie er war.
|
|
3. DIE FORM BLEIBT GLEICH. Fase, Silhouette und die drei
|
|
Eckwinkel werden vor und nach dem Hauswechsel verglichen,
|
|
Zeichen fuer Zeichen.
|
|
4. MAN ERKENNT ALLES NOCH GUT. Und das ist der Teil, der wirklich
|
|
Arbeit macht.
|
|
|
|
---------------------------------------------------------------------
|
|
WIE LESBARKEIT AUF EINEM STERNENHIMMEL GEMESSEN WIRD
|
|
|
|
Ein Sternenfeld ist kein Hintergrund, sondern viele. Hinter einem
|
|
Buchstaben liegt mal fast schwarzer Grund, mal Nebel, mal -- selten
|
|
-- der Kern eines Sterns. Eine einzige Durchschnittszahl wuerde
|
|
genau den Fall verschlucken, der stoert; eine Regel auf den
|
|
einzelnen hellsten Punkt dagegen waere eine Regel GEGEN Sterne, denn
|
|
ein Stern ist per Definition der hellste Punkt seiner Umgebung.
|
|
|
|
Deshalb drei Zahlen je Textstelle, alle aus denselben echten
|
|
Bildpunkten eines Bildschirmfotos:
|
|
|
|
* MITTEL -- die Flaeche, die ein Buchstabe wirklich sieht.
|
|
Muss 4,5:1 halten. Das ist die Regel.
|
|
* ANTEIL -- wie viel Prozent der Punkte hinter dem Text unter
|
|
4,5:1 liegen. Das ist die eigentliche Messung: Sie
|
|
unterscheidet einen PUNKT von einer FLAECHE. Ein
|
|
Nebel, der zu hell wird, faellt hier sofort auf,
|
|
auch wenn der Mittelwert noch durchgeht. Grenze:
|
|
1,5 Prozent. Gemessen wurden 0,25 Prozent, im
|
|
Agenturhaus an derselben Stelle 0,115 Prozent.
|
|
* HELLSTER -- der einzelne hellste Bildpunkt, also ein Sternkern.
|
|
Grenze 3,4:1 -- bewusst unter der Regel, denn dieser
|
|
Punkt ist kleiner als ein Buchstabenstrich breit.
|
|
Ohne diese Zahl koennte jemand die Sterne beliebig
|
|
hell machen, solange nur wenige davon da sind.
|
|
|
|
Gemessen wird an ECHTEN BILDPUNKTEN, nicht an Farbangaben. Das ist
|
|
hier keine Vorsicht, sondern Notwendigkeit: Was hinter dem Text
|
|
liegt, entsteht erst beim Zeichnen aus Grund, Nebel, Raster und
|
|
Schleier. Keine Farbangabe kennt das Ergebnis.
|
|
|
|
UND DER INHALT DER KACHEL WIRD DABEI UNSICHTBAR GESCHALTET. Der
|
|
erste Anlauf hat das nicht getan und Kontraste von 1,95:1 gemeldet
|
|
-- gegen das grosse Zeichen der Kachel, das gar keinen Text
|
|
verdeckt. Gefragt ist, was HINTER dem Text liegt, und das gehoert
|
|
der Kachel, nicht ihren Kindern.
|
|
|
|
---------------------------------------------------------------------
|
|
WARUM DIE ZWEITE ADRESSE HIER NICHT IM BROWSER AUFGERUFEN WIRD
|
|
|
|
Das Haus liefert `upgrade-insecure-requests` aus. Ruft man
|
|
crew.dogfather-universe.com im Testbrowser ueber eine umgebogene
|
|
Namensaufloesung auf, macht der Browser daraus https und findet
|
|
nichts. Diese Grenze ist bekannt und wird wie bei den anderen
|
|
Haus-Pruefungen umgangen, indem die Frage in zwei geteilt wird:
|
|
|
|
a) KOMMT DIE DATEI DORT AN? Das beantwortet eine echte Anfrage mit
|
|
gesetztem Host-Kopf ueber node:http -- ohne Browser, ohne CSP.
|
|
b) WIE SIEHT SIE AUS? Das beantwortet der Browser, indem genau
|
|
dieselbe Datei nachgeladen wird, an derselben Stelle wie im
|
|
Betrieb: als letzte Stilvorlage. pruef-css-klassen sichert ab,
|
|
dass sie im Betrieb auch wirklich zuletzt steht.
|
|
|
|
Nachgeladen wird ueber ihre ADRESSE, nicht ueber ihren Dateipfad.
|
|
Das ist kein Detail: Bei einer eingefuegten Datei rechnet der
|
|
Browser relative Verweise gegen die SEITE, und aus `../img/…` wird
|
|
ein 404, den es im Betrieb nicht gibt. Zwei Minuten Fehlersuche an
|
|
einem Fehler, den die Pruefung selbst erzeugt hatte.
|
|
===================================================================== */
|
|
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { request as httpAnfrage } from "node:http";
|
|
import { portMussFreiSein } from "./helfer-port.mjs";
|
|
|
|
const PORT = await portMussFreiSein(4401, "pruef-kachel-universum");
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-universum-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
process.env.PORT = String(PORT);
|
|
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 }); };
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
await import("./index.js");
|
|
notbremse(240_000, "pruef-kachel-universum");
|
|
await new Promise((r) => setTimeout(r, 700));
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
/* EIGENE ZUHOERER. index.js haengt fuer den Betrieb Auffangnetze an
|
|
`uncaughtException` -- eine Pruefung, die dieselbe Datei in denselben
|
|
Prozess laedt, erbt sie und wuerde nach einem Absturz einfach
|
|
weiterhaengen, statt rot zu werden. */
|
|
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
|
|
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
|
|
|
|
let fehler = 0, geprueft = 0;
|
|
const melde = (t) => console.log(t);
|
|
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 schluessel = randomBytes(32).toString("hex");
|
|
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
|
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
|
.run("code_kennung_schluessel", schluessel, 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", schluessel).update(code).digest("hex"), jetzt);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
|
|
anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
|
anlegen("Rieke", "hand", "CODE-HAND-0001");
|
|
anlegen("Frida", "modi", "CODE-MODI-0001");
|
|
anlegen("Tomma", "modi", "CODE-MODI-0002");
|
|
/* Die Teamseite zeichnet ihre Personenkarten aus dem, was das jeweilige
|
|
Haus zu sehen bekommt. Auf DIESER Adresse sind das Manager und
|
|
Scouts -- ohne sie bliebe `.tperson` leer, und die Messung der
|
|
Personenkarte waere ein uebersprungener Test, der nichts beweist.
|
|
Die Kachel ist in beiden Haeusern dieselbe; gemessen wird ihr Grund,
|
|
nicht wessen Name darin steht. */
|
|
anlegen("Mara", "manager", "CODE-MANA-0001");
|
|
anlegen("Sten", "scout", "CODE-SCOU-0001");
|
|
d.close();
|
|
|
|
const CREW_CSS_ADRESSE = "/workspace/assets/css/crew-haus.css";
|
|
const CREW_CSS = readFileSync(
|
|
new URL("../workspace/assets/css/crew-haus.css", import.meta.url).pathname
|
|
.replace(/^\/([A-Za-z]:)/, "$1"), "utf8");
|
|
|
|
/* =======================================================================
|
|
1. DIE DATEI KOMMT AUF DER ZWEITEN ADRESSE AN -- UND NUR DORT
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 1. Ausgeliefert wird sie da, wo sie hingehoert ===");
|
|
|
|
function holen(host) {
|
|
return new Promise((fertig, schief) => {
|
|
const a = httpAnfrage({ host: "127.0.0.1", port: PORT, path: "/workspace/assets/css/haus.css",
|
|
method: "GET", headers: { Host: host } }, (antwort) => {
|
|
let text = "";
|
|
antwort.on("data", (s) => { text += s; });
|
|
antwort.on("end", () => fertig({ code: antwort.statusCode, text }));
|
|
});
|
|
a.on("error", schief);
|
|
a.end();
|
|
});
|
|
}
|
|
{
|
|
const crew = await holen("crew.dogfather-universe.com");
|
|
const agentur = await holen("workspace.dogfather-universe.com");
|
|
ok(crew.code === 200 && crew.text.includes("--sternenfeld:"),
|
|
`die zweite Wand bekommt das Sternenfeld (${crew.code}, ${crew.text.length} Zeichen)`);
|
|
ok(crew.text.includes("--modul-schleier: .30"),
|
|
"und den Schleier, der dazugehoert");
|
|
ok(agentur.code === 200 && !agentur.text.includes("--sternenfeld"),
|
|
`die Agenturadresse bekommt es NICHT (${agentur.text.length} Zeichen)`);
|
|
ok(crew.text.length > agentur.text.length + 500,
|
|
"die beiden Haeuser liefern wirklich verschiedene Dateien aus");
|
|
}
|
|
|
|
/* KEINE ACHTE ABSCHRIFT DER KACHELLISTE.
|
|
Der eigentliche Gedanke hinter dem ganzen Umbau: crew-haus.css darf
|
|
keine Kachel beim Namen nennen. Sonst waere sie beim naechsten neuen
|
|
Bauteil unvollstaendig, ohne dass es auffiele. */
|
|
{
|
|
const kachelklassen = [".eintrag-karte", ".t-kachel", ".tperson", ".liste-kachel",
|
|
".steckbrief", ".zahlkarte", ".k-anlasskachel", ".leer-hinweis"];
|
|
const gefunden = kachelklassen.filter((k) => CREW_CSS.includes(k));
|
|
ok(gefunden.length === 0,
|
|
`crew-haus.css zaehlt keine Kachelklassen auf (${gefunden.join(" ") || "keine"})`);
|
|
ok(CREW_CSS.includes("--sternenfeld:") && CREW_CSS.includes("--sternenfeld-mass:")
|
|
&& CREW_CSS.includes("--sternenfeld-lage:"),
|
|
"sie setzt stattdessen die Werte, aus denen module.css den Grund baut");
|
|
|
|
const modul = readFileSync(
|
|
new URL("../workspace/assets/css/module.css", import.meta.url).pathname
|
|
.replace(/^\/([A-Za-z]:)/, "$1"), "utf8");
|
|
ok((modul.match(/var\(--sternenfeld,/g) || []).length === 1,
|
|
"und module.css benutzt das Bild an genau EINER Stelle");
|
|
ok(modul.includes("--sternenfeld: none;"),
|
|
"im Agenturhaus steht dort `none` -- eine Ebene ohne Bild zeichnet nichts");
|
|
}
|
|
|
|
/* KEIN KARO. Wiederholen sich zwei Sternebenen im selben Abstand, sieht
|
|
man sofort ein Muster statt eines Feldes. Die Abstaende duerfen
|
|
deshalb keinen gemeinsamen Teiler haben -- das ist keine Kosmetik,
|
|
sondern der Unterschied zwischen Himmel und Tapete. */
|
|
{
|
|
const zeile = CREW_CSS.split("--sternenfeld-mass:")[1].split(";")[0];
|
|
const zahlen = (zeile.match(/(\d+)px/g) || []).map((z) => parseInt(z, 10));
|
|
const ggt = (a, b) => (b === 0 ? a : ggt(b, a % b));
|
|
let groesster = 0;
|
|
for (let i = 0; i < zahlen.length; i++)
|
|
for (let j = i + 1; j < zahlen.length; j++)
|
|
groesster = Math.max(groesster, ggt(zahlen[i], zahlen[j]));
|
|
ok(zahlen.length >= 6, `sechs Abstaende fuer drei Sternebenen (${zahlen.join(" ")})`);
|
|
ok(groesster === 1, `kein gemeinsamer Teiler, also kein Karo (groesster: ${groesster})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
2. DER BROWSER: erst das Agenturhaus, dann dasselbe mit dem Himmel
|
|
======================================================================= */
|
|
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await pw.chromium.launch();
|
|
const ctx = await browser.newContext({ viewport: { width: 1440, height: 1100 } });
|
|
const seite = await ctx.newPage();
|
|
const konsole = [];
|
|
seite.on("pageerror", (f) => konsole.push("Absturz: " + f.message));
|
|
seite.on("console", (m) => { if (m.type() === "error") konsole.push("Konsole: " + m.text()); });
|
|
seite.on("response", (a) => { if (a.status() >= 400) konsole.push(`${a.status()} ${a.url()}`); });
|
|
|
|
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: 25000 });
|
|
await seite.goto(BASIS + "/workspace/start.html", { waitUntil: "networkidle" });
|
|
await seite.waitForSelector(".kachel", { timeout: 10000 });
|
|
await seite.waitForTimeout(600);
|
|
|
|
/* Alles, was sich beim Hauswechsel NICHT aendern darf, wird vorher
|
|
abgeholt -- Form, Silhouette, Eckwinkel, Seitengrund. */
|
|
async function formAufnehmen() {
|
|
return seite.evaluate(() => {
|
|
const k = document.querySelector(".kachel");
|
|
const s = getComputedStyle(k);
|
|
const n = getComputedStyle(k, "::after");
|
|
const v = getComputedStyle(document.documentElement);
|
|
/* Ein Hintergrund ist eine Liste, und in jeder Ebene stehen selbst
|
|
Kommas. Mit einem Muster kommt man da nicht weit -- Chromium
|
|
schreibt die Verlaeufe voll aus, Klammern in Klammern. Gezaehlt
|
|
wird deshalb, wie tief man gerade steht. */
|
|
let tiefe = 0, ebenen = 1;
|
|
for (const z of n.backgroundImage) {
|
|
if (z === "(") tiefe++;
|
|
else if (z === ")") tiefe--;
|
|
else if (z === "," && tiefe === 0) ebenen++;
|
|
}
|
|
return {
|
|
fase: v.getPropertyValue("--fase").trim(),
|
|
silhouette: s.clipPath,
|
|
/* Die sechs Eckwinkel sind die ersten sechs Ebenen. Ihre Groessen
|
|
und Lagen SIND die Form. */
|
|
winkelMass: n.backgroundSize.split(",").slice(0, 6).join(",").trim(),
|
|
winkelLage: n.backgroundPosition.split(",").slice(0, 6).join(",").trim(),
|
|
ebenen,
|
|
seitengrund: getComputedStyle(document.body).backgroundColor,
|
|
sternenfeld: v.getPropertyValue("--sternenfeld").trim(),
|
|
schleier: v.getPropertyValue("--modul-schleier").trim(),
|
|
hausname: v.getPropertyValue("--haus-name").trim(),
|
|
};
|
|
});
|
|
}
|
|
const vorher = await formAufnehmen();
|
|
|
|
melde("");
|
|
melde("=== 2. Das Agenturhaus bleibt unberuehrt ===");
|
|
ok(vorher.sternenfeld === "none",
|
|
`dort steht kein Sternenfeld (${vorher.sternenfeld})`);
|
|
ok(vorher.schleier === ".26",
|
|
`und der Schleier steht unveraendert auf .26 (${vorher.schleier})`);
|
|
ok(vorher.ebenen === 9,
|
|
`neun Ebenen: sechs Eckwinkel, zwei Rasterlinien, eine leere (${vorher.ebenen})`);
|
|
|
|
/* Bildpunkte aus einem Bildschirmfoto -- derselbe Weg wie in
|
|
pruef-lesbarkeit und pruef-personen-kachel. Zurueck kommen der
|
|
Mittelwert und die SORTIERTE Helligkeitsliste; wer daraus einen
|
|
Anteil rechnen will, braucht die ganze Liste, nicht eine Kennzahl. */
|
|
async function punkte(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;
|
|
const hell = (p) => {
|
|
const f = p.map((x) => { const s = x / 255; return s <= .03928 ? s / 12.92 : Math.pow((s + .055) / 1.055, 2.4); });
|
|
return .2126 * f[0] + .7152 * f[1] + .0722 * f[2];
|
|
};
|
|
const liste = [];
|
|
let sr = 0, sg = 0, sb = 0, hr = 0, hg = 0, hb = 0, best = -1;
|
|
for (let i = 0; i < dd.length; i += 4) {
|
|
sr += dd[i]; sg += dd[i + 1]; sb += dd[i + 2];
|
|
const h = hell([dd[i], dd[i + 1], dd[i + 2]]);
|
|
liste.push(h);
|
|
if (h > best) { best = h; hr = dd[i]; hg = dd[i + 1]; hb = dd[i + 2]; }
|
|
}
|
|
const n = liste.length;
|
|
liste.sort((a, b) => a - b);
|
|
return {
|
|
mittel: [Math.round(sr / n), Math.round(sg / n), Math.round(sb / n)],
|
|
hellsterPunkt: [hr, hg, hb],
|
|
hell: liste, anzahl: n,
|
|
};
|
|
}, bild.toString("base64"));
|
|
}
|
|
|
|
/* WAS HINTER DEM TEXT LIEGT, IST NICHT DER TEXT -- UND AUCH NICHT SEIN
|
|
NACHBAR. Fuer die Dauer der Messung wird der INHALT der Kachel
|
|
unsichtbar, die Kachel selbst nicht: Hintergrund, Kantenlicht,
|
|
Raster, Eckwinkel und Sternenfeld gehoeren ihr, nicht ihren Kindern. */
|
|
const KARTEN = ".kachel, .t-kachel, .tperson, .tl, .eintrag-karte, .person, .liste-kachel";
|
|
async function inhaltAus(waehler) {
|
|
await seite.evaluate(([w, karten]) => {
|
|
const e = document.querySelector(w);
|
|
const karte = e && (e.closest(karten) || e.parentElement);
|
|
if (!karte) return;
|
|
karte.dataset.grundmessung = "ja";
|
|
karte.querySelectorAll("*").forEach((x) => {
|
|
x.dataset.warSichtbar = x.style.visibility || "";
|
|
x.style.visibility = "hidden";
|
|
});
|
|
}, [waehler, KARTEN]);
|
|
}
|
|
async function inhaltAn() {
|
|
await seite.evaluate(() => {
|
|
const karte = document.querySelector('[data-grundmessung="ja"]');
|
|
if (!karte) return;
|
|
karte.querySelectorAll("*").forEach((x) => {
|
|
x.style.visibility = x.dataset.warSichtbar || "";
|
|
delete x.dataset.warSichtbar;
|
|
});
|
|
delete karte.dataset.grundmessung;
|
|
});
|
|
}
|
|
|
|
/* DER AUSSCHNITT LIEGT IN DER MITTE DER KACHEL. Am Rand sitzt das
|
|
Kantenlicht -- eine 1,6 px schmale Linie in fast vollem Kachelton,
|
|
und damit der hellste Punkt der ganzen Kachel. Wer den mitmisst,
|
|
misst die Form statt des Himmels: Der erste Anlauf meldete deshalb
|
|
"keine neuen hellen Punkte", obwohl das Sternenfeld laengst da war. */
|
|
const kachelRahmen = await seite.evaluate(() => {
|
|
const k = document.querySelector(".kachel");
|
|
k.scrollIntoView({ block: "center" });
|
|
const r = k.getBoundingClientRect();
|
|
return { x: Math.round(r.x + r.width * .22), y: Math.round(r.y + r.height * .22),
|
|
width: Math.round(r.width * .56), height: Math.round(r.height * .56) };
|
|
});
|
|
await inhaltAus(".kachel");
|
|
const grundAgentur = await punkte(kachelRahmen);
|
|
await inhaltAn();
|
|
|
|
/* ---- Jetzt dasselbe Fenster, aber im Haus von Team Dogi ---- */
|
|
await seite.addStyleTag({ url: CREW_CSS_ADRESSE });
|
|
await seite.waitForTimeout(400);
|
|
const nachher = await formAufnehmen();
|
|
|
|
melde("");
|
|
melde("=== 3. Die Form bleibt gleich, der Grund nicht ===");
|
|
ok(nachher.hausname === '"crew"',
|
|
`das Haus von Team Dogi ist angekommen (${nachher.hausname})`);
|
|
ok(nachher.fase === vorher.fase, `die Fase ist unveraendert (${nachher.fase})`);
|
|
ok(nachher.silhouette === vorher.silhouette, "die Silhouette der Kachel ist Zeichen fuer Zeichen dieselbe");
|
|
ok(nachher.winkelMass === vorher.winkelMass, "die drei Eckwinkel haben dieselbe Groesse");
|
|
ok(nachher.winkelLage === vorher.winkelLage, "und dieselbe Lage");
|
|
ok(nachher.ebenen === 13,
|
|
`aus der leeren Ebene sind fuenf geworden: drei Sternebenen, zwei Nebel (${nachher.ebenen})`);
|
|
ok(nachher.schleier === ".30", `der Schleier laesst etwas mehr durch (${nachher.schleier})`);
|
|
ok(nachher.sternenfeld.includes("radial-gradient"),
|
|
"und das Sternenfeld steht als Bild da, nicht als none");
|
|
|
|
await inhaltAus(".kachel");
|
|
const grundCrew = await punkte(kachelRahmen);
|
|
await inhaltAn();
|
|
{
|
|
const abstand = Math.max(...grundCrew.mittel.map((x, i) => Math.abs(x - grundAgentur.mittel[i])));
|
|
ok(abstand >= 4,
|
|
`der Kachelgrund ist messbar ein anderer geworden (${grundAgentur.mittel} -> ${grundCrew.mittel})`);
|
|
ok(grundCrew.hell[grundCrew.anzahl - 1] > grundAgentur.hell[grundAgentur.anzahl - 1] * 1.5,
|
|
`und es gibt jetzt helle Punkte, die es vorher nicht gab (${grundAgentur.hellsterPunkt} -> ${grundCrew.hellsterPunkt})`);
|
|
}
|
|
|
|
/* GEGENPROBE ZUM MESSGERAET. Nimmt man das Sternenfeld wieder weg, muss
|
|
der Grund zurueckspringen. Ohne diese Probe koennte die Messung oben
|
|
auch etwas ganz anderes gesehen haben -- die Farben haben sich beim
|
|
Hauswechsel ja ohnehin geaendert. */
|
|
{
|
|
await seite.addStyleTag({ content: ":root { --sternenfeld: none; }" });
|
|
await seite.waitForTimeout(250);
|
|
await inhaltAus(".kachel");
|
|
const ohne = await punkte(kachelRahmen);
|
|
await inhaltAn();
|
|
ok(ohne.hell[ohne.anzahl - 1] < grundCrew.hell[grundCrew.anzahl - 1] * .8,
|
|
`ohne Sternenfeld verschwinden die hellen Punkte wieder (${grundCrew.hellsterPunkt} -> ${ohne.hellsterPunkt})`);
|
|
await seite.evaluate(() => {
|
|
const letzte = [...document.querySelectorAll("style")].pop();
|
|
if (letzte && letzte.textContent.includes("--sternenfeld: none")) letzte.remove();
|
|
});
|
|
await seite.waitForTimeout(250);
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. NUR DIE KACHELN -- der Grund der Seite bleibt
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 4. Nur die Kacheln ===");
|
|
{
|
|
const zwischenraum = await seite.evaluate(() => {
|
|
/* Ein Fleck ZWISCHEN zwei Kacheln. Er gehoert der Seite, nicht
|
|
einer Kachel -- dort darf kein Stern stehen. */
|
|
/* IRGENDZWEI, DIE NEBENEINANDER LIEGEN -- nicht die ersten zwei.
|
|
|
|
Bis zum 16.09.2026 stand hier `kacheln[0]` und `kacheln[1]`. Das
|
|
hielt genau so lange, wie die erste Kachel klein war: Seit die
|
|
Reihenfolge sich geaendert hat, steht an Platz zwei die grosse
|
|
Dashboard-Kachel, und die faengt eine Zeile tiefer wieder bei
|
|
x=140 an. Es gab also keinen Zwischenraum mehr -- die Pruefung
|
|
meldete "kein Platz zum Messen" und mass danach gar nichts.
|
|
|
|
Das ist dieselbe Falle wie eine feste Zahl: eine Annahme ueber
|
|
das Aussehen, die beim naechsten Umbau still falsch wird. Jetzt
|
|
wird gesucht, was die Frage beantwortet, statt sie zu stellen. */
|
|
/* EIN FLECK, DER KEINE KACHEL BERUEHRT.
|
|
|
|
Die Frage ist: Steht das Sternenfeld wirklich nur auf den
|
|
Kacheln? Bis zum 16.09.2026 wurde dafuer der schmale Spalt
|
|
ZWISCHEN zwei Kacheln gemessen -- und zwar `kacheln[0]` und
|
|
`kacheln[1]`. Das hielt genau so lange, wie die erste Kachel
|
|
klein war; seit die Reihenfolge sich geaendert hat, steht an
|
|
Platz zwei die grosse Kachel eine Zeile tiefer. Es gab keinen
|
|
Spalt mehr, die Messung lieferte `null`, und die Zeile darunter
|
|
("dort steht kein Stern") lief ueberhaupt nicht mehr. Ein
|
|
uebersprungener Test beweist nichts.
|
|
|
|
Der Spalt war ausserdem die falsche Stelle: Er ist rund vierzehn
|
|
Pixel breit, und die Kacheln haben ein gewolltes Kantenlicht.
|
|
Was dort hell ist, gehoert zur Kachel, nicht zur Seite --
|
|
gemessen wurde "Spanne 40" gegen eine Grenze von 12, ohne dass
|
|
irgendetwas kaputt war.
|
|
|
|
Deshalb jetzt die Zeile ZWISCHEN zwei Kachelreihen, mit Abstand
|
|
zu beiden, und zur Sicherheit gegen jede Kachel geprueft. Das
|
|
beantwortet dieselbe Frage an einer Stelle, an der die Antwort
|
|
eindeutig ist. */
|
|
const felder = [...document.querySelectorAll(".kachel")]
|
|
.map((k) => k.getBoundingClientRect());
|
|
if (felder.length < 2) return null;
|
|
|
|
const unten = Math.max(...felder.map((f) => f.y + f.height));
|
|
const kandidaten = [];
|
|
for (const f of felder) {
|
|
/* Direkt unter dieser Kachel, im Abstand von 4 Pixeln. */
|
|
const y = f.y + f.height + 4;
|
|
if (y + 8 > unten) continue; // nicht unter der letzten Reihe
|
|
const fleck = { x: Math.round(f.x + f.width / 2 - 12), y: Math.round(y),
|
|
width: 24, height: 8 };
|
|
const beruehrt = felder.some((g) =>
|
|
fleck.x + fleck.width > g.x && fleck.x < g.x + g.width
|
|
&& fleck.y + fleck.height > g.y && fleck.y < g.y + g.height);
|
|
if (!beruehrt) kandidaten.push(fleck);
|
|
}
|
|
return kandidaten[0] || null;
|
|
});
|
|
ok(zwischenraum !== null, "ein Fleck ohne Kachel darunter ist gefunden");
|
|
if (zwischenraum) {
|
|
/* DIE FRAGE IST NICHT "IST ES DORT RUHIG", SONDERN "LIEGT DORT DAS
|
|
STERNENFELD".
|
|
|
|
Bis zum 16.09.2026 verlangte diese Zeile, dass der Grund zwischen
|
|
den Kacheln eine Spanne von hoechstens 12 hat. Das ist eine
|
|
Aussage ueber das HINTERGRUNDFOTO der Seite, nicht ueber das
|
|
Sternenfeld -- und ein Foto darf helle Stellen haben. Gemessen
|
|
wurden 27, ohne dass irgendetwas kaputt war.
|
|
|
|
Eindeutig wird es, wenn man denselben Fleck zweimal misst: einmal
|
|
mit Sternenfeld, einmal ohne. Liegt es wirklich nur auf den
|
|
Kacheln, aendert sich dort NICHTS. Das beantwortet die Frage
|
|
unabhaengig davon, was auf dem Foto zu sehen ist. */
|
|
const mit = await punkte(zwischenraum);
|
|
await seite.addStyleTag({ content: ":root { --sternenfeld: none; }" });
|
|
await seite.waitForTimeout(250);
|
|
const ohne = await punkte(zwischenraum);
|
|
await seite.evaluate(() => {
|
|
const letzte = [...document.querySelectorAll("style")].pop();
|
|
if (letzte && letzte.textContent.includes("--sternenfeld: none")) letzte.remove();
|
|
});
|
|
await seite.waitForTimeout(250);
|
|
|
|
const abstand = Math.max(...mit.mittel.map((x, i) => Math.abs(x - ohne.mittel[i])));
|
|
const spitze = Math.max(...mit.hellsterPunkt.map((x, i) => Math.abs(x - ohne.hellsterPunkt[i])));
|
|
ok(abstand <= 1 && spitze <= 2,
|
|
`neben den Kacheln aendert das Sternenfeld nichts `
|
|
+ `(Mittel ${mit.mittel} -> ${ohne.mittel}, hellster ${mit.hellsterPunkt} -> ${ohne.hellsterPunkt})`);
|
|
}
|
|
}
|
|
|
|
/* =======================================================================
|
|
5. MAN ERKENNT ALLES NOCH GUT
|
|
======================================================================= */
|
|
function kontrast(a, lb) {
|
|
const l = (p) => {
|
|
const f = p.map((x) => { const s = x / 255; return s <= .03928 ? s / 12.92 : Math.pow((s + .055) / 1.055, 2.4); });
|
|
return .2126 * f[0] + .7152 * f[1] + .0722 * f[2];
|
|
};
|
|
const x = typeof a === "number" ? a : l(a);
|
|
return (Math.max(x, lb) + .05) / (Math.min(x, lb) + .05);
|
|
}
|
|
function leuchtkraft(p) {
|
|
const f = p.map((x) => { const s = x / 255; return s <= .03928 ? s / 12.92 : Math.pow((s + .055) / 1.055, 2.4); });
|
|
return .2126 * f[0] + .7152 * f[1] + .0722 * f[2];
|
|
}
|
|
|
|
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 Schriftfarbe wird GEZEICHNET, nicht gelesen: color-mix()
|
|
liefert getComputedStyle als "color(srgb …)", und daraus Ziffern
|
|
zu klauben ergibt Unsinn. Ein Canvas versteht jede Schreibweise. */
|
|
const c = document.createElement("canvas");
|
|
c.width = 1; c.height = 1;
|
|
const g = c.getContext("2d", { willReadFrequently: true });
|
|
g.fillStyle = "#000000";
|
|
g.fillStyle = getComputedStyle(e).color;
|
|
g.fillRect(0, 0, 1, 1);
|
|
const px = g.getImageData(0, 0, 1, 1).data;
|
|
e.dataset.messen = "ja";
|
|
e.style.color = "transparent";
|
|
return { x: Math.round(r.x), y: Math.round(r.y),
|
|
w: Math.max(8, Math.round(r.width)), h: Math.round(Math.min(r.height, 40)),
|
|
farbe: [px[0], px[1], px[2]] };
|
|
}, waehler);
|
|
if (!stelle) return null;
|
|
await inhaltAus(waehler);
|
|
const hinten = await punkte({ x: stelle.x, y: stelle.y, width: stelle.w, height: stelle.h });
|
|
await inhaltAn();
|
|
await seite.evaluate(() => {
|
|
const e = document.querySelector('[data-messen="ja"]');
|
|
if (e) { e.style.color = ""; delete e.dataset.messen; }
|
|
});
|
|
const lt = leuchtkraft(stelle.farbe);
|
|
const unter = hinten.hell.filter((h) => kontrast(lt, h) < 4.5).length;
|
|
return {
|
|
farbe: stelle.farbe,
|
|
mittel: kontrast(lt, leuchtkraft(hinten.mittel)),
|
|
hellster: kontrast(lt, hinten.hell[hinten.anzahl - 1]),
|
|
anteil: 100 * unter / hinten.anzahl,
|
|
punkte: hinten.anzahl,
|
|
};
|
|
}
|
|
|
|
async function lesbar(name, waehler) {
|
|
const m = await messen(waehler);
|
|
if (m === null) { ok(false, `${name}: nicht gefunden (${waehler})`); return; }
|
|
ok(m.punkte > 200 && m.mittel >= 4.5 && m.anteil <= 1.5 && m.hellster >= 3.4,
|
|
`${name}: Mittel ${m.mittel.toFixed(2)} · unter 4,5 liegen ${m.anteil.toFixed(3)} % · hellster Punkt ${m.hellster.toFixed(2)} (${m.punkte} Bildpunkte)`);
|
|
}
|
|
|
|
melde("");
|
|
melde("=== 5. Lesbarkeit auf der Startseite ===");
|
|
for (const [name, w] of [
|
|
["Kachelname", ".kachel .kachel__name"],
|
|
["Kachelzeile", ".kachel .kachel__unter"],
|
|
["kleine Kachel, Name", '.kachel:not([data-gross="ja"]) .kachel__name'],
|
|
["kleine Kachel, Zeile", '.kachel:not([data-gross="ja"]) .kachel__unter'],
|
|
]) await lesbar(name, w);
|
|
|
|
/* GEGENPROBE: ein absichtlich zu dunkler Text MUSS durchfallen. Ohne
|
|
sie waere nicht bewiesen, dass diese Messung ueberhaupt "nicht in
|
|
Ordnung" sagen kann. */
|
|
{
|
|
await seite.evaluate(() => {
|
|
document.querySelector(".kachel .kachel__name").style.color = "#171326";
|
|
});
|
|
const schlecht = await messen(".kachel .kachel__name");
|
|
ok(schlecht !== null && schlecht.mittel < 4.5 && schlecht.anteil > 50,
|
|
`absichtlich zu dunkler Text faellt durch (${schlecht?.mittel.toFixed(2)}:1, ${schlecht?.anteil.toFixed(1)} % unter der Grenze)`);
|
|
await seite.evaluate(() => {
|
|
document.querySelector(".kachel .kachel__name").style.color = "";
|
|
});
|
|
}
|
|
|
|
/* GEGENPROBE ZUM ANTEIL: ein zu heller Nebel MUSS auffallen, auch wenn
|
|
der Mittelwert noch durchgeht. Das ist der Fall, fuer den diese Zahl
|
|
ueberhaupt gebaut wurde. */
|
|
{
|
|
await seite.addStyleTag({ content: ":root { --sternenfeld:"
|
|
+ " radial-gradient(ellipse 90% 90% at 50% 50%, rgba(255,255,255,.95) 0%, transparent 85%);"
|
|
+ " --sternenfeld-mass: 100% 100%; --sternenfeld-lage: 0 0; }" });
|
|
await seite.waitForTimeout(300);
|
|
const grell = await messen(".kachel .kachel__unter");
|
|
ok(grell !== null && grell.anteil > 1.5,
|
|
`ein zu heller Nebel faellt durch (${grell?.anteil.toFixed(1)} % unter der Grenze)`);
|
|
await seite.evaluate(() => {
|
|
const letzte = [...document.querySelectorAll("style")].pop();
|
|
if (letzte && letzte.textContent.includes("ellipse 90% 90%")) letzte.remove();
|
|
});
|
|
await seite.waitForTimeout(300);
|
|
}
|
|
|
|
melde("");
|
|
melde("=== 6. Lesbarkeit auf der Teamseite ===");
|
|
await seite.goto(BASIS + "/workspace/team.html", { waitUntil: "networkidle" });
|
|
await seite.addStyleTag({ url: CREW_CSS_ADRESSE });
|
|
await seite.waitForSelector(".t-kachel", { timeout: 15000 });
|
|
await seite.waitForTimeout(900);
|
|
/* Zugeklappte Abschnitte messen nichts -- erst aufklappen, dann messen. */
|
|
await seite.evaluate(() => {
|
|
document.querySelectorAll("details:not([open])").forEach((x) => { x.open = true; });
|
|
});
|
|
await seite.waitForTimeout(500);
|
|
{
|
|
const da = await seite.evaluate(() => ({
|
|
kachel: !!document.querySelector(".t-kachel"),
|
|
person: !!document.querySelector(".tperson"),
|
|
titel: !!document.querySelector(".t-kopf__titel"),
|
|
haus: getComputedStyle(document.documentElement).getPropertyValue("--haus-name").trim(),
|
|
}));
|
|
ok(da.kachel && da.titel && da.person && da.haus === '"crew"',
|
|
`die Teamseite ist gezeichnet und traegt das richtige Haus (${JSON.stringify(da)})`);
|
|
}
|
|
for (const [name, w] of [
|
|
["Abschnittstitel", ".t-kachel .t-kopf__titel"],
|
|
["Personenname", ".tperson .tperson__name"],
|
|
["Personenzeile (leisester Text im Haus)", ".tperson .tperson__unter"],
|
|
]) await lesbar(name, w);
|
|
|
|
melde("");
|
|
melde("=== 7. Nichts abgestuerzt ===");
|
|
ok(konsole.length === 0, `keine Fehler im Browser (${konsole.slice(0, 3).join(" | ") || "keine"})`);
|
|
|
|
await ctx.close();
|
|
await browser.close();
|
|
/* Der Server haelt die Datenbankdatei offen -- unter Windows laesst sie
|
|
sich dann nicht loeschen. Das ist kein Befund, sondern ein
|
|
Aufraeumrest im Temp-Ordner, und darf die Pruefung nicht rot machen. */
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
|
|
console.log("");
|
|
console.log(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
|
console.log(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG");
|
|
process.exit(fehler === 0 ? 0 : 1);
|