Filipe: "ich will dass die kachel an sich noch viel geiler und krass
aussieht. die soll aber auch garnicht so breit sein. es sollen immer
zwei neben einander stehen können und wenn man auf banner anzeigen
drückt soll das banner schön in der mitte sein."
GEMESSEN VORHER: Karte 1160 px breit, eine je Reihe (#liste stand auf
display: block). --ton: #cace02 an ALLEN drei Zustaenden -- ein seit
drei Tagen vorbeigelaufenes Event leuchtete wie eines, das laeuft. Und
border-left-width: 0px, border-radius: 0px: Die Dringlichkeitskante aus
bereich.css kommt seit dem Baukasten nicht mehr an (module.css setzt
border: 0, gleich stark und spaeter im Ladeweg).
ZWEI NEBENEINANDER -- die Regel SIEBT, statt zu schalten. #liste traegt
das Raster immer; alles, was keine Eventkarte ist, nimmt mit
`grid-column: 1 / -1` die ganze Breite. In den sieben anderen Bereichen
aendert sich damit kein Pixel. Gemessen: Eventkarten 573 px, die
Schulung daneben weiterhin 1160 px; auf 412 px steht wieder eine je
Reihe (379 px, kein seitliches Schieben).
DAS BANNER KOMMT IN DIE MITTE -- und zwar in der BILDSCHAU, die es seit
dem 25.09.2026 gibt und die hier nur nie angeschlossen war. Sie wurde
damals fuer genau diese Beschwerde gebaut ("wenn man bilder aufmacht,
will ich dass die in der mitte sind"). Ihr Stil ist dafuer aus chat.css
in eine eigene bildschau.css gezogen: bereich.html laedt chat.css nicht,
und 5815 Zeilen Chat auf einer Seite ohne Chat waere der falsche Preis.
Gemessen: 1178x662 px, Mitte 640 gegen 640, Escape schliesst, die Seite
springt nicht weg. Der Weg in den Tab bleibt (mittlere Maustaste, Strg).
ZWEI FEHLER, DIE DIE SCHMALERE KARTE ERST SICHTBAR GEMACHT HAT:
1. `justify-content: flex-end` an der Buehne laesst den Inhalt nach
OBEN herauslaufen, wenn der Platz nicht reicht -- und die Karte hat
ein clip-path, also ist er dann weg. Gemessen: Das Etikett
"Agentur-Events" sass auf einer Karte bei y = -28 px. Die alte
Messung fragte nach Farbe, Groesse, Sichtbarkeit und Deckkraft und
bekam auf alles die richtige Antwort -- nach der LAGE hatte sie nie
gefragt. Jetzt `margin-top: auto`, das kann nicht herauslaufen.
2. Die Buehnenhoehe kam aus der BREITE (aspect-ratio 2.6/1, bei
"vorbei" 3.6/1 mit max-height 190px). Bei halber Kartenbreite
bricht der Titel auf mehr Zeilen um: Inhalt 231 px, Kasten 220 px
-- der Zeitbalken lag auf dem Satz darunter. Jetzt eine
Mindesthoehe, der Rest kommt vom Inhalt. Das Banner liegt absolut
darin und fuellt jede Hoehe; die Rechnung brauchte es nie.
AUSSERDEM: Jeder Zustand traegt seinen Ton (laeuft = Agenturgelb,
kommt = kuehl, vorbei = stumpf) und faerbt damit Eckwinkel, Kantenlicht
und Zeitbalken mit. Die Farbkante der Karte liegt im Hintergrund statt
auf einem Pseudoelement (beide gehoeren dem Baukasten), 6 px, weil das
Kantenlicht 1,6 px verdeckt. Der Aufgabenzaehler ist eine Pille statt
grauer Text, gleich hohe Karten mit dem Fuss unten.
pruef-eventkarte: 77 -> 94 Pruefungen, gruen. Zwei Gegenproben gefahren
(Ueberlaufschutz entfernt -> rot mit "ev-buehne__inhalt oben -28px";
die neue Lagepruefung nennt den Schuldigen selbst). Neu darin: nichts
darf aus einer Karte herausragen (132 Teile), der Inhalt muss in jede
Buehne passen, und auf 412 px steht eine Karte je Reihe.
Dazu gruen: chat-anhaenge (168, die Bildschau nach dem Umzug),
chat-optik, chat, css-klassen, haus-trennung, lesbarkeit, eintrag-bild,
tippziele, fingermass, leerzustand.
Co-Authored-By: Claude Opus 5 <[email protected]>
1177 lines
59 KiB
JavaScript
1177 lines
59 KiB
JavaScript
/* =====================================================================
|
|
AGENTUR-EVENTS: KARTE, PUNKTE UND FORMULAR (03.10.2026)
|
|
|
|
Filipe, mit einem Bildschirmfoto des Oktober-Events: „ich will das
|
|
viel geiler viel perfekter. und auch wenn man die eintraegt und so
|
|
soll viel mehr perfektioniert und uebersichtlicher sein."
|
|
|
|
Umgebaut wurden drei Dinge, und alle drei werden hier nachgemessen:
|
|
|
|
1. DIE KARTE Buehne mit Titel auf dem Banner statt 638 px
|
|
leerem Bild darueber; Zeitraum einmal statt
|
|
dreimal; „noch 13 Tage" als Antwort.
|
|
2. DIE PUNKTE Aus Fliesstext wird eine Liste zum Abhaken,
|
|
jeder fuer sich. Neue Tabelle event_punkte.
|
|
3. DAS FORMULAR Ein Feld je Zeile statt eines leeren Textfeldes,
|
|
in das man Aufzaehlungszeichen selbst tippt.
|
|
|
|
WAS HIER AUSDRUECKLICH NICHT NUR BESTAETIGT WIRD:
|
|
|
|
Zu jeder Zusage gehoert eine Gegenprobe, die zeigt, dass die
|
|
Pruefung auch „nicht in Ordnung" sagen kann -- sonst bestaetigt sie
|
|
nichts (Projektgedaechtnis, 01.09.2026: `every()` auf einem leeren
|
|
Feld meldet `true`). Deshalb steht die Zahl der geprueften Dinge in
|
|
der BEDINGUNG, nicht nur im Meldetext.
|
|
|
|
KEINE WANDUHR ALS ANNAHME: Alle Daten sind relativ zu heute
|
|
gerechnet (`tag(-1)`, `tag(+12)`). Ein festes Datum waere eine
|
|
Zeitbombe -- gruen, bis der Kalender es einholt (Projektgedaechtnis,
|
|
06.09.2026, gate-oeffnung.mjs).
|
|
===================================================================== */
|
|
import { mkdtempSync, rmSync, appendFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { tagLokal } from "./helfer-zeit.mjs";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-evk-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
const { eigenerPort } = await import("./helfer-port.mjs");
|
|
const PORT = await eigenerPort(import.meta, "die Eventkarten-Pruefung");
|
|
|
|
process.env.PORT = String(PORT);
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-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 }); };
|
|
await import("./index.js");
|
|
notbremse(300_000, "pruef-eventkarte");
|
|
await new Promise((r) => setTimeout(r, 1000));
|
|
const BASIS = "http://127.0.0.1:" + PORT;
|
|
|
|
const PROTOKOLL = join(process.cwd(), "pruef-eventkarte-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("Dogfather", "admin", "CODE-DOGI-0001");
|
|
const idLuna = anlegen("Luna", "creator", "CODE-CREA-0001");
|
|
|
|
/* ORTSZEIT, NICHT UTC (berichtigt 06.10.2026, gefunden von pruef-struktur).
|
|
Hier stand `toISOString().slice(0, 10)`. Zwischen 00:00 und 02:00
|
|
deutscher Zeit liegt UTC noch im Vortag -- ein Lauf um Mitternacht
|
|
haette Termine einen Tag zu frueh angelegt und dann an der falschen
|
|
Stelle gesucht. `tagLokal` ist die Stelle, an der das Haus das
|
|
einmal richtig macht. */
|
|
const tag = (v) => tagLokal(v);
|
|
function evt(titel, von, bis, auf, reg) {
|
|
d.prepare("INSERT INTO eintraege (titel, text, bereich, art, status, datum, event_ende,"
|
|
+ " event_aufgaben, event_regeln, dringlichkeit, erstellt, erstellt_von)"
|
|
+ " VALUES (?,?,'agentur','kampagne','offen',?,?,?,?,'mittel',?,?)")
|
|
.run(titel, "Beschreibung zum Test.", von, bis, auf, reg, jetzt, idDogi);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
/* Die drei Zustaende, die eventStand kennt -- jeder einzeln, nicht als
|
|
Summe. Und die Aufgaben genau so krumm getippt wie im echten Event:
|
|
einmal ohne Leerzeichen hinter dem Punkt, einmal mit. */
|
|
const laufend = evt("Oktober Event", tag(-1), tag(12),
|
|
"·Jeden Tag Live gehen\n·22-Live-Stunden erreichen\n· 33k Diamanten erreichen",
|
|
"· Nur Streams auf dem eigenen Konto\n· Punkte zählen bis 23:59 am letzten Tag");
|
|
const kommend = evt("November Challenge", tag(20), tag(45),
|
|
"· 30 Live-Stunden", "· Nur eigene Konten");
|
|
const vorbei = evt("September Rückblick", tag(-40), tag(-12),
|
|
"· 20 Live-Stunden", "· Nur eigene Konten");
|
|
/* Ein gewoehnlicher Eintrag auf demselben Brett: Er darf sich NICHT
|
|
mitverwandeln. Ohne ihn wuerde "nur Events sind betroffen" hier gar
|
|
nicht geprueft. */
|
|
d.prepare("INSERT INTO eintraege (titel, text, bereich, art, status, datum, dringlichkeit, erstellt, erstellt_von)"
|
|
+ " VALUES (?,?,'agentur','schulung','offen',?, 'mittel', ?, ?)")
|
|
.run("Einarbeitung für neue Modis", "Kein Event.", tag(0), jetzt, idDogi);
|
|
/* EIN EINTRAG, DEN LUNA NICHT SEHEN DARF -- fuer die Gegenprobe zur
|
|
Bildweg-Reparatur weiter unten. `entwicklung` ist eine Aufzeichnung
|
|
UEBER eine Person; sie selbst sieht sie nicht. Ohne diesen Eintrag
|
|
waere "es wurden keine Rechte ausgeweitet" eine Behauptung ohne
|
|
Beleg. */
|
|
d.prepare("INSERT INTO eintraege (titel, text, bereich, art, status, datum, dringlichkeit, erstellt, erstellt_von, creator_id)"
|
|
+ " VALUES (?,?,'entwicklung','notiz','offen',?, 'mittel', ?, ?, ?)")
|
|
.run("Vertrauliche Aufzeichnung", "Nicht fuer Luna.", tag(0), jetzt, idDogi, idDogi);
|
|
const geheim = d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
d.close();
|
|
|
|
/* Ein Banner fuer das laufende Event -- ohne Bild waere der Knopf
|
|
"Banner ganz" nicht pruefbar, und die Buehne mit Bild ist der Fall,
|
|
um den es Filipe ging. */
|
|
import { pngBauen } from "./helfer-png-bauen.mjs";
|
|
const bannerBild = pngBauen(1280, 720, (x, y, b, h) => {
|
|
const g = Math.max(0, 1 - Math.hypot(x / b - .5, y / h - .5) * 1.8);
|
|
return [Math.round(26 + g * 120), Math.round(10 + g * 30), Math.round(46 + g * 150)];
|
|
});
|
|
|
|
/* ---- Zugang ----------------------------------------------------------- */
|
|
const { request } = await import("node:http");
|
|
const HOST = "workspace.dogfather-universe.com";
|
|
function roh(pfad, o = {}) {
|
|
return new Promise((f, s) => {
|
|
const daten = o.rumpf === undefined ? null
|
|
: (Buffer.isBuffer(o.rumpf) ? o.rumpf : Buffer.from(JSON.stringify(o.rumpf)));
|
|
const a = request({
|
|
host: "127.0.0.1", port: PORT, path: pfad,
|
|
method: o.methode || (daten ? "POST" : "GET"),
|
|
headers: {
|
|
Host: HOST, Origin: "https://" + HOST,
|
|
...(o.keks ? { Cookie: o.keks } : {}),
|
|
...(daten ? { "Content-Length": daten.length } : {}),
|
|
...(o.kopf || {}),
|
|
},
|
|
}, (r) => {
|
|
const t = [];
|
|
r.on("data", (x) => t.push(x));
|
|
r.on("end", () => f({ code: r.statusCode, kopf: r.headers, text: Buffer.concat(t).toString("utf8") }));
|
|
});
|
|
a.on("error", s);
|
|
if (daten) a.write(daten);
|
|
a.end();
|
|
});
|
|
}
|
|
async function anmelden(rolle, code) {
|
|
const a = await roh("/workspace/api/anmelden", {
|
|
rumpf: { rolle, code }, kopf: { "Content-Type": "application/json" } });
|
|
if (a.code !== 200) { melde("ABBRUCH: Anmeldung " + rolle + " -> " + a.code); process.exit(3); }
|
|
return [].concat(a.kopf["set-cookie"] || []).map((c) => c.split(";")[0]).join("; ");
|
|
}
|
|
const keksDogi = await anmelden("admin", "CODE-DOGI-0001");
|
|
const keksLuna = await anmelden("creator", "CODE-CREA-0001");
|
|
|
|
const brett = async (keks) => JSON.parse((await roh("/workspace/api/bereich/agentur", { keks })).text);
|
|
|
|
let bannerId = null, geheimBildId = null;
|
|
{
|
|
const h = await roh("/workspace/api/bereich/agentur/" + laufend + "/bild", {
|
|
keks: keksDogi, rumpf: bannerBild, methode: "POST",
|
|
kopf: { "Content-Type": "image/png", "x-name": encodeURIComponent("banner.png") } });
|
|
if (h.code !== 201) {
|
|
melde("ABBRUCH: Banner liess sich nicht anhaengen (" + h.code + ") -- ohne es");
|
|
melde(" prueft diese Datei die Buehne MIT Bild gar nicht.");
|
|
process.exit(3);
|
|
}
|
|
bannerId = JSON.parse(h.text).id;
|
|
|
|
const g = await roh("/workspace/api/bereich/entwicklung/" + geheim + "/bild", {
|
|
keks: keksDogi, rumpf: bannerBild, methode: "POST",
|
|
kopf: { "Content-Type": "image/png", "x-name": encodeURIComponent("geheim.png") } });
|
|
if (g.code !== 201) {
|
|
melde("ABBRUCH: das Bild fuer die Gegenprobe ging nicht durch (" + g.code + ").");
|
|
melde(" Ohne es wuerde unten eine Rechtefrage geprueft, die gar nicht stattfindet.");
|
|
process.exit(3);
|
|
}
|
|
geheimBildId = JSON.parse(g.text).id;
|
|
}
|
|
|
|
melde("\n=== 0. Der Bildweg: wer den Eintrag sieht, sieht sein Bild ===");
|
|
{
|
|
/* GEMESSEN AM 03.10.2026, und es war ein echter Mangel: Das Banner
|
|
eines Agentur-Events kam bei einer Creatorin mit 404 zurueck --
|
|
an genau der Karte, die fuer sie gemacht ist. Grund: Die Regel
|
|
"wer das Brett sieht, sieht das Bild" galt nur fuer die sieben
|
|
Community-Bretter, und danach griff die Ablage-Regel, die an
|
|
Creator-Zuordnungen haengt. Ein Agentur-Event hat keine. */
|
|
const alsDogi = await roh("/workspace/api/dateien/" + bannerId + "/bild", { keks: keksDogi });
|
|
const alsLuna = await roh("/workspace/api/dateien/" + bannerId + "/bild", { keks: keksLuna });
|
|
ok(alsDogi.code === 200, `DogFather bekommt das Banner (HTTP ${alsDogi.code})`);
|
|
ok(alsLuna.code === 200, `und die Creatorin auch (HTTP ${alsLuna.code}, vorher 404)`);
|
|
|
|
/* DIE GEGENPROBE, auf die es ankommt: Die Reparatur darf NICHT mehr
|
|
oeffnen als noetig. Ein Eintrag, den Luna nicht sehen darf, muss
|
|
sein Bild weiterhin verweigern -- sonst waere aus einer Luecke
|
|
eine groessere geworden. */
|
|
const verboten = await roh("/workspace/api/dateien/" + geheimBildId + "/bild", { keks: keksLuna });
|
|
ok(verboten.code === 404,
|
|
`ein Bild an einem fremden Eintrag bleibt verborgen (HTTP ${verboten.code})`);
|
|
/* Und dass dieses Bild ueberhaupt existiert -- sonst waere die
|
|
Gegenprobe gruen, weil es nichts zu sehen gab. */
|
|
const existiert = await roh("/workspace/api/dateien/" + geheimBildId + "/bild", { keks: keksDogi });
|
|
ok(existiert.code === 200,
|
|
`es gibt dieses Bild wirklich (DogFather: HTTP ${existiert.code})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
1. Die Punkte: aus Fliesstext wird eine Liste
|
|
======================================================================= */
|
|
melde("\n=== 1. Die Punkte kommen als Liste vom Server ===");
|
|
{
|
|
const daten = await brett(keksDogi);
|
|
const e = (daten.eintraege || []).find((x) => x.titel === "Oktober Event");
|
|
ok(!!e, "das laufende Event ist auf dem Brett");
|
|
|
|
const p = e?.event_punkte || [];
|
|
ok(p.length === 3, `3 Aufgabenpunkte (${p.length})`);
|
|
/* DAS IST DER EIGENTLICHE MANGEL AUS FILIPES BILDSCHIRMFOTO: Im
|
|
Original stand „·Jeden Tag Live gehen" neben „· 33k Diamanten
|
|
erreichen" -- einmal ohne, einmal mit Leerzeichen. */
|
|
ok(p[0]?.text === "Jeden Tag Live gehen",
|
|
`ohne Leerzeichen getippt -> sauber: ${JSON.stringify(p[0]?.text)}`);
|
|
ok(p[2]?.text === "33k Diamanten erreichen",
|
|
`mit Leerzeichen getippt -> gleich sauber: ${JSON.stringify(p[2]?.text)}`);
|
|
ok(p.every((x) => /^[0-9a-f]{16}$/.test(x.schluessel || "")) && p.length > 0,
|
|
"jeder Punkt hat einen Schluessel (16 Hexzeichen)");
|
|
ok((e?.event_regelpunkte || []).length === 2,
|
|
`2 Regelpunkte (${(e?.event_regelpunkte || []).length})`);
|
|
ok(e?.event_geschafft === 0, `noch nichts abgehakt (${e?.event_geschafft})`);
|
|
|
|
/* GEGENPROBE: Ein Eintrag, der KEIN Event ist, bekommt keine Punkte.
|
|
Ohne sie wuesste niemand, ob die Zerlegung nicht einfach ueberall
|
|
laeuft -- und dann waere "nur Events sind betroffen" unbelegt. */
|
|
const schulung = (daten.eintraege || []).find((x) => x.art === "schulung");
|
|
ok(!!schulung, "der Nicht-Event-Eintrag ist da (sonst prueft die Gegenprobe nichts)");
|
|
ok(schulung && schulung.event_punkte === undefined,
|
|
"ein Eintrag ohne Eventfelder bekommt keine Punkteliste");
|
|
}
|
|
|
|
/* =======================================================================
|
|
2. Abhaken -- und was dabei NICHT geht
|
|
======================================================================= */
|
|
melde("\n=== 2. Abhaken: jeder fuer sich ===");
|
|
let schluesselA = null, schluesselB = null;
|
|
{
|
|
const daten = await brett(keksDogi);
|
|
const e = daten.eintraege.find((x) => x.titel === "Oktober Event");
|
|
schluesselA = e.event_punkte[1].schluessel; /* 22-Live-Stunden */
|
|
schluesselB = e.event_punkte[2].schluessel; /* 33k Diamanten */
|
|
|
|
const a = await roh(`/workspace/api/eintrag/${laufend}/punkt/${schluesselA}`, {
|
|
keks: keksLuna, methode: "PUT", rumpf: {}, kopf: { "Content-Type": "application/json" } });
|
|
ok(a.code === 200, `Luna hakt einen Punkt ab (HTTP ${a.code})`);
|
|
const j = JSON.parse(a.text || "{}");
|
|
ok(j.geschafft === 1 && j.gesamt === 3, `Antwort sagt ${j.geschafft} von ${j.gesamt} (erwartet 1 von 3)`);
|
|
|
|
/* HAELT ES AUCH? Die Zahl aus der Antwort koennte gerechnet sein,
|
|
ohne dass etwas gespeichert wurde. Deshalb ein frischer Abruf. */
|
|
const nachher = await brett(keksLuna);
|
|
const eL = nachher.eintraege.find((x) => x.titel === "Oktober Event");
|
|
ok(eL.event_geschafft === 1, `nach frischem Abruf: ${eL.event_geschafft} abgehakt`);
|
|
ok(eL.event_punkte.filter((p) => p.meiner).length === 1
|
|
&& eL.event_punkte[1].meiner === true,
|
|
"und zwar genau der angeklickte Punkt");
|
|
|
|
/* DER KERN DER ZUSAGE: DogFather sieht Lunas Haken NICHT als seinen
|
|
eigenen. Ohne diese Pruefung waere "jeder hakt fuer sich ab" eine
|
|
Behauptung. */
|
|
const beiDogi = await brett(keksDogi);
|
|
const eD = beiDogi.eintraege.find((x) => x.titel === "Oktober Event");
|
|
ok(eD.event_geschafft === 0,
|
|
`DogFather sieht Lunas Haken nicht als seinen (${eD.event_geschafft})`);
|
|
ok(eD.event_punkte.every((p) => p.meiner === false),
|
|
"kein einziger Punkt ist bei ihm angehakt");
|
|
|
|
/* ... sieht aber, DASS jemand dabei ist -- er ist die Leitung. */
|
|
ok(Array.isArray(eD.event_stand) && eD.event_stand.length === 1
|
|
&& eD.event_stand[0].name === "Luna" && eD.event_stand[0].geschafft === 1,
|
|
`die Leitung sieht den Stand: ${JSON.stringify(eD.event_stand)}`);
|
|
/* GEGENPROBE ZUR LEITUNG: Luna selbst darf diese Liste NICHT
|
|
bekommen -- sonst waere "wer macht mit" eine Auskunft fuer alle. */
|
|
ok(eL.event_stand === undefined,
|
|
"eine Creatorin bekommt die Mitmacherliste gar nicht erst");
|
|
|
|
/* Zweimal dasselbe ist kein Fehler, aendert aber auch nichts. */
|
|
const nochmal = await roh(`/workspace/api/eintrag/${laufend}/punkt/${schluesselA}`, {
|
|
keks: keksLuna, methode: "PUT", rumpf: {}, kopf: { "Content-Type": "application/json" } });
|
|
const j2 = JSON.parse(nochmal.text || "{}");
|
|
ok(nochmal.code === 200 && j2.geschafft === 1,
|
|
`zweimal derselbe Haken bleibt 1 (HTTP ${nochmal.code}, ${j2.geschafft})`);
|
|
|
|
/* Und wieder weg. */
|
|
const ab = await roh(`/workspace/api/eintrag/${laufend}/punkt/${schluesselA}`, {
|
|
keks: keksLuna, methode: "DELETE" });
|
|
const j3 = JSON.parse(ab.text || "{}");
|
|
ok(ab.code === 200 && j3.geschafft === 0, `Haken wieder weg (${j3.geschafft})`);
|
|
/* Fuer die folgenden Pruefungen wieder setzen. */
|
|
await roh(`/workspace/api/eintrag/${laufend}/punkt/${schluesselA}`, {
|
|
keks: keksLuna, methode: "PUT", rumpf: {}, kopf: { "Content-Type": "application/json" } });
|
|
}
|
|
|
|
melde("\n=== 2b. Was NICHT geht ===");
|
|
{
|
|
/* EIN ERFUNDENER SCHLUESSEL. Ohne diese Schranke koennte sich jeder
|
|
„3 von 3" setzen, ohne dass eine der drei Aufgaben je dastand --
|
|
und eine Zahl, die man sich ausdenken kann, zaehlt nichts. */
|
|
const a = await roh(`/workspace/api/eintrag/${laufend}/punkt/00112233445566ff`, {
|
|
keks: keksLuna, methode: "PUT", rumpf: {}, kopf: { "Content-Type": "application/json" } });
|
|
ok(a.code === 404, `erfundener Punkt wird abgelehnt (HTTP ${a.code})`);
|
|
|
|
/* Unsinn in der Adresse. */
|
|
const b = await roh(`/workspace/api/eintrag/${laufend}/punkt/..%2F..%2Fetc`, {
|
|
keks: keksLuna, methode: "PUT", rumpf: {}, kopf: { "Content-Type": "application/json" } });
|
|
ok(b.code === 400 || b.code === 404, `kein Schluesselformat -> abgelehnt (HTTP ${b.code})`);
|
|
|
|
/* Ohne Anmeldung gar nichts. */
|
|
const c = await roh(`/workspace/api/eintrag/${laufend}/punkt/${schluesselB}`, {
|
|
methode: "PUT", rumpf: {}, kopf: { "Content-Type": "application/json" } });
|
|
ok(c.code === 401, `ohne Anmeldung abgewiesen (HTTP ${c.code})`);
|
|
|
|
/* Fremde Herkunft (CSRF). */
|
|
const e = await new Promise((f, s) => {
|
|
const a2 = request({
|
|
host: "127.0.0.1", port: PORT,
|
|
path: `/workspace/api/eintrag/${laufend}/punkt/${schluesselB}`,
|
|
method: "PUT",
|
|
headers: { Host: HOST, Origin: "https://boeswillig.example", Cookie: keksLuna,
|
|
"Content-Type": "application/json", "Content-Length": 2 },
|
|
}, (r) => { const t = []; r.on("data", (x) => t.push(x));
|
|
r.on("end", () => f({ code: r.statusCode })); });
|
|
a2.on("error", s); a2.write("{}"); a2.end();
|
|
});
|
|
ok(e.code === 403, `fremde Herkunft abgewiesen (HTTP ${e.code})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
3. Der Schluessel ueberlebt das Umsortieren -- aber nicht das Umschreiben
|
|
======================================================================= */
|
|
melde("\n=== 3. Haken bleiben am richtigen Punkt ===");
|
|
{
|
|
/* UMSORTIEREN: Der Haken an „22-Live-Stunden" muss bleiben, wo er
|
|
ist -- auch wenn eine Zeile DAVOR eingefuegt wird. Haengt er an
|
|
einer Zeilennummer, wandert er still auf eine andere Aufgabe, und
|
|
jemand hat etwas abgehakt, was er nie gemacht hat. */
|
|
const a = await roh(`/workspace/api/bereich/agentur/${laufend}`, {
|
|
keks: keksDogi, methode: "PATCH",
|
|
rumpf: { event_aufgaben: "· GANZ NEU ganz oben\n· 33k Diamanten erreichen\n· 22-Live-Stunden erreichen\n· Jeden Tag Live gehen" },
|
|
kopf: { "Content-Type": "application/json" } });
|
|
ok(a.code === 200, `Aufgaben umsortiert und ergaenzt (HTTP ${a.code})`);
|
|
|
|
const nach = await brett(keksLuna);
|
|
const e = nach.eintraege.find((x) => x.titel === "Oktober Event");
|
|
ok((e.event_punkte || []).length === 4, `jetzt 4 Punkte (${(e.event_punkte || []).length})`);
|
|
const haken = (e.event_punkte || []).filter((p) => p.meiner);
|
|
ok(haken.length === 1 && haken[0].text === "22-Live-Stunden erreichen",
|
|
`der Haken sitzt weiterhin an: ${JSON.stringify(haken.map((h) => h.text))}`);
|
|
|
|
/* GEGENPROBE: Wird die Bedingung SELBST geaendert, muss der Haken
|
|
fallen -- eine geaenderte Bedingung ist eine andere Bedingung, und
|
|
ein Haken daran waere eine Falschauskunft. */
|
|
const b = await roh(`/workspace/api/bereich/agentur/${laufend}`, {
|
|
keks: keksDogi, methode: "PATCH",
|
|
rumpf: { event_aufgaben: "· GANZ NEU ganz oben\n· 33k Diamanten erreichen\n· 30-Live-Stunden erreichen\n· Jeden Tag Live gehen" },
|
|
kopf: { "Content-Type": "application/json" } });
|
|
ok(b.code === 200, `Bedingung umgeschrieben: 22 -> 30 Stunden (HTTP ${b.code})`);
|
|
const nach2 = await brett(keksLuna);
|
|
const e2 = nach2.eintraege.find((x) => x.titel === "Oktober Event");
|
|
ok((e2.event_punkte || []).filter((p) => p.meiner).length === 0,
|
|
"der Haken ist gefallen -- 30 Stunden sind nicht 22");
|
|
ok(e2.event_geschafft === 0, `und die Zahl stimmt dazu (${e2.event_geschafft})`);
|
|
|
|
/* Zurueckstellen fuer die Oberflaechenpruefung. */
|
|
await roh(`/workspace/api/bereich/agentur/${laufend}`, {
|
|
keks: keksDogi, methode: "PATCH",
|
|
rumpf: { event_aufgaben: "·Jeden Tag Live gehen\n·22-Live-Stunden erreichen\n· 33k Diamanten erreichen" },
|
|
kopf: { "Content-Type": "application/json" } });
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. Die Oberflaeche
|
|
======================================================================= */
|
|
melde("\n=== 4. Die Karte im Browser ===");
|
|
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await pw.chromium.launch();
|
|
const klagen = [];
|
|
|
|
async function alsRolle(rolle, code, breite = 1280) {
|
|
const ctx = await browser.newContext({ viewport: { width: breite, height: 1000 }, hasTouch: breite <= 860 });
|
|
await ctx.route("**/*", (route) => {
|
|
const u = new URL(route.request().url());
|
|
if (u.hostname === "127.0.0.1") {
|
|
return route.continue({ headers: { ...route.request().headers(), host: HOST } });
|
|
}
|
|
return route.continue();
|
|
});
|
|
const seite = await ctx.newPage();
|
|
seite.on("pageerror", (f) => klagen.push("pageerror: " + f.message));
|
|
seite.on("console", (m) => { if (m.type() === "error") klagen.push("console: " + m.text().slice(0, 160)); });
|
|
/* WELCHE Datei fehlt -- die Konsolenmeldung sagt nur "404 (Not
|
|
Found)". Beim ersten Lauf standen hier drei davon, und sie waren
|
|
nicht zuzuordnen; eine Pruefung, die nicht sagen kann, woran sie
|
|
scheitert, kostet eine Stunde Suchen (Projektgedaechtnis,
|
|
11.09.2026). */
|
|
seite.on("response", (r) => {
|
|
if (r.status() >= 400) klagen.push(`HTTP ${r.status()}: ${new URL(r.url()).pathname}`);
|
|
});
|
|
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/bereich.html?b=agentur", { waitUntil: "networkidle" });
|
|
await seite.waitForSelector(".eintrag-karte", { timeout: 15000 });
|
|
await seite.waitForTimeout(700);
|
|
return { ctx, seite };
|
|
}
|
|
|
|
{
|
|
const { ctx, seite } = await alsRolle("admin", "CODE-DOGI-0001", 1280);
|
|
|
|
const karten = await seite.$$('.eintrag-karte[data-eventkarte="ja"]');
|
|
ok(karten.length === 3, `3 Eventkarten (${karten.length}) -- die Schulung ist keine`);
|
|
|
|
/* GEGENPROBE: Die Schulung bleibt eine gewoehnliche Karte. Ohne sie
|
|
waere "nur Events sind umgebaut" unbelegt. */
|
|
const schulung = await seite.$('.eintrag-karte:not([data-eventkarte="ja"])');
|
|
ok(!!schulung, "der Nicht-Event-Eintrag ist weiterhin eine gewoehnliche Karte");
|
|
|
|
const masse = await seite.evaluate(() => {
|
|
const k = [...document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')]
|
|
.find((x) => x.textContent.includes("Oktober Event"));
|
|
const b = k.querySelector(".ev-buehne");
|
|
const bild = k.querySelector(".ev-buehne__bild");
|
|
const t = k.querySelector(".eintrag-titel");
|
|
const kr = k.getBoundingClientRect();
|
|
const tr = t.getBoundingClientRect();
|
|
return {
|
|
karte: Math.round(kr.height),
|
|
buehne: b ? Math.round(b.getBoundingClientRect().height) : 0,
|
|
bildBreite: bild ? Math.round(bild.getBoundingClientRect().width) : 0,
|
|
buehneBreite: b ? Math.round(b.getBoundingClientRect().width) : 0,
|
|
titelGroesse: parseFloat(getComputedStyle(t).fontSize),
|
|
textGroesse: parseFloat(getComputedStyle(k.querySelector(".eintrag-text")).fontSize),
|
|
markeGroesse: parseFloat(getComputedStyle(k.querySelector(".marke-art")).fontSize),
|
|
/* Wie weit oben steht der Titel INNERHALB der Karte? Das war der
|
|
eigentliche Mangel: vorher unter 638 px Bild. */
|
|
titelInKarte: Math.round(tr.top - kr.top),
|
|
datumZeilen: [...k.querySelectorAll(".ev-spanne, .im-kalender, .event-zeitraum")]
|
|
.map((x) => x.textContent.trim()),
|
|
noch: k.querySelector(".ev-noch")?.textContent || null,
|
|
};
|
|
});
|
|
melde(" -- gemessen: " + JSON.stringify(masse));
|
|
|
|
/* ==== NUR MESSEN (07.10.2026): Breite, Ton und tote Regeln ==== */
|
|
const lage = await seite.evaluate(() => {
|
|
const k = [...document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')];
|
|
const erste = k[0], zweite = k[1];
|
|
const s = getComputedStyle(erste);
|
|
const liste = document.getElementById("liste");
|
|
return {
|
|
kartenBreite: Math.round(erste.getBoundingClientRect().width),
|
|
listenBreite: Math.round(liste.getBoundingClientRect().width),
|
|
nebeneinander: zweite
|
|
? Math.abs(erste.getBoundingClientRect().top - zweite.getBoundingClientRect().top) < 5 : null,
|
|
ton: s.getPropertyValue("--ton").trim() || "(nicht gesetzt)",
|
|
randLinks: s.borderLeftWidth, randOben: s.borderTopWidth,
|
|
radius: s.borderTopLeftRadius,
|
|
anzeige: getComputedStyle(liste).display,
|
|
/* Wo haengt die Karte wirklich? Im 4000-Zeilen-Modul stehen drei
|
|
Formen (Kanalspalten, Abschnitte nach Art, Zeitstrahl) -- die
|
|
Antwort aus dem Quelltext zu raten waere Raten. */
|
|
eltern: (() => {
|
|
const weg = []; let n = erste.parentElement;
|
|
for (let i = 0; i < 4 && n; i++, n = n.parentElement) {
|
|
weg.push((n.tagName + "." + (n.className || "")).trim()
|
|
+ (n.id ? "#" + n.id : "") + ":" + getComputedStyle(n).display);
|
|
}
|
|
return weg;
|
|
})(),
|
|
geschwister: erste.parentElement
|
|
? [...erste.parentElement.children].map((c) => c.tagName + "." + (c.className || "").split(" ")[0])
|
|
: [],
|
|
reihen: [...liste.children].map((c) => {
|
|
const r = c.getBoundingClientRect();
|
|
return {
|
|
event: c.dataset.eventkarte === "ja",
|
|
titel: (c.querySelector(".eintrag-titel")?.textContent || "?").slice(0, 18),
|
|
x: Math.round(r.x), y: Math.round(r.y), b: Math.round(r.width),
|
|
};
|
|
}),
|
|
};
|
|
});
|
|
melde(" -- Lage: " + JSON.stringify(lage));
|
|
|
|
/* ===== ZWEI NEBENEINANDER (07.10.2026) ============================
|
|
|
|
Filipe: „die soll aber auch garnicht so breit sein. es sollen immer
|
|
zwei neben einander stehen können."
|
|
|
|
Gemessen VOR dem Umbau: 1160 px breit, eine je Reihe. */
|
|
const events = lage.reihen.filter((r) => r.event);
|
|
const andere = lage.reihen.filter((r) => !r.event);
|
|
ok(events.length >= 2 && events.every((r) => r.b < lage.listenBreite * 0.55),
|
|
`jede Eventkarte ist hoechstens halb so breit wie die Liste `
|
|
+ `(${events.map((r) => r.b).join(", ")} von ${lage.listenBreite} px)`);
|
|
/* DASS SIE SCHMAL SIND, HEISST NOCH NICHT, DASS ZWEI NEBENEINANDER
|
|
STEHEN -- sie koennten auch untereinander in einer halben Spalte
|
|
haengen. Gemessen wird deshalb ein PAAR: zwei Karten, gleiche
|
|
Hoehe, verschiedene Seite. */
|
|
const paar = events.some((a, i) => events.slice(i + 1)
|
|
.some((b2) => Math.abs(a.y - b2.y) < 5 && Math.abs(a.x - b2.x) > 100));
|
|
ok(paar, `zwei Eventkarten stehen wirklich nebeneinander `
|
|
+ `(${events.map((r) => `${r.titel}@${r.x}/${r.y}`).join(" ")})`);
|
|
/* GEGENPROBE: Was KEIN Event ist, bleibt ueber die ganze Breite --
|
|
sonst waere „die Liste ist jetzt zweispaltig" ein Umbau aller acht
|
|
Bereiche und nicht der Eventkarten. */
|
|
ok(andere.length > 0 && andere.every((r) => r.b > lage.listenBreite * 0.95),
|
|
`die Nicht-Event-Karte behaelt die volle Breite (${andere.map((r) => r.b).join(", ")} px)`);
|
|
|
|
/* ===== DIE KARTE SAGT, OB DAS EVENT LAEUFT ======================== */
|
|
const toene = await seite.evaluate(() => {
|
|
const raus = {};
|
|
for (const k of document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')) {
|
|
raus[k.dataset.event] = getComputedStyle(k).getPropertyValue("--ton").trim();
|
|
}
|
|
return raus;
|
|
});
|
|
/* ==== NUR MESSEN: Wie sieht eine Karte OHNE Banner aus? ====
|
|
Im Quelltext steht „OHNE BANNER GIBT ES TROTZDEM EINE BUEHNE --
|
|
dann aus Farbe". Auf dem Bild vom 07.10.2026 war davon nichts zu
|
|
sehen: Die Karte ohne Bild fing direkt mit dem Titel an. */
|
|
const buehnenLage = await seite.evaluate(() => {
|
|
const raus = [];
|
|
for (const k of document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')) {
|
|
const b = k.querySelector(".ev-buehne");
|
|
raus.push({
|
|
titel: (k.querySelector(".eintrag-titel")?.textContent || "?").slice(0, 16),
|
|
mitbild: b?.dataset.mitbild || "nein",
|
|
buehneHoehe: b ? Math.round(b.getBoundingClientRect().height) : 0,
|
|
buehneGrund: b ? getComputedStyle(b).backgroundImage.slice(0, 30) : "-",
|
|
marke: (() => { const m = k.querySelector(".marke-art"); if (!m) return null;
|
|
const r = m.getBoundingClientRect(); const st = getComputedStyle(m);
|
|
return { text: m.textContent.trim(), b: Math.round(r.width), h: Math.round(r.height),
|
|
farbe: st.color, grund: st.backgroundColor, sicht: st.visibility, deck: st.opacity,
|
|
yImKarton: Math.round(r.top - k.getBoundingClientRect().top),
|
|
xImKarton: Math.round(r.left - k.getBoundingClientRect().left) }; })(),
|
|
});
|
|
}
|
|
return raus;
|
|
});
|
|
melde(" -- Buehnen: " + JSON.stringify(buehnenLage));
|
|
|
|
/* ===== NICHTS DARF AUS DER KARTE HERAUSLAUFEN (07.10.2026) ========
|
|
|
|
Die Karte traegt ein `clip-path` -- was oben herausragt, ist nicht
|
|
verschoben, sondern WEG. Genau das ist am 07.10.2026 passiert, als
|
|
die Karten halb so breit wurden: Das Etikett „Agentur-Events" sass
|
|
auf der Karte „September Rueckblick" bei y = -28 px.
|
|
|
|
WARUM DIE ALTE MESSUNG ES NICHT FAND: Sie fragte nach Farbe,
|
|
Groesse, Sichtbarkeit und Deckkraft -- und bekam auf alles die
|
|
richtige Antwort. Nach der LAGE hatte sie nie gefragt. Ein Element
|
|
kann vollstaendig „sichtbar" sein und trotzdem ausserhalb des
|
|
Bildes liegen.
|
|
|
|
Deshalb jetzt fuer JEDES Stueck jeder Eventkarte, nicht nur fuer
|
|
das Etikett: Die naechste Sache, die oben herausfaellt, ist
|
|
bestimmt eine andere. */
|
|
const ueberstand = await seite.evaluate(() => {
|
|
const raus = [];
|
|
for (const k of document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')) {
|
|
const kr = k.getBoundingClientRect();
|
|
for (const t of k.querySelectorAll("*")) {
|
|
const r = t.getBoundingClientRect();
|
|
if (r.width === 0 && r.height === 0) continue;
|
|
if (r.top < kr.top - 1 || r.left < kr.left - 1 || r.right > kr.right + 1) {
|
|
raus.push(`${(k.querySelector(".eintrag-titel")?.textContent || "?").slice(0, 14)}: `
|
|
+ `${(t.className || t.tagName).toString().split(" ")[0]} `
|
|
+ `oben ${Math.round(r.top - kr.top)}px / links ${Math.round(r.left - kr.left)}px`);
|
|
}
|
|
}
|
|
}
|
|
return raus;
|
|
});
|
|
const geprueftTeile = await seite.evaluate(() =>
|
|
[...document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"] *')].length);
|
|
/* DIE ZAHL STEHT IN DER BEDINGUNG: "nichts ragt heraus" waere auf
|
|
einer leeren Liste ebenfalls wahr. */
|
|
ok(geprueftTeile > 30 && ueberstand.length === 0,
|
|
`keines der ${geprueftTeile} Teile ragt aus seiner Karte heraus (${ueberstand.slice(0, 2).join(" | ") || "keins"})`);
|
|
|
|
/* ===== UND DIE BUEHNE DARF NICHT GEQUETSCHT WERDEN ================
|
|
|
|
Dieselbe Sache eine Ebene tiefer: Die Buehne ist ein Flex-Element
|
|
der Karte und darf deshalb schrumpfen. Tut sie das, laeuft ihr
|
|
Inhalt unten heraus -- im Bildschirmfoto vom 07.10.2026 lag der
|
|
Zeitbalken auf dem Satz „Beschreibung zum Test." darunter.
|
|
|
|
Gemessen wird der Inhalt gegen den Kasten, nicht das Aussehen:
|
|
`scrollHeight > clientHeight` heisst „es passt nicht hinein", egal
|
|
ob man es sieht oder nicht. */
|
|
const gequetscht = await seite.evaluate(() =>
|
|
[...document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"] .ev-buehne')]
|
|
.map((b) => ({
|
|
titel: (b.closest(".eintrag-karte").querySelector(".eintrag-titel")?.textContent || "?").slice(0, 14),
|
|
innen: b.scrollHeight, kasten: b.clientHeight,
|
|
stil: (() => {
|
|
const st = getComputedStyle(b);
|
|
return { h: st.height, maxH: st.maxHeight, minH: st.minHeight,
|
|
flex: st.flex, ar: st.aspectRatio, pad: st.paddingTop + "/" + st.paddingBottom };
|
|
})(),
|
|
kinder: [...b.children].map((c) =>
|
|
(c.className || c.tagName).toString().split(" ")[0] + ":"
|
|
+ Math.round(c.getBoundingClientRect().height)),
|
|
})));
|
|
melde(" -- Buehnen-Inhalt: " + JSON.stringify(gequetscht));
|
|
ok(gequetscht.length >= 3 && gequetscht.every((b) => b.innen <= b.kasten + 1),
|
|
`der Inhalt passt in jede der ${gequetscht.length} Buehnen `
|
|
+ `(${gequetscht.map((b) => `${b.titel} ${b.innen}/${b.kasten}`).join(", ")})`);
|
|
|
|
const tonListe = Object.values(toene);
|
|
ok(tonListe.length >= 3 && new Set(tonListe).size === tonListe.length,
|
|
`jeder Zustand traegt einen eigenen Ton (${JSON.stringify(toene)})`);
|
|
|
|
/* Zum PAAR scrollen, nicht einfach den Bildschirm fotografieren: Oben
|
|
stehen Kopf, Filter und die Vorschlaege -- auf einem Bild davon
|
|
waere von den zwei Karten nebeneinander nichts zu sehen. */
|
|
await seite.evaluate(() => {
|
|
const k = [...document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')];
|
|
const paar = k.find((a, i) => k.slice(i + 1).some((b2) =>
|
|
Math.abs(a.getBoundingClientRect().top - b2.getBoundingClientRect().top) < 5));
|
|
(paar || k[0]).scrollIntoView({ block: "start", behavior: "instant" });
|
|
window.scrollBy(0, -24);
|
|
});
|
|
await seite.waitForTimeout(400);
|
|
await seite.screenshot({ path: "server/pruef-eventkarte-raster.png", fullPage: false });
|
|
|
|
/* ===== „BANNER GANZ" ZEIGT ES IN DER MITTE (07.10.2026) ===========
|
|
|
|
Filipe: „wenn man auf banner anzeigen drückt soll das banner schön
|
|
in der mitte sein."
|
|
|
|
Vorher oeffnete der Knopf die DATEI in einem neuen Tab -- also die
|
|
eingebaute Bildanzeige des Browsers: Bild oben links, Rest schwarz.
|
|
Jetzt die Bildschau des Hauses, die genau dafuer schon existierte
|
|
und hier nur nie angeschlossen war. */
|
|
const vorherAdresse = seite.url();
|
|
const bannerKnopf = await seite.$(".ev-buehne__ganz");
|
|
ok(!!bannerKnopf, "die Karte mit Banner hat den Knopf „Banner ganz“");
|
|
await bannerKnopf.click();
|
|
/* AUF DAS BILD WARTEN, nicht auf eine Zahl: Vor dem Laden ist
|
|
`naturalWidth` null, und null mal null steht weder in der Mitte
|
|
noch sonst wo. */
|
|
await seite.waitForFunction(() => {
|
|
const b = document.querySelector("dialog.bildschau .bildschau__bild");
|
|
return b && b.naturalWidth > 0;
|
|
}, null, { timeout: 15000 }).catch(() => {});
|
|
await seite.waitForTimeout(400);
|
|
|
|
const schau = await seite.evaluate(() => {
|
|
const d = document.querySelector("dialog.bildschau");
|
|
if (!d) return { da: false };
|
|
const b = d.querySelector(".bildschau__bild");
|
|
const r = b.getBoundingClientRect();
|
|
return {
|
|
da: true, offen: d.open,
|
|
mitteX: Math.round(r.x + r.width / 2), mitteY: Math.round(r.y + r.height / 2),
|
|
fensterX: Math.round(window.innerWidth / 2), fensterY: Math.round(window.innerHeight / 2),
|
|
breite: Math.round(r.width), hoehe: Math.round(r.height),
|
|
grund: getComputedStyle(d.querySelector(".bildschau__grund")).backgroundImage.slice(0, 22),
|
|
};
|
|
});
|
|
melde(" -- Bildschau: " + JSON.stringify(schau));
|
|
ok(schau.da && schau.offen, `die Bildschau geht auf (${schau.da ? "offen=" + schau.offen : "gar nicht da"})`);
|
|
/* IN DER MITTE, in Pixeln gemessen. Waagerecht genau, senkrecht mit
|
|
etwas mehr Spiel: Oben liegt die Leiste mit Namen und Knoepfen,
|
|
das Bild sitzt deshalb ein Stueck tiefer als die halbe Hoehe. */
|
|
ok(schau.da && Math.abs(schau.mitteX - schau.fensterX) <= 8,
|
|
`das Banner steht waagerecht in der Mitte (${schau.mitteX} gegen ${schau.fensterX})`);
|
|
ok(schau.da && Math.abs(schau.mitteY - schau.fensterY) <= 60,
|
|
`und senkrecht ebenfalls (${schau.mitteY} gegen ${schau.fensterY})`);
|
|
ok(schau.da && schau.breite > 300 && schau.hoehe > 100,
|
|
`es ist wirklich gross (${schau.breite}x${schau.hoehe} px)`);
|
|
ok(schau.da && schau.grund.includes("gradient"),
|
|
`mit dem Hintergrund der Schau statt schwarzer Leere (${schau.grund}…)`);
|
|
/* GEGENPROBE ZUM ABFANGEN: Haette der Klick NICHT gegriffen, waere
|
|
der Browser zur Bilddatei gesprungen. Dass die Adresse dieselbe
|
|
ist, beweist, dass die Schau den Klick wirklich uebernommen hat --
|
|
und nicht nur zufaellig irgendwo im Dokument liegt. */
|
|
ok(seite.url() === vorherAdresse,
|
|
`die Seite ist dabei nicht weggesprungen (${seite.url() === vorherAdresse ? "dieselbe Adresse" : seite.url()})`);
|
|
|
|
await seite.screenshot({ path: "server/pruef-eventkarte-schau.png", fullPage: false });
|
|
await seite.keyboard.press("Escape");
|
|
await seite.waitForTimeout(300);
|
|
const zu = await seite.evaluate(() =>
|
|
!document.querySelector("dialog.bildschau")?.open);
|
|
ok(zu, "und Escape schliesst sie wieder");
|
|
|
|
/* DIE ZAHLEN AUS DEM BEFUND, jede einzeln. */
|
|
ok(masse.titelInKarte < 180,
|
|
`der Titel steht ${masse.titelInKarte} px unter der Kartenkante (vorher 638+, Grenze 180)`);
|
|
ok(masse.titelGroesse >= 18,
|
|
`Titel ${masse.titelGroesse} px (vorher 15,36 -- Grenze 18)`);
|
|
ok(masse.titelGroesse >= masse.textGroesse * 1.3,
|
|
`Titel deutlich groesser als der Text darunter (${masse.titelGroesse} vs ${masse.textGroesse})`);
|
|
/* Die projekteigene Grenze fuer Grossbuchstaben mit Sperrung. Sie
|
|
galt bisher nur unter 760 px Fensterbreite -- eine Grenze, die nur
|
|
bei schmalen Fenstern gilt, ist keine. */
|
|
ok(masse.markeGroesse >= 11.5,
|
|
`Etikett ${masse.markeGroesse} px (vorher 10,88 -- Grenze 11,5)`);
|
|
ok(masse.buehne <= 320 && masse.buehne > 0,
|
|
`Buehne ${masse.buehne} px hoch (vorher 638)`);
|
|
ok(masse.datumZeilen.length === 1,
|
|
`das Datum steht einmal, nicht dreimal: ${JSON.stringify(masse.datumZeilen)}`);
|
|
ok(/^noch \d+ Tage?$/.test(masse.noch || ""),
|
|
`der Countdown beantwortet "wie lange noch": ${JSON.stringify(masse.noch)}`);
|
|
|
|
/* Die drei Zustaende -- jeder einzeln nachgewiesen, nicht als Summe. */
|
|
const staende = await seite.evaluate(() => {
|
|
const raus = {};
|
|
for (const k of document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')) {
|
|
const t = k.querySelector(".eintrag-titel")?.textContent || "?";
|
|
raus[t] = { stand: k.dataset.event, wort: k.querySelector(".ev-noch")?.textContent };
|
|
}
|
|
return raus;
|
|
});
|
|
ok(staende["Oktober Event"]?.stand === "laeuft",
|
|
`laufend: ${JSON.stringify(staende["Oktober Event"])}`);
|
|
ok(staende["November Challenge"]?.stand === "kommt"
|
|
&& /^startet in \d+ Tagen$/.test(staende["November Challenge"]?.wort || ""),
|
|
`kommend: ${JSON.stringify(staende["November Challenge"])}`);
|
|
ok(staende["September Rückblick"]?.stand === "vorbei"
|
|
&& /^vorbei seit \d+ Tagen$/.test(staende["September Rückblick"]?.wort || ""),
|
|
`vorbei: ${JSON.stringify(staende["September Rückblick"])}`);
|
|
|
|
/* EIN VORBEIGELAUFENES EVENT WIRD NICHT MEHR ABGEHAKT -- ein Haken
|
|
waere eine Behauptung ueber etwas, das nicht mehr stattfindet. */
|
|
const knoepfeVorbei = await seite.evaluate(() => {
|
|
const k = [...document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')]
|
|
.find((x) => x.textContent.includes("September"));
|
|
return {
|
|
knoepfe: k.querySelectorAll(".ev-punkt__knopf").length,
|
|
zeilen: k.querySelectorAll(".ev-punkt").length,
|
|
};
|
|
});
|
|
ok(knoepfeVorbei.zeilen > 0 && knoepfeVorbei.knoepfe === 0,
|
|
`vorbei: ${knoepfeVorbei.zeilen} Zeilen lesbar, ${knoepfeVorbei.knoepfe} Knoepfe`);
|
|
|
|
/* DER WEG ZUM GANZEN BANNER. Er war beim ersten Entwurf verloren
|
|
gegangen: Vorher war das Bild ein Verweis, als Buehne nicht mehr --
|
|
und bei Filipes Banner steht die eigentliche Ansage IM Bild. */
|
|
const ganz = await seite.evaluate(() => {
|
|
const k = [...document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')]
|
|
.find((x) => x.textContent.includes("Oktober Event"));
|
|
const a = k.querySelector(".ev-buehne__ganz");
|
|
const t = k.querySelector(".eintrag-titel");
|
|
if (!a) return null;
|
|
const ar = a.getBoundingClientRect(), tr = t.getBoundingClientRect();
|
|
return {
|
|
ziel: a.getAttribute("href"),
|
|
neuesFenster: a.getAttribute("target") === "_blank" && /noopener/.test(a.getAttribute("rel") || ""),
|
|
wort: a.textContent.trim(),
|
|
/* Deckt er den Titel zu? Das haeufigste Problem bei einem Knopf,
|
|
der absolut in einer Ecke sitzt. */
|
|
ueberlappt: !(ar.bottom < tr.top || ar.top > tr.bottom || ar.right < tr.left || ar.left > tr.right),
|
|
hoehe: Math.round(ar.height),
|
|
};
|
|
});
|
|
ok(!!ganz && /^\/workspace\/api\/dateien\/\d+\/bild$/.test(ganz.ziel || ""),
|
|
`"Banner ganz" fuehrt zum vollen Bild: ${JSON.stringify(ganz)}`);
|
|
ok(!!ganz && ganz.neuesFenster, "und zwar in einem neuen Fenster, mit noopener");
|
|
ok(!!ganz && !ganz.ueberlappt, "er liegt nicht ueber dem Titel");
|
|
|
|
/* GEGENPROBE: Ohne Bild gibt es ihn nicht -- ein Knopf, der auf eine
|
|
Fehlerseite fuehrt, ist schlimmer als keiner. */
|
|
const ohneBild = await seite.evaluate(() => {
|
|
const k = [...document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')]
|
|
.find((x) => x.textContent.includes("November Challenge"));
|
|
return { mitbild: k.querySelector(".ev-buehne")?.dataset.mitbild || null,
|
|
knopf: k.querySelectorAll(".ev-buehne__ganz").length };
|
|
});
|
|
ok(ohneBild.mitbild === null && ohneBild.knopf === 0,
|
|
`ohne Banner kein Knopf: ${JSON.stringify(ohneBild)}`);
|
|
|
|
/* Der Loeschknopf unterscheidet sich jetzt von den anderen. */
|
|
const gefahr = await seite.evaluate(() => {
|
|
const k = [...document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')]
|
|
.find((x) => x.textContent.includes("Oktober Event"));
|
|
const l = k.querySelector(".schritt--gefahr");
|
|
const andere = k.querySelector(".eintrag-knoepfe .schritt:not(.schritt--gefahr)");
|
|
return l && andere
|
|
? { l: getComputedStyle(l).color, a: getComputedStyle(andere).color, text: l.textContent.trim() }
|
|
: null;
|
|
});
|
|
ok(!!gefahr && gefahr.text === "löschen" && gefahr.l !== gefahr.a,
|
|
`"löschen" sieht anders aus als die uebrigen Knoepfe: ${JSON.stringify(gefahr)}`);
|
|
|
|
await ctx.close();
|
|
}
|
|
|
|
melde("\n=== 5. Abhaken im Browser, mit Neuladen ===");
|
|
{
|
|
const { ctx, seite } = await alsRolle("creator", "CODE-CREA-0001", 1280);
|
|
|
|
/* DEN ZAEHLER DER RICHTIGEN KARTE LESEN.
|
|
Beim ersten Lauf stand hier `seite.textContent(".ev-zaehler")` --
|
|
das nimmt den ERSTEN auf der Seite, und der gehoerte zur November-
|
|
Challenge ("0 von 1"). Die Pruefung meldete daraufhin FEHL, obwohl
|
|
am Zaehler nichts falsch war. Eine Pruefung, die das Falsche misst,
|
|
kostet dieselbe Zeit wie ein echter Fehler. */
|
|
const OKT = '.eintrag-karte[data-eventkarte="ja"]:has(.eintrag-titel:text-is("Oktober Event"))';
|
|
const zaehlerLesen = async () =>
|
|
(await seite.textContent(OKT + " .ev-zaehler") || "").trim();
|
|
|
|
const vorher = await zaehlerLesen();
|
|
ok(/^\d+ von \d+$/.test(vorher), `Zaehler steht da: ${JSON.stringify(vorher)}`);
|
|
|
|
/* Luna hat aus Teil 2 noch einen Haken -- also 1 von 3. */
|
|
ok(vorher === "1 von 3", `Lunas Stand vom Server: ${vorher}`);
|
|
|
|
const knoepfe = await seite.$$(OKT + " .ev-punkt__knopf");
|
|
ok(knoepfe.length === 3, `${knoepfe.length} abhakbare Punkte an diesem Event`);
|
|
/* Den NICHT bereits abgehakten nehmen -- sonst zaehlt der Klick
|
|
herunter statt herauf, und die Pruefung misst das Gegenteil. */
|
|
await knoepfe[0].click();
|
|
await seite.waitForTimeout(600);
|
|
const danach = await zaehlerLesen();
|
|
ok(danach === "2 von 3", `nach dem Klick: ${danach}`);
|
|
|
|
/* HAELT ES EINEN SEITENWECHSEL AUS? Eine Anzeige, die nur im
|
|
Browser zaehlt, sieht genauso aus -- und ist nichts wert. */
|
|
await seite.reload({ waitUntil: "networkidle" });
|
|
await seite.waitForSelector(".ev-zaehler", { timeout: 15000 });
|
|
await seite.waitForTimeout(500);
|
|
const nachNeuladen = await zaehlerLesen();
|
|
ok(nachNeuladen === "2 von 3", `nach dem Neuladen immer noch: ${nachNeuladen}`);
|
|
|
|
/* Und wieder ab. */
|
|
const k2 = await seite.$$(OKT + " .ev-punkt__knopf");
|
|
await k2[0].click();
|
|
await seite.waitForTimeout(600);
|
|
const ab = await zaehlerLesen();
|
|
ok(ab === "1 von 3", `wieder abgewaehlt: ${ab}`);
|
|
|
|
await ctx.close();
|
|
}
|
|
|
|
melde("\n=== 6. Das Formular ===");
|
|
{
|
|
const { ctx, seite } = await alsRolle("admin", "CODE-DOGI-0001", 1280);
|
|
await seite.click("#neu-oeffnen");
|
|
await seite.waitForTimeout(250);
|
|
await seite.selectOption("#f-art", "kampagne");
|
|
await seite.waitForTimeout(350);
|
|
|
|
const editoren = await seite.$$(".zeileneditor");
|
|
ok(editoren.length === 2, `zwei Zeilen-Editoren (Aufgaben, Regeln): ${editoren.length}`);
|
|
|
|
/* DIE TEXTGESTALTUNGS-LEISTE GEHOERT ZUM VERBORGENEN FELD und darf
|
|
nicht allein unter der Punkteliste schweben. */
|
|
const leisten = await seite.evaluate(() => {
|
|
const raus = [];
|
|
for (const id of ["f-aufgaben", "f-regeln"]) {
|
|
const f = document.getElementById(id);
|
|
const l = f?.parentElement?.querySelector(".textform");
|
|
raus.push({ id, feldVerborgen: !!f?.hidden, leisteVerborgen: l ? !!l.hidden : null });
|
|
}
|
|
return raus;
|
|
});
|
|
ok(leisten.length === 2 && leisten.every((x) => x.feldVerborgen && x.leisteVerborgen === true),
|
|
`Leiste ist mit ihrem Feld verborgen: ${JSON.stringify(leisten)}`);
|
|
|
|
/* Platzhalter passend zum Feld -- "22 Live-Stunden" unter REGELN
|
|
waere eine Fehlanleitung. */
|
|
const halter = await seite.evaluate(() =>
|
|
[...document.querySelectorAll(".zeileneditor")].map((r) =>
|
|
r.querySelector(".zeileneditor__feld")?.placeholder || ""));
|
|
ok(halter.length === 2 && halter[0] !== halter[1]
|
|
&& /Live-Stunden/.test(halter[0]) && /Konto/.test(halter[1]),
|
|
`je Feld ein eigenes Beispiel: ${JSON.stringify(halter)}`);
|
|
|
|
await seite.fill("#f-titel", "Testevent");
|
|
await seite.fill("#f-datum", tag(0));
|
|
await seite.fill("#f-ende", tag(13));
|
|
await seite.waitForTimeout(250);
|
|
const dauer = (await seite.textContent("#hinweis-dauer") || "").trim();
|
|
ok(dauer === "Läuft 14 Tage.", `Dauer wird mitgerechnet: ${JSON.stringify(dauer)}`);
|
|
|
|
/* GEGENPROBE: Ein Ende VOR dem Anfang muss auffallen -- sonst stuende
|
|
auf der frischen Karte "vorbei seit 3 Tagen". */
|
|
await seite.fill("#f-ende", tag(-3));
|
|
await seite.waitForTimeout(250);
|
|
const warnung = (await seite.textContent("#hinweis-dauer") || "").trim();
|
|
const warnMerkmal = await seite.getAttribute("#hinweis-dauer", "data-warnung");
|
|
ok(/vor dem Anfang/.test(warnung) && warnMerkmal === "ja",
|
|
`verdrehter Zeitraum faellt auf: ${JSON.stringify(warnung)}`);
|
|
await seite.fill("#f-ende", tag(13));
|
|
await seite.waitForTimeout(250);
|
|
|
|
/* Die Vorschau zeigt die echte Buehne. */
|
|
const vorschau = await seite.evaluate(() => {
|
|
const v = document.getElementById("ev-vorschau");
|
|
return {
|
|
sichtbar: v ? !v.hidden : false,
|
|
buehne: !!v?.querySelector(".ev-buehne"),
|
|
titel: v?.querySelector(".eintrag-titel")?.textContent || null,
|
|
noch: v?.querySelector(".ev-noch")?.textContent || null,
|
|
};
|
|
});
|
|
ok(vorschau.sichtbar && vorschau.buehne && vorschau.titel === "Testevent"
|
|
&& /^noch \d+ Tage?$/.test(vorschau.noch || ""),
|
|
`Vorschau zeigt die echte Buehne: ${JSON.stringify(vorschau)}`);
|
|
|
|
/* DER KERN DES FORMULAR-UMBAUS: Was am Ende gespeichert wird, ist
|
|
gleichmaessig -- egal wie jemand tippt. */
|
|
const felder = await seite.$$(".zeileneditor__feld");
|
|
await felder[0].fill("Jeden Tag Live gehen");
|
|
await seite.click(".zeileneditor__mehr");
|
|
const f2 = await seite.$$(".zeileneditor__feld");
|
|
await f2[1].fill("22 Live-Stunden erreichen");
|
|
await seite.waitForTimeout(250);
|
|
const roh2 = await seite.inputValue("#f-aufgaben");
|
|
ok(roh2 === "· Jeden Tag Live gehen\n· 22 Live-Stunden erreichen",
|
|
`gleichmaessig gesetzte Zeichen: ${JSON.stringify(roh2)}`);
|
|
|
|
/* Und das Ganze wirklich abschicken -- bis auf die Karte. */
|
|
await seite.click("#neu-speichern");
|
|
await seite.waitForTimeout(1200);
|
|
/* IM BRETT SUCHEN, NICHT IM FORMULAR.
|
|
Beim ersten Lauf stand hier ein Suchlauf ueber ALLE Karten der
|
|
Seite -- und die VORSCHAU im Formular ist ebenfalls eine Karte mit
|
|
demselben Merkmal und demselben Titel. Gefunden wurde sie zuerst,
|
|
und sie hat naturgemaess keine Aufgabenliste. Die Pruefung meldete
|
|
einen Fehler, den es nicht gab. */
|
|
const neu = await seite.evaluate(() => {
|
|
const liste = document.getElementById("liste");
|
|
const k = [...liste.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')]
|
|
.find((x) => x.textContent.includes("Testevent"));
|
|
return k ? {
|
|
punkte: [...k.querySelectorAll(".ev-block--aufgaben .ev-punkt__text")].map((x) => x.textContent),
|
|
zaehler: k.querySelector(".ev-zaehler")?.textContent || null,
|
|
} : null;
|
|
});
|
|
ok(!!neu && neu.punkte.length === 2
|
|
&& neu.punkte[0] === "Jeden Tag Live gehen"
|
|
&& neu.punkte[1] === "22 Live-Stunden erreichen",
|
|
`aus dem Editor wurde die Liste auf der Karte: ${JSON.stringify(neu)}`);
|
|
ok(neu?.zaehler === "0 von 2", `und der Zaehler steht auf null: ${neu?.zaehler}`);
|
|
|
|
/* NACH DEM SPEICHERN IST DIE VORSCHAU LEER. Sonst stuende beim
|
|
naechsten "Neuer Eintrag" das vorige Event darin -- und man
|
|
glaubte, es sei nicht gespeichert worden. */
|
|
const nachher = await seite.evaluate(() => {
|
|
const v = document.getElementById("ev-vorschau");
|
|
return { verborgen: !!v?.hidden, inhalt: (v?.textContent || "").trim().length };
|
|
});
|
|
ok(nachher.verborgen && nachher.inhalt === 0,
|
|
`die Vorschau ist zurueckgesetzt: ${JSON.stringify(nachher)}`);
|
|
|
|
await ctx.close();
|
|
}
|
|
|
|
melde("\n=== 7. Auf dem Handy (412 px) ===");
|
|
{
|
|
const { ctx, seite } = await alsRolle("creator", "CODE-CREA-0001", 412);
|
|
const handy = await seite.evaluate(() => {
|
|
const k = [...document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')]
|
|
.find((x) => x.textContent.includes("Oktober Event"));
|
|
const knopf = k.querySelector(".ev-punkt__knopf");
|
|
const b = k.querySelector(".ev-buehne");
|
|
return {
|
|
buehne: Math.round(b.getBoundingClientRect().height),
|
|
knopfHoehe: knopf ? Math.round(knopf.getBoundingClientRect().height) : 0,
|
|
/* Laeuft etwas ueber den Rand? Das haeufigste Handy-Problem. */
|
|
ueberlauf: Math.round(k.scrollWidth - k.clientWidth),
|
|
bloeckeSpalten: getComputedStyle(k.querySelector(".ev-bloecke")).gridTemplateColumns,
|
|
ganzHoehe: (() => { const a = k.querySelector(".ev-buehne__ganz");
|
|
return a ? Math.round(a.getBoundingClientRect().height) : null; })(),
|
|
ganzBreite: (() => { const a = k.querySelector(".ev-buehne__ganz");
|
|
return a ? Math.round(a.getBoundingClientRect().width) : null; })(),
|
|
};
|
|
});
|
|
melde(" -- gemessen: " + JSON.stringify(handy));
|
|
ok(handy.knopfHoehe >= 44,
|
|
`das Beruehrziel ist ${handy.knopfHoehe} px hoch (Grenze 44)`);
|
|
|
|
/* ===== AUF DEM HANDY STEHT WIEDER EINE JE REIHE (07.10.2026) ======
|
|
|
|
Die Gegenprobe zum Raster: Zwei Karten nebeneinander sind am
|
|
Rechner richtig und auf 412 px unbedienbar -- dort waere jede
|
|
199 px breit. Dass die Regel UMSCHALTET, muss gemessen werden;
|
|
sonst beweist „zwei nebeneinander" am Rechner nur, dass es dort
|
|
passt, und nichts darueber, was auf dem Telefon passiert. */
|
|
const schmal = await seite.evaluate(() => {
|
|
const k = [...document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')]
|
|
.map((x) => { const r = x.getBoundingClientRect(); return { x: Math.round(r.x), y: Math.round(r.y), b: Math.round(r.width) }; });
|
|
return {
|
|
karten: k,
|
|
/* Stehen zwei auf gleicher Hoehe? Dann waere nicht umgeschaltet. */
|
|
paar: k.some((a, i) => k.slice(i + 1).some((b2) => Math.abs(a.y - b2.y) < 5)),
|
|
quer: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
|
};
|
|
});
|
|
ok(schmal.karten.length >= 2 && !schmal.paar,
|
|
`auf 412 px steht eine Karte je Reihe (${schmal.karten.map((a) => `${a.b}px@${a.y}`).join(", ")})`);
|
|
ok(schmal.karten.every((a) => a.b > 330),
|
|
`und sie ist breit genug zum Bedienen (${schmal.karten.map((a) => a.b).join(", ")} px)`);
|
|
ok(schmal.quer <= 1,
|
|
`die Seite laesst sich nicht seitlich schieben (${schmal.quer} px)`);
|
|
/* UND WENN DOCH, DANN WELCHES ELEMENT? Eine Zahl allein schickt
|
|
einen auf die Suche; der Schuldige steht im DOM und wird nur nicht
|
|
genannt. (Projektgedaechtnis: eine Pruefung, die nicht sagen kann,
|
|
woran sie scheitert, kostet eine Stunde.) */
|
|
if (handy.ueberlauf > 1) {
|
|
const taeter = await seite.evaluate(() => {
|
|
const k = [...document.querySelectorAll('.eintrag-karte[data-eventkarte="ja"]')]
|
|
.find((x) => x.textContent.includes("Oktober Event"));
|
|
const kr = k.getBoundingClientRect();
|
|
return [...k.querySelectorAll("*")]
|
|
.map((e) => ({ was: e.className || e.tagName,
|
|
rechts: Math.round(e.getBoundingClientRect().right - kr.right) }))
|
|
.filter((x) => x.rechts > 0)
|
|
.sort((a, b) => b.rechts - a.rechts).slice(0, 5);
|
|
});
|
|
melde(" -- was ueberlaeuft: " + JSON.stringify(taeter)
|
|
+ (taeter.length === 0
|
|
? " (KEIN Element -- dann ist es ein ::before/::after, das"
|
|
+ " querySelectorAll nicht findet. Beim ersten Mal war es"
|
|
+ " .ev-buehne__inhalt::before mit festem -20px gegen 14px Polsterung.)"
|
|
: ""));
|
|
}
|
|
ok(handy.ueberlauf <= 1, `nichts laeuft ueber den Rand (${handy.ueberlauf} px)`);
|
|
ok(handy.bloeckeSpalten.split(" ").length === 1,
|
|
`auf 412 px eine Spalte, nicht zwei: ${handy.bloeckeSpalten}`);
|
|
ok(handy.buehne <= 240 && handy.buehne > 0, `Buehne ${handy.buehne} px`);
|
|
/* Auf dem Handy schrumpft "Banner ganz" auf die Lupe -- das Ziel
|
|
darf dabei nicht unter 32 px fallen. (32 statt 44: Er sitzt frei
|
|
in einer Ecke ohne Nachbarn, und sein Wort bleibt fuer
|
|
Vorleseprogramme erhalten.) */
|
|
ok(handy.ganzHoehe !== null && handy.ganzHoehe >= 32 && handy.ganzBreite >= 32,
|
|
`der Banner-Knopf bleibt greifbar: ${handy.ganzBreite} x ${handy.ganzHoehe} px`);
|
|
await ctx.close();
|
|
}
|
|
|
|
melde("\n=== 8. Das andere Haus bleibt unberuehrt ===");
|
|
{
|
|
/* DIE REGEL VOM 24.09.2026: "Ab sofort wird JEDER Umbau getrennt
|
|
gefuehrt. Eine Aenderung an einem der beiden Haeuser darf im
|
|
anderen nichts bewirken ... Wer etwas baut, sagt vorher, fuer
|
|
WELCHES Haus es ist, und weist danach nach, dass das andere
|
|
unveraendert ist."
|
|
|
|
DAS IST HIER NICHT SELBSTVERSTAENDLICH: `bereich.css` wird von
|
|
JEDEM Brett geladen, auch von denen der Crew, und `tuKnopf` ist
|
|
die Knopffabrik aller Bretter. Beides wurde angefasst. Dass
|
|
trotzdem nur Agentur-Events betroffen sind, haengt daran, dass
|
|
jede neue Regel an `[data-eventkarte="ja"]` oder einer `ev-`
|
|
Klasse haengt -- und dieses Merkmal setzt nur istEventArt().
|
|
Eine Behauptung, solange sie niemand nachmisst. */
|
|
const ctx = await browser.newContext({ viewport: { width: 1280, height: 1000 } });
|
|
await ctx.route("**/*", (route) => {
|
|
const u = new URL(route.request().url());
|
|
if (u.hostname === "127.0.0.1") {
|
|
return route.continue({ headers: { ...route.request().headers(), host: "crew.dogfather-universe.com" } });
|
|
}
|
|
return route.continue();
|
|
});
|
|
const seite = await ctx.newPage();
|
|
seite.on("pageerror", (f) => klagen.push("crew pageerror: " + f.message));
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
|
await seite.click('.rolle[data-rolle="admin"]');
|
|
await seite.fill("#code", "CODE-DOGI-0001");
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForURL("**/start.html", { timeout: 25000 });
|
|
|
|
/* Das Brett der Agentur gibt es auf dieser Adresse gar nicht. */
|
|
await seite.goto(BASIS + "/workspace/bereich.html?b=agentur", { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(900);
|
|
const dort = await seite.evaluate(() => ({
|
|
adresse: location.pathname,
|
|
eventkarten: document.querySelectorAll('[data-eventkarte="ja"]').length,
|
|
evTeile: document.querySelectorAll('[class*="ev-buehne"], [class*="ev-punkt"], .zeileneditor').length,
|
|
}));
|
|
/* ==== EIN BEFUND, DER NICHT AUS DIESEM UMBAU STAMMT ===============
|
|
|
|
Hier stand zuerst die Zusage "auf der Crew-Adresse nichts davon" --
|
|
und sie wurde rot: Das Agentur-Brett ist dort erreichbar, mit
|
|
Inhalt. Vor dem Schreiben einer Reparatur erst nachgemessen, ob
|
|
ich das verursacht habe (03.10.2026, server/_mess/haus.mjs, einmal
|
|
mit und einmal ohne den Umbau):
|
|
|
|
Host crew.dogfather-universe.com haus=crew
|
|
/workspace/api/bereich/agentur -> 200, 1 Eintrag
|
|
|
|
ZWEIMAL DASSELBE, mit und ohne. Der Durchgriff ist also aelter als
|
|
dieser Umbau und gehoert zur Regel vom 24.09.2026 ("Kein Bestand
|
|
wird geteilt"), nicht hierher -- er ist Filipe gemeldet und wird
|
|
getrennt entschieden. Ihn hier nebenbei zuzumauern hiesse, am
|
|
Rechtesystem beider Haeuser zu drehen, waehrend eigentlich eine
|
|
Karte umgebaut wird.
|
|
|
|
WAS DIESE DATEI DESHALB ZUSAGT, ist das, wofuer sie zustaendig
|
|
ist: Der Umbau aendert an der Crew-Adresse NICHTS -- die Karten
|
|
sehen dort aus wie auf der Agenturadresse, und die eigenen Bretter
|
|
der Crew bleiben unberuehrt (gleich darunter).
|
|
|
|
Die Zahl wird trotzdem ausgegeben: Faellt sie eines Tages auf 0,
|
|
ist der Durchgriff behoben, und das soll man hier sehen. */
|
|
melde(" -- Crew-Adresse, Brett `agentur`: " + JSON.stringify(dort)
|
|
+ (dort.eventkarten > 0
|
|
? " (vorbestehender Durchgriff, siehe Kommentar -- nicht aus diesem Umbau)"
|
|
: " (der Durchgriff ist behoben)"));
|
|
/* Wenn das Brett dort erscheint, dann wenigstens vollstaendig und
|
|
ohne Fehler -- eine halb gebaute Karte waere ein echter Schaden. */
|
|
ok(dort.eventkarten === 0 || dort.evTeile > 0,
|
|
`was dort erscheint, ist vollstaendig gebaut (${dort.eventkarten} Karten, ${dort.evTeile} Teile)`);
|
|
|
|
/* Und ein Brett, das es dort WIRKLICH gibt, sieht aus wie immer.
|
|
Ohne diese Haelfte waere oben nur bewiesen, dass die Seite leer
|
|
ist -- das waere sie auch bei einem kaputten Brett. */
|
|
await seite.goto(BASIS + "/workspace/bereich.html?b=treff", { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(900);
|
|
const treff = await seite.evaluate(() => ({
|
|
liste: !!document.getElementById("liste"),
|
|
formular: !!document.getElementById("neu"),
|
|
knoepfe: document.querySelectorAll(".schritt").length,
|
|
gefahr: document.querySelectorAll(".schritt--gefahr").length,
|
|
evTeile: document.querySelectorAll('[class*="ev-buehne"], [class*="ev-punkt"], .zeileneditor').length,
|
|
}));
|
|
ok(treff.liste && treff.formular,
|
|
`ein Crew-Brett steht und funktioniert: ${JSON.stringify(treff)}`);
|
|
ok(treff.evTeile === 0, `und zeigt keinen Teil des Event-Umbaus (${treff.evTeile})`);
|
|
/* Auch der Loeschknopf bleibt dort der alte: Die Markierung wird
|
|
zwar ueberall gesetzt, aber gestaltet wird sie nur innerhalb einer
|
|
Eventkarte. */
|
|
const farben = await seite.evaluate(() => {
|
|
const g = document.querySelector(".schritt--gefahr");
|
|
const n = document.querySelector(".schritt:not(.schritt--gefahr)");
|
|
return g && n ? { gefahr: getComputedStyle(g).color, normal: getComputedStyle(n).color } : null;
|
|
});
|
|
ok(farben === null || farben.gefahr === farben.normal,
|
|
`der Loeschknopf sieht dort aus wie immer: ${JSON.stringify(farben)}`);
|
|
|
|
await ctx.close();
|
|
}
|
|
|
|
await browser.close();
|
|
|
|
melde("\n=== Browser-Meldungen ===");
|
|
ok(klagen.length === 0, klagen.length ? "Klagen: " + klagen.join(" | ") : "keine Fehler in der Konsole");
|
|
|
|
melde("");
|
|
melde(`${geprueft} geprueft, ${fehler} Fehler`);
|
|
if (!geprueft) { melde("ABBRUCH: nichts geprueft -- das ist ein Fehler, kein Erfolg."); process.exit(3); }
|
|
process.exit(fehler ? 1 : 0);
|