=== 1. BILDER OEFFNEN SICH MITTIG, MIT HUSKY UND HASE ===
Filipe: "wenn man bilder aufmacht, will ich dass die in der mitte sind.
beim hochformat soll links der husky sein und rechts der hase und beim
querformat soll oben dogfather und unten team dogi stehen da wo platz
ist ... ich will nicht dahin geschmissen sondern was richtig geiles mit
einem geilen hintergrund."
WAS VORHER PASSIERTE, und es war kein Fehler im Code: Der Klick fuehrte
auf die DATEI. Was man dann sah, war die eingebaute Bildanzeige des
Browsers -- schwarzer Grund, Bild oben links in der Ecke, auf seinem
Bildschirmfoto 1200 px Schwarz daneben. Eine Datei hat keine
Gestaltung. Wer Gestaltung will, muss eine SEITE zeigen.
Die Schau liegt jetzt UEBER dem Chat. Escape bringt einen dorthin
zurueck, wo man war, und das Gespraech laeuft im Hintergrund weiter --
ein zweiter Tab braeuchte eine eigene Seite, eine eigene Anmeldung und
einen eigenen Weg zurueck.
DER WEG IN DEN TAB BLEIBT TROTZDEM. Der Verweis hat unveraendert
`href`, `target` und `rel`; mittlere Maustaste, "In neuem Tab oeffnen"
und Herunterladen gehen wie seit jeher. Abgefangen wird nur der
gewoehnliche Klick -- und auch der nur, wenn die Schau geladen ist.
Wer Strg, Umschalt oder Befehl haelt, bekommt den alten Weg; das sind
die Griffe, die Leute seit zwanzig Jahren benutzen.
DIE BEGLEITER STEHEN DA, WO PLATZ IST -- und die Antwort darauf wird
GEMESSEN, nicht geschaetzt: nicht die Fensterbreite, sondern der Rest
neben dem Bild, nachdem es eingepasst ist. Eine Schwelle wie "ab
1100 px" waere fuer ein 3:4-Foto richtig und fuer ein 9:16-Foto auf
demselben Schirm falsch. Unter 170 px bleibt der Husky weg: Eine Figur,
die ein Streifen ist, steht besser nicht da.
Husky und Hase sind eigens verkleinert (2,1 MB und 1,0 MB werden 40 kB
und 32 kB) und bekommen weiche Raender -- ihr Hintergrund ist dunkel,
aber nicht durchsichtig; als Rechteck saehe man zwei Kaesten.
Der erste Anlauf spiegelte den Hasen, damit beide "nach innen" schauen
-- ein alter Reflex aus dem Plakatsatz. Auf dem Probebild stand danach
"Hasi Dog" seitenverkehrt auf seinem Hoodie. Schrift im Bild spiegelt
man nie.
pruef-chat-anhaenge bekommt 14 neue Punkte, darunter beide Formate und
zwei Gegenproben: Strg+Klick oeffnet die Schau NICHT (sonst hiesse "sie
geht auf" nur, dass der alte Weg verloren ist), und im Hochformat liegt
KEINE der Figuren ueber dem Foto.
NEBENBEFUND, den die Pruefung gefunden hat: Die Sprachnachricht war auf
165 px geschrumpft -- zu schmal fuer den Schieber. Ursache war keine
Aenderung an ihr, sondern eine Folge: Die Blase ist so breit wie ihr
breitester Inhalt, und seit die Handgriffe darunter Zeichen statt
Woerter sind (123 statt ueber 400 px), bestimmt das Abspielgeraet die
Breite selbst. Eine Reihe kuerzer zu machen hat einen Schieber schmaler
gemacht, drei Bildschirme weiter.
=== 2. DER FARBKREIS: HELLER UND DUNKLER ===
Filipe: "dieser kreis muss viel perfekter sein. ich will dass die leute
auch heller und dunkler aussuchen koennen ... so dass die leute viel
krassere moeglichkeiten haben."
WARUM DAS BIS HEUTE NICHT GING: Die 360 Toene liegen alle auf DERSELBEN
Leuchtdichte. Das war kein Zufall -- solange die Blase in der gewaehlten
Farbe stand, musste jede dieser Flaechen dieselbe Schrift tragen. Eine
hellere Farbe zuzulassen hiesse damals: irgendwo wird eine Nachricht
unlesbar.
SEIT DEM 25.09.2026 IST DIE BEDINGUNG EINE ANDERE. Die Blase ist fuer
alle dunkles Glas; der Ton ist Kante und NAME. Damit faellt die alte
Regel weg, und an ihre Stelle tritt: der Ton muss als Name auf dem
Blasengrund lesbar bleiben.
DIE ZAHLEN SIND GEMESSEN, NICHT GEWAEHLT. Ueber alle 360 Winkel, jeweils
der schlechteste Fall:
40 % schwarz 4,468 -- unter der Schwelle, faellt raus
34 % schwarz 4,555 -- ginge, aber ohne Reserve
30 % schwarz 4,619 <- die dunkelste Stufe
0 % 5,12 <- die Mitte, der alte Kreis
55 % weiss 9,6 <- die hellste Stufe
Sieben Stufen, 2520 Farben statt 360. pruef-chat-neu rechnet jede
einzelne nach -- die knappste hat 2,6 Prozent Reserve.
DIE ALTEN SCHLUESSEL BLEIBEN GUELTIG. "ton-214" heisst weiterhin genau
dieselbe Farbe; die Stufe steht als Zusatz dahinter ("ton-214-s6"). Wer
seit dem 23.09. eine Farbe traegt, traegt nach diesem Umbau dieselbe.
DIE PROBE IN DER MITTE ZEIGT ENDLICH, WAS MAN BEKOMMT. Dort stand eine
gefuellte Flaeche mit heller Schrift -- so sah die Blase bis heute frueh
aus. Jetzt steht dort derselbe Blasengrund und darauf der Ton als Name,
mit genau der Rechnung aus chat.css. Das ist nicht nur ehrlicher, es
macht die sieben Stufen erst moeglich: Eine gefuellte Flaeche muesste
Schrift tragen, und bei sehr hellen Toenen schafft das keine der beiden
Schriftfarben mehr (4,1 statt noetiger 7). Als Name auf dunklem Grund
ist derselbe helle Ton besonders gut lesbar (8,0).
Der Erklaersatz im Fenster war damit falsch geworden ("Alle Toene
leuchten gleich stark") und sagt jetzt, was stimmt.
ZWEI PRUEFUNGEN WURDEN GENAUER:
pruef-chat-neu mass den HINTERGRUND der Mitte -- eine Darstellung,
die es nicht mehr gibt. Sie misst jetzt die Schriftfarbe und rechnet
die Mischung nach. Dabei stolperte sie ueber eine dritte
Farbschreibweise: Chromium gibt `color-mix`-Ergebnisse als
`color(srgb 0.676 0.645 0.504)` zurueck. Die Zeile war rot, obwohl
die Farbe auf den Bildpunkt genau stimmte -- ein Fehlalarm, der
teurer ist als ein echter Befund, weil man am Aussehen sucht und der
Fehler im Lesegeraet liegt.
Und sie prueft nicht mehr 360, sondern 360 x 7 -- nur den Grundton zu
messen hiesse, sechs Siebtel ungeprueft auszuliefern.
Geprueft: pruef-chat-anhaenge (123), pruef-chat-neu (36),
pruef-chatkachel (40), pruef-chat, pruef-chat-ausbau (69),
pruef-chat-optik, pruef-tippziele (11), pruef-css-klassen.
Co-Authored-By: Claude Opus 5 <[email protected]>
636 lines
31 KiB
JavaScript
636 lines
31 KiB
JavaScript
/* =====================================================================
|
|
pruef-chat-neu.mjs — Sprachnachricht, GIF-Kiste und der Ruf ans Rudel
|
|
|
|
Alle drei sind in der Nacht zum 23.09.2026 entstanden:
|
|
|
|
„dan will ich auch dass man im chat auch sprachnachrichten und
|
|
gifts reinschicken kann. also nur die modis rechte linke hand
|
|
und dogfather."
|
|
„dan will ich auch dass nur die modis, rechte hand, linke hand
|
|
und dogfather, alle auch auf einmal markieren koennen im chat
|
|
mit einem, @rudel ,dann sollen alle eine benarichtigung
|
|
bekommen."
|
|
|
|
---------------------------------------------------------------------
|
|
WARUM ES DIESE DATEI GIBT, obwohl jedes Stueck schon geprueft ist
|
|
|
|
Geprueft war jedes einzeln -- und jedes in einer anderen Lage als
|
|
der echten:
|
|
|
|
pruef-chat-anhaenge Sprachnachricht und GIF, aber als DogFather
|
|
auf der AGENTURadresse, mit der Maus
|
|
pruef-erwaehnung der Rudel-Ruf, aber gegen den Server
|
|
|
|
Der Fall, den Filipe wirklich hat -- ein MODI auf crew., mit dem
|
|
FINGER, und ein zweiter Mensch, der es bekommen soll -- war nie am
|
|
Stueck gemessen. Genau in solchen Luecken sitzen die Fehler, die
|
|
niemand sieht: Jeder Einzeltest ist gruen, und zusammen geht es
|
|
trotzdem nicht.
|
|
|
|
ZWEI BROWSER, ZWEI MENSCHEN. Eine Sprachnachricht, die nur beim
|
|
Absender im Verlauf steht, ist keine. Deshalb wartet jede der drei
|
|
Proben darauf, dass die ZWEITE Person sie von selbst bekommt --
|
|
ohne Neuladen.
|
|
|
|
---------------------------------------------------------------------
|
|
ZWEI EIGENE FEHLER BEIM BAUEN, beide aufgeschrieben, weil sie sich
|
|
wiederholen werden:
|
|
|
|
(1) OHNE NOTBREMSE. Der erste Lauf blieb stehen und sah von aussen
|
|
aus wie „laeuft noch". Ein Werkzeug, das haengt, ist schlimmer
|
|
als eines, das scheitert.
|
|
|
|
(2) MIT ENTER ABGESCHICKT. Auf einem Beruehrgeraet schickt Enter
|
|
ABSICHTLICH nicht ab -- sonst kaeme man nie zu einer zweiten
|
|
Zeile. Die Messung meldete daraufhin „kommt nicht an"; in
|
|
Wahrheit war nie etwas abgeschickt worden. Dasselbe Muster wie
|
|
beim `pointer: coarse` am selben Tag: Wer im falschen
|
|
Geraeteprofil misst, misst ein anderes Programm.
|
|
|
|
AUFRUF: node server/pruef-chat-neu.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 { execFileSync } from "node:child_process";
|
|
import { createServer as httpsServer } from "node:https";
|
|
|
|
import { eigenerPort } from "./helfer-port.mjs";
|
|
const PORT = await eigenerPort(import.meta, "pruef-chat-neu");
|
|
const CREW = "crew.dogfather-universe.com";
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-drei-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
process.env.PORT = String(PORT);
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
|
process.env.TREFF_NACHT_AB = "0";
|
|
process.env.TREFF_NACHT_BIS = "0";
|
|
|
|
const express = (await import("express")).default;
|
|
const ec = express.response.cookie;
|
|
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
|
await import("./index.js");
|
|
/* NOTBREMSE. Ein Messwerkzeug, das HAENGT, ist schlimmer als eines,
|
|
das scheitert: Von aussen sieht Stillstand aus wie "laeuft noch".
|
|
Genau das ist mir hier gerade passiert. */
|
|
const { notbremse } = await import("./helfer-notbremse.mjs");
|
|
notbremse(240_000, "pruef-chat-neu");
|
|
await new Promise((r) => setTimeout(r, 1300));
|
|
|
|
|
|
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 k = randomBytes(32).toString("hex");
|
|
d.prepare("INSERT INTO einstellungen (schluessel,wert,geaendert,von) VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
|
.run("code_kennung_schluessel", k, jetzt);
|
|
const mach = (n, r, c) => {
|
|
const s = randomBytes(16).toString("hex");
|
|
const h = scryptSync(c, s, 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(n, r, h, s, 32768, createHmac("sha256", k).update(c).digest("hex"), jetzt);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
};
|
|
const idDogi = mach("Filipe", "admin", "CODE-DOGI-0001");
|
|
mach("Rieke", "hand", "CODE-HAND-0001");
|
|
const idA = mach("Frida", "modi", "CODE-MODI-0001");
|
|
const idB = mach("Nele", "modi", "CODE-MODI-0002");
|
|
d.close();
|
|
|
|
const schl = join(ordner, "b.key"), zert = join(ordner, "b.crt");
|
|
execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", schl,
|
|
"-out", zert, "-days", "2", "-subj", `/CN=${CREW}`, "-addext", `subjectAltName=DNS:${CREW}`],
|
|
{ stdio: "ignore" });
|
|
const HP = PORT + 1;
|
|
const vorbau = httpsServer({ key: readFileSync(schl), cert: readFileSync(zert) }, (von, nach) => {
|
|
const w = httpAnfrage({ host: "127.0.0.1", port: PORT, path: von.url, method: von.method,
|
|
headers: { ...von.headers, host: `${CREW}:${HP}` } },
|
|
(a) => { nach.writeHead(a.statusCode || 502, a.headers); a.pipe(nach); });
|
|
von.pipe(w);
|
|
});
|
|
await new Promise((r) => vorbau.listen(HP, "127.0.0.1", r));
|
|
const SEITE = `https://${CREW}:${HP}`;
|
|
|
|
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await pw.chromium.launch({
|
|
args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors",
|
|
"--use-fake-device-for-media-stream", "--use-fake-ui-for-media-stream"] });
|
|
|
|
async function alsPerson(rolle, code) {
|
|
const ctx = await browser.newContext({
|
|
viewport: { width: 390, height: 844 }, deviceScaleFactor: 2,
|
|
hasTouch: true, isMobile: true, reducedMotion: "reduce",
|
|
permissions: ["microphone"] });
|
|
const seite = await ctx.newPage();
|
|
const meckern = [];
|
|
seite.on("console", (m) => { if (m.type() === "error" && !/favicon|ERR_CERT|net::ERR/i.test(m.text())) meckern.push(m.text().slice(0, 110)); });
|
|
seite.on("pageerror", (e) => meckern.push("Absturz: " + String(e).slice(0, 110)));
|
|
await seite.goto(`${SEITE}/workspace/`, { waitUntil: "networkidle" });
|
|
await seite.click(`.rolle[data-rolle="${rolle}"]`);
|
|
await seite.fill("#code", code);
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForTimeout(1300);
|
|
return { ctx, seite, meckern };
|
|
}
|
|
|
|
let fehler = 0, geprueft = 0;
|
|
const ok = (b, t) => {
|
|
geprueft++;
|
|
console.log((b ? " ok " : " FEHL ") + t);
|
|
if (!b) fehler++;
|
|
};
|
|
|
|
const frida = await alsPerson("modi", "CODE-MODI-0001");
|
|
const nele = await alsPerson("modi", "CODE-MODI-0002");
|
|
|
|
/* Beide in den Treff -- dort sitzen alle. */
|
|
for (const p of [frida, nele]) {
|
|
await p.seite.goto(`${SEITE}/workspace/chat.html?raum=treff`, { waitUntil: "networkidle" });
|
|
await p.seite.waitForTimeout(1800);
|
|
}
|
|
|
|
console.log("\n=== 1. Sprachnachricht ===");
|
|
{
|
|
const vorher = await nele.seite.locator(".chat-nachricht").count();
|
|
const knopfDa = await frida.seite.locator("#ton-auf").isVisible();
|
|
ok(knopfDa, "der Modi sieht den Mikrofonknopf auf crew.");
|
|
await frida.seite.click("#ton-auf");
|
|
await frida.seite.waitForSelector("#tonleiste:not([hidden])", { timeout: 8000 });
|
|
await frida.seite.waitForTimeout(2400);
|
|
const zeit = (await frida.seite.locator("#ton-zeit").innerText()).trim();
|
|
ok(/^0:0[2-9]$/.test(zeit), `die Zeit laeuft (${zeit})`);
|
|
await frida.seite.click("#ton-senden");
|
|
await frida.seite.waitForTimeout(3200);
|
|
const blase = await frida.seite.evaluate(() => {
|
|
const t = document.querySelector(".chat-ton");
|
|
return t ? { marke: t.querySelector(".chat-ton__marke")?.textContent.trim(),
|
|
dauer: t.querySelector(".chat-ton__dauer")?.textContent.trim(),
|
|
breit: Math.round(t.getBoundingClientRect().width) } : null;
|
|
});
|
|
ok(!!blase, `sie steht in seinem Verlauf (${blase?.marke} ${blase?.dauer})`);
|
|
ok((blase?.breit || 0) >= 200 && (blase?.breit || 0) <= 390,
|
|
`und passt auf den Bildschirm (${blase?.breit} px)`);
|
|
/* UND KOMMT BEIM ANDEREN AN, ohne dass der etwas tut. */
|
|
const beiNele = await nele.seite.waitForFunction(
|
|
(v) => document.querySelectorAll(".chat-nachricht").length > v && document.querySelector(".chat-ton"),
|
|
vorher, { timeout: 12000 }).then(() => true).catch(() => false);
|
|
ok(beiNele, "die zweite Modi bekommt sie von selbst -- ohne Neuladen");
|
|
}
|
|
|
|
console.log("\n=== 2. GIF aus der Kiste ===");
|
|
{
|
|
const GIF = "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
|
ok(await frida.seite.locator("#gif-auf").isVisible(), "der Modi sieht den GIF-Knopf");
|
|
await frida.seite.click("#gif-auf");
|
|
await frida.seite.waitForSelector("#gif-tafel:not([hidden])", { timeout: 8000 });
|
|
const tafel = await frida.seite.evaluate(() => {
|
|
const t = document.getElementById("gif-tafel");
|
|
const r = t.getBoundingClientRect();
|
|
return { b: Math.round(r.width), h: Math.round(r.height),
|
|
imBild: r.left >= -1 && r.right <= innerWidth + 1 && r.top >= -1,
|
|
leer: (document.getElementById("gif-leer")?.textContent || "").trim().slice(0, 40) };
|
|
});
|
|
ok(tafel.imBild, `die Kiste passt auf den Bildschirm (${tafel.b}x${tafel.h})`);
|
|
ok(/noch leer/.test(tafel.leer), `und sagt, was zu tun ist ("${tafel.leer}…")`);
|
|
await frida.seite.setInputFiles("#gif-feld", {
|
|
name: "daumen.gif", mimeType: "image/gif", buffer: Buffer.from(GIF, "base64") });
|
|
await frida.seite.waitForTimeout(2400);
|
|
const kacheln = await frida.seite.locator(".gif-kachel").count();
|
|
ok(kacheln === 1, `ein GIF liegt in der Kiste (${kacheln})`);
|
|
const vorher2 = await nele.seite.locator(".chat-nachricht").count();
|
|
await frida.seite.click(".gif-kachel__knopf");
|
|
await frida.seite.waitForTimeout(2600);
|
|
const beiNele = await nele.seite.waitForFunction(
|
|
(v) => document.querySelectorAll(".chat-nachricht").length > v,
|
|
vorher2, { timeout: 12000 }).then(() => true).catch(() => false);
|
|
ok(beiNele, "das GIF kommt bei der zweiten Modi an");
|
|
/* Und die Kiste ist fuer BEIDE da -- sie gehoert dem Rudel. */
|
|
await nele.seite.click("#gif-auf");
|
|
await nele.seite.waitForTimeout(1600);
|
|
const beiIhr = await nele.seite.locator(".gif-kachel").count();
|
|
ok(beiIhr === 1, `die zweite Modi sieht dasselbe GIF in ihrer Kiste (${beiIhr})`);
|
|
await nele.seite.keyboard.press("Escape").catch(() => {});
|
|
}
|
|
|
|
console.log("\n=== 3. Der Ruf ans Rudel ===");
|
|
{
|
|
const vorher = await nele.seite.locator(".chat-nachricht").count();
|
|
await frida.seite.click("#text");
|
|
await frida.seite.type("#text", "@", { delay: 60 });
|
|
await frida.seite.waitForTimeout(700);
|
|
const liste = await frida.seite.evaluate(() => {
|
|
const l = document.getElementById("erwaehn-liste");
|
|
if (!l || l.hidden) return null;
|
|
const erste = l.querySelector(".erwaehn-liste__eintrag");
|
|
const r = erste?.getBoundingClientRect();
|
|
return { erster: l.querySelector(".erwaehn-liste__name")?.textContent.trim() || "",
|
|
zusatz: l.querySelector(".erwaehn-liste__rolle")?.textContent.trim() || "",
|
|
hoch: Math.round(r?.height || 0),
|
|
imBild: r ? (r.left >= -1 && r.right <= innerWidth + 1) : false };
|
|
});
|
|
ok(!!liste, "ein @ oeffnet die Liste");
|
|
ok(liste?.erster === "rudel", `der Ruf steht oben (${liste?.erster})`);
|
|
ok(/alle in diesem Chat/i.test(liste?.zusatz || ""), `mit der Erklaerung daneben (${liste?.zusatz})`);
|
|
ok((liste?.hoch || 0) >= 44 && liste?.imBild,
|
|
`am Daumen treffbar und im Bild (${liste?.hoch} px)`);
|
|
await frida.seite.keyboard.press("Enter");
|
|
await frida.seite.waitForTimeout(400);
|
|
await frida.seite.type("#text", "bitte alle kurz schauen", { delay: 10 });
|
|
/* NICHT MIT ENTER. Auf einem Beruehrgeraet schickt Enter
|
|
ABSICHTLICH nicht ab -- sonst kaeme man nie zu einer zweiten
|
|
Zeile (steht so in chat.html). Der erste Anlauf dieser Messung
|
|
hat genau das uebersehen und einen Fehler gemeldet, den es nicht
|
|
gibt: Die Nachricht war nie abgeschickt worden. */
|
|
await frida.seite.click("#senden");
|
|
await frida.seite.waitForTimeout(2800);
|
|
const beiNele = await nele.seite.waitForFunction(
|
|
(v) => document.querySelectorAll(".chat-nachricht").length > v,
|
|
vorher, { timeout: 12000 }).then(() => true).catch(() => false);
|
|
ok(beiNele, "die Nachricht kommt bei der zweiten Modi an");
|
|
const marke = await nele.seite.evaluate(() => {
|
|
const m = document.querySelector('.chat-erwaehnung[data-rudel="ja"]');
|
|
return m ? { wort: m.textContent.trim(),
|
|
gewicht: getComputedStyle(m).fontWeight } : null;
|
|
});
|
|
ok(!!marke, `und der Ruf leuchtet bei ihr (${marke?.wort}, Gewicht ${marke?.gewicht})`);
|
|
/* Und in der Datenbank steht, dass sie gemeint war. */
|
|
const db2 = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true });
|
|
const gemeint = db2.prepare(`SELECT e.person_id AS wer FROM chat_erwaehnungen e
|
|
JOIN chat_nachrichten n ON n.id = e.nachricht_id WHERE n.rudel = 1`).all().map((x) => x.wer);
|
|
db2.close();
|
|
ok(gemeint.includes(idB) && gemeint.includes(idDogi) && !gemeint.includes(idA),
|
|
`gerufen wurden ${gemeint.length} -- die andere Modi und DogFather, nicht sie selbst`);
|
|
}
|
|
|
|
/* =====================================================================
|
|
4. DER FARBRING (23.09.2026)
|
|
|
|
Filipe: "anstatt sowas um die farben auszusuchen, kannst du nicht
|
|
so eine art diagramm machen wo man mit einem kreis herum gehen kann
|
|
und sie die farbe ganz genau selber auswaehlen kann?"
|
|
|
|
WAS HIER WIRKLICH AUF DEM SPIEL STEHT, ist nicht die Optik. Die
|
|
zwoelf Kacheln waren gerechnet, damit niemand sich unlesbar machen
|
|
kann -- alle auf derselben Leuchtdichte. Ein Ring mit freier Wahl
|
|
ist genau die Gelegenheit, dieses Versprechen zu verlieren, und
|
|
zwar unbemerkt: Die Farbe sieht huebsch aus, und erst die anderen
|
|
koennen den Text nicht mehr lesen.
|
|
|
|
Deshalb misst diese Probe zuerst den KONTRAST und dann die
|
|
Bedienung.
|
|
===================================================================== */
|
|
{
|
|
console.log("");
|
|
console.log("=== 4. Der Farbring ===");
|
|
|
|
const { KREIS } = await import("./chat-kreis.js");
|
|
|
|
/* ---- 4a. Das Versprechen, an allen 360 Toenen ------------------ */
|
|
const schriftFarben = await frida.seite.evaluate(() => {
|
|
/* AM ECHTEN ELEMENT MESSEN, nicht an einem selbstgebauten.
|
|
Erster Anlauf legte ein <div class="chat-nachricht__blase"> an
|
|
und las dort -- und bekam zweimal nichts zurueck. Die beiden
|
|
Werte stehen naemlich eine Ebene hoeher, auf .chat-nachricht,
|
|
und werden von dort vererbt. Ergebnis war ein roter Haken an
|
|
einer Stelle, an der nichts kaputt war: die Messung war falsch,
|
|
nicht das Programm.
|
|
|
|
Erst die Blase im Verlauf; gibt es keine, ein Ersatzstueck mit
|
|
der RICHTIGEN Klasse. */
|
|
const echt = document.querySelector(".chat-nachricht");
|
|
const ziel = echt || document.body.appendChild(
|
|
Object.assign(document.createElement("div"), { className: "chat-nachricht" }));
|
|
const s2 = getComputedStyle(ziel);
|
|
const raus = { text: s2.getPropertyValue("--blase-text").trim(),
|
|
leise: s2.getPropertyValue("--blase-leise").trim(),
|
|
woher: echt ? "aus dem Verlauf" : "aus einem Ersatzstueck" };
|
|
if (!echt) ziel.remove();
|
|
return raus;
|
|
});
|
|
|
|
const zuZahl = (f) => {
|
|
const t = /^#?([0-9a-f]{6})$/i.exec(String(f).trim());
|
|
if (t) return [0, 2, 4].map((i) => parseInt(t[1].slice(i, i + 2), 16));
|
|
/* ==== DIE DRITTE SCHREIBWEISE (25.09.2026) ====================
|
|
|
|
Seit die Probe ihre Farbe aus `color-mix(in srgb, …)` bekommt,
|
|
gibt Chromium sie als `color(srgb 0.676 0.645 0.504)` zurueck --
|
|
Anteile von 0 bis 1, keine Ganzzahlen, kein Komma. Die zwei
|
|
Muster darueber trafen das nicht, `zuZahl` lieferte `null`, und
|
|
die Zeile wurde rot, obwohl die Farbe auf den Bildpunkt genau
|
|
stimmte (172,5 / 164,5 / 128,5 gegen erwartete 173/165/129).
|
|
|
|
Das ist die Sorte Fehlalarm, die teurer ist als ein echter
|
|
Befund: Man sucht am Aussehen, und der Fehler liegt im
|
|
Lesegeraet. */
|
|
const s = /color\(srgb\s+([^)]+)\)/i.exec(String(f));
|
|
if (s) {
|
|
return s[1].trim().split(/[\s/]+/).slice(0, 3)
|
|
.map((x) => Math.round(parseFloat(x) * 255));
|
|
}
|
|
const r = /rgba?\(([^)]+)\)/i.exec(String(f));
|
|
return r ? r[1].split(",").slice(0, 3).map((x) => Math.round(parseFloat(x))) : null;
|
|
};
|
|
const linear = (v) => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);
|
|
const leucht = (rgb) => 0.2126 * linear(rgb[0] / 255) + 0.7152 * linear(rgb[1] / 255)
|
|
+ 0.0722 * linear(rgb[2] / 255);
|
|
const kontrast = (a, b) => {
|
|
const x = leucht(a), y = leucht(b);
|
|
return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
|
|
};
|
|
|
|
const textRgb = zuZahl(schriftFarben.text);
|
|
const leiseRgb = zuZahl(schriftFarben.leise);
|
|
ok(!!textRgb && !!leiseRgb,
|
|
`die Schriftfarben der Blase sind lesbar, ${schriftFarben.woher}`
|
|
+ ` (${schriftFarben.text || "nichts"} / ${schriftFarben.leise || "nichts"})`);
|
|
|
|
/* ==== DIESE MESSUNG WURDE NACHGEZOGEN (25.09.2026) ==============
|
|
|
|
Sie rechnete: „Jeder der 360 Ringtoene traegt als FLAECHE die
|
|
Schrift der Blase." Das war richtig, solange die Blase in der
|
|
gewaehlten Farbe stand.
|
|
|
|
Seit dem 25.09.2026 tut sie das nicht mehr -- sie ist dunkles
|
|
Glas fuer alle, und der gewaehlte Ton ist Kante und NAME. Die
|
|
alte Rechnung haette also eine Beziehung geprueft, die es nicht
|
|
mehr gibt; sie wurde beim Umbau sofort rot (Winkel 299), und das
|
|
war die richtige Reaktion einer Pruefung auf eine geaenderte
|
|
Sache.
|
|
|
|
WAS SIE JETZT MISST, und es ist mehr wert als vorher: Jeder der
|
|
360 Toene muss als NAME auf dem Blasengrund lesbar sein. Das ist
|
|
die Stelle, an der ein Ton heute durchfallen kann -- ein dunkles
|
|
Blau etwa waere als Name auf dunklem Glas nicht zu lesen. Die
|
|
Oberflaeche hellt jeden Ton dafuer mit Weiss auf; wie stark, steht
|
|
als `--namen-anteil` in chat.css und wird hier GELESEN, nicht
|
|
abgeschrieben.
|
|
|
|
4,5 UND NICHT 7: Ein Name ist ein kurzes Wort in fetter Schrift.
|
|
Dieselbe Schwelle wie in pruef-chatkachel, aus demselben Grund.
|
|
================================================================= */
|
|
{
|
|
const { readFileSync: lies } = await import("node:fs");
|
|
const { fileURLToPath: alsPfad } = await import("node:url");
|
|
const { dirname: ordnerVon, join: fuege } = await import("node:path");
|
|
const cssTxt = lies(fuege(ordnerVon(alsPfad(import.meta.url)), "..",
|
|
"workspace", "assets", "css", "chat.css"), "utf8");
|
|
const grundTreffer = /--blasengrund:\s*(#[0-9a-fA-F]{3,6})\s*;/.exec(cssTxt);
|
|
const anteilTreffer = /--namen-anteil:\s*([0-9.]+)%\s*;/.exec(cssTxt);
|
|
if (!grundTreffer || !anteilTreffer) {
|
|
/* DER DRITTE AUSGANG: nicht gruen, nicht rot -- "kann nicht
|
|
nachsehen", mit Grund. Ein Rueckfall auf feste Werte waere
|
|
genau die zweite Wahrheit, gegen die hier gebaut wird. */
|
|
ok(false, "ABBRUCH: --blasengrund oder --namen-anteil fehlt in chat.css — "
|
|
+ "ohne sie laesst sich die Namensfarbe nicht nachrechnen");
|
|
} else {
|
|
const grund = zuZahl(grundTreffer[1]);
|
|
/* Die Blase ist zu 92 % deckend; dahinter liegt ein Foto. Weiss
|
|
ist der hellste denkbare Grund -- damit wird gerechnet. */
|
|
const schlimmerGrund = grund.map((v) => Math.round(v * 0.92 + 255 * 0.08));
|
|
const anteil = Number(anteilTreffer[1]) / 100;
|
|
const hell = zuZahl("#f2f8ff");
|
|
/* ==== JEDE STUFE AN JEDEM WINKEL (25.09.2026) =================
|
|
|
|
Filipe: „ich will dass die leute auch heller und dunkler
|
|
aussuchen koennen."
|
|
|
|
Seit heute gibt es zu jedem der 360 Winkel sieben
|
|
Helligkeiten -- 2520 Farben statt 360. Nur den Grundton zu
|
|
pruefen hiesse, sechs Siebtel des Angebots ungeprueft
|
|
auszuliefern; und die dunkelste Stufe ist genau die, bei der
|
|
es kippen wuerde (gemessen 4,62 gegen noetige 4,5 -- die
|
|
naechste Stufe darunter faellt durch).
|
|
|
|
DIE MISCHANTEILE WERDEN GELESEN, nicht abgeschrieben: Sie
|
|
stehen in chat-kreis-stufen.js, und wer sie dort aendert,
|
|
aendert damit diese Pruefung mit. Eine zweite Liste hier waere
|
|
die, die beim naechsten Umbau nicht mitzieht. */
|
|
const { KREIS_STUFEN } = await import("./chat-kreis-stufen.js");
|
|
const mischen2 = (rgb, misch) => {
|
|
if (!misch) return rgb;
|
|
const ziel = misch < 0 ? 0 : 255;
|
|
const t2 = Math.abs(misch);
|
|
return rgb.map((v) => Math.round(v * (1 - t2) + ziel * t2));
|
|
};
|
|
let schlimmster = Infinity, schlimmsterWinkel = -1, gemessen = 0;
|
|
let schlimmsteStufe = "";
|
|
for (let w = 0; w < KREIS.length; w++) {
|
|
const f = zuZahl(KREIS[w]);
|
|
if (!f) continue;
|
|
for (const st of KREIS_STUFEN) {
|
|
gemessen++;
|
|
const ton = mischen2(f, st.misch);
|
|
const name = ton.map((v, i) => Math.round(v * anteil + hell[i] * (1 - anteil)));
|
|
const v2 = kontrast(name, schlimmerGrund) / 4.5;
|
|
if (v2 < schlimmster) {
|
|
schlimmster = v2; schlimmsterWinkel = w; schlimmsteStufe = st.wert;
|
|
}
|
|
}
|
|
}
|
|
/* DIE ANZAHL STEHT IN DER BEDINGUNG. Waere die Tabelle leer,
|
|
bliebe schlimmster auf Infinity und die Zeile waere gruen,
|
|
ohne eine einzige Farbe angesehen zu haben. */
|
|
/* DIE ANZAHL STEHT IN DER BEDINGUNG -- und sie ist seit heute
|
|
360 mal die Zahl der Stufen. Waere die Stufenliste leer,
|
|
bliebe `gemessen` bei 0 und die Zeile waere gruen, ohne eine
|
|
einzige Farbe angesehen zu haben. */
|
|
ok(gemessen === 360 * KREIS_STUFEN.length && schlimmster >= 1,
|
|
`alle ${gemessen} Farben des Rings (360 Winkel x ${KREIS_STUFEN.length} `
|
|
+ `Helligkeiten) sind als Name auf der Blase lesbar (knappste: Winkel `
|
|
+ `${schlimmsterWinkel}, Stufe ${schlimmsteStufe}, Faktor ${schlimmster.toFixed(3)})`);
|
|
}
|
|
}
|
|
if (textRgb && leiseRgb) {
|
|
/* Und die Schrift der Blase selbst -- sie ist seit dem Umbau fest
|
|
und steht auf einem festen Grund. Zwei Zahlen statt 360. */
|
|
void textRgb; void leiseRgb;
|
|
}
|
|
|
|
/* ---- 4b. Steht der Ring da, und ist er zu treffen? ------------- */
|
|
/* ERST ZURUECK ZUR LISTE -- und das ist keine Umstaendlichkeit,
|
|
sondern der Weg, den ein Mensch am Handy geht. Der Farbknopf
|
|
steht in der Spalte mit den Gespraechen, und die weicht auf
|
|
einem schmalen Bildschirm zur Seite, sobald ein Gespraech offen
|
|
ist. Der erste Anlauf dieser Probe hat genau hier 30 Sekunden
|
|
lang auf einen Knopf gewartet, den es zwar gab, den aber niemand
|
|
sehen konnte -- ein gutes Beispiel dafuer, dass "ist im HTML"
|
|
und "ist erreichbar" zwei verschiedene Aussagen sind. */
|
|
const zurueck = frida.seite.locator("#zur-liste");
|
|
if (await zurueck.isVisible().catch(() => false)) {
|
|
await zurueck.click();
|
|
await frida.seite.waitForTimeout(600);
|
|
}
|
|
const knopfDa = await frida.seite.locator("#kachel-oeffnen").isVisible().catch(() => false);
|
|
ok(knopfDa, "der Farbknopf ist am Handy erreichbar (ueber die Gespraechsliste)");
|
|
if (!knopfDa) throw new Error("ohne den Knopf ist der Rest nicht messbar");
|
|
await frida.seite.click("#kachel-oeffnen");
|
|
await frida.seite.waitForTimeout(600);
|
|
|
|
const rad = await frida.seite.evaluate(() => {
|
|
const r = document.getElementById("kachelrad-ring");
|
|
if (!r) return null;
|
|
const k = r.getBoundingClientRect();
|
|
const g = getComputedStyle(r);
|
|
return {
|
|
breit: Math.round(k.width), hoch: Math.round(k.height),
|
|
links: Math.round(k.left), oben: Math.round(k.top),
|
|
imBild: k.left >= -1 && k.right <= innerWidth + 1
|
|
&& k.top >= -1 && k.bottom <= innerHeight + 1,
|
|
farben: (g.backgroundImage.match(/rgb\(/g) || []).length,
|
|
fassen: g.touchAction,
|
|
rolle: r.getAttribute("role"),
|
|
};
|
|
});
|
|
ok(!!rad, "der Ring ist da");
|
|
/* 180 px sind nicht geraten: Ein Ring, den man mit dem Daumen im
|
|
Kreis fuehren soll, braucht sichtbar mehr als die 44 px einer
|
|
Schaltflaeche -- sonst rutscht man beim Drehen heraus. */
|
|
ok((rad?.breit || 0) >= 180 && rad?.imBild,
|
|
`er passt auf den Bildschirm und ist gross genug (${rad?.breit}x${rad?.hoch} px)`);
|
|
/* Der Verlauf muss aus VIELEN Farben bestehen. Waere die Liste
|
|
nicht angekommen, stuende dort ein einzelner Ton -- und der Ring
|
|
saehe aus wie eine Scheibe. */
|
|
ok((rad?.farben || 0) >= 300, `er ist aus ${rad?.farben} Farben gemalt`);
|
|
/* OHNE DAS SCHIEBT DAS HANDY DIE SEITE WEG, statt den Ring zu
|
|
drehen. Genau diese Zeile ist es, die auf einem Beruehrgeraet
|
|
ueber brauchbar und unbrauchbar entscheidet. */
|
|
ok(rad?.fassen === "none", `Wischen dreht den Ring, statt zu scrollen (${rad?.fassen})`);
|
|
ok(rad?.rolle === "slider", `und eine Sprachausgabe versteht ihn (${rad?.rolle})`);
|
|
|
|
/* ---- 4c. Drehen -- und kommt die richtige Farbe heraus? -------- */
|
|
if (rad) {
|
|
/* Winkel 90 heisst DREI UHR: der Verlauf beginnt oben und laeuft
|
|
im Uhrzeigersinn. Waere die Rechnung im Browser um 90 Grad
|
|
verdreht (der uebliche atan2-Fehler), kaeme hier eine andere
|
|
Farbe heraus als die, die unter dem Finger liegt. */
|
|
const mx = rad.links + rad.breit / 2;
|
|
const my = rad.oben + rad.hoch / 2;
|
|
const r80 = (rad.breit / 2) * 0.8;
|
|
await frida.seite.mouse.move(mx, my - r80);
|
|
await frida.seite.mouse.down();
|
|
await frida.seite.mouse.move(mx + r80, my, { steps: 12 });
|
|
await frida.seite.mouse.up();
|
|
await frida.seite.waitForTimeout(900);
|
|
|
|
const stand = await frida.seite.evaluate(() => {
|
|
const r = document.getElementById("kachelrad-ring");
|
|
const p2 = document.querySelector(".kachelrad__probe");
|
|
const leiter = document.getElementById("kachelhell-leiter");
|
|
return { winkel: Number(r?.getAttribute("aria-valuenow")),
|
|
wort: r?.getAttribute("aria-valuetext") || "",
|
|
/* NICHT MEHR DER GRUND, SONDERN DIE SCHRIFT. Siehe unten. */
|
|
probe: p2 ? getComputedStyle(p2).color : "",
|
|
stufen: leiter ? leiter.children.length : 0,
|
|
stufeJetzt: leiter?.getAttribute("aria-valuenow") || "" };
|
|
});
|
|
ok(Math.abs((stand.winkel || 0) - 90) <= 2,
|
|
`nach dem Drehen auf drei Uhr steht der Ring bei ${stand.winkel} Grad (erwartet 90)`);
|
|
|
|
/* ==== DIE PROBE MISST SEIT HEUTE ETWAS ANDERES (25.09.2026) =====
|
|
|
|
Hier stand: „die Mitte zeigt genau die Farbe dieses Winkels" --
|
|
gemessen am HINTERGRUND der Mitte. Das war richtig, solange die
|
|
Blase in der gewaehlten Farbe stand.
|
|
|
|
Seit dem 25.09.2026 ist die Blase fuer alle dunkles Glas, und
|
|
der gewaehlte Ton ist der NAME darauf. Die Probe zeigte damit
|
|
eine Darstellung, die es im Chat nicht mehr gibt -- und die
|
|
Pruefung haette sie festgehalten.
|
|
|
|
GEMESSEN WIRD JETZT DIE SCHRIFT der Probe: Sie muss genau die
|
|
Farbe tragen, die ein Name in der Blase traegt -- der Ton, mit
|
|
`--namen-anteil` nach #f2f8ff aufgehellt. Das ist dieselbe
|
|
Rechnung wie in chat.css, und sie wird hier NACHGERECHNET statt
|
|
abgeschrieben. */
|
|
const grundTon = zuZahl(KREIS[stand.winkel] || "");
|
|
const hell = zuZahl("#f2f8ff");
|
|
const anteil = 0.5; /* --namen-anteil; weiter unten aus dem CSS geprueft */
|
|
const erwartet = grundTon
|
|
? grundTon.map((v, i) => Math.round(v * anteil + hell[i] * (1 - anteil)))
|
|
: null;
|
|
const gezeigt = zuZahl(stand.probe);
|
|
ok(!!erwartet && !!gezeigt
|
|
&& Math.abs(erwartet[0] - gezeigt[0]) <= 2
|
|
&& Math.abs(erwartet[1] - gezeigt[1]) <= 2
|
|
&& Math.abs(erwartet[2] - gezeigt[2]) <= 2,
|
|
`die Probe zeigt den Namen in genau dieser Farbe (${stand.probe} gegen `
|
|
+ `${erwartet ? "rgb(" + erwartet.join(", ") + ")" : "?"})`);
|
|
|
|
/* ==== UND DIE SIEBEN HELLIGKEITEN (25.09.2026) =================
|
|
|
|
Filipe: „ich will dass die leute auch heller und dunkler
|
|
aussuchen koennen … so dass die leute viel krassere
|
|
moeglichkeiten haben." */
|
|
ok(stand.stufen === 7,
|
|
`die Helligkeitsleiter hat sieben Stufen (${stand.stufen})`);
|
|
ok(stand.stufeJetzt === "4",
|
|
`und steht von sich aus in der Mitte (${stand.stufeJetzt})`);
|
|
|
|
/* EINE STUFE WAEHLEN UND NACHSEHEN, OB SIE ANKOMMT. Ohne das
|
|
waere „es gibt sieben Felder" ein Haken ueber Zierde: Sieben
|
|
Kaestchen, die nichts tun, sehen genauso aus. */
|
|
await frida.seite.click(".kachelhell__feld[data-stufe='s7']");
|
|
await frida.seite.waitForTimeout(700);
|
|
const hellStand = await frida.seite.evaluate(() => {
|
|
const p2 = document.querySelector(".kachelrad__probe");
|
|
const leiter = document.getElementById("kachelhell-leiter");
|
|
const an = leiter?.querySelector('[data-an="ja"]');
|
|
return { probe: p2 ? getComputedStyle(p2).color : "",
|
|
stufe: leiter?.getAttribute("aria-valuenow") || "",
|
|
markiert: an?.dataset.stufe || "" };
|
|
});
|
|
ok(hellStand.stufe === "7" && hellStand.markiert === "s7",
|
|
`eine hellere Stufe laesst sich waehlen (${hellStand.stufe}, ${hellStand.markiert})`);
|
|
const vorher2 = zuZahl(stand.probe), nachher2 = zuZahl(hellStand.probe);
|
|
ok(!!vorher2 && !!nachher2 && leucht(nachher2) > leucht(vorher2),
|
|
`und die Probe wird dabei wirklich heller `
|
|
+ `(${leucht(vorher2).toFixed(3)} auf ${leucht(nachher2).toFixed(3)})`);
|
|
|
|
await frida.seite.screenshot({ path: "server/pruef-farbkreis.png" });
|
|
|
|
/* Und zurueck in die Mitte, damit die Pruefungen danach mit dem
|
|
gewohnten Stand weiterarbeiten. */
|
|
await frida.seite.click(".kachelhell__feld[data-stufe='s4']");
|
|
await frida.seite.waitForTimeout(500);
|
|
ok(/\d+ Grad/.test(stand.wort) && stand.wort.length > 8,
|
|
`und sie wird vorgelesen als "${stand.wort}"`);
|
|
|
|
/* ---- 4d. Wird es gespeichert? ------------------------------- */
|
|
const db3 = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true });
|
|
const gespeichert = db3.prepare("SELECT chat_kachel FROM personen WHERE id = ?")
|
|
.get(idA)?.chat_kachel;
|
|
db3.close();
|
|
ok(gespeichert === "ton-" + stand.winkel,
|
|
`und in der Datenbank steht die Wahl (${gespeichert})`);
|
|
|
|
/* ---- 4e. Und mit der Tastatur? ------------------------------ */
|
|
await frida.seite.focus("#kachelrad-ring");
|
|
await frida.seite.keyboard.press("ArrowRight");
|
|
await frida.seite.waitForTimeout(200);
|
|
const nachher = await frida.seite.evaluate(() =>
|
|
Number(document.getElementById("kachelrad-ring")?.getAttribute("aria-valuenow")));
|
|
ok(nachher === (stand.winkel + 1) % 360,
|
|
`eine Pfeiltaste dreht um genau ein Grad weiter (${stand.winkel} -> ${nachher})`);
|
|
}
|
|
}
|
|
|
|
const alleMeldungen = [...frida.meckern, ...nele.meckern];
|
|
console.log("");
|
|
ok(alleMeldungen.length === 0,
|
|
alleMeldungen.length
|
|
? `ein Browser meldet etwas: ${alleMeldungen[0]}`
|
|
: "kein Browser meldet etwas");
|
|
|
|
await frida.ctx.close(); await nele.ctx.close();
|
|
await browser.close();
|
|
await new Promise((r) => vorbau.close(r));
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Rest */ }
|
|
|
|
console.log("");
|
|
console.log(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
|
console.log(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
|
process.exit(fehler ? 1 : 0);
|