WAS DAS BILD GEZEIGT HAT
Beim Durchsehen der Einzelbilder einer Videoaufnahme -- vier
Sekunden lang, gleich nach dem Laden:
---- SPICY MEDIA ----
Zentrale
WIRD GELADEN
Und zwar auf der Startseite eines COMMUNITY-MITGLIEDS.
In start.html steht die Vorgabe des Agenturhauses. Das Teamhaus
bekommt eigene Worte ("Team Dogi" / "Die IrrenAnstalt"), aber erst
wenn /api/ich geantwortet hat. Bis dahin las jeder Modi und jedes
Community-Mitglied die Marke der Agentur in der groessten Schrift
der Seite. Auf einem langsamen Telefon sekundenlang, bei jedem
Aufruf.
Das ist kein Schoenheitsfehler. Spicy Media ist der Betrieb hinter
der Agentur, und seit dem 24.09. sind die beiden Haeuser getrennt.
Genau diese Zeile hat die Trennung bei jedem Seitenaufruf kurz
aufgehoben -- und niemandem ist es aufgefallen, weil die Seite
DANACH richtig aussah. Wer hinsieht, sieht den Endzustand.
GELOEST OHNE SPRUNG
`data-wartet` macht die zwei Zeilen durchsichtig, nicht leer -- der
Platz bleibt stehen, es ruckt nichts. start.js nimmt das Merkmal
weg, sobald es weiss, in welchem Haus es ist; gemessen dauert das
351 ms.
Eine Notbremse in der Seite nimmt es nach vier Sekunden notfalls
selbst weg. Ohne sie waere die groesste Schrift der Seite fuer immer
unsichtbar, falls start.js gar nicht erst laeuft -- und "unsichtbar"
waere schlimmer als das falsche Wort.
DIE PRUEFUNG DAZU -- UND ZWEI MESSFEHLER DARIN
pruef-start-ansicht misst ab jetzt beide Richtungen: nach dem Laden
muessen beide Zeilen sichtbar sein, mit gesetztem `data-wartet`
unsichtbar. Ohne die zweite Haelfte koennte die Regel spurlos
verschwinden, ohne dass etwas rot wird.
Die Pruefung selbst hat mich zweimal getaeuscht, und beide Male
lehrreich:
1. Sie mass 900 ms nach der Anmeldung -- eine feste Pause. Ob
/api/ich in dieser Zeit geantwortet hat, haengt vom Rechner ab.
Dieselbe Pruefung war einmal gruen und einmal rot, bei
unveraendertem Code. Gewartet wird jetzt auf das Merkmal selbst,
und wie lange es gedauert hat, steht im Meldetext.
2. `opacity` hat einen Uebergang von 180 ms, und getComputedStyle
liefert waehrenddessen den laufenden Zwischenwert statt des
Ziels. Erst meldete die Gegenprobe "nicht verdeckt", dann die
Messung davor "nicht sichtbar" -- beide Male war die Regel in
Ordnung und nur die Animation im Weg. Fuer die Messung wird der
Uebergang jetzt abgeschaltet.
DAS VIDEOWERKZEUG
server/tiktok-videos.mjs nimmt die vier Clips auf. Sechs Aenderungen,
jede aus einem Einzelbild:
- DIE NACHTRUHE HAT DIE VORBEREITUNG MITBLOCKIERT. Video 2 zeigte
einen leeren Chat. Gemeldet wurde "gefuellt", weil nur geprueft
war, dass es den RAUM gibt. Jetzt werden die Nachrichten
zurueckgelesen und gezaehlt; unter acht wird nicht gefilmt.
- DOGI-MEDIA WAR LEER -- ein Video ueber Material zum Mitnehmen,
und auf dem Bildschirm stand "Gerade ist nichts frei". Sechs
echte Marken-Bilder werden eingestellt, zwei davon schon
genommen, damit die Aussage des Films auch im Bild steht.
- "WILL ICH AUCH - 0" an jedem Wunsch, waehrend der Untertitel
sagte "die anderen sehen, wer mitwill". Ein Versprechen, das das
Bild gleich wieder einkassiert, ist schlimmer als ein leeres
Brett. Jetzt wird ueber die echte Route gestimmt, absteigend
6/5/4 -- unter zehn Stimmen wird nicht gefilmt.
- DER WAECHTER FUER VIDEO 2 verglich zwei feste Zahlen
(TREFF_NACHT_AB === "0"). Das ist die Abschrift einer Einstellung
und keine Frage nach dem Zustand: Jedes andere Fenster, das den
Chat genauso schlafen legt, wurde abgewiesen. Gefragt wird jetzt
`istNachtruhe()` -- dieselbe Funktion, die auch die Seite
befragt. Mit 12 bis 6 steht im Bild "Ab 06:00 Uhr geht es
weiter" statt "Ab 24:00 Uhr", und das ergibt fuer einen
Zuschauer ueberhaupt erst Sinn.
- ROLLEN IM KASTEN STATT IN DER SEITE. `window.scrollBy` bewegt
die Seite; im Chat rollt aber der Verlauf in seinem eigenen
Kasten. Drei Bilder hintereinander sahen gleich aus.
- EINE FESTE PIXELZAHL TRAF DEN FALSCHEN BLOCK. Video 4 filmte
den Katalog der fertigen Vorschlaege statt der Wuensche, weil
"480 nach unten" zufaellig dort endete. `b.zu(wahl)` misst, wo
das Element steht.
Dazu: Umlaute in allen Bildtexten ("Tueren" stand im Video), der
Abspann bleibt am Ende stehen (vorher sah man die letzte Sekunde
wieder die App), und die Dateigroessen im Regal sind echt.
KEIN LINK, UND ZWAR GEMESSEN
Nach jedem Seitenwechsel wird der SICHTBARE Text nach Adressen
abgesucht -- dogfather-universe, https://, www., jeder Domainname.
Bei einem Fund bricht die Aufnahme ab, und es wird keine Datei
geschrieben. Das mit dem Auge zu pruefen waere genau die Sorte
Kontrolle, die beim vierten Video nachlaesst.
Mit PROBE_LECK=ja schiebt die Suche selbst eine Adresse ins Bild --
nachgefahren, Rueckgabewert 2. Eine Suche, die nur "nichts gefunden"
sagen kann, hat nichts bewiesen.
Gemessen: pruef-start-ansicht 160/0 (vorher 157), pruef-buehne
242/0, pruef-handy 177/0, pruef-kachelraster 24/0,
pruef-css-klassen 33/0.
1235 lines
66 KiB
JavaScript
1235 lines
66 KiB
JavaScript
/* Browserpruefung der Startseite: Gruppen, Zeichen, Farbtoene, Zahlen auf
|
||
den Kacheln -- und was jede Rolle davon zu sehen bekommt. */
|
||
import { mkdtempSync, rmSync } from "node:fs";
|
||
import { tmpdir } from "node:os";
|
||
import { join } from "node:path";
|
||
|
||
const ordner = mkdtempSync(join(tmpdir(), "ws-start-"));
|
||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||
/* DER PORT MUSS MEINER SEIN (nachgeruestet 19.09.2026).
|
||
Ohne diese Zeile startet der eigene Server bei belegtem Port
|
||
STILL nicht -- und alles Folgende misst gegen einen fremden
|
||
Stand. Das ist der dritte Ausgang: belegt ist etwas anderes
|
||
als kaputt. */
|
||
const PORT = await eigenerPort(import.meta, "pruef-start-ansicht");
|
||
process.env.PORT = `${PORT}`;
|
||
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
||
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
|
||
process.env.SITE_ACCESS_CODE_VANVAN = "VANVAN-TEST-1";
|
||
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
||
|
||
const express = (await import("express")).default;
|
||
const ec = express.response.cookie;
|
||
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
||
import { notbremse } from "./helfer-notbremse.mjs";
|
||
import { eigenerPort } from "./helfer-port.mjs";
|
||
await import("./index.js");
|
||
/* DIE NOTBREMSE. Muss NACH index.js stehen: Der Server haengt dort
|
||
zwei Auffangnetze ein, die einen Absturz nur protokollieren --
|
||
richtig fuer den Betrieb, fatal fuer eine Pruefung. Ohne diese
|
||
Zeile bleibt der Prozess nach einem Fehler ewig stehen, weil der
|
||
Server ihn am Leben haelt (siehe helfer-notbremse.mjs). */
|
||
notbremse(480_000, "pruef-start-ansicht");
|
||
await new Promise((r) => setTimeout(r, 700));
|
||
const BASIS = `http://127.0.0.1:${PORT}`;
|
||
setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 200_000).unref?.();
|
||
|
||
/* ORTSZEIT, nicht UTC. Die Anwendung rechnet "heute" nach der Uhr des
|
||
Benutzers; toISOString() liefert dagegen UTC und damit zwischen
|
||
Mitternacht und 2 Uhr den VORTAG. In diesem Fenster schlug diese
|
||
Pruefung fehl, ohne dass etwas kaputt war. */
|
||
const tagText = (d = new Date()) =>
|
||
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`
|
||
+ `-${String(d.getDate()).padStart(2, "0")}`;
|
||
|
||
let fehler = 0;
|
||
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||
|
||
await fetch(BASIS + "/workspace/api/anmelden", {
|
||
method: "POST", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
|
||
|
||
const { DatabaseSync } = await import("node:sqlite");
|
||
const { scryptSync, randomBytes } = await import("node:crypto");
|
||
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
||
const jetzt = new Date().toISOString();
|
||
const heute = tagText();
|
||
const gestern = tagText(new Date(Date.now() - 86400_000));
|
||
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, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
|
||
.run(name, rolle, hash, salt, 32768, jetzt);
|
||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||
}
|
||
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
||
const idScout = anlegen("Patrick", "scout", "CODE-SCOU-0001");
|
||
const idAnna = anlegen("Tili", "creator", "CODE-ANNA-0001");
|
||
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idAnna, idScout, jetzt);
|
||
|
||
/* Arbeit an mehreren Stellen -- damit auf den Kacheln wirklich Zahlen
|
||
stehen und man sieht, ob sie an der richtigen landen. */
|
||
for (const [status, frist] of [["offen", gestern], ["offen", gestern], ["offen", heute],
|
||
["arbeit", null], ["review", null]]) {
|
||
d.prepare("INSERT INTO aufgaben (titel, status, creator_id, frist, erstellt, erstellt_von) VALUES ('Test',?,?,?,?,?)")
|
||
.run(status, idAnna, frist, jetzt, idDogi);
|
||
}
|
||
d.prepare("INSERT INTO termine (titel, beginn, creator_id, erledigt, erstellt, erstellt_von) VALUES ('Heute',?,?,0,?,?)")
|
||
.run(heute + "T18:00", idAnna, jetzt, idDogi);
|
||
d.prepare("INSERT INTO eintraege (bereich, art, titel, datum, dringlichkeit, status, creator_id, erstellt, erstellt_von) VALUES ('technik','problem','Ton weg',?,'hoch','offen',?,?,?)")
|
||
.run(heute, idAnna, jetzt, idDogi);
|
||
d.close();
|
||
|
||
const { chromium } = await import(
|
||
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||
const browser = await chromium.launch();
|
||
|
||
async function anmelden(kontext, rolle, code) {
|
||
const seite = await kontext.newPage();
|
||
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
||
await seite.click(`.rolle[data-rolle="${rolle}"]`);
|
||
await seite.fill("#code", code);
|
||
await seite.click("button[type=submit]");
|
||
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
||
await seite.waitForTimeout(900);
|
||
return seite;
|
||
}
|
||
|
||
const lesen = (seite) => seite.evaluate(() => ({
|
||
gruss: document.getElementById("gruss")?.textContent,
|
||
/* Die Begrüßung ist ein Kopf aus vier Teilen geworden: Datum mit
|
||
Kalenderwoche, ein Wort zum Tag, die Plakette in Rollenfarbe und
|
||
ein Satz zur Lage. Jeder Teil wird einzeln nachgesehen -- "sieht
|
||
gut aus" ist kein Nachweis. */
|
||
datum: document.getElementById("datum")?.textContent || "",
|
||
grussWort: document.querySelector(".gruss__wort")?.textContent || "",
|
||
grussName: document.querySelector(".gruss__name")?.textContent || "",
|
||
plakette: document.getElementById("plakette")?.textContent || "",
|
||
plaketteRolle: document.getElementById("plakette")?.dataset.rolle || "",
|
||
plaketteRund: getComputedStyle(document.getElementById("plakette") || document.body).borderRadius,
|
||
marker: document.getElementById("tagesmarker")?.hidden === false
|
||
? document.getElementById("tagesmarker").textContent : null,
|
||
lage: document.getElementById("lage")?.hidden === false
|
||
? document.getElementById("lage").textContent : null,
|
||
lageStufe: document.getElementById("lage")?.dataset.stufe || null,
|
||
/* Der Schriftzug oben links, aufgeteilt in Rang. */
|
||
markeHaupt: document.querySelector(".kopfleiste .marke__haupt")?.textContent || "",
|
||
markeLeise: document.querySelector(".kopfleiste .marke__leise")?.textContent || "",
|
||
markeRauten: document.querySelectorAll(".kopfleiste .marke__raute").length,
|
||
gruppenNamen: [...document.querySelectorAll(".gruppe__name")].map((g) => g.textContent),
|
||
gruppen: [...document.querySelectorAll(".gruppe")].map((g) => ({
|
||
name: g.querySelector(".gruppe__name")?.textContent,
|
||
kacheln: g.querySelectorAll(".kachel").length,
|
||
})),
|
||
kacheln: [...document.querySelectorAll(".kachel")].map((k) => ({
|
||
name: k.querySelector(".kachel__name")?.textContent,
|
||
unter: k.querySelector(".kachel__unter")?.textContent,
|
||
ton: k.dataset.ton,
|
||
/* DIE SILBERNE KACHEL ÜBERSCHREIBT IHREN TON (siehe start.css,
|
||
23.09.2026) -- absichtlich, und nur in der Anzeige. Wer das
|
||
nicht mitliest, vergleicht ihre Silberfläche mit dem Grün ihres
|
||
Bereichs und meldet einen Fehler, den es nicht gibt. */
|
||
silber: k.dataset.silber === "ja",
|
||
hatZeichen: !!k.querySelector(".kachel__svg"),
|
||
farbe: k.querySelector(".kachel__svg")
|
||
? getComputedStyle(k.querySelector(".kachel__svg")).stroke : null,
|
||
pfade: k.querySelectorAll(".kachel__svg path").length,
|
||
zahl: k.querySelector(".kachel__zahl")?.textContent || null,
|
||
warn: k.querySelector(".kachel__zahl")?.dataset.warn === "ja",
|
||
ziel: k.querySelector(".kachel__link")?.getAttribute("href"),
|
||
})),
|
||
/* Abgeschnittener Text ist der haeufigste stille Fehler in einer
|
||
Kachel -- er sieht auf den ersten Blick nach Absicht aus. Gemessen
|
||
wird die tatsaechliche Textbreite gegen die verfuegbare. */
|
||
abgeschnitten: [...document.querySelectorAll(".kachel__name, .kachel__unter")]
|
||
.filter((e) => e.scrollWidth > e.clientWidth + 1)
|
||
.map((e) => e.textContent),
|
||
hinweise: [...document.querySelectorAll("#dran .dran__punkt")]
|
||
.filter((z) => z.offsetParent !== null).length,
|
||
hinweiseGesamt: document.querySelectorAll("#dran .dran__punkt").length,
|
||
/* Jede Hinweiszeile traegt Zeichen und Farbe der Kachel, zu der sie
|
||
fuehrt. Geprueft wird beides an den echten Bildpunkten, nicht an der
|
||
Absicht im Quelltext. */
|
||
dranZeilen: [...document.querySelectorAll("#dran .dran__punkt")].map((z) => ({
|
||
ton: z.dataset.ton || null,
|
||
stufe: z.dataset.stufe,
|
||
pfade: z.querySelectorAll(".dran__svg path").length,
|
||
/* KEIN ABSTURZ BEI EINEM FEHLENDEN TEIL (20.09.2026).
|
||
Hier stand `getComputedStyle(z.querySelector(".dran__svg"))` --
|
||
und als gestern ein Hinweis ohne Kachel dazukam, hatte die Zeile
|
||
kein Zeichen. Die Pruefung starb mit "parameter 1 is not of type
|
||
Element" und sagte damit gar nichts ueber die Seite. Ein
|
||
fehlendes Teil ist ein BEFUND, kein Absturz. */
|
||
hatZeichen: !!z.querySelector(".dran__svg"),
|
||
strich: z.querySelector(".dran__svg")
|
||
? getComputedStyle(z.querySelector(".dran__svg")).stroke : null,
|
||
zahl: z.querySelector(".dran__zahl")?.textContent || null,
|
||
zahlFarbe: z.querySelector(".dran__zahl")
|
||
? getComputedStyle(z.querySelector(".dran__zahl")).color : null,
|
||
text: z.querySelector(".dran__text")?.textContent || "",
|
||
label: z.querySelector(".dran__link")?.getAttribute("aria-label") || "",
|
||
})),
|
||
/* DIE GROSSE KACHEL AUSDRUECKLICH, NICHT DIE ERSTE (11.09.2026).
|
||
|
||
Hier stand `querySelector(".kachel__zeichen")` -- also die erste
|
||
Kachel der Seite, welche auch immer das gerade ist. Solange
|
||
"Taeglich" oben stand, war das zufaellig die grosse Dashboard-Kachel
|
||
mit 46 px. Seit "Rund um das Team" davor steht, ist es eine normale
|
||
mit 40 px, und die Pruefung wurde rot -- ohne dass an einer Kachel
|
||
irgendetwas kleiner geworden waere.
|
||
|
||
Die Pruefung hat also nie gemessen, was ihr Kommentar behauptet
|
||
("die Kacheln sollen spuerbar groesser sein"), sondern "die erste
|
||
ist die grosse". Jetzt werden BEIDE Groessen gemessen: die grosse
|
||
Kachel an ihrem Mass und die kleinste aller Kacheln an ihrem. */
|
||
zeichenGroesse: Math.round(document.querySelector('[data-gross="ja"] .kachel__zeichen')
|
||
?.getBoundingClientRect().width || 0),
|
||
zeichenKleinstes: Math.round(Math.min(...[...document.querySelectorAll(".kachel__zeichen")]
|
||
.map((e) => e.getBoundingClientRect().width), Infinity)),
|
||
namenKleinstes: Math.round(Math.min(...[...document.querySelectorAll(".kachel__name")]
|
||
.map((e) => parseFloat(getComputedStyle(e).fontSize) || 0), Infinity)),
|
||
wasserzeichen: document.querySelectorAll(".kachel__wasserzeichen").length,
|
||
wasserzeichenDeckung: getComputedStyle(document.querySelector(".kachel__wasserzeichen") || document.body).opacity,
|
||
wasserzeichenSchwach: Number(getComputedStyle(
|
||
document.querySelector(".kachel__wasserzeichen") || document.body).opacity) <= 0.15,
|
||
namenGroesse: Math.round(parseFloat(getComputedStyle(
|
||
document.querySelector('[data-gross="ja"] .kachel__name')
|
||
|| document.querySelector(".kachel__name")).fontSize) || 0),
|
||
kachelHoehe: Math.round(document.querySelectorAll(".kachel")[1]?.getBoundingClientRect().height || 0),
|
||
zahlenLeer: !document.getElementById("zahlen-leer")?.hidden,
|
||
zahlen: [...document.querySelectorAll(".zahl")].length,
|
||
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||
ueberstehend: [...document.querySelectorAll("main *")]
|
||
.filter((e) => e.getBoundingClientRect().right > window.innerWidth + 1)
|
||
.map((e) => String(e.className).slice(0, 40)).slice(0, 4),
|
||
}));
|
||
|
||
for (const [breite, wie] of [[1440, "Computer"], [390, "Handy"]]) {
|
||
console.log(`\n=== DogFather · ${wie}`);
|
||
const kontext = await browser.newContext({ viewport: { width: breite, height: 1200 } });
|
||
const konsole = [];
|
||
const seite = await anmelden(kontext, "admin", "CODE-DOGI-0001");
|
||
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
|
||
seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message));
|
||
await seite.reload({ waitUntil: "networkidle" });
|
||
await seite.waitForTimeout(900);
|
||
|
||
const r = await lesen(seite);
|
||
console.log(" Gruß: ", r.gruss);
|
||
console.log(" Gruppen:", r.gruppen.map((g) => `${g.name} (${g.kacheln})`).join(" · "));
|
||
console.log(" Zahlen: ", r.kacheln.filter((k) => k.zahl).map((k) => `${k.name}=${k.zahl}${k.warn ? "!" : ""}`).join(" · "));
|
||
|
||
/* VIER, nicht drei (berichtigt 10.09.2026).
|
||
|
||
Die Erwartung stand auf drei, seit b45de94 eine vierte Gruppe
|
||
hinzugefuegt hat ("Rund um das Team -- die Arbeitslage von Managern
|
||
und Scouts"). Die Anwendung war richtig, die Zahl hier alt -- und
|
||
diese Pruefung damit rot, ohne dass es jemandem aufgefallen ist.
|
||
|
||
Gefunden hat sie nicht ein Gesamtlauf, sondern die Frage "was kann
|
||
an meiner Aenderung kaputtgehen?" -- und danach die Gegenprobe auf
|
||
dem unveraenderten Stand, die zeigte: schon vorher rot, nicht von
|
||
mir.
|
||
|
||
GEPRUEFT WERDEN JETZT DIE NAMEN, nicht nur die Anzahl. Vier Gruppen
|
||
koennten auch drei richtige und eine doppelte sein, und dann stuende
|
||
hier wieder eine gruene Zahl ueber einem falschen Bildschirm. */
|
||
/* NACHTRAG 10.09.2026: FUENF -- "Team Dogi" an zweiter Stelle.
|
||
|
||
Filipe musste die Kacheln seines Teams zwischen den uebrigen suchen.
|
||
Sie haben jetzt eine eigene Gruppe, und die steht bewusst NICHT am
|
||
Ende: `gruppeNach` in server/workspace.js schiebt sie direkt hinter
|
||
die taegliche Arbeit. Die REIHENFOLGE ist hier deshalb die eigentliche
|
||
Aussage -- eine Gruppe, die ans Ende rutscht, ist genau der Fehler,
|
||
den diese Zeile finden soll, und auf dem Bildschirm faellt er kaum auf.
|
||
|
||
Die Gruppe kommt vom Server (`bereiche_zusatz`), nicht aus
|
||
bereiche.js -- sie steht in keiner Datei, die jeder herunterladen kann.
|
||
Die Kachelzahl blieb bei 24: der Eingang ist umgezogen, das Ideen-Board
|
||
aus bereiche.js hierher gewandert, keine Kachel ist neu. */
|
||
/* NACHTRAG 11.09.2026: SECHS -- "Entwicklung & Nachwuchs" an dritter
|
||
Stelle.
|
||
|
||
Sie steht dort nicht zufaellig: `gruppeNach: "Team Dogi"` setzt sie
|
||
direkt hinter das Team. Was ueber Menschen aufgeschrieben wird,
|
||
gehoert neben das Team und nicht zwischen die taeglichen
|
||
Werkzeuge -- und schon gar nicht ans Ende, wo es niemand ansieht.
|
||
|
||
Die Kachelzahl steigt von 25 auf 27; beide Kacheln sind neu
|
||
(Entwicklung, Talente), keine ist umgezogen. */
|
||
/* NEUE REIHENFOLGE AM 11.09.2026 (Filipe, zwei Wuensche an einem Tag).
|
||
|
||
1. "ich will dass die kategorie [Rund um das Team] ueber der
|
||
kategorie ist [Taeglich]" -- wer die Leitung hat, fragt zuerst,
|
||
wo es im Team klemmt.
|
||
2. "diese zwei kategorien sollen ... ganz unten sein ... goldene
|
||
regel das sind private informationen von meiner community."
|
||
|
||
Der Absatz darueber sagte bis heute das Gegenteil ("und schon gar
|
||
nicht ans Ende, wo es niemand ansieht"). Das war meine Begruendung,
|
||
nicht seine Entscheidung -- und bei privaten Dingen wiegt "faellt
|
||
niemandem ins Auge" schwerer als "wird oft angesehen". */
|
||
/* SIEBEN SEIT DEM 11.09.2026 -- "Community" kam dazu, der Treff.
|
||
|
||
Sie steht GANZ UNTEN, und zwar aus demselben Grund wie die beiden
|
||
Gruppen davor: Dort stehen Namen und Saetze von Zuschauern, also
|
||
private Dinge fremder Leute. Wer sich den Bildschirm teilt, hat sie
|
||
dann nicht als Erstes im Blick.
|
||
|
||
Sie hat deshalb ausdruecklich KEIN `gruppeNach` -- eine Gruppe ohne
|
||
Angabe haengt hinten an, und das ist hier die richtige Stelle. */
|
||
/* ACHT SEIT DEM 19.09.2026 -- "Moderation" kam dazu.
|
||
|
||
Filipe: "ich will das oben die sachen fuer dogfather sind, dan die
|
||
sachen fuer modis und dan community bereich."
|
||
|
||
Die Moderationskachel trug bis dahin dieselbe Gruppe wie die
|
||
sieben Bretter und verschwand deshalb HINTER ihnen. Jetzt hat sie
|
||
eine eigene Gruppe und steht davor -- wer moderiert, muss nicht an
|
||
der ganzen Community vorbeiscrollen, um an seine Arbeit zu kommen.
|
||
|
||
DIE BEGRUENDUNG OBEN GILT WEITER: Die Community steht trotzdem
|
||
ganz unten, denn dort stehen Namen und Saetze von Zuschauern. Die
|
||
Moderation davor ist kein Widerspruch dazu -- sie zeigt Vorgaenge,
|
||
keine Beitraege.
|
||
|
||
Und die Arbeitsgruppen behalten ihre gewachsene Reihenfolge: Die
|
||
Sortierregel in start.js schiebt ausdruecklich NUR die drei
|
||
hinteren ans Ende. Ein erster Versuch schrieb auch die vorderen
|
||
fest und hat diese Prueflinie sofort rot gemacht -- zu Recht. */
|
||
/* DIE REGEL STATT DER AUFZAEHLUNG (21.09.2026).
|
||
|
||
Hier standen acht Gruppennamen in fester Reihenfolge. Am
|
||
21.09.2026 hat Filipe die Kacheln getrennt -- "ich will keine
|
||
mischung mehr von kacheln" --, und auf der Agenturadresse stehen
|
||
seitdem fuenf statt acht. Die Zeile war rot, obwohl genau das
|
||
Gewuenschte passiert ist.
|
||
|
||
WAS SIE WIRKLICH SCHUETZEN SOLLTE, steht im Kommentar darueber:
|
||
"Die Community steht ganz unten, denn dort stehen Namen und
|
||
Saetze von Zuschauern." Das ist eine Aussage ueber die
|
||
REIHENFOLGE, nicht ueber den Bestand -- und sie bleibt richtig,
|
||
egal wie viele Gruppen es gibt.
|
||
|
||
GEPRUEFT WIRD SIE DESHALB BEDINGT: Ist die Community da, steht
|
||
sie zuletzt. Ist die Moderation da, steht sie direkt davor. Auf
|
||
einer Adresse, auf der es beide nicht gibt, misst die Zeile
|
||
nichts -- und sagt das auch, statt gruen zu tun. */
|
||
const namen = r.gruppen.map((g) => g.name);
|
||
ok(namen.length >= 3, `${namen.length} Gruppen (${namen.join(" · ")})`);
|
||
|
||
const stelle = (n) => namen.indexOf(n);
|
||
const community = stelle("Community");
|
||
const moderation = stelle("Moderation");
|
||
|
||
if (community === -1 && moderation === -1) {
|
||
/* DRITTER AUSGANG: Auf dieser Adresse gibt es beide nicht. Das ist
|
||
kein Befund -- aber auch kein bestandener Test. */
|
||
console.log(" -- auf dieser Adresse gibt es weder Community noch Moderation");
|
||
} else {
|
||
ok(community === -1 || community === namen.length - 1,
|
||
community === -1
|
||
? "keine Community-Gruppe auf dieser Adresse"
|
||
: `die Community steht ganz unten (Platz ${community + 1} von ${namen.length})`);
|
||
ok(moderation === -1 || community === -1 || moderation === community - 1,
|
||
moderation === -1
|
||
? "keine Moderations-Gruppe auf dieser Adresse"
|
||
: `die Moderation direkt davor (Platz ${moderation + 1})`);
|
||
}
|
||
|
||
/* UND DIE ARBEIT STEHT VORN. Das ist die andere Haelfte derselben
|
||
Aussage: Wer den Arbeitsplatz oeffnet, soll nicht an fremden
|
||
Namen vorbeiscrollen, um an seine Aufgaben zu kommen. */
|
||
const arbeit = namen.findIndex((n) => n === "Täglich");
|
||
ok(arbeit === -1 || community === -1 || arbeit < community,
|
||
arbeit === -1
|
||
? "keine Gruppe „Täglich“ auf dieser Adresse"
|
||
: community === -1
|
||
? `die tägliche Arbeit steht auf Platz ${arbeit + 1}; eine Community-Gruppe `
|
||
+ "gibt es hier nicht, also ist nichts zu vergleichen"
|
||
: `die tägliche Arbeit steht vor der Community (${arbeit + 1} vor ${community + 1})`);
|
||
/* WIE VIELE KACHELN? Nicht abgeschrieben, sondern aus bereiche.js
|
||
gezaehlt (06.09.2026).
|
||
|
||
Hier stand `=== 18`, mit einem Kommentar, der erklaerte, warum es
|
||
genau achtzehn sind. Am 06.09. kamen Chat, Zahlen und Agentur dazu
|
||
-- und die Pruefung meldete drei Tage lang einen Fehler, den es
|
||
nicht gab. Eine Pruefung, die bei jeder neuen Kachel rot wird,
|
||
erzieht dazu, ihr Rotwerden zu ignorieren; genau dann uebersieht
|
||
man das Mal, an dem WIRKLICH eine Kachel fehlt.
|
||
|
||
Gezaehlt wird deshalb, was diese Rolle laut bereiche.js sehen
|
||
SOLL. Fehlt eine auf dem Bildschirm, faellt es weiterhin auf --
|
||
und eine neue kostet keinen Handgriff mehr. */
|
||
/* NACHTRAG 10.09.2026: Es gibt eine ZWEITE Quelle.
|
||
|
||
Seit dem 09.09. haengt der Server einzelnen Rollen Kacheln an, die
|
||
gar nicht in bereiche.js stehen (`bereiche_zusatz`) -- gerade damit
|
||
sie nicht in einer Datei stehen, die jeder herunterladen kann. Die
|
||
Zaehlung oben kannte nur die Datei und meldete deshalb "alle 23
|
||
Kacheln stehen auf der Seite (24)": ein Fehler, den es nicht gab.
|
||
|
||
Das ist genau derselbe Fall wie am 06.09., nur eine Ebene weiter --
|
||
und dieselbe Antwort: nicht die Zahl anpassen, sondern die zweite
|
||
Quelle mitzaehlen. Gefragt wird jetzt BEIDE. */
|
||
const sollKacheln = await seite.evaluate(async () => {
|
||
const ausDatei = window.Bereiche.GRUPPEN
|
||
.flatMap((g) => g.bereiche).filter((b) => b.rollen.includes("admin")).length;
|
||
const a = await fetch("/workspace/api/ich", { credentials: "same-origin" });
|
||
const ich = a.ok ? await a.json() : {};
|
||
return ausDatei + (ich.bereiche_zusatz || []).length;
|
||
});
|
||
ok(sollKacheln > 15, `Datei und Server nennen zusammen ${sollKacheln} Kacheln fuer DogFather`);
|
||
ok(r.kacheln.length === sollKacheln,
|
||
`alle ${sollKacheln} Kacheln stehen auf der Seite (${r.kacheln.length})`);
|
||
ok(/^(Guten Morgen|Guten Mittag|Guten Tag|Guten Abend|Späten Abend|Noch wach), Filipe$/
|
||
.test(r.gruss || ""), `Begrüßung nach Tageszeit ("${r.gruss}")`);
|
||
/* Der Name muss eigenständig ausgezeichnet sein -- er ist das Wichtigste
|
||
an der Zeile und wird kräftiger gesetzt als der Gruß davor. */
|
||
ok(r.grussName === "Filipe", `der Name steht für sich (${r.grussName})`);
|
||
ok(r.grussWort.trim().endsWith(","), `und das Grußwort davor (${r.grussWort.trim()})`);
|
||
|
||
/* Datumszeile: Wochentag, Datum, Kalenderwoche. Die KW wird selbst
|
||
gerechnet (ISO 8601) -- eine falsche KW wäre schlimmer als keine. */
|
||
console.log(" Datum: ", r.datum, r.marker ? `· ${r.marker}` : "");
|
||
/* Kein \w im Monatsnamen: "März" enthält einen Umlaut, den \w ohne
|
||
Unicode-Schalter nicht kennt. Diese Prüfung wäre im März stillschweigend
|
||
rot geworden -- ein Fehler, der sieben Monate lang unsichtbar wartet. */
|
||
ok(/^(Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag|Sonntag), \d{1,2}\. [A-Za-zÄÖÜäöü]+ · KW \d{1,2}$/
|
||
.test(r.datum), `Datum mit Kalenderwoche ("${r.datum}")`);
|
||
const kw = Number((r.datum.match(/KW (\d+)/) || [])[1]);
|
||
ok(kw >= 1 && kw <= 53, `die Kalenderwoche ist plausibel (${kw})`);
|
||
/* An Tagen mit Charakter steht ein Wort dabei -- an den übrigen nicht.
|
||
Beides ist richtig, aber es muss zum Wochentag passen. */
|
||
const tag = r.datum.split(",")[0];
|
||
const erwartet = { Montag: "Neue Woche", Freitag: "Endspurt",
|
||
Samstag: "Wochenende", Sonntag: "Wochenende" }[tag] || null;
|
||
ok(r.marker === erwartet,
|
||
`das Wort zum Tag passt zum ${tag} (${r.marker || "keins"}, erwartet ${erwartet || "keins"})`);
|
||
|
||
/* Die Plakette: Anfangsbuchstabe, rund, in der Farbe der Rolle. */
|
||
ok(r.plakette === "F", `die Plakette trägt den Anfangsbuchstaben (${r.plakette})`);
|
||
ok(r.plaketteRolle === "admin", `und die Rolle für ihre Farbe (${r.plaketteRolle})`);
|
||
ok(/50%|9999px/.test(r.plaketteRund), `und ist rund (${r.plaketteRund})`);
|
||
|
||
/* Der Satz zur Lage kommt aus denselben Hinweisen wie die Liste
|
||
darunter -- keine zweite Zählung. Hier liegt Überfälliges vor,
|
||
also muss er das sagen und rot sein. */
|
||
console.log(" Lage: ", r.lage);
|
||
ok(r.lageStufe === "warnung", `die Lage meldet Überfälliges (${r.lageStufe})`);
|
||
ok(/^\d+ Sachen? (ist|sind) überfällig/.test(r.lage || ""), `und sagt es im Klartext ("${r.lage}")`);
|
||
/* Sie muss zu den Hinweisen passen, sonst stünden zwei Wahrheiten
|
||
übereinander auf einem Bildschirm. */
|
||
const dringend = r.dranZeilen.filter((z) => z.stufe === "warnung")
|
||
.reduce((s, z) => s + Number(z.zahl), 0);
|
||
ok(Number((r.lage.match(/^\d+/) || [])[0]) === dringend,
|
||
`und stimmt mit den Hinweisen überein (${r.lage.match(/^\d+/)} = ${dringend})`);
|
||
|
||
/* Der Schriftzug oben links hat jetzt einen Rang statt einer Farbe.
|
||
Die geschützten Leerzeichen aus dem Quelltext bleiben erhalten (der
|
||
Schriftzug darf nicht umbrechen) -- beim Vergleich werden sie
|
||
deshalb gleichgesetzt, sonst prüft man Unsichtbares. */
|
||
const ohneNbsp = (t) => t.replace(/ /g, " ");
|
||
/* "Spicy & Dogi" seit dem 08.09.2026 (Wunsch von Filipe): links die
|
||
Chili von Spicy Media, rechts der Husky von Dogi, und der Name
|
||
dazwischen in genau dieser Reihenfolge. */
|
||
ok(ohneNbsp(r.markeHaupt) === "Spicy & Dogi", `der Name steht vorn ("${r.markeHaupt}")`);
|
||
ok(ohneNbsp(r.markeLeise) === "Creator Workspace", `der Ort dahinter ("${r.markeLeise}")`);
|
||
ok(r.markeRauten === 1, `und dazwischen genau eine Raute (${r.markeRauten})`);
|
||
/* Der geschützte Leerraum muss WIRKLICH noch da sein -- ginge er beim
|
||
Aufteilen verloren, bräche "Creator Workspace" auf dem Handy um. */
|
||
ok(r.markeHaupt.includes(" ") && r.markeLeise.includes(" "),
|
||
"und der Schriftzug kann weiterhin nicht umbrechen");
|
||
|
||
/* Jede Kachel muss ein Zeichen haben -- eine ohne waere ein leeres
|
||
Kaestchen und faellt sofort auf. */
|
||
/* Das Wasserzeichen: dasselbe Zeichen noch einmal, riesig und schwach
|
||
im Hintergrund. Es macht jede Kachel unverwechselbar -- fehlt es,
|
||
sehen alle wieder gleich aus. */
|
||
ok(r.wasserzeichen === r.kacheln.length,
|
||
`jede Kachel trägt ihr Wasserzeichen (${r.wasserzeichen} von ${r.kacheln.length})`);
|
||
ok(r.wasserzeichenSchwach, `und es ist schwach genug, um nicht zu stören (${r.wasserzeichenDeckung})`);
|
||
|
||
ok(r.kacheln.every((k) => k.pfade > 0),
|
||
`jede Kachel hat ein Zeichen (${r.kacheln.filter((k) => !k.pfade).map((k) => k.name).join(", ") || "alle"})`);
|
||
ok(r.kacheln.every((k) => k.unter && k.unter.length > 4), "jede Kachel sagt, was drin ist");
|
||
ok(r.kacheln.every((k) => k.ziel && k.ziel !== "#"), "jede Kachel führt irgendwohin");
|
||
|
||
/* Acht verschiedene Farben, und nirgends steht "ÖFFNEN" mehr. */
|
||
const toene = new Set(r.kacheln.map((k) => k.farbe));
|
||
/* JEDE Kachel ihre eigene Farbe -- siebzehn verschiedene, keine
|
||
doppelt. (Der Creator sieht "Mein Profil" statt "Creator-Profile";
|
||
das sind zwei Namen fuer denselben Bereich und teilen sich einen
|
||
Ton -- deshalb wird hier gegen die Zahl der KACHELN geprueft.) */
|
||
ok(toene.size === r.kacheln.length,
|
||
`jede Kachel hat ihre eigene Farbe (${toene.size} Farben auf ${r.kacheln.length} Kacheln)`);
|
||
const oeffnen = await seite.evaluate(() => document.body.textContent.includes("ÖFFNEN"));
|
||
ok(!oeffnen, "das Wort ÖFFNEN kommt nicht mehr vor");
|
||
|
||
/* Die Zahlen müssen auf der RICHTIGEN Kachel stehen. */
|
||
const auf = (n) => r.kacheln.find((k) => k.name === n);
|
||
ok(auf("Aufgaben")?.zahl === "4",
|
||
`Aufgaben trägt 4 (2 überfällig + 1 heute + 1 Review) (${auf("Aufgaben")?.zahl})`);
|
||
ok(auf("Aufgaben")?.warn === true, "und ist als Warnung markiert");
|
||
ok(auf("Kalender")?.zahl === "1", `Kalender trägt 1 (${auf("Kalender")?.zahl})`);
|
||
ok(auf("LIVE-Analyse")?.zahl === "1", `der dringende Bereichspunkt (${auf("LIVE-Analyse")?.zahl})`);
|
||
ok(!auf("Wissens-Bibliothek")?.zahl, "wo nichts liegt, steht keine Zahl");
|
||
|
||
/* Die Kacheln sollen spürbar größer sein als vorher (Zeichenfeld 38px,
|
||
Name .93rem). Gemessen statt geglaubt -- "sieht größer aus" ist kein
|
||
Nachweis. */
|
||
ok(r.zeichenGroesse >= 46, `grosse Kachel: Zeichenfeld mindestens 46px (${r.zeichenGroesse}px)`);
|
||
ok(r.namenGroesse >= 16, `grosse Kachel: Name mindestens 16px (${r.namenGroesse}px)`);
|
||
/* UND DIE KLEINSTE KACHEL HAT AUCH EIN MASS (11.09.2026).
|
||
|
||
Das ist der Teil, den die Pruefung bisher gar nicht hatte: Sie sah
|
||
immer nur EINE Kachel an. Die Untergrenzen sind die heute
|
||
gemessenen Werte -- sie sagen also "nicht kleiner als jetzt". Eine
|
||
ausgedachte Zahl waere hier falsch: Sie wuerde entweder sofort rot
|
||
oder nie. */
|
||
ok(r.zeichenKleinstes >= 40,
|
||
`kleinste Kachel: Zeichenfeld mindestens 40px (${r.zeichenKleinstes}px)`);
|
||
ok(r.namenKleinstes >= 12,
|
||
`kleinste Kachel: Name mindestens 12px (${r.namenKleinstes}px)`);
|
||
ok(r.kachelHoehe >= 80, `Kachel mindestens 80px hoch (${r.kachelHoehe}px)`);
|
||
|
||
ok(r.abgeschnitten.length === 0,
|
||
`kein Text wird abgeschnitten (${r.abgeschnitten.join(" | ") || "keiner"})`);
|
||
ok(r.hinweise <= 4, `höchstens vier Hinweise sichtbar (${r.hinweise} von ${r.hinweiseGesamt})`);
|
||
|
||
/* --- Die Hinweiszeilen ------------------------------------------------
|
||
Eine Zeile ohne Zeichen waere ein halbes Bauteil und faellt beim
|
||
Ueberfliegen sofort auf.
|
||
|
||
ZWEI SORTEN HINWEISE (20.09.2026). Bis gestern zaehlte jeder
|
||
Hinweis etwas -- "2 Aufgaben sind ueberfaellig". Seit es
|
||
"Bei dir klingelt nichts" gibt, stimmt das nicht mehr: Das ist
|
||
ein ZUSTAND, keine Menge. Eine erfundene "1" davor waere Rauschen,
|
||
und eine geborgte Kachelfarbe wuerde einen Bereich behaupten, zu
|
||
dem der Hinweis nicht gehoert.
|
||
|
||
Die Pruefung unterscheidet deshalb jetzt, statt einer Sorte alles
|
||
abzuverlangen. Das ist nicht die schwaechere Pruefung, sondern die
|
||
genauere: Sie sagt ausserdem, WIE VIELE von jeder Sorte da sind --
|
||
faellt eine Sorte ganz weg, sieht man es. */
|
||
ok(r.dranZeilen.length > 0, `es liegen Hinweise vor (${r.dranZeilen.length})`);
|
||
ok(r.dranZeilen.every((z) => z.pfade > 0),
|
||
`JEDE Hinweiszeile trägt ihr Zeichen (${r.dranZeilen.filter((z) => !z.pfade).length} ohne)`);
|
||
|
||
const zaehlende = r.dranZeilen.filter((z) => z.zahl !== null);
|
||
const zustaende = r.dranZeilen.filter((z) => z.zahl === null);
|
||
ok(zaehlende.length > 0 && zaehlende.every((z) => /^[1-9]\d*$/.test(z.zahl)),
|
||
`jeder ZÄHLENDE Hinweis stellt seine Zahl voran `
|
||
+ `(${zaehlende.length} zählend, ${zustaende.length} Zustand)`);
|
||
/* Und ein Zustand traegt dafuer einen Satz, der fuer sich steht. */
|
||
ok(zustaende.every((z) => z.text.length > 12),
|
||
`jeder ZUSTANDS-Hinweis sagt es in Worten `
|
||
+ `(${zustaende.map((z) => z.text.slice(0, 32)).join(" | ") || "keiner"})`);
|
||
/* Die Zahl steht vorn -- im Satz darf sie nicht noch einmal stehen. */
|
||
ok(r.dranZeilen.every((z) => !/^\d/.test(z.text)),
|
||
`und keine Zahl steht doppelt (${r.dranZeilen.map((z) => z.text).find((t) => /^\d/.test(t)) || "keine"})`);
|
||
/* Vorleseprogramme bekommen den ganzen Satz, nicht "2" und "Aufgaben sind
|
||
überfällig" als zwei Bruchstuecke. */
|
||
ok(r.dranZeilen.every((z) => z.label.length > 10
|
||
&& (z.zahl === null || z.label.includes(z.zahl))),
|
||
`und der ganze Satz steht für Vorleseprogramme dran `
|
||
+ `(${r.dranZeilen.filter((z) => z.label.length <= 10).length} zu kurz)`);
|
||
/* Farbe folgt dem Bereich, nicht dem Bauteil: Der Ton der Zeile muss
|
||
der Ton einer wirklich vorhandenen Kachel sein. */
|
||
const kachelToene = new Set(r.kacheln.map((k) => k.ton));
|
||
/* Ein Hinweis, der zu einem Bereich gehoert, traegt DESSEN Ton.
|
||
Einer, der zu keinem gehoert, traegt GAR KEINEN -- er soll keinen
|
||
Bereich behaupten. Beides wird geprueft; "hat irgendeinen Ton"
|
||
waere die schwaechere Aussage. */
|
||
const mitBereich = r.dranZeilen.filter((z) => z.ton !== null);
|
||
const ohneBereich = r.dranZeilen.filter((z) => z.ton === null);
|
||
ok(mitBereich.length > 0 && mitBereich.every((z) => kachelToene.has(z.ton)),
|
||
`jede Zeile MIT Bereich trägt dessen Farbton `
|
||
+ `(${mitBereich.map((z) => z.ton).join(",")})`);
|
||
ok(ohneBereich.every((z) => z.pfade > 0),
|
||
`und eine Zeile OHNE Bereich hat trotzdem ihr Zeichen `
|
||
+ `(${ohneBereich.length} solche)`);
|
||
/* Und zwar wirklich DIESELBE Farbe, an echten Bildpunkten gemessen --
|
||
"hat einen Ton" reicht nicht, sonst faellt nicht auf, wenn alle
|
||
Zeilen denselben tragen. */
|
||
/* ==== EINE AUSNAHME, UND SIE IST BEGRÜNDET (24.09.2026) ==========
|
||
|
||
Diese Zeile war rot, seit „Aufgaben" am 23.09.2026 die silberne
|
||
Kachel bekommen hat -- und es war kein Fehler. Die Kachel
|
||
überschreibt ihr `--ton` absichtlich (start.css: „Filipe hat nach
|
||
einer silbernen KACHEL gefragt, nicht nach einer silbernen
|
||
Füllung"). Der Hinweis in „Was ansteht" trägt weiter das Grün
|
||
SEINES BEREICHS, und das ist richtig: Er gehört zum Bereich
|
||
Aufgaben, nicht zur Gestaltung dieser einen Kachel.
|
||
|
||
Damit ist es dasselbe Muster wie bei den acht Prüfungen vom
|
||
23./24.09.: Die Prüfung ist nicht falsch geworden, sie ist
|
||
UNZUSTÄNDIG geworden -- eine Zusage, die jemand bewusst
|
||
zurückgenommen hat, und sie fordert sie weiter ein. Eine rote
|
||
Prüfung, die niemand mehr liest, sichert nichts.
|
||
|
||
STATT SIE ZU LOCKERN, WIRD SIE GENAUER: Die Silberkachel fällt
|
||
aus dem Farbvergleich heraus -- und bekommt dafür ihre eigene
|
||
Prüfung darunter. */
|
||
const silberToene = new Set(r.kacheln.filter((k) => k.silber).map((k) => k.ton));
|
||
const kachelFarbe = new Map(r.kacheln.map((k) => [k.ton, k.farbe]));
|
||
const falsch = r.dranZeilen.filter((z) => z.stufe !== "warnung")
|
||
.filter((z) => z.ton !== null && z.zahlFarbe !== null)
|
||
.filter((z) => !silberToene.has(z.ton))
|
||
.filter((z) => z.zahlFarbe !== kachelFarbe.get(z.ton));
|
||
ok(falsch.length === 0,
|
||
`und die Farbe ist die der Kachel (${falsch.map((z) => `${z.ton}: ${z.zahlFarbe} statt ${kachelFarbe.get(z.ton)}`).join(" | ") || "alle"})`);
|
||
/* UND DIE AUSNAHME WIRD SELBST GEPRÜFT. Ohne diese zwei Zeilen wäre
|
||
„ausgenommen" eine Lücke: Jede Kachel könnte sich `data-silber`
|
||
anhängen und dem Farbvergleich entkommen. */
|
||
const silber = r.kacheln.filter((k) => k.silber);
|
||
ok(silber.length <= 1,
|
||
`höchstens EINE silberne Kachel (${silber.length}: ${silber.map((k) => k.name).join(", ") || "keine"})`);
|
||
ok(silber.every((k) => !!k.ton),
|
||
`und sie trägt weiterhin ihre Tonnummer (${silber.map((k) => k.ton).join(",") || "—"}) `
|
||
+ "– die Farbwerkzeuge rechnen mit Zahlen, nicht mit dem, was man sieht");
|
||
/* Ueberfaellig schlaegt Bereichsfarbe -- dort muss Rot stehen, sonst
|
||
geht die Dringlichkeit in siebzehn Farben unter. */
|
||
const warnZeilen = r.dranZeilen.filter((z) => z.stufe === "warnung");
|
||
ok(warnZeilen.length > 0 && warnZeilen.every((z) => z.zahlFarbe === "rgb(255, 154, 162)"),
|
||
`überfällige Zeilen sind rot (${warnZeilen.length} Stück, ${warnZeilen[0]?.zahlFarbe})`);
|
||
/* Und die uebrigen eben NICHT rot -- sonst waere die Regel wertlos. */
|
||
ok(r.dranZeilen.filter((z) => z.stufe !== "warnung")
|
||
.every((z) => z.zahlFarbe !== "rgb(255, 154, 162)"),
|
||
"und nur die");
|
||
/* SIEBEN seit dem 08.09.2026: "Abgebrochen" ist als eigene Kategorie
|
||
dazugekommen (screen12). Vorher zaehlten abgebrochene Aufgaben
|
||
unsichtbar bei "Ueberfaellig" und "Heute faellig" mit.
|
||
Die Zahl steht bewusst hier in der Bedingung und nicht nur im
|
||
Meldetext -- eine Kategorie, die still verschwindet, faellt sonst
|
||
niemandem auf. */
|
||
ok(r.zahlen === 7, `die Aufgabenzahlen stehen (${r.zahlen})`);
|
||
ok(!r.zahlenLeer, "und nicht der Leer-Satz");
|
||
ok(r.ueber <= 0, `nichts steht über (${r.ueber}px, ${r.ueberstehend.join(", ")})`);
|
||
ok(konsole.length === 0, `keine Fehler in der Konsole${konsole.length ? " -> " + konsole.join(" | ") : ""}`);
|
||
|
||
await seite.screenshot({ path: breite === 1440 ? "pruef-start-computer.png" : "pruef-start-handy.png", fullPage: true });
|
||
await kontext.close();
|
||
}
|
||
|
||
console.log("\n=== Creator");
|
||
{
|
||
const kontext = await browser.newContext({ viewport: { width: 1440, height: 1200 } });
|
||
const seite = await anmelden(kontext, "creator", "CODE-ANNA-0001");
|
||
const r = await lesen(seite);
|
||
console.log(" Gruppen:", r.gruppen.map((g) => `${g.name} (${g.kacheln})`).join(" · "));
|
||
const namen = r.kacheln.map((k) => k.name);
|
||
ok(namen.includes("Mein Profil"), "sieht sein eigenes Profil");
|
||
ok(!namen.includes("Creator-Profile"), "und nicht die Liste aller Creator");
|
||
ok(!namen.includes("Personen & Zugänge"), "keine Personenverwaltung");
|
||
ok(!namen.includes("Scout-Pipeline"), "keine Scout-Pipeline");
|
||
ok(!namen.includes("Automationen"), "keine Automationen");
|
||
ok(r.gruppen.length === 3, `trotzdem drei Gruppen (${r.gruppen.length})`);
|
||
ok(r.kacheln.every((k) => k.pfade > 0), "alle Zeichen da");
|
||
/* Von "Team & System" bleibt für ihn genau eine Kachel übrig. Eine
|
||
Überschrift, die Team und System verspricht und nur die Bibliothek
|
||
zeigt, verspricht etwas Falsches -- deshalb heißt sie hier "Wissen". */
|
||
ok(r.gruppenNamen.includes("Wissen"), `die Gruppe heißt "Wissen" (${r.gruppenNamen.join(" · ")})`);
|
||
ok(!r.gruppenNamen.includes("Team & System"), "und nicht mehr \"Team & System\"");
|
||
/* Seine Begrüßung: eigene Rollenfarbe, und da nichts anliegt, der
|
||
ruhige Satz statt eines erfundenen Alarms. */
|
||
ok(r.plaketteRolle === "creator", `die Plakette kennt seine Rolle (${r.plaketteRolle})`);
|
||
/* Tili hat selbst überfällige Aufgaben -- die Lage MUSS das sagen.
|
||
Entscheidend ist nicht, welche Stufe herauskommt, sondern dass sie
|
||
zu dem passt, was direkt darunter steht. Zwei verschiedene Zahlen
|
||
auf einem Bildschirm wären der eigentliche Fehler. */
|
||
const cDringend = r.dranZeilen.filter((z) => z.stufe === "warnung")
|
||
.reduce((s, z) => s + Number(z.zahl), 0);
|
||
ok(cDringend > 0 ? r.lageStufe === "warnung" : r.lageStufe !== "warnung",
|
||
`die Lage passt zu seinen Hinweisen (${r.lageStufe}: "${r.lage}")`);
|
||
ok(cDringend === 0 || Number((r.lage.match(/^\d+/) || [])[0]) === cDringend,
|
||
`und zur Zahl darunter (${cDringend})`);
|
||
await seite.screenshot({ path: "pruef-start-creator.png", fullPage: true });
|
||
await kontext.close();
|
||
}
|
||
|
||
console.log("\n=== Scout");
|
||
{
|
||
const kontext = await browser.newContext({ viewport: { width: 1440, height: 1200 } });
|
||
const seite = await anmelden(kontext, "scout", "CODE-SCOU-0001");
|
||
const r = await lesen(seite);
|
||
const namen = r.kacheln.map((k) => k.name);
|
||
ok(namen.includes("Scout-Pipeline"), "sieht seine Pipeline");
|
||
ok(!namen.includes("Personen & Zugänge"), "keine Personenverwaltung");
|
||
ok(!namen.includes("Automationen"), "keine Automationen");
|
||
ok(namen.includes("Creator-Profile"), "aber die Creator-Profile");
|
||
await kontext.close();
|
||
}
|
||
|
||
/* Der Satz "Nichts offen" ist ein Kasten mit display:flex geworden. Damit
|
||
ist er staerker als das hidden des Browsers -- ohne eine eigene Regel
|
||
stuende er IMMER da, auch wenn Arbeit offen ist. Genau das wird hier
|
||
gemessen, in beide Richtungen. */
|
||
console.log("\n=== Der Satz 'Nichts offen' erscheint nur, wenn nichts offen ist");
|
||
{
|
||
const kontext = await browser.newContext({ viewport: { width: 1440, height: 1200 } });
|
||
const seite = await anmelden(kontext, "admin", "CODE-DOGI-0001");
|
||
const r = await seite.evaluate(() => {
|
||
const k = document.getElementById("zahlen-leer");
|
||
const zu = getComputedStyle(k).display;
|
||
k.hidden = false;
|
||
const auf = getComputedStyle(k).display;
|
||
const haken = k.querySelectorAll("svg path").length;
|
||
k.hidden = true;
|
||
return { zu, auf, haken };
|
||
});
|
||
ok(r.zu === "none", `versteckt heisst versteckt (${r.zu})`);
|
||
ok(r.auf !== "none", `und sichtbar heisst sichtbar (${r.auf})`);
|
||
ok(r.haken > 0, `der Haken ist da (${r.haken})`);
|
||
await kontext.close();
|
||
}
|
||
|
||
console.log("\n=== Weniger Bewegung: keine Animation");
|
||
{
|
||
const kontext = await browser.newContext({
|
||
viewport: { width: 1440, height: 1000 }, reducedMotion: "reduce" });
|
||
const seite = await anmelden(kontext, "admin", "CODE-DOGI-0001");
|
||
const bewegung = await seite.evaluate(() => {
|
||
const k = document.querySelector(".kachel");
|
||
const s = getComputedStyle(k);
|
||
return { animation: s.animationName, sichtbar: s.opacity };
|
||
});
|
||
ok(bewegung.animation === "none", `keine Animation (${bewegung.animation})`);
|
||
ok(Number(bewegung.sichtbar) === 1, `und trotzdem voll sichtbar (${bewegung.sichtbar})`);
|
||
await kontext.close();
|
||
}
|
||
|
||
console.log("\n=== Das Licht folgt dem Zeiger");
|
||
{
|
||
const kontext = await browser.newContext({ viewport: { width: 1440, height: 1200 } });
|
||
const seite = await anmelden(kontext, "admin", "CODE-DOGI-0001");
|
||
const kachel = seite.locator(".kachel").nth(1);
|
||
|
||
/* ERST INS BILD ROLLEN, DANN MESSEN -- und zwar VOR JEDER Benutzung.
|
||
|
||
Hier stand `const kasten = await kachel.boundingBox()` ein einziges
|
||
Mal, ganz am Anfang, ohne vorher zu rollen. Am 08.09.2026 hat das
|
||
drei Pruefungen dieses Blocks umgeworfen, ohne dass am Licht
|
||
irgendetwas kaputt war: Die Begruessungskachel wuchs beim Umbau zur
|
||
Zentrale von 244 auf 404 px, die zweite Kachel rutschte damit von
|
||
y=971 auf y=1131, und ihre Mitte lag bei 1207 -- ausserhalb eines
|
||
1200 px hohen Fensters. Dorthin kann kein Zeiger fahren, also
|
||
entstand kein Licht.
|
||
|
||
Verraten hat es die MISCHUNG aus gruen und rot: "links" (30 % der
|
||
Hoehe) bestand, "rechts" (60 %) nicht. Eine Kachel, die nur zur
|
||
Haelfte getroffen wird, ist nicht kaputt -- sie haengt halb aus dem
|
||
Bild. Genau dieser Fehler steht zwanzig Zeilen weiter unten schon
|
||
einmal beschrieben, fuer den 07.09.2026. Dort wurde er an der
|
||
einzelnen Stelle geflickt, hier nicht.
|
||
|
||
Deshalb jetzt nicht noch einmal eine feste Annahme, sondern eine
|
||
Funktion: Sie holt die Kachel MITTIG ins Bild und misst danach.
|
||
`block: "center"` statt `scrollIntoViewIfNeeded()` mit Absicht --
|
||
letzteres rollt nur so weit, dass die Kante gerade eben anliegt,
|
||
und dann liegt der 60-%-Punkt wieder am Rand. Wer die Kachel spaeter
|
||
noch hoeher macht, merkt davon nichts mehr. */
|
||
const kachelHolen = async () => {
|
||
await kachel.evaluate((k) => k.scrollIntoView({ block: "center", behavior: "instant" }));
|
||
await seite.waitForTimeout(150);
|
||
const k = await kachel.boundingBox();
|
||
/* Die Anzahl gehoert in die Bedingung, nicht in den Meldetext: Eine
|
||
Kachel, die trotz Rollen nicht ganz ins Fenster passt, wuerde die
|
||
naechsten Messungen still verfaelschen. Dann lieber hier lautstark
|
||
scheitern als drei Zeilen weiter unten raetselhaft. */
|
||
const fenster = await seite.evaluate(() => window.innerHeight);
|
||
if (k.y < 0 || k.y + k.height > fenster) {
|
||
throw new Error(`Die Kachel passt auch nach dem Rollen nicht ins Fenster `
|
||
+ `(y=${Math.round(k.y)}, Hoehe=${Math.round(k.height)}, Fenster=${fenster}). `
|
||
+ `Ist sie groesser geworden?`);
|
||
}
|
||
return k;
|
||
};
|
||
|
||
const kasten = await kachelHolen();
|
||
|
||
const ruhe = await kachel.evaluate((k) => k.style.getPropertyValue("--mx") || "(Ruhelage)");
|
||
|
||
/* Seit dem 01.09.2026 gibt es zum Fleck auch einen RAND, der auf der
|
||
Kante mitwandert. Er entsteht aus einem Verlauf, von dem eine Maske
|
||
nur den Saum stehen laesst -- geht die Maske verloren, waere die
|
||
ganze Karte eingefaerbt und der Text unlesbar. Deshalb wird hier
|
||
nicht "sieht gut aus" geprueft, sondern an ECHTEN Bildpunkten
|
||
gemessen, ob die Kante heller ist als die Mitte. */
|
||
{
|
||
await seite.mouse.move(kasten.x + kasten.width * 0.5, kasten.y + kasten.height * 0.5);
|
||
await seite.waitForTimeout(450);
|
||
const bild = await seite.screenshot({ type: "png" });
|
||
const r = await seite.evaluate(async ({ b64, kasten }) => {
|
||
const bild = new Image();
|
||
bild.src = "data:image/png;base64," + b64;
|
||
await bild.decode();
|
||
const c = document.createElement("canvas");
|
||
c.width = bild.width; c.height = bild.height;
|
||
const g = c.getContext("2d");
|
||
g.drawImage(bild, 0, 0);
|
||
const skala = bild.width / window.innerWidth;
|
||
const hell = (x, y) => {
|
||
const p = g.getImageData(Math.round(x * skala), Math.round(y * skala), 1, 1).data;
|
||
return 0.2126 * p[0] + 0.7152 * p[1] + 0.0722 * p[2];
|
||
};
|
||
/* Der Zeiger steht in der Mitte. Gemessen wird die Kante direkt
|
||
darueber (dort muss der Rand leuchten) gegen dieselbe Kante an
|
||
der weit entfernten Ecke (dort nicht). */
|
||
return {
|
||
licht: !!document.querySelector(".kachel .licht"),
|
||
kanteNah: hell(kasten.x + kasten.width * 0.5, kasten.y + 1.5),
|
||
kanteFern: hell(kasten.x + 3, kasten.y + kasten.height - 2),
|
||
};
|
||
}, { b64: bild.toString("base64"), kasten });
|
||
|
||
ok(r.licht, "die Karten haben ein Licht-Element bekommen");
|
||
/* Die nahe Kante muss spuerbar heller sein als die ferne. Ohne die
|
||
Maske waere die ganze Karte gleich hell -- der Unterschied ist
|
||
genau der Nachweis, dass der Rand wirklich der Maus folgt. */
|
||
ok(r.kanteNah > r.kanteFern + 8,
|
||
`der Rand leuchtet dort, wo der Zeiger ist `
|
||
+ `(nah ${r.kanteNah.toFixed(0)} gegen fern ${r.kanteFern.toFixed(0)})`);
|
||
await seite.mouse.move(5, 5);
|
||
await seite.waitForTimeout(400);
|
||
}
|
||
|
||
/* --- Bleibt das Licht haengen? -----------------------------------------
|
||
Beanstandet am 01.09.2026: "teilweise bleibt das Licht haengen".
|
||
Es gab dafuer vier Wege, und jeder einzelne wird hier durchgespielt.
|
||
Alle vier haben dasselbe Merkmal: Der Zeiger ist weg, aber die Karte
|
||
leuchtet weiter. */
|
||
{
|
||
const anzahlAn = () => seite.evaluate(() =>
|
||
document.querySelectorAll('[data-licht="an"]').length);
|
||
|
||
/* 1. Ueber die Luecke zwischen zwei Kacheln hinaus.
|
||
|
||
Auch hier ueber `kachelHolen()` statt ueber ein blankes
|
||
`boundingBox()`: Die Mitte dieser Kachel lag am 08.09.2026
|
||
ausserhalb des Fensters, der Zeiger kam nie an, und die Pruefung
|
||
meldete "leuchtet genau eine Kachel (0)" -- als waere das Licht
|
||
kaputt. Es war nur niemand da, der es haette ausloesen koennen. */
|
||
const k1 = await kachelHolen();
|
||
await seite.mouse.move(k1.x + k1.width / 2, k1.y + k1.height / 2);
|
||
await seite.waitForTimeout(300);
|
||
ok(await anzahlAn() === 1, `beim Ueberfahren leuchtet genau eine Kachel (${await anzahlAn()})`);
|
||
await seite.mouse.move(k1.x + k1.width / 2, k1.y - 30); // in die Luecke darueber
|
||
await seite.waitForTimeout(350);
|
||
ok(await anzahlAn() === 0, `in der Luecke geht es aus (${await anzahlAn()})`);
|
||
|
||
/* 2. Direkt von einer Kachel auf die naechste -- es darf nie mehr
|
||
als eine gleichzeitig leuchten.
|
||
|
||
ERST INS BILD ROLLEN, DANN MESSEN, DANN ZIELEN. Diese Datei
|
||
schreibt denselben Satz zwanzig Zeilen weiter unten schon
|
||
einmal, und trotzdem stand er hier nicht: Am 07.09.2026 wurde
|
||
die Begruessungskachel hoeher, Kachel 3 rutschte auf y = 1134 bei
|
||
einem 1200 px hohen Fenster -- ihre Mitte lag bei 1210, also
|
||
UNTER dem Bildrand. Der Zeiger landete im Nichts, keine Kachel
|
||
leuchtete, und die Pruefung meldete einen Fehler am Licht,
|
||
obwohl das Licht in Ordnung war.
|
||
|
||
Der Kasten wird NACH dem Rollen neu gelesen. Ein vor dem Rollen
|
||
gemessener Kasten ist danach falsch -- das waere derselbe Fehler
|
||
mit einem Zwischenschritt mehr. */
|
||
await seite.locator(".kachel").nth(2).scrollIntoViewIfNeeded();
|
||
await seite.waitForTimeout(250);
|
||
const k2 = await seite.locator(".kachel").nth(2).boundingBox();
|
||
const k1neu = await seite.locator(".kachel").nth(1).boundingBox();
|
||
await seite.mouse.move(k1neu.x + k1neu.width / 2, k1neu.y + k1neu.height / 2);
|
||
await seite.waitForTimeout(250);
|
||
await seite.mouse.move(k2.x + k2.width / 2, k2.y + k2.height / 2);
|
||
await seite.waitForTimeout(300);
|
||
ok(await anzahlAn() === 1, `beim Wechsel leuchtet weiterhin nur eine (${await anzahlAn()})`);
|
||
|
||
/* 3. Gescrollt, ohne die Maus zu bewegen. Die Karte wandert unter
|
||
dem stehenden Zeiger weg -- es kommt KEIN Zeigerereignis. Das war
|
||
der haeufigste Fall. */
|
||
await seite.mouse.wheel(0, 600);
|
||
await seite.waitForTimeout(450);
|
||
/* GEFRAGT WIRD, WAS GEMEINT IST -- nicht, was bei einer bestimmten
|
||
Kachelzahl herauskam (19.09.2026).
|
||
|
||
Hier stand `anzahlAn() === 0`, mit der stillen Annahme: nach 600
|
||
px liegt unter dem Zeiger keine Kachel mehr. Das galt, solange
|
||
die Seite so lang war wie an dem Tag, an dem die Zeile
|
||
geschrieben wurde. Am 19.09. kam eine Gruppe dazu -- danach lag
|
||
dort eine ANDERE Kachel, sie leuchtete zu Recht, und die
|
||
Pruefung meldete einen Fehler am Licht, obwohl das Licht genau
|
||
das tat, was es soll.
|
||
|
||
Dieselbe Falle wie die feste Umbruchschwelle in der Kopfleiste:
|
||
eine Zahl, die eine Rechnung von gestern festhaelt. Also wird
|
||
jetzt gefragt, was wirklich gemeint ist: Die Kachel, die VORHER
|
||
unter dem Zeiger lag, darf danach nicht mehr leuchten -- und
|
||
wenn dort gar keine mehr liegt, darf ueberhaupt nichts leuchten. */
|
||
const unterZeiger = await seite.evaluate(([x, y]) => {
|
||
const e = document.elementFromPoint(x, y);
|
||
return !!(e && e.closest(".kachel"));
|
||
}, [k2.x + k2.width / 2, k2.y + k2.height / 2]);
|
||
|
||
/* Das Merkmal heisst `data-licht="an"` -- nachgesehen bei
|
||
`anzahlAn` ein paar Zeilen weiter oben, nicht geraten. */
|
||
const alteLeuchtet = await seite.evaluate(() =>
|
||
[...document.querySelectorAll(".kachel")][2]?.dataset.licht === "an");
|
||
|
||
if (unterZeiger) {
|
||
ok(!alteLeuchtet,
|
||
"nach dem Scrollen leuchtet die alte Kachel nicht mehr"
|
||
+ " (unter dem Zeiger liegt jetzt eine andere)");
|
||
} else {
|
||
ok(await anzahlAn() === 0,
|
||
`nach dem Scrollen ist es aus (${await anzahlAn()}, keine Kachel unter dem Zeiger)`);
|
||
}
|
||
/* WIEDER HOCH. Ohne das lagen die Kacheln fuer alle folgenden
|
||
Pruefungen 600 px weiter oben -- die naechste Pruefung bewegte den
|
||
Zeiger dann ins Leere und meldete einen Fehler, den es nicht gab. */
|
||
await seite.mouse.wheel(0, -600);
|
||
await seite.waitForTimeout(400);
|
||
|
||
/* 4. Der Wettlauf: schnell hinein und sofort wieder hinaus. Der
|
||
angemeldete Bildaufbau lief frueher NACH dem Verlassen und
|
||
schaltete das Licht wieder an -- so blieb es haengen. */
|
||
await seite.mouse.move(0, 0);
|
||
await seite.waitForTimeout(200);
|
||
const k3 = await seite.locator(".kachel").nth(0).boundingBox();
|
||
if (k3) {
|
||
await seite.mouse.move(k3.x + 20, k3.y + 20);
|
||
await seite.mouse.move(k3.x - 60, k3.y - 60); // im selben Wimpernschlag wieder raus
|
||
await seite.waitForTimeout(500);
|
||
ok(await anzahlAn() === 0, `hinein und sofort wieder hinaus: aus (${await anzahlAn()})`);
|
||
}
|
||
}
|
||
|
||
/* --- Die Farbe des Lichts gehoert zur Kachel ---------------------------
|
||
Beanstandet: Es leuchtete in der Farbe der SEITE statt in der der
|
||
Kachel. Geprueft wird, dass zwei verschieden gefaerbte Kacheln auch
|
||
verschieden gefaerbtes Licht bekommen. */
|
||
{
|
||
const farben = [];
|
||
for (const nr of [1, 2, 3]) {
|
||
/* Auch hier: erst ins Bild rollen, dann den Kasten lesen. Eine
|
||
Kachel unter dem Bildrand bekommt keinen Zeiger, also auch
|
||
kein Licht-Element -- und `licht: null` fuer zwei von drei
|
||
Kacheln sah aus wie "zwei Kacheln leuchten gleich". Ein Fehler,
|
||
der etwas ganz anderes meldet, als er ist. */
|
||
await seite.locator(".kachel").nth(nr).scrollIntoViewIfNeeded();
|
||
await seite.waitForTimeout(200);
|
||
const b = await seite.locator(".kachel").nth(nr).boundingBox();
|
||
if (!b) continue;
|
||
await seite.mouse.move(b.x + b.width / 2, b.y + b.height / 2);
|
||
await seite.waitForTimeout(300);
|
||
farben.push(await seite.evaluate((n) => {
|
||
const k = document.querySelectorAll(".kachel")[n];
|
||
const l = k.querySelector(".licht");
|
||
return {
|
||
ton: getComputedStyle(k).getPropertyValue("--ton").trim(),
|
||
licht: l ? getComputedStyle(l, "::before").backgroundImage.slice(0, 90) : null,
|
||
};
|
||
}, nr));
|
||
}
|
||
/* DIE ANZAHL GEHOERT IN DIE BEDINGUNG. Waeren keine Kacheln
|
||
messbar, waere `farben` leer -- und "0 verschiedene Toene bei 0
|
||
Kacheln" ist immer wahr. Grün, ohne etwas angesehen zu haben. */
|
||
ok(farben.length === 3, `drei Kacheln wirklich gemessen (${farben.length})`);
|
||
const toene = new Set(farben.map((f) => f.ton));
|
||
ok(toene.size === farben.length,
|
||
`jede Kachel hat ihren eigenen Ton (${[...toene].join(", ")})`);
|
||
/* Die Farbe im Verlauf NICHT als Text vergleichen: Der Browser
|
||
rechnet color-mix() aus und schreibt das Ergebnis je nach Fassung
|
||
als rgb(), color(srgb ...) oder oklab() zurueck. Ein
|
||
Zeichenkettenvergleich schlaegt dann fehl, obwohl die Farbe
|
||
stimmt -- genau das ist beim ersten Lauf passiert.
|
||
|
||
Verglichen werden stattdessen die Lichter UNTEREINANDER: Drei
|
||
verschieden gefaerbte Kacheln muessen drei verschiedene Verlaeufe
|
||
ergeben. Kaeme die Farbe weiterhin von der SEITE (der Fehler, den
|
||
Filipe gesehen hat), waeren alle drei gleich. */
|
||
const lichter = new Set(farben.map((f) => f.licht));
|
||
ok(lichter.size === farben.length,
|
||
`und das Licht ist je Kachel ein anderes (${lichter.size} von ${farben.length})`);
|
||
|
||
/* Zusaetzlich an echten Bildpunkten -- das kann keine Zeichenkette
|
||
vortaeuschen.
|
||
|
||
GEMESSEN GEGEN DIE EIGENE FARBE DER KACHEL, nicht gegen eine
|
||
Farbe aus dem Gedaechtnis (06.09.2026). Hier stand vorher: "das
|
||
Licht der Aufgaben-Kachel (orange) muss mehr Rot als Blau haben,
|
||
das der Kalender-Kachel (tuerkis) mehr Blau als Rot." Beide
|
||
Klammern waren Wissen von aussen -- und als die 21 Farbtoene neu
|
||
gerechnet wurden, war der Kalender rosa statt tuerkis. Die
|
||
Pruefung meldete einen Fehler, den es nicht gab.
|
||
|
||
Jetzt wird der Ton der Kachel selbst ausgelesen und verlangt,
|
||
dass das gemessene Licht dieselbe Reihenfolge der Kanaele hat
|
||
(welcher ist am staerksten, welcher am schwaechsten). Das ist
|
||
genau die Aussage, um die es geht -- "das Licht traegt die Farbe
|
||
DIESER Kachel" -- und sie bleibt wahr, egal wie die Palette
|
||
gerechnet wird. */
|
||
for (const name of ["Aufgaben", "Kalender", "Agentur"]) {
|
||
/* ERST INS BILD ROLLEN. Die Agentur-Kachel liegt weit unten; ihr
|
||
Mittelpunkt lag ausserhalb des Fensters, und der Bildpunkt dort
|
||
war schwarz (0/0/0). Die Pruefung meldete daraufhin einen
|
||
Fehler, der nur bedeutete: "ausserhalb des Bildes gemessen".
|
||
Ein Wert von genau null ist fast nie ein Messergebnis. */
|
||
await seite.evaluate((n) => {
|
||
[...document.querySelectorAll(".kachel")]
|
||
.find((x) => x.querySelector(".kachel__name")?.textContent === n)
|
||
?.scrollIntoView({ block: "center", behavior: "instant" });
|
||
}, name);
|
||
await seite.waitForTimeout(250);
|
||
|
||
const b = await seite.evaluate((n) => {
|
||
const k = [...document.querySelectorAll(".kachel")]
|
||
.find((x) => x.querySelector(".kachel__name")?.textContent === n);
|
||
if (!k) return null;
|
||
const r = k.getBoundingClientRect();
|
||
/* Der Ton der Kachel, in Zahlen. getComputedStyle liefert ihn
|
||
als "rgb(a, b, c)". */
|
||
const ton = getComputedStyle(k).getPropertyValue("--ton").trim();
|
||
const zahlen = (ton.match(/\d+/g) || []).map(Number);
|
||
const hexTreffer = /^#([0-9a-f]{6})$/i.exec(ton);
|
||
const soll = hexTreffer
|
||
? [0, 2, 4].map((i) => parseInt(hexTreffer[1].slice(i, i + 2), 16))
|
||
: zahlen.slice(0, 3);
|
||
/* WO DIE FARBE LIEGT, HAT SICH AM 07.09.2026 GEAENDERT.
|
||
Bis dahin lag ein Farbschleier ueber der ganzen Kachel, und
|
||
die Mitte war der richtige Messpunkt. Die neue Form traegt
|
||
die Farbe auf der KANTE -- die Flaeche ist mit Absicht
|
||
neutral. An der alten Stelle gemessen kam deshalb Grau
|
||
heraus, und die Pruefung meldete einen Fehler, den es nicht
|
||
gab.
|
||
|
||
Gemessen wird jetzt das obere Kantenlicht: ein schmales Band
|
||
von 4 px Hoehe ueber dem linken Drittel, beginnend hinter
|
||
der Schraege. Nicht EIN Bildpunkt -- eine 1,6 px dicke Linie
|
||
auf einer 1-zu-1-Aufloesung trifft man mit einem Punkt nur
|
||
mit Glueck. Aus dem Band wird der farbigste Punkt genommen. */
|
||
return { x: r.x + r.width / 2, y: r.y + r.height / 2, soll, ton,
|
||
band: { x: r.x + 22, y: r.y, breite: Math.max(40, r.width / 3), hoehe: 4 } };
|
||
}, name);
|
||
if (!b || b.soll.length !== 3) {
|
||
ok(false, `"${name}": Farbton nicht lesbar (${b?.ton})`);
|
||
continue;
|
||
}
|
||
await seite.mouse.move(b.x, b.y);
|
||
await seite.waitForTimeout(350);
|
||
const bild = await seite.screenshot({ type: "png" });
|
||
const gemessen = await seite.evaluate(async ({ b64, band }) => {
|
||
const i = new Image(); i.src = "data:image/png;base64," + b64; await i.decode();
|
||
const c = document.createElement("canvas");
|
||
c.width = i.width; c.height = i.height;
|
||
const g = c.getContext("2d"); g.drawImage(i, 0, 0);
|
||
const s = i.width / window.innerWidth;
|
||
const d = g.getImageData(Math.round(band.x * s), Math.round(band.y * s),
|
||
Math.round(band.breite * s), Math.round(band.hoehe * s)).data;
|
||
/* Der farbigste Punkt des Bandes: groesster Abstand zwischen
|
||
staerkstem und schwaechstem Kanal. Ein grauer Streifen hat
|
||
dort fast null -- daran erkennt man auch, wenn gar kein
|
||
Licht da ist. */
|
||
let best = [0, 0, 0], spanne = -1, punkte = 0;
|
||
for (let k = 0; k + 3 < d.length; k += 4) {
|
||
punkte++;
|
||
const v = [d[k], d[k + 1], d[k + 2]];
|
||
const sp = Math.max(...v) - Math.min(...v);
|
||
if (sp > spanne) { spanne = sp; best = v; }
|
||
}
|
||
return { punkte, spanne, farbe: best };
|
||
}, { b64: bild.toString("base64"), band: b.band });
|
||
const p = gemessen.farbe;
|
||
/* DIE ANZAHL DER GEMESSENEN PUNKTE GEHOERT IN DIE BEDINGUNG.
|
||
Ein leeres Band haette sonst "Reihenfolge stimmt" gemeldet --
|
||
0 gegen 0 ist immer gleich. */
|
||
ok(gemessen.punkte > 50 && gemessen.spanne >= 6,
|
||
`"${name}": auf der Oberkante liegt ueberhaupt Farbe `
|
||
+ `(${gemessen.punkte} Punkte, Spanne ${gemessen.spanne})`);
|
||
/* DER FARBWINKEL, NICHT DIE REIHENFOLGE DER KANAELE (08.09.2026).
|
||
|
||
Hier stand ein Vergleich der Kanal-REIHENFOLGE: Welcher Kanal
|
||
ist der staerkste, welcher der zweite. Der Gedanke war richtig
|
||
-- absolute Werte taugen nicht, weil das Licht stark
|
||
abgedunkelt auf dunklem Grund liegt.
|
||
|
||
Er hat aber eine Voraussetzung, die niemand aufgeschrieben
|
||
hatte: Die Kanaele muessen deutlich verschieden sein. Seit die
|
||
Kachelfarben am 08.09.2026 neu gerechnet wurden, trifft das
|
||
nicht mehr ueberall zu. "Aufgaben" ist jetzt Tuerkis (#18bfa3)
|
||
mit G=191 und B=163 -- nach Abdunklung gemessen 63 gegen 65.
|
||
ZWEI Stufen von 255. Die Reihenfolge kippt dort durch
|
||
Rundung, und die Pruefung meldete einen Fehler, wo keiner war.
|
||
|
||
Der Farbwinkel misst dasselbe, nur ohne diese Voraussetzung:
|
||
Er sagt, WELCHE Farbe es ist, unabhaengig von Helligkeit und
|
||
Saettigung -- genau die Frage. 40 Grad Toleranz, weil das Licht
|
||
sich mit dem dunklen Untergrund mischt; verwechselbare Toene
|
||
der Palette liegen weiter auseinander (der kleinste Abstand
|
||
zweier Kachelfarben betraegt nach der Neuberechnung 0,097 in
|
||
OKLab, das sind hier gut 60 Grad).
|
||
|
||
Die Gegenprobe unten beweist, dass sie weiterhin anschlaegt. */
|
||
const winkel = (v) => {
|
||
const [r, g, bl] = v.map((x) => x / 255);
|
||
const max = Math.max(r, g, bl), min = Math.min(r, g, bl), d = max - min;
|
||
if (d < 0.004) return null; // grau, kein Ton
|
||
let h;
|
||
if (max === r) h = ((g - bl) / d) % 6;
|
||
else if (max === g) h = (bl - r) / d + 2;
|
||
else h = (r - g) / d + 4;
|
||
return ((h * 60) % 360 + 360) % 360;
|
||
};
|
||
const abstand = (a, c) => {
|
||
if (a === null || c === null) return 999;
|
||
const d = Math.abs(a - c);
|
||
return d > 180 ? 360 - d : d;
|
||
};
|
||
const sollWinkel = winkel(b.soll), istWinkel = winkel(p);
|
||
const ab = abstand(sollWinkel, istWinkel);
|
||
ok(ab <= 40,
|
||
`das Licht auf "${name}" traegt den Ton der Kachel `
|
||
+ `(Farbwinkel ${sollWinkel === null ? "grau" : Math.round(sollWinkel) + "°"} `
|
||
+ `gegen ${istWinkel === null ? "grau" : Math.round(istWinkel) + "°"}, `
|
||
+ `Abstand ${Math.round(ab)}° von 40 erlaubt) `
|
||
+ `(${b.ton}: ${b.soll.join("/")} → gemessen ${p[0]}/${p[1]}/${p[2]})`);
|
||
|
||
/* GEGENPROBE, einmal je Kachel: Eine Pruefung, die immer
|
||
bestaetigt, bestaetigt nichts. Der gemessene Ton wird gegen
|
||
eine ABSICHTLICH falsche Farbe gehalten -- den Gegenton auf dem
|
||
Farbkreis. Schlaegt das nicht an, misst die Pruefung nichts.
|
||
|
||
Der Gegenton ist der haerteste Fall: Er ist so weit entfernt,
|
||
wie zwei Farben ueberhaupt sein koennen. Wuerde die Toleranz
|
||
einmal versehentlich auf 180 Grad hochgesetzt, faellt es hier
|
||
sofort auf. */
|
||
const falsch = sollWinkel === null ? 180 : (sollWinkel + 180) % 360;
|
||
ok(abstand(falsch, istWinkel) > 40,
|
||
` und sie erkennt den Gegenton als falsch `
|
||
+ `(${Math.round(falsch)}° gegen gemessen `
|
||
+ `${istWinkel === null ? "grau" : Math.round(istWinkel) + "°"})`);
|
||
}
|
||
|
||
await seite.mouse.move(2, 2);
|
||
/* UND WIEDER GANZ NACH OBEN. Der Abschnitt oben rollt zu den
|
||
Kacheln; was danach kommt, misst am oberen Rand. Ohne diese Zeile
|
||
meldeten die folgenden Pruefungen "der Fleck steht nicht links" --
|
||
er stand sehr wohl, nur ausserhalb des Bildausschnitts. Eine
|
||
Pruefung, die den Zustand hinterlaesst, den sie vorgefunden hat,
|
||
ist die einzige, der die naechste glauben kann. */
|
||
await seite.evaluate(() => window.scrollTo({ top: 0, behavior: "instant" }));
|
||
await seite.waitForTimeout(300);
|
||
}
|
||
ok(ruhe === "(Ruhelage)", `ruhend keine eigene Position gesetzt (${ruhe})`);
|
||
|
||
/* NEU GEMESSEN, nicht der Kasten von oben. Der Block darueber endet
|
||
mit `window.scrollTo({ top: 0 })` -- ab dieser Zeile steht die Seite
|
||
also wieder ganz oben, und jede Koordinate von vorhin zeigt um die
|
||
gerollte Strecke daneben. Genau daran ist "rechts" gescheitert. */
|
||
const kastenJetzt = await kachelHolen();
|
||
|
||
await seite.mouse.move(kastenJetzt.x + kastenJetzt.width * 0.2,
|
||
kastenJetzt.y + kastenJetzt.height * 0.3);
|
||
await seite.waitForTimeout(250);
|
||
const links = await kachel.evaluate((k) => k.style.getPropertyValue("--mx"));
|
||
|
||
await seite.mouse.move(kastenJetzt.x + kastenJetzt.width * 0.85,
|
||
kastenJetzt.y + kastenJetzt.height * 0.6);
|
||
await seite.waitForTimeout(250);
|
||
const rechts = await kachel.evaluate((k) => k.style.getPropertyValue("--mx"));
|
||
|
||
console.log(` links: ${links} · rechts: ${rechts}`);
|
||
ok(parseFloat(links) < 40, `links steht der Fleck links (${links})`);
|
||
ok(parseFloat(rechts) > 60, `rechts steht er rechts (${rechts})`);
|
||
|
||
/* Beim Verlassen zurück in die Ruhelage. Ohne das blieben die Kacheln
|
||
nach einer Weile alle unterschiedlich beleuchtet stehen, ohne dass
|
||
jemand wüsste, warum. */
|
||
await seite.mouse.move(kastenJetzt.x - 250, kastenJetzt.y - 250);
|
||
await seite.waitForTimeout(250);
|
||
const danach = await kachel.evaluate((k) => k.style.getPropertyValue("--mx") || "(Ruhelage)");
|
||
ok(danach === "(Ruhelage)", `nach dem Verlassen wieder in Ruhe (${danach})`);
|
||
await kontext.close();
|
||
}
|
||
|
||
console.log("\n=== Ohne feinen Zeiger bleibt es aus");
|
||
{
|
||
const kontext = await browser.newContext({
|
||
viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true });
|
||
const seite = await anmelden(kontext, "admin", "CODE-DOGI-0001");
|
||
await seite.touchscreen.tap(200, 700).catch(() => {});
|
||
await seite.waitForTimeout(200);
|
||
const gesetzt = await seite.evaluate(() =>
|
||
[...document.querySelectorAll(".kachel")].some((k) => k.style.getPropertyValue("--mx")));
|
||
ok(!gesetzt, "auf dem Handy wird nichts gesetzt");
|
||
await kontext.close();
|
||
}
|
||
|
||
/* =======================================================================
|
||
MARKE UND TITEL VERRATEN NICHT DAS FALSCHE HAUS (27.09.2026)
|
||
|
||
In start.html steht die Vorgabe des AGENTURHAUSES: "Spicy Media" und
|
||
"Zentrale". Das Teamhaus bekommt eigene Worte -- aber erst, wenn
|
||
/api/ich geantwortet hat. Bis dahin stand bei jedem Modi und jedem
|
||
Community-Mitglied die Marke der Agentur in der groessten Schrift
|
||
der Seite.
|
||
|
||
Gefunden hat das keine Pruefung, sondern ein Einzelbild aus einer
|
||
Videoaufnahme -- die Seite sah danach ja richtig aus, und genau so
|
||
lange hat niemand hingesehen. Deshalb steht es ab jetzt hier.
|
||
|
||
GEMESSEN WIRD IN BEIDE RICHTUNGEN: Am Ende muessen beide Zeilen
|
||
sichtbar sein (sonst haette die Reparatur die groesste Schrift der
|
||
Seite dauerhaft weggenommen), und mit gesetztem `data-wartet`
|
||
muessen sie unsichtbar sein (sonst tut die Regel gar nichts, und
|
||
die Pruefung darueber waere wertlos). */
|
||
console.log("\n=== Marke und Titel warten, bis das Haus bekannt ist");
|
||
{
|
||
const kontext = await browser.newContext({ viewport: { width: 1440, height: 1200 } });
|
||
const seite = await anmelden(kontext, "admin", "CODE-DOGI-0001");
|
||
/* Gewartet wird auf das Merkmal, nicht auf die Uhr: Wie lange
|
||
/api/ich braucht, haengt vom Rechner ab. Mit einer festen Pause
|
||
war dieselbe Pruefung mal gruen und mal rot, ohne dass sich
|
||
etwas geaendert hatte. */
|
||
const t0 = Date.now();
|
||
await seite.waitForFunction(
|
||
() => !document.querySelector(".zmarke[data-wartet], #ztitel[data-wartet]"),
|
||
null, { timeout: 5000 }).catch(() => {});
|
||
const dauerte = Date.now() - t0;
|
||
const r = await seite.evaluate(() => {
|
||
const m = document.querySelector(".zmarke");
|
||
const t = document.getElementById("ztitel");
|
||
const lies = (e) => (e ? Number(getComputedStyle(e).opacity) : -1);
|
||
/* OHNE UEBERGANG MESSEN, UND ZWAR VOR DER ERSTEN ABLESUNG.
|
||
`opacity` blendet in 180 ms, und getComputedStyle liefert
|
||
waehrenddessen den laufenden Zwischenwert. Das hat diese
|
||
Pruefung zweimal getaeuscht: einmal meldete die Gegenprobe
|
||
"nicht verdeckt", einmal die Messung davor "nicht sichtbar" --
|
||
beide Male war die Regel in Ordnung und nur die Uhr im Weg.
|
||
Ohne Uebergang steht sofort der Zielwert da. */
|
||
for (const e of [m, t]) if (e) e.style.transition = "none";
|
||
const jetzt = { marke: lies(m), titel: lies(t) };
|
||
m?.setAttribute("data-wartet", "ja");
|
||
t?.setAttribute("data-wartet", "ja");
|
||
const gedeckt = { marke: lies(m), titel: lies(t) };
|
||
m?.removeAttribute("data-wartet");
|
||
t?.removeAttribute("data-wartet");
|
||
for (const e of [m, t]) if (e) e.style.transition = "";
|
||
return { jetzt, gedeckt, breit: (t?.getBoundingClientRect().width || 0) };
|
||
});
|
||
ok(r.jetzt.marke === 1 && r.jetzt.titel === 1,
|
||
"nach dem Laden sind beide sichtbar "
|
||
+ `(Marke ${r.jetzt.marke}, Titel ${r.jetzt.titel}, nach ${dauerte} ms)`);
|
||
ok(r.gedeckt.marke === 0 && r.gedeckt.titel === 0,
|
||
"Gegenprobe: mit data-wartet sind beide verdeckt "
|
||
+ `(Marke ${r.gedeckt.marke}, Titel ${r.gedeckt.titel})`);
|
||
/* Der Platz muss auch im Wartezustand stehen -- sonst springt die
|
||
halbe Kachel, sobald das Wort ankommt. `opacity` tut das,
|
||
`display: none` taete es nicht. */
|
||
ok(r.breit > 40, `der Titel nimmt Platz ein und rueckt nichts (${Math.round(r.breit)} px)`);
|
||
await kontext.close();
|
||
}
|
||
|
||
await browser.close();
|
||
console.log(`\n${fehler === 0 ? "Alles in Ordnung." : fehler + " Prüfung(en) fehlgeschlagen."}`);
|
||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||
process.exit(fehler === 0 ? 0 : 1);
|