Gestern Mittag gemessen: 39 `newContext`-Aufrufe nehmen ihre Breite
aus einer Variablen und setzen kein `hasTouch`. Ohne das meldet der
Browser `pointer: fine`, und KEINE Regel aus `@media (pointer:
coarse)` greift — dort stehen im ganzen Haus die 44-Pixel-
Beruehrziele, die ausgeblendeten Tastenkuerzel und die
eingeklappte Reiterleiste.
Was das anrichtet, war an pruef-breiten zu sehen: drei Befunde auf
320, 390 und 430 Pixeln, die mit dem Finger allesamt verschwanden —
Messfehler, keine Fehler.
37 DAVON HABEN IHN JETZT. Nur `hasTouch`, nicht `isMobile`: Gefragt
ist genau die eine Sache, um die es geht. `isMobile` waere eine
zweite Aenderung in derselben Zeile, und wenn danach etwas anders
aussieht, wuesste niemand, welche von beiden es war.
ZWEI BLEIBEN STEHEN, beide in pruef-grosscheck.mjs. Die Aenderung
dort waere ein Zweizeiler; sie zu pruefen hiesse, 206 Seiten ueber
vier Rollen laufen zu lassen, und das braucht Filipes Zusage. Eine
Aenderung, die ich nicht pruefen darf, liefere ich nicht aus.
Grundlinie in pruef-fingermass steht deshalb auf 2.
JEDE EINZELN NACHGELAUFEN — 37 Laeufe:
34 gruen, darunter pruef-handy 186, pruef-material 159,
pruef-start-ansicht 160, pruef-kalender 141, pruef-erwaehnung 129,
pruef-bewerbung-aufgaben 163, pruef-neue-seiten 109
3 mit Befunden, ALLE DREI VORBESTEHEND (Gegenprobe: alter Stand
derselben Datei, gleicher Lauf, gleiche Zahl):
pruef-browser 3 (WebKit startet auf diesem Rechner nicht)
pruef-chat-anhaenge 2
pruef-crew-wand-bild 3
EINE ZEITBOMBE GEFUNDEN UND ENTSCHAERFT
pruef-dabei-optik meldete „Haekchen: 0 -> 0" — aber nur, wenn vier
andere Pruefungen gleichzeitig liefen. Allein: „0 -> 1", gruen.
Dort stand `waitForTimeout(250)` mit der Begruendung „250 ms sind
reichlich ueber den 160" (der Dauer der Blende). Auf einem Rechner,
auf dem nebenher vier Browser messen, sind sie es nicht. Die
Pruefung war damit gruen, solange nichts anderes lief, und rot im
Gesamtlauf — also genau dann, wenn niemand sie einzeln nachstellen
kann.
Eine Wartezeit ist eine Annahme ueber den Rechner. Gewartet wird
jetzt auf das, worauf es ankommt: dass das Haekchen da ist. Laeuft
die Frist ab, faellt die Pruefung mit dem ECHTEN Wert um und nicht
mit einem Messfehler. Gegenprobe: unter derselben vierfachen Last,
die sie vorher rot gemacht hat, jetzt gruen.
UND DIE GRUNDLINIE WIEDER STRENG. Ich hatte sie kurz auf
„hoechstens" gestellt — damit haette ein Rueckgang stillschweigend
Platz fuer die naechste Suende gedeckt. Genau davor warnt der Kopf
derselben Datei bei der anderen Grundlinie, und ich habe es eine
Stunde spaeter selbst falsch gemacht.
Co-Authored-By: Claude Opus 5 <[email protected]>
724 lines
36 KiB
JavaScript
724 lines
36 KiB
JavaScript
/* CHAT — DER AUSBAU VOM 09.09.2026
|
||
|
||
Filipe: "die kategorie chat, boah ich will dass du die wirklich
|
||
überall perfektionierst. wirklich alles was gratis ist und
|
||
hinzugefügt werden kann ohne problem will ich drin haben bitte."
|
||
|
||
Sechs Dinge sind dazugekommen. Diese Prüfung misst sie einzeln, an
|
||
einem echten Browser, mit zwei angemeldeten Personen:
|
||
|
||
1. Adressen im Text sind anklickbar -- und zwar als echte Knoten,
|
||
nicht über innerHTML. Ein Chat ist die eine Stelle, an der
|
||
JEDER schreiben darf; hier wäre eine Einschleusung am teuersten.
|
||
2. Jede Nachricht lässt sich kopieren.
|
||
3. Der Verlauf reisst niemanden mehr weg, der weiter oben liest --
|
||
stattdessen sagt ein Knopf, wie viel unten wartet.
|
||
4. Ein angefangener Satz überlebt den Wechsel des Gesprächs.
|
||
5. Eine Emoji-Auswahl, die an der Cursorstelle einfügt.
|
||
6. Ein Zeichenzähler, der erst erscheint, wenn es eng wird.
|
||
|
||
ZU JEDEM PUNKT EINE GEGENPROBE. Eine Prüfung, die immer bestätigt,
|
||
bestätigt nichts -- deshalb steht neben jedem "es passiert" auch ein
|
||
"und es passiert NICHT, wenn es nicht soll".
|
||
|
||
DREI AUSGÄNGE: 0 in Ordnung · 1 Befunde · 2 konnte nicht arbeiten. */
|
||
|
||
import { mkdtempSync, rmSync, appendFileSync } from "node:fs";
|
||
import { tmpdir } from "node:os";
|
||
import { join } from "node:path";
|
||
|
||
const ordner = mkdtempSync(join(tmpdir(), "ws-chatausbau-"));
|
||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||
|
||
import { notbremse } from "./helfer-notbremse.mjs";
|
||
import { bestaetige } from "./helfer-nachfrage.mjs";
|
||
const { eigenerPort } = await import("./helfer-port.mjs");
|
||
const PORT = await eigenerPort(import.meta, "die Chat-Ausbaupruefung");
|
||
|
||
process.env.PORT = `${PORT}`;
|
||
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
||
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
||
|
||
const express = (await import("express")).default;
|
||
const ec = express.response.cookie;
|
||
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
||
await import("./index.js");
|
||
notbremse(480_000, "pruef-chat-ausbau");
|
||
await new Promise((r) => setTimeout(r, 1000));
|
||
const BASIS = `http://127.0.0.1:${PORT}`;
|
||
|
||
const PROTOKOLL = join(process.cwd(), "pruef-chat-ausbau-lauf.txt");
|
||
try { rmSync(PROTOKOLL, { force: true }); } catch { /* gab es noch nicht */ }
|
||
const melde = (z) => { console.log(z); try { appendFileSync(PROTOKOLL, z + "\n"); } catch { /* Zugabe */ } };
|
||
let fehler = 0, geprueft = 0;
|
||
const ok = (b, t) => { geprueft++; melde((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
||
|
||
/* ---- Bestand ------------------------------------------------------------ */
|
||
const { DatabaseSync } = await import("node:sqlite");
|
||
const { scryptSync, randomBytes } = await import("node:crypto");
|
||
await fetch(BASIS + "/workspace/api/anmelden", {
|
||
method: "POST", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
|
||
for (let i = 0; i < 60; i++) {
|
||
try {
|
||
const p = new DatabaseSync(process.env.WORKSPACE_DB);
|
||
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
|
||
} catch { await new Promise((r) => setTimeout(r, 250)); }
|
||
}
|
||
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
||
const jetzt = new Date().toISOString();
|
||
function anlegen(name, rolle, code) {
|
||
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 idPat = anlegen("Patrick", "scout", "CODE-SCOU-0001");
|
||
const idLuna = anlegen("Luna", "creator", "CODE-CREA-0001");
|
||
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idLuna, idPat, jetzt);
|
||
d.close();
|
||
|
||
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||
const browser = await pw.chromium.launch();
|
||
|
||
async function alsRolle(rolle, code, breite = 1280) {
|
||
const ctx = await browser.newContext({
|
||
viewport: { width: breite, height: 900 }, hasTouch: breite <= 860,
|
||
permissions: ["clipboard-read", "clipboard-write"],
|
||
});
|
||
const seite = await ctx.newPage();
|
||
const konsole = [];
|
||
/* MIT HERKUNFT. Eine Meldung ohne Datei und Zeile kostet die halbe
|
||
Suche -- genau das ist mir beim ersten Lauf passiert. */
|
||
seite.on("pageerror", (f) => konsole.push("Absturz auf " + seite.url() + ": " + f.message
|
||
+ " || " + String(f.stack || "").split(/\r?\n/).slice(1, 4).join(" / ")));
|
||
seite.on("console", (m) => { if (m.type() === "error") konsole.push("Konsole: " + m.text()); });
|
||
await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
||
await seite.click(`.rolle[data-rolle="${rolle}"]`);
|
||
await seite.fill("#code", code);
|
||
await seite.click("button[type=submit]");
|
||
await seite.waitForURL("**/start.html", { timeout: 25000 });
|
||
await seite.goto(BASIS + "/workspace/chat.html", { waitUntil: "networkidle" });
|
||
await seite.waitForTimeout(900);
|
||
return { ctx, seite, konsole };
|
||
}
|
||
|
||
/* Eine Nachricht auf dem kurzen Weg -- über die echte Schnittstelle der
|
||
Seite, damit Herkunftsprüfung und Sitzung mitlaufen. */
|
||
async function schicke(s, raum, text) {
|
||
return s.evaluate(async ([r, t]) => {
|
||
const a = await fetch(`/workspace/api/chat/raeume/${r}/nachrichten`, {
|
||
method: "POST", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ text: t }),
|
||
});
|
||
return a.ok;
|
||
}, [raum, text]);
|
||
}
|
||
|
||
const luna = await alsRolle("creator", "CODE-CREA-0001");
|
||
const pat = await alsRolle("scout", "CODE-SCOU-0001");
|
||
|
||
/* Zwei Gespräche: eines mit Patrick, eines mit DogFather. Das zweite
|
||
braucht es für die Entwürfe -- ohne Wechsel kein Wechseltest. */
|
||
async function direkt(s, mit) {
|
||
return s.evaluate(async (m) => {
|
||
const a = await fetch("/workspace/api/chat/direkt", {
|
||
method: "POST", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ mit: m }) });
|
||
return (await a.json()).raum_id;
|
||
}, mit);
|
||
}
|
||
const raumId = await direkt(luna.seite, idPat);
|
||
const raumDogi = await direkt(luna.seite, idDogi);
|
||
ok(Number.isInteger(raumId) && raumId > 0, `das Gespräch mit Patrick steht (Raum ${raumId})`);
|
||
ok(Number.isInteger(raumDogi) && raumDogi !== raumId, `und eines mit DogFather (Raum ${raumDogi})`);
|
||
|
||
async function oeffne(s, id) {
|
||
await s.goto(`${BASIS}/workspace/chat.html?raum=${id}`, { waitUntil: "networkidle" });
|
||
await s.waitForSelector("#schreiben:not([hidden])", { timeout: 8000 });
|
||
await s.waitForTimeout(500);
|
||
}
|
||
|
||
/* =======================================================================
|
||
1. ADRESSEN IM TEXT
|
||
======================================================================= */
|
||
melde("\n=== 1. Adressen im Text ===");
|
||
const MIT_LINKS = "Schau mal https://workspace.dogfather-universe.com/start.html "
|
||
+ "und www.dogfather-universe.com, danke!";
|
||
{
|
||
await schicke(luna.seite, raumId, MIT_LINKS);
|
||
await oeffne(luna.seite, raumId);
|
||
|
||
const links = await luna.seite.$$eval(".chat-nachricht:last-of-type .chat-nachricht__link",
|
||
(a) => a.map((x) => ({ text: x.textContent, href: x.href, ziel: x.target, rel: x.rel })));
|
||
ok(links.length === 2, `beide Adressen sind Links (${links.length} gefunden)`);
|
||
ok(links[0]?.href === "https://workspace.dogfather-universe.com/start.html",
|
||
`die erste zeigt dorthin, wo sie steht (${links[0]?.href})`);
|
||
ok(links[1]?.href === "https://www.dogfather-universe.com/",
|
||
`bei "www." wird https ergänzt (${links[1]?.href})`);
|
||
ok(links[1]?.text === "www.dogfather-universe.com",
|
||
`das Komma dahinter gehört zum Satz, nicht zur Adresse ("${links[1]?.text}")`);
|
||
ok(links.every((l) => l.ziel === "_blank"), "sie gehen in einen neuen Tab");
|
||
ok(links.every((l) => /noopener/.test(l.rel) && /noreferrer/.test(l.rel)),
|
||
`rel="noopener noreferrer" steht dran (${links[0]?.rel})`);
|
||
|
||
/* NICHTS DARF VERLOREN GEHEN. Beim Zerlegen eines Textes in Stücke ist
|
||
genau das der Fehler, den man nicht sieht: ein Zeichen zu viel oder
|
||
zu wenig an einer Nahtstelle. */
|
||
const ganz = await luna.seite.$eval(".chat-nachricht:last-of-type .chat-nachricht__text",
|
||
(p) => p.textContent);
|
||
ok(ganz === MIT_LINKS, "der Text ist Zeichen für Zeichen derselbe geblieben");
|
||
|
||
/* GEGENPROBE A: ohne Adresse kein Link. */
|
||
await schicke(luna.seite, raumId, "Hier steht z.b. gar keine Adresse, drei.punkte inklusive.");
|
||
await oeffne(luna.seite, raumId);
|
||
const ohne = await luna.seite.$$eval(".chat-nachricht:last-of-type .chat-nachricht__link", (a) => a.length);
|
||
ok(ohne === 0, `ein Satz ohne Adresse bekommt keinen Link (${ohne})`);
|
||
|
||
/* GEGENPROBE B: Auszeichnung wird NICHT ausgeführt. Das ist der
|
||
eigentliche Grund, warum hier nicht innerHTML steht. */
|
||
const BOESE = '<b>fett</b> <img src=x onerror="alert(1)"> & "Zitat"';
|
||
await schicke(luna.seite, raumId, BOESE);
|
||
await oeffne(luna.seite, raumId);
|
||
const roh = await luna.seite.$eval(".chat-nachricht:last-of-type .chat-nachricht__text",
|
||
(p) => ({ text: p.textContent, kinder: p.querySelectorAll("b, img, script").length }));
|
||
ok(roh.kinder === 0, `keine eingeschleuste Auszeichnung (${roh.kinder} Fremdelemente)`);
|
||
ok(roh.text === BOESE, "der Text steht als Text da");
|
||
}
|
||
|
||
/* =======================================================================
|
||
2. KOPIEREN
|
||
======================================================================= */
|
||
melde("\n=== 2. Kopieren ===");
|
||
{
|
||
await oeffne(luna.seite, raumId);
|
||
const anzahl = await luna.seite.$$eval(".chat-nachricht__kopie-knopf", (b) => b.length);
|
||
const wieviel = await luna.seite.$$eval(".chat-nachricht", (n) => n.length);
|
||
ok(anzahl === wieviel && anzahl >= 3,
|
||
`jede der ${wieviel} Nachrichten hat einen Kopier-Knopf (${anzahl})`);
|
||
|
||
await luna.seite.click(".chat-nachricht:first-of-type .chat-nachricht__kopie-knopf");
|
||
await luna.seite.waitForTimeout(300);
|
||
const inZwischenablage = await luna.seite.evaluate(() => navigator.clipboard.readText());
|
||
ok(inZwischenablage === MIT_LINKS,
|
||
`der Text liegt in der Zwischenablage (${inZwischenablage.length} Zeichen)`);
|
||
const beschriftung = await luna.seite.$eval(
|
||
".chat-nachricht:first-of-type .chat-nachricht__kopie-knopf", (b) => b.textContent);
|
||
ok(beschriftung === "kopiert", `der Knopf bestätigt sich selbst ("${beschriftung}")`);
|
||
await luna.seite.waitForTimeout(1800);
|
||
const zurueck = await luna.seite.$eval(
|
||
".chat-nachricht:first-of-type .chat-nachricht__kopie-knopf", (b) => b.textContent);
|
||
ok(zurueck === "kopieren", `und fällt danach zurück ("${zurueck}")`);
|
||
|
||
/* ==== DIE WOERTER SIND NICHT WEG, NUR NICHT ZU SEHEN (25.09.2026) ==
|
||
|
||
Filipe: „ich will dass du das viel geiler machst und es soll viel
|
||
kuerzer gestaltet sein ... aber jeder sieht dass es da ist."
|
||
|
||
Aus den fuenf Woertern unter jeder Nachricht wurden fuenf Zeichen.
|
||
Das Wort steht weiter im Dokument -- nur fuers Auge weggeschnitten.
|
||
Ohne diese Pruefung waere der naechste Schritt, es „aufzuraeumen",
|
||
und dann waeren die Knoepfe fuer ein Vorleseprogramm fuenf leere
|
||
Felder. Das faellt niemandem auf, der sehen kann.
|
||
|
||
DREI DINGE, UND JEDES EINZELN:
|
||
1. Jeder Knopf hat ein Wort (textContent) und einen Satz fuer
|
||
Vorleseprogramme (aria-label).
|
||
2. KEINES der Woerter ist zu sehen -- sonst waere „kuerzer"
|
||
nicht eingehalten.
|
||
3. Die Reihe ist schmaler als vorher. Eine Zusage ueber Platz,
|
||
die man nicht misst, ist keine.
|
||
|
||
DIE ANZAHL STEHT IN DER BEDINGUNG: Faende der Selektor nichts,
|
||
waeren „alle 0 haben ein Wort" sonst gruen. */
|
||
const griffe = await luna.seite.evaluate(() => {
|
||
const knoepfe = [...document.querySelectorAll(
|
||
".chat-nachricht:first-of-type .chat-nachricht__fuss button")];
|
||
const fuss = document.querySelector(".chat-nachricht:first-of-type .chat-nachricht__fuss");
|
||
return {
|
||
zahl: knoepfe.length,
|
||
mitWort: knoepfe.filter((b) => (b.textContent || "").trim().length > 1).length,
|
||
mitSatz: knoepfe.filter((b) => (b.getAttribute("aria-label") || "").trim().length > 1).length,
|
||
/* Sichtbar heisst: es nimmt Flaeche ein. `clip-path` laesst die
|
||
Breite stehen, deshalb wird die HOEHE gemessen -- sie ist bei
|
||
einem weggeschnittenen Stueck Text 1 px. */
|
||
sichtbareWoerter: [...document.querySelectorAll(
|
||
".chat-nachricht:first-of-type .chat-nachricht__wort")]
|
||
.filter((e) => e.getBoundingClientRect().height > 4).length,
|
||
/* NICHT DIE BREITE DES KASTENS -- die ist die der Blase, weil
|
||
der Fuss ein Block ist. Beim ersten Anlauf stand hier 486 px
|
||
und sah aus wie ein Befund; gemessen war die Blase.
|
||
|
||
GEMESSEN WIRD, WORUM ES GEHT: wie viele ZEILEN die Reihe
|
||
braucht. „Nicht zu viel platzraubend" ist eine Aussage ueber
|
||
Hoehe, nicht ueber Breite. Vor dem Umbau waren es auf 412 px
|
||
drei Zeilen; eine ist das Ziel.
|
||
|
||
Ueber die y-Werte und nicht ueber die Hoehe des Kastens: Die
|
||
Hoehe haengt auch an Polsterung und Zeilenabstand, die
|
||
verschiedenen y-Werte sagen genau eines -- hier ist umgebrochen
|
||
worden. */
|
||
zeilen: new Set(knoepfe.map((b) => Math.round(b.getBoundingClientRect().top))).size,
|
||
hoehe: fuss ? Math.round(fuss.getBoundingClientRect().height) : 0,
|
||
inhalt: knoepfe.length
|
||
? Math.round(knoepfe.at(-1).getBoundingClientRect().right
|
||
- knoepfe[0].getBoundingClientRect().left)
|
||
: 0,
|
||
};
|
||
});
|
||
ok(griffe.zahl >= 3 && griffe.mitWort === griffe.zahl,
|
||
`jeder der ${griffe.zahl} Handgriffe hat weiterhin sein Wort (${griffe.mitWort})`);
|
||
ok(griffe.zahl >= 3 && griffe.mitSatz === griffe.zahl,
|
||
`und einen Satz für Vorleseprogramme (${griffe.mitSatz})`);
|
||
ok(griffe.sichtbareWoerter === 0,
|
||
`Gegenprobe: zu SEHEN ist keines davon (${griffe.sichtbareWoerter})`);
|
||
ok(griffe.zeilen === 1,
|
||
`und alle stehen in EINER Zeile (${griffe.zeilen})`);
|
||
/* Die Zeichen zusammen brauchen rund 150 px. Vor dem Umbau standen
|
||
dort fuenf Woerter mit Zeichen -- deutlich ueber 400. */
|
||
ok(griffe.inhalt > 0 && griffe.inhalt < 220,
|
||
`und nehmen zusammen nur ${griffe.inhalt} px Breite`);
|
||
|
||
/* GEGENPROBE, NEU GEFASST (22.09.2026 abends).
|
||
------------------------------------------------------------------
|
||
Hier wurde geprueft, dass an einer ZURUECKGENOMMENEN Nachricht kein
|
||
Kopier-Knopf steht. Es gibt keine zurueckgenommenen mehr -- Filipe:
|
||
„Geloeschte Nachrichten verschwinden vollstaendig."
|
||
|
||
Die Frage dahinter bleibt richtig und wird jetzt direkt gestellt:
|
||
Nach dem Loeschen ist die Nachricht WEG, samt allem, was an ihr
|
||
hing. Gemessen an der Zahl der Nachrichten davor und danach --
|
||
eine Zahl, die sich um genau eins aendern muss. */
|
||
const vorLoeschen = await luna.seite.$$eval('.chat-nachricht', (b) => b.length);
|
||
await bestaetige(luna.seite, () => luna.seite.click(
|
||
".chat-nachricht:last-of-type .chat-nachricht__weg-knopf"));
|
||
await luna.seite.waitForTimeout(900);
|
||
const nachLoeschen = await luna.seite.$$eval('.chat-nachricht', (b) => b.length);
|
||
const restWeg = await luna.seite.$$eval('.chat-nachricht[data-weg="ja"]', (b) => b.length);
|
||
ok(vorLoeschen > 0 && nachLoeschen === vorLoeschen - 1 && restWeg === 0,
|
||
`die geloeschte ist aus dem Bild (${vorLoeschen} -> ${nachLoeschen}),`
|
||
+ ` und es bleibt keine Spur (${restWeg})`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
3. DER VERLAUF REISST NIEMANDEN MEHR WEG
|
||
======================================================================= */
|
||
melde("\n=== 3. Zum neuesten ===");
|
||
{
|
||
for (let i = 1; i <= 40; i++) await schicke(pat.seite, raumId, `Zeile ${i} zum Vollmachen des Verlaufs.`);
|
||
await oeffne(luna.seite, raumId);
|
||
await oeffne(pat.seite, raumId);
|
||
|
||
const lang = await luna.seite.$eval("#verlauf", (e) => e.scrollHeight > e.clientHeight + 200);
|
||
ok(lang, "der Verlauf ist lang genug, um überhaupt scrollen zu können");
|
||
|
||
const amEnde = await luna.seite.$eval("#verlauf", (e) => e.scrollHeight - e.scrollTop - e.clientHeight < 40);
|
||
const knopfZu = await luna.seite.$eval("#zum-neuesten", (e) => e.hidden);
|
||
ok(amEnde && knopfZu, "beim Öffnen steht er unten, und der Knopf ist weg");
|
||
|
||
/* Luna liest weiter oben -- UND SIE SCROLLT WIE EIN MENSCH.
|
||
|
||
Hier stand `e.scrollTop = 0`. Das setzt die Bildlaufposition zu,
|
||
ohne dass ein Rad gedreht, ein Finger gewischt oder eine Taste
|
||
gedrueckt wurde. Genau daran haengt im Chat aber eine
|
||
Entscheidung: „Wer selbst scrollt, hat entschieden" -- ab da
|
||
wird nicht mehr zur Neu-Linie gesprungen (die Zuhoerer sind
|
||
`wheel`, `touchstart`, `keydown`, `pointerdown`).
|
||
|
||
Eine Zuweisung loest keines davon aus. Die Anwendung hielt Luna
|
||
deshalb weiter fuer jemanden, der nicht selbst gescrollt hat,
|
||
sprang zur Neu-Linie und setzte den Zaehler zurueck -- voellig
|
||
richtig. Dieselbe Lehre wie bei den Fingergeraeten:
|
||
`setViewportSize` macht aus einer Maus keinen Finger, und
|
||
`scrollTop = 0` macht aus einem Programm keinen Leser. */
|
||
await luna.seite.hover("#verlauf");
|
||
for (let i = 0; i < 12; i++) {
|
||
await luna.seite.mouse.wheel(0, -2000);
|
||
await luna.seite.waitForTimeout(60);
|
||
const oben = await luna.seite.$eval("#verlauf", (e) => e.scrollTop);
|
||
if (oben <= 0) break;
|
||
}
|
||
await luna.seite.waitForTimeout(400);
|
||
const obenAngekommen = await luna.seite.$eval("#verlauf", (e) => e.scrollTop);
|
||
ok(obenAngekommen < 40,
|
||
`Luna ist mit dem Rad nach oben gescrollt (${obenAngekommen})`);
|
||
const vorher = await luna.seite.$eval("#verlauf", (e) => e.scrollTop);
|
||
|
||
await schicke(pat.seite, raumId, "Und noch etwas Neues dazu.");
|
||
await luna.seite.waitForFunction(
|
||
() => !document.getElementById("zum-neuesten").hidden, { timeout: 8000 })
|
||
.then(() => true).catch(() => false);
|
||
const nachher = await luna.seite.$eval("#verlauf", (e) => e.scrollTop);
|
||
ok(Math.abs(nachher - vorher) < 3, `die Stelle bleibt stehen (${vorher} -> ${nachher})`);
|
||
const text1 = await luna.seite.$eval("#zum-neuesten", (e) => e.textContent);
|
||
ok(text1 === "1 neue Nachricht", `der Knopf sagt, wie viel wartet ("${text1}")`);
|
||
|
||
await schicke(pat.seite, raumId, "Und noch eine.");
|
||
await luna.seite.waitForFunction(
|
||
() => document.getElementById("zum-neuesten").textContent === "2 neue Nachrichten",
|
||
{ timeout: 8000 }).then(() => true).catch(() => false);
|
||
const text2 = await luna.seite.$eval("#zum-neuesten", (e) => e.textContent);
|
||
ok(text2 === "2 neue Nachrichten", `er zählt mit ("${text2}")`);
|
||
|
||
await luna.seite.click("#zum-neuesten");
|
||
await luna.seite.waitForTimeout(400);
|
||
const jetztUnten = await luna.seite.$eval("#verlauf", (e) => e.scrollHeight - e.scrollTop - e.clientHeight < 40);
|
||
const wiederZu = await luna.seite.$eval("#zum-neuesten", (e) => e.hidden);
|
||
ok(jetztUnten && wiederZu, "ein Klick bringt ans Ende, und der Knopf verschwindet");
|
||
|
||
/* GEGENPROBE: Wer schon unten steht, wird weiterhin mitgenommen --
|
||
sonst hätte ich die eine Unart gegen die andere getauscht. */
|
||
await schicke(pat.seite, raumId, "Eine letzte, während Luna unten steht.");
|
||
await luna.seite.waitForFunction(
|
||
() => [...document.querySelectorAll(".chat-nachricht__text")]
|
||
.some((t) => /während Luna unten steht/.test(t.textContent)), { timeout: 8000 })
|
||
.then(() => true).catch(() => false);
|
||
await luna.seite.waitForTimeout(400);
|
||
const bleibtUnten = await luna.seite.$eval("#verlauf", (e) => e.scrollHeight - e.scrollTop - e.clientHeight < 40);
|
||
const bleibtZu = await luna.seite.$eval("#zum-neuesten", (e) => e.hidden);
|
||
ok(bleibtUnten && bleibtZu, "wer unten steht, wird weiterhin mitgenommen — ohne Knopf");
|
||
}
|
||
|
||
/* =======================================================================
|
||
4. DER ANGEFANGENE SATZ
|
||
======================================================================= */
|
||
melde("\n=== 4. Entwürfe ===");
|
||
{
|
||
await oeffne(luna.seite, raumId);
|
||
await luna.seite.fill("#text", "Halber Satz an Patrick");
|
||
await luna.seite.waitForTimeout(250);
|
||
await oeffne(luna.seite, raumDogi);
|
||
const leerDrueben = await luna.seite.$eval("#text", (e) => e.value);
|
||
ok(leerDrueben === "", `im anderen Gespräch ist das Feld leer ("${leerDrueben}")`);
|
||
|
||
await luna.seite.fill("#text", "Und hier etwas ganz anderes");
|
||
await luna.seite.waitForTimeout(250);
|
||
await oeffne(luna.seite, raumId);
|
||
const wieder = await luna.seite.$eval("#text", (e) => e.value);
|
||
ok(wieder === "Halber Satz an Patrick", `der Satz ist wieder da ("${wieder}")`);
|
||
await oeffne(luna.seite, raumDogi);
|
||
const wieder2 = await luna.seite.$eval("#text", (e) => e.value);
|
||
ok(wieder2 === "Und hier etwas ganz anderes", "und jedes Gespräch behält seinen eigenen");
|
||
|
||
/* GEGENPROBE: Abgeschickt heisst weg. Eine Ablage, die nur wächst,
|
||
wäre ein Leck -- und beim nächsten Öffnen stünde der Satz doppelt. */
|
||
await oeffne(luna.seite, raumId);
|
||
await luna.seite.click("#senden");
|
||
await luna.seite.waitForTimeout(700);
|
||
const feldLeer = await luna.seite.$eval("#text", (e) => e.value);
|
||
const gespeichert = await luna.seite.evaluate((r) => localStorage.getItem(`dfw-chat-entwurf-${r}`), raumId);
|
||
ok(feldLeer === "" && gespeichert === null,
|
||
`nach dem Abschicken ist der Entwurf weg (Ablage: ${gespeichert})`);
|
||
await oeffne(luna.seite, raumId);
|
||
const nachSenden = await luna.seite.$eval("#text", (e) => e.value);
|
||
ok(nachSenden === "", "und er kommt beim nächsten Öffnen nicht zurück");
|
||
|
||
/* Und ein geleertes Feld löscht ihn auch. */
|
||
await oeffne(luna.seite, raumDogi);
|
||
await luna.seite.fill("#text", "");
|
||
await luna.seite.waitForTimeout(250);
|
||
const weg = await luna.seite.evaluate((r) => localStorage.getItem(`dfw-chat-entwurf-${r}`), raumDogi);
|
||
ok(weg === null, `wer sein Feld leert, hat auch keinen Entwurf mehr (${weg})`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
5. EMOJI
|
||
======================================================================= */
|
||
melde("\n=== 5. Emoji ===");
|
||
{
|
||
await oeffne(luna.seite, raumId);
|
||
const zuAmAnfang = await luna.seite.$eval("#emoji-tafel", (e) => e.hidden);
|
||
const auf1 = await luna.seite.$eval("#emoji-auf", (e) => e.getAttribute("aria-expanded"));
|
||
ok(zuAmAnfang && auf1 === "false", "die Tafel ist zu, bis jemand sie aufmacht");
|
||
|
||
await luna.seite.click("#emoji-auf");
|
||
await luna.seite.waitForTimeout(250);
|
||
const offenJetzt = await luna.seite.$eval("#emoji-tafel", (e) => !e.hidden);
|
||
const auf2 = await luna.seite.$eval("#emoji-auf", (e) => e.getAttribute("aria-expanded"));
|
||
const wieviele = await luna.seite.$$eval(".emoji-tafel__knopf", (b) => b.length);
|
||
ok(offenJetzt && auf2 === "true", "ein Klick macht sie auf, und die Vorlesehilfe weiss es");
|
||
ok(wieviele === 30, `dreissig Zeichen stehen zur Auswahl (${wieviele})`);
|
||
const beschriftet = await luna.seite.$$eval(".emoji-tafel__knopf",
|
||
(b) => b.every((x) => (x.getAttribute("aria-label") || "").length > 6));
|
||
ok(beschriftet, "jedes hat eine Beschriftung für das Vorleseprogramm");
|
||
|
||
/* GEGENPROBE: Der Knopf, mit dem sie aufgeht, darf sie nicht sofort
|
||
wieder zumachen -- genau das wäre der naheliegende Fehler bei einem
|
||
Zuhörer auf dem ganzen Dokument. */
|
||
ok(offenJetzt, "der eigene Knopf schliesst sie nicht versehentlich gleich wieder");
|
||
|
||
/* AN DER CURSORSTELLE, nicht am Ende. */
|
||
await luna.seite.fill("#text", "Hallo Welt");
|
||
await luna.seite.$eval("#text", (e) => { e.focus(); e.selectionStart = e.selectionEnd = 5; });
|
||
const zeichen = await luna.seite.$eval(".emoji-tafel__knopf:nth-child(11)", (b) => b.textContent);
|
||
await luna.seite.click(".emoji-tafel__knopf:nth-child(11)");
|
||
await luna.seite.waitForTimeout(250);
|
||
const danach = await luna.seite.$eval("#text", (e) => ({ wert: e.value, pos: e.selectionStart }));
|
||
ok(danach.wert === "Hallo" + zeichen + " Welt", `es landet an der Cursorstelle ("${danach.wert}")`);
|
||
ok(danach.pos === 5 + zeichen.length, `und der Cursor steht dahinter (${danach.pos})`);
|
||
|
||
/* Sie steht ÜBER dem Feld und ragt nicht seitlich hinaus. */
|
||
const lage = await luna.seite.evaluate(() => {
|
||
const t = document.getElementById("emoji-tafel").getBoundingClientRect();
|
||
const f = document.getElementById("text").getBoundingClientRect();
|
||
return { l: t.left, r: t.right, u: t.bottom, o: f.top, breite: innerWidth };
|
||
});
|
||
ok(lage.u <= lage.o + 2, `sie liegt über dem Schreibfeld (${lage.u.toFixed(0)} <= ${lage.o.toFixed(0)})`);
|
||
ok(lage.l >= 0 && lage.r <= lage.breite,
|
||
`und ragt nicht seitlich hinaus (${lage.l.toFixed(0)}–${lage.r.toFixed(0)} in ${lage.breite})`);
|
||
|
||
/* Escape und ein Klick daneben schliessen sie. */
|
||
/* Escape dort, wo der Finger gerade ist -- nach dem Aufmachen steht
|
||
die Schreibmarke auf dem ersten Zeichen der Tafel. */
|
||
await luna.seite.keyboard.press("Escape");
|
||
await luna.seite.waitForTimeout(250);
|
||
ok(await luna.seite.$eval("#emoji-tafel", (e) => e.hidden), "Escape schliesst sie");
|
||
await luna.seite.click("#emoji-auf");
|
||
await luna.seite.waitForTimeout(200);
|
||
await luna.seite.click("#verlauf-titel");
|
||
await luna.seite.waitForTimeout(250);
|
||
const zuNachKlick = await luna.seite.$eval("#emoji-tafel", (e) => e.hidden);
|
||
const auf3 = await luna.seite.$eval("#emoji-auf", (e) => e.getAttribute("aria-expanded"));
|
||
ok(zuNachKlick && auf3 === "false", "ein Klick daneben ebenfalls");
|
||
await luna.seite.fill("#text", "");
|
||
}
|
||
|
||
/* =======================================================================
|
||
6. ZEICHENZÄHLER
|
||
======================================================================= */
|
||
melde("\n=== 6. Zeichenzähler ===");
|
||
{
|
||
const grenze = await luna.seite.$eval("#text", (e) => Number(e.getAttribute("maxlength")));
|
||
ok(grenze === 4000, `die Grenze steht am Feld selbst (${grenze})`);
|
||
|
||
await luna.seite.fill("#text", "x".repeat(100));
|
||
await luna.seite.waitForTimeout(200);
|
||
ok(await luna.seite.$eval("#zeichen", (e) => e.hidden), "bei 100 Zeichen sagt er nichts");
|
||
|
||
await luna.seite.fill("#text", "x".repeat(grenze - 300));
|
||
await luna.seite.waitForTimeout(250);
|
||
const nah = await luna.seite.$eval("#zeichen", (e) => ({ zu: e.hidden, text: e.textContent, eng: e.dataset.eng }));
|
||
ok(!nah.zu && nah.text === "noch 300", `ab 400 Rest meldet er sich ("${nah.text}")`);
|
||
ok(nah.eng === "nein", "und ist dabei noch nicht rot");
|
||
|
||
await luna.seite.fill("#text", "x".repeat(grenze - 40));
|
||
await luna.seite.waitForTimeout(250);
|
||
const eng = await luna.seite.$eval("#zeichen", (e) => ({ text: e.textContent, eng: e.dataset.eng }));
|
||
ok(eng.text === "noch 40" && eng.eng === "ja", `zum Schluss wird er deutlich ("${eng.text}")`);
|
||
|
||
/* GEGENPROBE: Er verschwindet auch wieder. Ein Hinweis, der einmal
|
||
erschienen ist und dann stehen bleibt, ist ein Fehler. */
|
||
await luna.seite.fill("#text", "kurz");
|
||
await luna.seite.waitForTimeout(250);
|
||
ok(await luna.seite.$eval("#zeichen", (e) => e.hidden), "und geht wieder weg, wenn wieder Platz ist");
|
||
await luna.seite.fill("#text", "");
|
||
await luna.seite.waitForTimeout(200);
|
||
}
|
||
|
||
/* =======================================================================
|
||
7. AM HANDY — und nichts ragt hinaus
|
||
======================================================================= */
|
||
melde("\n=== 7. Am Handy (390 px) ===");
|
||
{
|
||
const handy = await alsRolle("creator", "CODE-CREA-0001", 390);
|
||
await handy.seite.goto(`${BASIS}/workspace/chat.html?raum=${raumId}`, { waitUntil: "networkidle" });
|
||
await handy.seite.waitForSelector("#schreiben:not([hidden])", { timeout: 8000 });
|
||
await handy.seite.waitForTimeout(700);
|
||
|
||
await handy.seite.click("#emoji-auf");
|
||
await handy.seite.waitForTimeout(300);
|
||
const m = await handy.seite.evaluate(() => {
|
||
const t = document.getElementById("emoji-tafel").getBoundingClientRect();
|
||
return { l: t.left, r: t.right, doku: document.documentElement.scrollWidth, sicht: innerWidth };
|
||
});
|
||
ok(m.l >= 0 && m.r <= 390, `die Tafel bleibt im Bild (${m.l.toFixed(0)}–${m.r.toFixed(0)})`);
|
||
ok(m.doku <= m.sicht + 1, `die Seite hat keinen seitlichen Überstand (${m.doku} zu ${m.sicht})`);
|
||
|
||
/* Und die Knöpfe sind gross genug zum Treffen. Unter 38 px tippt man
|
||
daneben -- das ist keine Zierde, das ist Bedienbarkeit. */
|
||
const klein = await handy.seite.$$eval("#emoji-auf, .chat__senden",
|
||
(b) => b.map((x) => ({ n: x.id || x.className,
|
||
h: Math.round(x.getBoundingClientRect().height),
|
||
w: Math.round(x.getBoundingClientRect().width) }))
|
||
.filter((x) => x.h < 38 || x.w < 38));
|
||
ok(klein.length === 0, `Emoji- und Senden-Knopf sind gross genug (${JSON.stringify(klein)})`);
|
||
await handy.ctx.close();
|
||
}
|
||
|
||
/* =======================================================================
|
||
8. ZWISCHEN GESPRÄCHEN WECHSELN, OHNE NEU ZU LADEN
|
||
|
||
Diese Prüfung gab es nicht, und deshalb war der Fehler drin: Der
|
||
Absatz "Links ein Gespräch auswählen" liegt IM Verlaufskasten, und
|
||
der wurde beim Zeichnen komplett geleert. Ab dem zweiten Öffnen war
|
||
`#verlauf-leer` weg -- `raumOeffnen` stürzte dort ab und ließ den
|
||
ganzen Rest liegen: Adresszeile, Lesemeldung, Liste.
|
||
|
||
Von aussen sah das normal aus. Nur der Ereignisstrom holte nach
|
||
einem Verbindungsabriss nichts mehr nach, und die ungelesen-Zahl
|
||
blieb beim Wechseln stehen.
|
||
======================================================================= */
|
||
melde("\n=== 8. Wechseln ohne Neuladen ===");
|
||
{
|
||
await luna.seite.goto(BASIS + "/workspace/chat.html", { waitUntil: "networkidle" });
|
||
await luna.seite.waitForSelector(".chat-raum__knopf", { timeout: 8000 });
|
||
await luna.seite.waitForTimeout(500);
|
||
const vorher = luna.konsole.length;
|
||
|
||
const zuPatrick = luna.seite.locator(".chat-raum__knopf", { hasText: "Patrick" }).first();
|
||
const zuDogi = luna.seite.locator(".chat-raum__knopf", { hasText: "Filipe" }).first();
|
||
|
||
await zuPatrick.click();
|
||
await luna.seite.waitForSelector("#schreiben:not([hidden])", { timeout: 8000 });
|
||
await luna.seite.waitForTimeout(400);
|
||
ok(/raum=/.test(luna.seite.url()), `die Adresszeile merkt sich das Gespräch (${luna.seite.url().split("/").pop()})`);
|
||
|
||
await zuDogi.click();
|
||
await luna.seite.waitForTimeout(700);
|
||
const titel2 = await luna.seite.$eval("#verlauf-titel", (e) => e.textContent);
|
||
ok(titel2 === "Filipe", `der zweite Wechsel kommt an ("${titel2}")`);
|
||
|
||
await zuPatrick.click();
|
||
await luna.seite.waitForTimeout(700);
|
||
const titel3 = await luna.seite.$eval("#verlauf-titel", (e) => e.textContent);
|
||
const zahl = await luna.seite.$$eval(".chat-nachricht", (n) => n.length);
|
||
ok(titel3 === "Patrick" && zahl > 5,
|
||
`und der dritte auch, samt Verlauf ("${titel3}", ${zahl} Nachrichten)`);
|
||
ok(/raum=/.test(luna.seite.url()), "die Adresszeile geht beim Wechseln mit");
|
||
ok(luna.konsole.length === vorher,
|
||
`dabei kein einziger Absturz (${luna.konsole.length - vorher} neue Meldungen)`);
|
||
|
||
/* GEGENPROBE: Der Absatz, über den es gestolpert ist, muss noch da
|
||
sein -- und zwar versteckt, nicht gelöscht. Wäre er weg, ginge es
|
||
beim nächsten Umbau wieder los. */
|
||
const leer = await luna.seite.evaluate(() => {
|
||
const e = document.getElementById("verlauf-leer");
|
||
return e ? { da: true, zu: e.hidden, drin: e.parentElement.id } : { da: false };
|
||
});
|
||
ok(leer.da && leer.zu && leer.drin === "verlauf",
|
||
`"#verlauf-leer" steht noch im Kasten und ist versteckt (${JSON.stringify(leer)})`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
9. AUGENSCHONEND — gemessen, nicht behauptet
|
||
|
||
Vier neue Textstellen sind dazugekommen. Jede davon ist klein
|
||
(0,72 rem) und liegt auf einem durchscheinenden Grund, hinter dem ein
|
||
Bild steht -- die Sorte Stelle, an der Kontrast gern verlorengeht,
|
||
ohne dass es beim Hinsehen auffällt.
|
||
|
||
Gemessen wird gegen die WIRKLICHE Fläche dahinter: Der Text wird kurz
|
||
unsichtbar gemacht und die Fläche fotografiert. Ein Blick in die
|
||
CSS-Datei würde nur die oberste Schicht sehen.
|
||
======================================================================= */
|
||
melde("\n=== 9. Augenschonend ===");
|
||
{
|
||
const leuchtkraft = (r, g, b) => {
|
||
const f = (w) => { const v = w / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
|
||
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
|
||
};
|
||
const kontrast = (a, b) => {
|
||
const l1 = leuchtkraft(...a), l2 = leuchtkraft(...b);
|
||
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
||
};
|
||
|
||
/* Der Text wird unsichtbar gemacht, NICHT entfernt -- ein entferntes
|
||
Element ändert das Layout, und dann wird an der falschen Stelle
|
||
gemessen. Und die Farbe wird ZUERST abgeschrieben: getComputedStyle
|
||
liefert ein lebendes Objekt, das sonst gleich mit auf
|
||
"transparent" umspringt. */
|
||
async function messen(waehler) {
|
||
const stelle = await luna.seite.evaluate((w) => {
|
||
const e = document.querySelector(w);
|
||
if (!e) return null;
|
||
e.scrollIntoView({ block: "center" });
|
||
const r = e.getBoundingClientRect();
|
||
if (r.width < 8 || r.height < 6) return null;
|
||
const farbe = getComputedStyle(e).color.match(/\d+/g).slice(0, 3).map(Number);
|
||
e.dataset.messen = "ja";
|
||
e.style.color = "transparent";
|
||
return { x: Math.round(r.x), y: Math.round(r.y),
|
||
w: Math.round(r.width), h: Math.round(Math.min(r.height, 40)), farbe };
|
||
}, waehler);
|
||
if (!stelle) return null;
|
||
const bild = await luna.seite.screenshot({
|
||
clip: { x: stelle.x, y: stelle.y, width: stelle.w, height: stelle.h } });
|
||
await luna.seite.evaluate(() => {
|
||
const e = document.querySelector('[data-messen="ja"]');
|
||
if (e) { e.style.color = ""; delete e.dataset.messen; }
|
||
});
|
||
const hinten = await luna.seite.evaluate(async (b64) => {
|
||
const bild = new Image();
|
||
await new Promise((r) => { bild.onload = r; bild.src = "data:image/png;base64," + b64; });
|
||
const c = document.createElement("canvas");
|
||
c.width = bild.width; c.height = bild.height;
|
||
const g = c.getContext("2d", { willReadFrequently: true });
|
||
g.drawImage(bild, 0, 0);
|
||
const dd = g.getImageData(0, 0, c.width, c.height).data;
|
||
let r = 0, gr = 0, bl = 0, n = 0;
|
||
for (let i = 0; i < dd.length; i += 4) { r += dd[i]; gr += dd[i + 1]; bl += dd[i + 2]; n++; }
|
||
return n ? [Math.round(r / n), Math.round(gr / n), Math.round(bl / n)] : null;
|
||
}, bild.toString("base64"));
|
||
if (!hinten) return null;
|
||
return { farbe: stelle.farbe, hinten, wert: kontrast(stelle.farbe, hinten) };
|
||
}
|
||
|
||
/* Alle vier gleichzeitig sichtbar machen: den Zähler über die Länge,
|
||
den Sprungknopf über den Bildlauf. */
|
||
await luna.seite.fill("#text", "x".repeat(3800));
|
||
await luna.seite.$eval("#verlauf", (e) => { e.scrollTop = 0; });
|
||
await luna.seite.waitForTimeout(400);
|
||
await luna.seite.evaluate(() => { document.getElementById("zum-neuesten").hidden = false; });
|
||
await luna.seite.waitForTimeout(200);
|
||
|
||
for (const [name, waehler] of [
|
||
["Zeichenzähler", "#zeichen"],
|
||
["Kopier-Knopf", ".chat-nachricht__kopie-knopf"],
|
||
["Antwort-Knopf (Vergleich)", ".chat-nachricht__antwort-knopf"],
|
||
["Zum-neuesten-Knopf", "#zum-neuesten"],
|
||
["Link in einer Nachricht", ".chat-nachricht__link"],
|
||
]) {
|
||
const m = await messen(waehler);
|
||
ok(m !== null && m.wert >= 4.5,
|
||
`${name}: ${m ? m.wert.toFixed(2) + ":1 (" + m.farbe.join(",") + " auf " + m.hinten.join(",") + ")" : "nicht gefunden"}`);
|
||
}
|
||
|
||
/* GEGENPROBE: Ein absichtlich zu dunkler Text MUSS durchfallen --
|
||
sonst misst der Block oben nichts. */
|
||
await luna.seite.evaluate(() => {
|
||
const e = document.getElementById("zeichen");
|
||
e.dataset.probe = "ja"; e.style.color = "#20262e";
|
||
});
|
||
const schlecht = await messen("#zeichen");
|
||
ok(schlecht !== null && schlecht.wert < 4.5,
|
||
`absichtlich zu dunkler Text fällt durch (${schlecht?.wert.toFixed(2)}:1)`);
|
||
await luna.seite.evaluate(() => { document.getElementById("zeichen").style.color = ""; });
|
||
await luna.seite.fill("#text", "");
|
||
}
|
||
|
||
melde("\n=== 10. Stille Fehler ===");
|
||
{
|
||
const alle = [...luna.konsole, ...pat.konsole];
|
||
ok(alle.length === 0, alle.length ? "Fehler in der Konsole: " + alle.join(" | ")
|
||
: "keine Skript- oder Konsolenfehler in beiden Browsern");
|
||
|
||
/* GEGENPROBE: Der Lauscher greift überhaupt. Ohne diesen Nachweis
|
||
wäre der Haken darüber wertlos -- er wäre auch grün, wenn niemand
|
||
zuhört. */
|
||
await luna.seite.evaluate(() => { setTimeout(() => { throw new Error("Absicht"); }, 0); });
|
||
await luna.seite.waitForTimeout(500);
|
||
ok(luna.konsole.some((z) => /Absicht/.test(z)),
|
||
`ein absichtlicher Fehler wird bemerkt (${luna.konsole.length} Meldung(en))`);
|
||
}
|
||
|
||
await luna.ctx.close();
|
||
await pat.ctx.close();
|
||
await browser.close();
|
||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||
|
||
melde(`\n${geprueft} Prüfungen`);
|
||
melde(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`);
|
||
process.exit(fehler ? 1 : 0);
|