Kacheln: 37 Farben neu gerechnet, und in jeder ein eigenes Universum
Filipe, zwei Wuensche: "ich will das jede kachel eine andere farbe hat
... keine die sich irgendwie aehnlich sind ... richtig geil und
speziell" und "der hintergrund ... soll noch mehr viel mehr nach
universum sein. nicht einfach so paar weisse punkte sondern richtig
geil hochwertiger geiler universum ... in den kacheln ueberall".
BEIDES WAR BERECHTIGT, und beides liess sich messen:
kleinster Farbabstand 0,0529 (Rueckmeldung / Steckbrief)
Paare unter 0,10 26 von 666
schlechtester Kontrast 1,64:1 (Ton 35 -- praktisch unlesbar)
sieben Toene unter 4,5:1
Universum auf GENAU EINER Kachel
WIE DAS PASSIEREN KONNTE, obwohl es ein Farbwerkzeug gab: Es kannte 21
Farben. Seit dem 08.09. waren SECHZEHN von Hand dazugekommen (Ton 22
bis 37) -- jede einzeln plausibel, keine gegen die anderen gerechnet.
Das Werkzeug meldete weiter "0,0973, alle Bedingungen erfuellt" und
meinte einen Stand, den es nicht mehr gab. Dieselbe Krankheit wie eine
abgeschriebene Spaltenliste: Die Zahl wird nicht falsch, sie wird
UNZUSTAENDIG. Es liest die Anzahl jetzt aus den Dateien.
NEU GERECHNET, alle 37 auf einmal:
kleinster Abstand 0,0529 -> 0,1010 (fast doppelt)
Paare unter 0,10 26 -> 0
schlechtester Kontrast 1,64 -> 4,50:1
Buntheit bis 0,300 statt 0,170, Helligkeit 0,58 bis 0,92
Drei Stellschrauben: Buntheitsgrenze hoch (augenschonend heisst dunkles
Schema und kein Flackern, NICHT blass), Raster von 2 Grad auf 1 Grad
und von fuenf auf zwanzig Helligkeitsstufen, und sechzehn Startpunkte
statt einem. Der Kontrast 4,5:1 bleibt hart -- eine Kachelfarbe traegt
Text.
DAS UNIVERSUM STEHT JETZT IM HINTERGRUND JEDER KACHEL, nicht in einem
Element: Beide Pseudo-Elemente sind vergeben (Leuchtschiene, Glanz),
und ein neues Element muesste an jeder Stelle nachgetragen werden, die
Kacheln baut. Eine Ebene, die man vergessen kann, wird vergessen.
Siebzehn Ebenen: drei nahe Sterne mit Hof, vier mittlere (zwei im
Kachelton), fuenf Staubkoerner, eine Milchstrasse als Schraege, drei
Nebel im Kachelton, ein kuehler Gegenpol, eine Vignette. Die Nebel
tragen `var(--ton)` -- 37 Kacheln sind damit 37 verschiedene Nebel.
Keine Bewegung: 28 driftende Felder waeren 28 Dauerlaeufer auf der
Grafikkarte.
Nach dem ersten Bildschirmfoto nachgeschaerft -- die Sterne lagen bei
6 bis 14 Prozent und waren aus der Naehe nicht zu sehen, genau die
"paar weissen punkte". Der Grund steht in der Kachel selbst: Sie ist
halbdurchsichtig, darunter liegt das Buehnenbild. Ein Sternenfeld muss
sich hier gegen ein FOTO durchsetzen, nicht gegen Schwarz.
NEU: server/helfer-png.mjs -- ein PNG-Leser (zlib, 90 Zeilen). Er
beantwortet die Frage, die aus dem CSS nicht mehr zu beantworten war:
Zwischen Textfarbe und Flaeche liegen jetzt sieben Ebenen plus das
durchscheinende Buehnenbild. Gemessen am Bildschirmfoto: schlechtester
Textkontrast 8,02:1 (Grenze 4,5).
NEU: server/pruef-kachel-universum.mjs, 12 Pruefungen.
DREI ANLAEUFE FUER DIE STERNPRUEFUNG, und die ersten zwei waren gruen
und wertlos:
1. Ebenen im CSS zaehlen -- `0px` ist auch eine Zahl. Alle Sterne
auf null: blieb gruen.
2. Helle Punkte im Bildschirmfoto zaehlen -- die Kachel ist
halbdurchsichtig, das Foto darunter hat selbst Punktstruktur.
Gemessen: 459 Punkte mit Sternen, 441 ohne. Vier Prozent sind
kein Nachweis, sondern Rauschen.
3. Jetzt die GROESSEN aus dem CSS (>= 0,7 px). Schwaecher, aber
ehrlich -- und die Gegenprobe greift: 12 -> 0.
Dabei fiel ein eigener Messfehler auf: Der Bereich wurde aus der
Textposition geschaetzt und lag OBERHALB der Kachel. Verraten hat es
die Gegenprobe, die zweimal exakt 153 lieferte -- eine Messung, deren
Ergebnis sich nicht aendert, wenn man das Gemessene entfernt, misst
etwas anderes.
Gegenproben, alle zielgenau:
zwei gleiche Farben -> 3 rot Sterne auf 0px -> 1 rot
Nebel ohne Kachelton -> 1 rot (nach dem Schaerfen: vorher blieb es
gruen, weil `--ton` auch im Grundverlauf steht)
Gruen: kachel-universum, css-klassen, start-ansicht, namen.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
/* =====================================================================
|
||||
EINEN BILDSCHIRMFOTO-PIXEL LESEN (17.09.2026)
|
||||
|
||||
ANLASS: Der Kachelgrund hat ein Universum bekommen -- Sterne, drei
|
||||
Nebel, eine Vignette, dazu der Glanz und das durchscheinende
|
||||
Buehnenbild. Die Frage "ist der Text darauf noch lesbar?" laesst
|
||||
sich aus dem CSS nicht mehr beantworten: Zwischen Textfarbe und
|
||||
`--flaeche` liegen sieben Ebenen.
|
||||
|
||||
WAS ZAEHLT, IST DER PUNKT AUF DEM SCHIRM. Und den liefert nur ein
|
||||
Bildschirmfoto.
|
||||
|
||||
WARUM SELBST GESCHRIEBEN: Das Projekt hat kein Bildmodul, und fuer
|
||||
drei Pixel eines nachzuinstallieren waere eine Abhaengigkeit, die
|
||||
jemand pflegen muss. Node bringt `zlib` mit; ein PNG ist danach nur
|
||||
noch Zeilenfilter, und davon gibt es fuenf.
|
||||
|
||||
ES LIEST NUR, WAS PLAYWRIGHT SCHREIBT: 8 Bit je Kanal, RGB oder
|
||||
RGBA, nicht verschraenkt. Alles andere meldet es als Fehler, statt
|
||||
zu raten -- ein Bildleser, der bei unbekanntem Format irgendetwas
|
||||
zurueckgibt, faelscht Messwerte.
|
||||
===================================================================== */
|
||||
import { inflateSync } from "node:zlib";
|
||||
|
||||
/** Liest ein PNG aus einem Puffer.
|
||||
* @returns {{breite:number, hoehe:number, kanaele:number, daten:Buffer}} */
|
||||
export function pngLesen(puffer) {
|
||||
if (puffer.length < 8 || puffer.readUInt32BE(0) !== 0x89504e47) {
|
||||
throw new Error("Kein PNG");
|
||||
}
|
||||
let pos = 8;
|
||||
let breite = 0, hoehe = 0, tiefe = 0, art = 0;
|
||||
const teile = [];
|
||||
while (pos < puffer.length) {
|
||||
const laenge = puffer.readUInt32BE(pos);
|
||||
const name = puffer.toString("ascii", pos + 4, pos + 8);
|
||||
const inhalt = puffer.subarray(pos + 8, pos + 8 + laenge);
|
||||
if (name === "IHDR") {
|
||||
breite = inhalt.readUInt32BE(0);
|
||||
hoehe = inhalt.readUInt32BE(4);
|
||||
tiefe = inhalt[8];
|
||||
art = inhalt[9];
|
||||
if (tiefe !== 8) throw new Error(`Nur 8 Bit je Kanal, nicht ${tiefe}`);
|
||||
if (art !== 2 && art !== 6) throw new Error(`Nur RGB/RGBA, nicht Typ ${art}`);
|
||||
if (inhalt[12] !== 0) throw new Error("Verschraenkte PNG werden nicht gelesen");
|
||||
} else if (name === "IDAT") {
|
||||
teile.push(inhalt);
|
||||
} else if (name === "IEND") {
|
||||
break;
|
||||
}
|
||||
pos += 12 + laenge; // Laenge + Name + Inhalt + Pruefsumme
|
||||
}
|
||||
const kanaele = art === 6 ? 4 : 3;
|
||||
const roh = inflateSync(Buffer.concat(teile));
|
||||
|
||||
/* Die Filter rueckgaengig machen. Jede Zeile beginnt mit einem Byte,
|
||||
das sagt, wie sie kodiert wurde -- PNG-Spezifikation, Abschnitt 9. */
|
||||
const zeile = breite * kanaele;
|
||||
const daten = Buffer.alloc(zeile * hoehe);
|
||||
for (let y = 0; y < hoehe; y++) {
|
||||
const filter = roh[y * (zeile + 1)];
|
||||
const quelle = roh.subarray(y * (zeile + 1) + 1, y * (zeile + 1) + 1 + zeile);
|
||||
const ziel = daten.subarray(y * zeile, (y + 1) * zeile);
|
||||
const oben = y > 0 ? daten.subarray((y - 1) * zeile, y * zeile) : null;
|
||||
for (let x = 0; x < zeile; x++) {
|
||||
const a = x >= kanaele ? ziel[x - kanaele] : 0; // links
|
||||
const b = oben ? oben[x] : 0; // oben
|
||||
const c = oben && x >= kanaele ? oben[x - kanaele] : 0; // links oben
|
||||
let wert = quelle[x];
|
||||
if (filter === 1) wert += a;
|
||||
else if (filter === 2) wert += b;
|
||||
else if (filter === 3) wert += (a + b) >> 1;
|
||||
else if (filter === 4) {
|
||||
/* Paeth: der Nachbar, der der Vorhersage am naechsten liegt. */
|
||||
const p = a + b - c;
|
||||
const pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c);
|
||||
wert += (pa <= pb && pa <= pc) ? a : (pb <= pc ? b : c);
|
||||
} else if (filter !== 0) {
|
||||
throw new Error(`Unbekannter Zeilenfilter ${filter}`);
|
||||
}
|
||||
ziel[x] = wert & 0xff;
|
||||
}
|
||||
}
|
||||
return { breite, hoehe, kanaele, daten };
|
||||
}
|
||||
|
||||
/** Die Farbe an einer Stelle -- [r, g, b]. */
|
||||
export function pixel(bild, x, y) {
|
||||
const i = (Math.round(y) * bild.breite + Math.round(x)) * bild.kanaele;
|
||||
return [bild.daten[i], bild.daten[i + 1], bild.daten[i + 2]];
|
||||
}
|
||||
|
||||
const linear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
||||
|
||||
/** Relative Helligkeit nach WCAG. */
|
||||
export const helligkeit = ([r, g, b]) =>
|
||||
0.2126 * linear(r / 255) + 0.7152 * linear(g / 255) + 0.0722 * linear(b / 255);
|
||||
|
||||
/** Kontrast zweier Farben nach WCAG -- immer >= 1. */
|
||||
export function kontrast(a, b) {
|
||||
const x = helligkeit(a), y = helligkeit(b);
|
||||
return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
|
||||
}
|
||||
+313
-628
@@ -1,99 +1,223 @@
|
||||
/* =====================================================================
|
||||
DER GRUND JEDER KACHEL IM HAUS VON TEAM DOGI (11.09.2026)
|
||||
DIE KACHELN: FARBEN UND UNIVERSUM (17.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."
|
||||
Filipe, zwei Wünsche in einem Satz:
|
||||
|
||||
Daraus werden vier pruefbare Saetze, und jeder einzelne kann
|
||||
scheitern:
|
||||
„ich will das jede kachel eine andere farbe hat, es soll keine die
|
||||
gleiche farben haben bitte und auch keine die sich irgendwie
|
||||
ähnlich sind ... die farben sollen auch richtig geil und speziell
|
||||
sein."
|
||||
|
||||
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.
|
||||
„der hintergrund von allen kacheln haben wir richtung universum
|
||||
mäßig gemacht, aber bin noch nicht ganz zufrieden ... nicht
|
||||
einfach so paar weiße punkte sondern richtig geil hochwertiger
|
||||
geiler universum als hintergrund in den kacheln überall."
|
||||
|
||||
BEIDES WAR BERECHTIGT, und beides ließ sich messen:
|
||||
|
||||
kleinster Farbabstand 0,0529 (Rückmeldung / Steckbrief)
|
||||
Paare unter 0,10 26 von 666
|
||||
schlechtester Kontrast 1,64:1 (Ton 35 — unlesbar)
|
||||
Universum auf GENAU EINER Kachel
|
||||
|
||||
Der Grund für das erste: Ein Werkzeug rechnete die Farben, kannte
|
||||
aber nur 21 — seit dem 08.09. waren sechzehn von Hand dazugekommen.
|
||||
Es meldete weiter „alle Bedingungen erfüllt" und meinte einen Stand,
|
||||
den es nicht mehr gab.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WIE LESBARKEIT AUF EINEM STERNENHIMMEL GEMESSEN WIRD
|
||||
WAS HIER GEPRÜFT WIRD — und warum es drei Abschnitte braucht:
|
||||
|
||||
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.
|
||||
1. Die Farben stehen weit genug auseinander (OKLab, alle Paare).
|
||||
2. Jede hält 4,5:1 gegen den dunklen Grund — sie trägt auch Text.
|
||||
3. DER TEXT AUF DER FERTIGEN KACHEL ist lesbar.
|
||||
|
||||
Deshalb drei Zahlen je Textstelle, alle aus denselben echten
|
||||
Bildpunkten eines Bildschirmfotos:
|
||||
Der dritte ist der, den sonst niemand macht, und der einzige, der
|
||||
die Frage wirklich beantwortet: Zwischen Textfarbe und `--flaeche`
|
||||
liegen inzwischen SIEBEN Ebenen (drei Nebel, Sterne, Milchstraße,
|
||||
Vignette, Glanz) plus das durchscheinende Bühnenbild. Aus dem CSS
|
||||
lässt sich das nicht mehr ausrechnen — gemessen wird deshalb am
|
||||
Bildschirmfoto, an den echten Bildpunkten.
|
||||
|
||||
* 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.
|
||||
Aufrufen mit: node server/pruef-kachel-universum.mjs
|
||||
===================================================================== */
|
||||
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 { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { portMussFreiSein } from "./helfer-port.mjs";
|
||||
import { pngLesen, pixel, kontrast } from "./helfer-png.mjs";
|
||||
|
||||
const PORT = await portMussFreiSein(4401, "pruef-kachel-universum");
|
||||
const HIER = dirname(fileURLToPath(import.meta.url));
|
||||
const CSS = join(HIER, "..", "workspace", "assets", "css", "start.css");
|
||||
const GRUND = [10, 18, 30]; // #0a121e
|
||||
|
||||
const ordner = mkdtempSync(join(tmpdir(), "ws-universum-"));
|
||||
let fehler = 0, geprueft = 0, offen = 0;
|
||||
const melde = (t) => console.log(t);
|
||||
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||||
const unklar = (t) => { offen++; console.log(" -- " + t); };
|
||||
|
||||
/* ---------- OKLab ---------------------------------------------------- */
|
||||
const linear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
||||
const srgb = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
|
||||
function oklab(hex) {
|
||||
const [r, g, b] = srgb(hex).map(linear);
|
||||
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
||||
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
||||
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
||||
return [
|
||||
0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
|
||||
1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
|
||||
0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s,
|
||||
];
|
||||
}
|
||||
const abstand = (a, b) => {
|
||||
const x = oklab(a), y = oklab(b);
|
||||
return Math.hypot(x[0] - y[0], x[1] - y[1], x[2] - y[2]);
|
||||
};
|
||||
const alsRgb = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
|
||||
|
||||
/* =======================================================================
|
||||
1. DIE FARBEN STEHEN AUSEINANDER
|
||||
======================================================================= */
|
||||
melde("=== 1. Die Farben ===");
|
||||
|
||||
const cssText = readFileSync(CSS, "utf8");
|
||||
const TOENE = new Map(
|
||||
[...cssText.matchAll(/\[data-ton="(\d+)"\]\s*\{\s*--ton:\s*(#[0-9a-fA-F]{6})/g)]
|
||||
.map((m) => [Number(m[1]), m[2].toLowerCase()]));
|
||||
|
||||
/* DIE ANZAHL KOMMT AUS DER DATEI. Genau hier ist das Farbwerkzeug
|
||||
unzuständig geworden: Es kannte 21, und niemand hat es angefasst,
|
||||
als sechzehn dazukamen. Eine Zahl, die niemand pflegt, kann nicht
|
||||
veralten. */
|
||||
ok(TOENE.size >= 20, `${TOENE.size} Farben in start.css gefunden`);
|
||||
|
||||
/* Werden alle auch benutzt? Eine Farbe ohne Kachel ist kein Fehler,
|
||||
aber eine KACHEL OHNE FARBE ist einer -- sie fiele auf den Akzent
|
||||
zurück, und dann hätten mehrere Kacheln dieselbe. */
|
||||
{
|
||||
const quellen = ["workspace.js", "../workspace/assets/js/bereiche.js",
|
||||
"../workspace/assets/js/start.js", "../workspace/assets/js/kopf.js",
|
||||
"../workspace/assets/js/befinden.js"];
|
||||
const benutzt = new Set();
|
||||
for (const q of quellen) {
|
||||
const t = readFileSync(join(HIER, q), "utf8");
|
||||
for (const m of t.matchAll(/ton:\s*(\d+)/g)) benutzt.add(Number(m[1]));
|
||||
}
|
||||
const ohne = [...benutzt].filter((n) => !TOENE.has(n));
|
||||
ok(ohne.length === 0,
|
||||
ohne.length ? `Kacheln ohne Farbe: Ton ${ohne.join(", ")}`
|
||||
: `alle ${benutzt.size} benutzten Tonnummern haben eine Farbe`);
|
||||
}
|
||||
|
||||
const liste = [...TOENE.entries()].map(([n, hex]) => ({ n, hex }));
|
||||
const paare = [];
|
||||
for (let i = 0; i < liste.length; i++) {
|
||||
for (let j = i + 1; j < liste.length; j++) {
|
||||
paare.push({ a: liste[i], b: liste[j], d: abstand(liste[i].hex, liste[j].hex) });
|
||||
}
|
||||
}
|
||||
paare.sort((x, y) => x.d - y.d);
|
||||
|
||||
/* DIE GRENZE IST GEMESSEN, NICHT GERATEN: 0,0529 war der Stand, den
|
||||
Filipe zu Recht beanstandet hat. Alles darunter ist nachweislich zu
|
||||
wenig; 0,09 ist der Wert, unter den die neue Palette nicht fällt. */
|
||||
const GRENZE = 0.09;
|
||||
ok(paare[0].d >= GRENZE,
|
||||
`kleinster Abstand ${paare[0].d.toFixed(4)} (Grenze ${GRENZE}) `
|
||||
+ `-- ${paare[0].a.hex}/${paare[0].b.hex}`);
|
||||
{
|
||||
const eng = paare.filter((p) => p.d < 0.10).length;
|
||||
ok(eng === 0,
|
||||
eng ? `${eng} Paare unter 0,10: ${paare.slice(0, 3).map((p) => `${p.a.hex}/${p.b.hex}`).join(", ")}`
|
||||
: `kein einziges der ${paare.length} Paare liegt unter 0,10`);
|
||||
}
|
||||
{
|
||||
/* KEINE ZWEI GLEICHEN. Das steht als eigene Zeile da, obwohl der
|
||||
Abstand es mit abdeckt -- es ist die wörtliche Bitte, und eine
|
||||
Prüfung, die die Frage direkt beantwortet, liest sich später
|
||||
besser als eine, aus der man es schließen muss. */
|
||||
const doppelt = liste.length - new Set(liste.map((x) => x.hex)).size;
|
||||
ok(doppelt === 0, doppelt ? `${doppelt} Farben kommen doppelt vor` : "keine Farbe kommt zweimal vor");
|
||||
}
|
||||
|
||||
/* Jede Farbe trägt Text -- sie muss gegen den dunklen Grund halten. */
|
||||
{
|
||||
const schlecht = liste.filter((x) => kontrast(alsRgb(x.hex), GRUND) < 4.5);
|
||||
ok(schlecht.length === 0,
|
||||
schlecht.length
|
||||
? `unter 4,5:1 -> ${schlecht.map((x) => `Ton ${x.n} ${x.hex} `
|
||||
+ `${kontrast(alsRgb(x.hex), GRUND).toFixed(2)}:1`).join(", ")}`
|
||||
: `alle halten 4,5:1 gegen den Grund (schlechteste `
|
||||
+ `${Math.min(...liste.map((x) => kontrast(alsRgb(x.hex), GRUND))).toFixed(2)}:1)`);
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
2. DAS UNIVERSUM IST DA -- UND ÜBERALL
|
||||
|
||||
Gezählt wird im CSS: Wie viele Ebenen hat der Kachelhintergrund? Ein
|
||||
Universum aus „ein paar weißen Punkten" hat drei, das gebaute hat
|
||||
siebzehn. Die Zahl allein sagt nichts über Schönheit -- sie sagt,
|
||||
dass die Ebenen NICHT VERSEHENTLICH verschwinden, und genau das ist
|
||||
schon einmal passiert (die Leuchtschiene, 07.09.: Regeln blieben,
|
||||
Inhalt weg).
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 2. Das Universum im Hintergrund ===");
|
||||
{
|
||||
const block = cssText.slice(cssText.indexOf(".kachel {"),
|
||||
cssText.indexOf("background-size:", cssText.indexOf(".kachel {")));
|
||||
/* DIE GROESSEN, NICHT NUR DIE ANZAHL.
|
||||
|
||||
Der erste Anlauf zaehlte `radial-gradient(...px` -- und `0px` ist
|
||||
auch eine Zahl. Setzt man alle Sterne auf null, blieb die Zaehlung
|
||||
bei zwoelf und die Pruefung gruen, waehrend das Universum leer war.
|
||||
|
||||
UND WARUM NICHT AM BILD GEMESSEN WIRD: Der zweite Anlauf zaehlte
|
||||
Lichtpunkte im Bildschirmfoto. Auch das trug nicht -- die Kachel
|
||||
ist halbdurchsichtig, darunter liegt das Buehnenbild, und ein Foto
|
||||
hat selbst Punktstruktur. Gemessen: 459 Punkte mit Sternen, 441
|
||||
ohne. Vier Prozent Unterschied sind kein Nachweis, sondern
|
||||
Rauschen. Eine Pruefung, die das als "bestanden" wertet, prueft
|
||||
nichts -- lieber eine ehrliche schwache als eine starke, die luegt. */
|
||||
const groessen = [...block.matchAll(/radial-gradient\(\s*([\d.]+)px/g)]
|
||||
.map((m) => Number(m[1]));
|
||||
const sterne = groessen.filter((g) => g >= 0.7).length;
|
||||
const nebel = (block.match(/radial-gradient\(\s*\d+%/g) || []).length;
|
||||
ok(sterne >= 10,
|
||||
`${sterne} Sternebenen mit sichtbarer Groesse `
|
||||
+ `(${groessen.length} Punktebenen insgesamt, kleinste `
|
||||
+ `${Math.min(...groessen).toFixed(1)}px)`);
|
||||
ok(nebel >= 3, `${nebel} Nebel- und Tiefenebenen`);
|
||||
{
|
||||
/* GEZAEHLT, NICHT NUR GESUCHT. Der erste Anlauf fragte, ob
|
||||
`var(--ton)` im Block VORKOMMT -- das tut es auch im
|
||||
Grundverlauf, der seit jeher da ist. Die Gegenprobe (einem Nebel
|
||||
die Kachelfarbe wegnehmen) blieb deshalb gruen. Gezaehlt werden
|
||||
jetzt die FLAECHIGEN Verlaeufe, die den Ton wirklich tragen. */
|
||||
const nebelMitTon = (block.match(
|
||||
/radial-gradient\(\s*\d+%[^)]*\)[^,]*var\(--ton\)|radial-gradient\(\s*\d+%[^;]*?color-mix\(in srgb, var\(--ton\)/g) || []).length;
|
||||
ok(nebelMitTon >= 2,
|
||||
`${nebelMitTon} Nebel tragen die Farbe der Kachel `
|
||||
+ "-- jede Kachel ihr eigenes Universum");
|
||||
}
|
||||
ok(/118deg|milchstra/i.test(block), "und es gibt eine Schräge (die Milchstraße)");
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
3. DER TEXT DARAUF IST LESBAR
|
||||
|
||||
Der Abschnitt, den sonst niemand macht. Gemessen am Bildschirmfoto:
|
||||
Zwischen Textfarbe und Fläche liegen sieben Ebenen plus das
|
||||
durchscheinende Bühnenbild -- aus dem CSS ist das nicht mehr
|
||||
auszurechnen.
|
||||
======================================================================= */
|
||||
melde("");
|
||||
melde("=== 3. Der Text auf der fertigen Kachel ===");
|
||||
|
||||
const PORT = await portMussFreiSein(4194, "pruef-kachel-universum");
|
||||
const ordner = mkdtempSync(join(tmpdir(), "ws-kachel-"));
|
||||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||||
process.env.PORT = String(PORT);
|
||||
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
||||
@@ -102,570 +226,131 @@ 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";
|
||||
const { notbremse } = await import("./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}`;
|
||||
notbremse(180_000, "pruef-kachel-universum");
|
||||
await new Promise((r) => setTimeout(r, 800));
|
||||
|
||||
/* 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 { DatabaseSync } = await import("node:sqlite");
|
||||
const { scryptSync, randomBytes } = await import("node:crypto");
|
||||
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
const hash = scryptSync(code, salt, 64,
|
||||
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, 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;
|
||||
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();
|
||||
}
|
||||
|
||||
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();
|
||||
let chromium;
|
||||
try {
|
||||
({ chromium } = await import(
|
||||
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"));
|
||||
} catch { chromium = null; }
|
||||
|
||||
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 }));
|
||||
if (!chromium) {
|
||||
unklar("kein Playwright gefunden -- die Kacheln wurden nicht angesehen");
|
||||
} else {
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
const kontext = await browser.newContext({
|
||||
viewport: { width: 1440, height: 900 }, deviceScaleFactor: 2,
|
||||
});
|
||||
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");
|
||||
}
|
||||
const seite = await kontext.newPage();
|
||||
await seite.goto(`http://127.0.0.1:${PORT}/workspace/`, { waitUntil: "domcontentloaded" });
|
||||
await seite.click('.rolle[data-rolle="admin"]');
|
||||
await seite.fill("#code", "CODE-DOGI-0001");
|
||||
await seite.click("button[type=submit]");
|
||||
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
||||
await seite.waitForTimeout(2500);
|
||||
|
||||
/* 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");
|
||||
/* ERST ZU DEN KACHELN SCROLLEN. Beim ersten Lauf wurden NULL
|
||||
gemessen: Der Filter verlangt, dass die Kachel ganz im Bild
|
||||
liegt, und oben steht die Zentrale mit Uhr und Zahlen. Eine
|
||||
Messung, die nichts findet, ist keine Messung -- sie sah nur
|
||||
aus wie eine. */
|
||||
/* In die Mitte des Kachelfeldes, nicht an seinen Anfang: Dort
|
||||
stehen mehrere Reihen gleichzeitig im Bild, und je mehr Kacheln
|
||||
gemessen werden, desto eher ist die schlechteste dabei. */
|
||||
const anzahl = await seite.locator(".kachel").count();
|
||||
await seite.locator(".kachel").nth(Math.min(6, anzahl - 1)).scrollIntoViewIfNeeded();
|
||||
await seite.waitForTimeout(700);
|
||||
|
||||
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");
|
||||
}
|
||||
const stellen = await seite.evaluate(() =>
|
||||
[...document.querySelectorAll(".kachel")].map((k) => {
|
||||
const name = k.querySelector(".kachel__name");
|
||||
if (!name) return null;
|
||||
const r = name.getBoundingClientRect();
|
||||
/* SICHTBAR HEISST: die Zeile liegt im Bild -- nicht die ganze
|
||||
Kachel. Der erste Filter verlangte letzteres und liess genau
|
||||
EINE uebrig; gemessen wird aber der Punkt unter dem TEXT,
|
||||
und der braucht nur sich selbst. */
|
||||
if (r.width < 10 || r.top < 8 || r.bottom > window.innerHeight - 8) return null;
|
||||
const [tr, tg, tb] = getComputedStyle(name).color.match(/\d+/g).map(Number);
|
||||
/* DIE KACHELGRENZEN KOMMEN MIT. Ohne sie wurde der Messbereich
|
||||
aus der Textposition geschaetzt (`y - 70`) -- und lag damit
|
||||
OBERHALB der Kachel, im Seitenhintergrund. Die Gegenprobe
|
||||
lieferte zweimal exakt 153 Punkte; genau diese Gleichheit hat
|
||||
den Fehler verraten. Eine Messung, deren Ergebnis sich nicht
|
||||
aendert, wenn man das Gemessene entfernt, misst etwas
|
||||
anderes. */
|
||||
const kb = k.getBoundingClientRect();
|
||||
return {
|
||||
name: name.textContent.trim(), text: [tr, tg, tb],
|
||||
x: Math.round(r.left + r.width / 2), y: Math.round(r.top + r.height / 2),
|
||||
kachel: {
|
||||
links: Math.round(kb.left), oben: Math.round(kb.top),
|
||||
breite: Math.round(kb.width), hoehe: Math.round(kb.height),
|
||||
},
|
||||
};
|
||||
}).filter(Boolean));
|
||||
|
||||
/* 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})`);
|
||||
}
|
||||
/* DIE ANZAHL GEHÖRT IN DIE BEDINGUNG: Ohne sichtbare Kacheln wäre
|
||||
„alle lesbar" auch dann grün, wenn keine einzige gemessen wurde. */
|
||||
ok(stellen.length >= 4, `${stellen.length} Kacheln im Bild gemessen`);
|
||||
|
||||
/* =======================================================================
|
||||
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()}`); });
|
||||
/* DER DRITTE AUSGANG. Ohne diesen Sprung stuerzte der Abschnitt ab
|
||||
(`werte[0].k` auf einem leeren Feld) -- und ein Absturz sagt
|
||||
weniger als ein Fehlschlag: Er verschluckt die Zeilen danach und
|
||||
sieht aus wie ein Problem der Pruefung statt der Sache. */
|
||||
if (!stellen.length) {
|
||||
ok(false, "ohne sichtbare Kachel laesst sich kein Kontrast messen");
|
||||
} else {
|
||||
|
||||
const bild = pngLesen(await seite.screenshot({ type: "png" }));
|
||||
const werte = stellen.map((s) => {
|
||||
/* Mehrere Punkte je Kachel -- der Untergrund ist nicht
|
||||
gleichmäßig (Nebel, Vignette, Sterne). Der SCHLECHTESTE zählt:
|
||||
An der hellsten Stelle steht der Text am schwersten. */
|
||||
let schlecht = Infinity;
|
||||
for (const dx of [-30, -15, 0, 15, 30]) {
|
||||
for (const dy of [-4, 0, 4]) {
|
||||
const k = kontrast(s.text, pixel(bild, (s.x + dx) * 2, (s.y + dy) * 2));
|
||||
if (k < schlecht) schlecht = k;
|
||||
}
|
||||
}
|
||||
return { name: s.name, k: schlecht };
|
||||
}).sort((a, b) => a.k - b.k);
|
||||
|
||||
ok(stellen.length > 0 && werte[0].k >= 4.5,
|
||||
`schlechtester Textkontrast ${werte[0].k.toFixed(2)}:1 auf „${werte[0].name}" (Grenze 4,5)`);
|
||||
melde(` die drei schwächsten: ${werte.slice(0, 3)
|
||||
.map((w) => `${w.name} ${w.k.toFixed(1)}:1`).join(" · ")}`);
|
||||
|
||||
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})`);
|
||||
await seite.screenshot({ path: "server/pruef-kachel-universum.png", fullPage: false });
|
||||
melde(" -- Bildschirmfoto: server/pruef-kachel-universum.png");
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================================================================
|
||||
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);
|
||||
melde(`${geprueft} Pruefungen, ${fehler} Fehler`
|
||||
+ (offen ? `, ${offen} nicht nachsehbar` : ""));
|
||||
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
||||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
|
||||
process.exit(fehler ? 1 : 0);
|
||||
|
||||
Reference in New Issue
Block a user