Filipes Frage: „koennen die leute auch schon mehrere fotos schicken wenn die im support was melden wollen?" Technisch seit dem 02.10. (VanVans Meldung #11), und heute mehrfach am laufenden Server nachgemessen. Beim Nachsehen auf der SEITE stand aber: Knopf: „Bild anhängen" -- Einzahl Unterzeile: „häng ein Bild dran" -- Einzahl daneben: (nichts) Wer nicht zufaellig ein zweites Mal auf den Knopf tippt, schickt eines. Aus seiner Sicht waere VanVans Meldung ungeloest -- und er haette recht. EINE MOEGLICHKEIT, VON DER MAN NICHTS WEISS, GIBT ES NICHT. Das ist dieselbe Sorte wie ein Knopf, der eine Absage holt, nur andersherum: Dort verspricht die Oberflaeche zu viel, hier zu wenig. Beides kostet denselben Menschen dieselbe Zeit. GEAENDERT Knopf: „Bilder anhängen" Unterzeile: „häng Bilder dran" daneben: „bis zu 3 Bilder" -- bevor man etwas waehlt nach einem: „eins.png · 24 KB · noch 2 möglich" Die letzte Zeile ist die wichtigere von beiden: „eins.png · 24 KB" allein sagt nicht, dass ein zweites geht -- und genau an dieser Stelle hoert jemand auf. DIE ZAHL KOMMT VOM SERVER (`bilder_max`) und wird neu geschrieben, sobald sie da ist. Ohne das stuende beim ersten Laden der Vorgabewert dort -- heute zufaellig derselbe, morgen vielleicht nicht. Eine Zahl, die zufaellig stimmt, ist keine Auskunft. GEPRUEFT -- pruef-support-bilder 52 -> 56 ok der Knopf steht in der Mehrzahl („Bilder anhängen") und daneben steht, wie viele gehen („bis zu 3 Bilder") auch die Unterzeile sagt nicht mehr „ein Bild" und daneben steht, dass noch Platz ist („eins.png · 0 KB · noch 2 möglich") Dazu gruen: pruef-zeichen 7 · pruef-css-klassen. Co-Authored-By: Claude Opus 5 <[email protected]>
666 lines
31 KiB
JavaScript
666 lines
31 KiB
JavaScript
/* =====================================================================
|
||
MEHRERE BILDER IM SUPPORT — IM ECHTEN BROWSER (02.10.2026)
|
||
=====================================================================
|
||
|
||
VanVan im Support, Meldung #11 -- VIERMAL gemeldet: „Hier im
|
||
Supportbereich kann man immer nur ein Bild hinzufügen bei einer
|
||
Meldung. 2-3 wären besser." Und in der zweiten Runde der Satz, auf
|
||
den es ankommt:
|
||
|
||
„wenn man es nacheinander versucht hinzuzufügen wird das Bild
|
||
immer nur ersetzt."
|
||
|
||
WARUM `pruef-support` DAFÜR NICHT REICHT. Die dortigen Prüfungen
|
||
sprechen mit dem Server und bekommen drei Bilder zurück -- grün,
|
||
und trotzdem wäre VanVans Problem unverändert da. Denn „es wird
|
||
nur ersetzt" passiert NICHT am Server, sondern in `support.js`:
|
||
Eine neue Auswahl im Dateifeld wirft die vorige weg, und zwar auch
|
||
dann, wenn das Feld `multiple` trägt. Ein Server, der drei Bilder
|
||
annimmt, hilft niemandem, dem der Browser nur eines mitgibt.
|
||
|
||
Also wird hier das gemessen, was SIE tut: dreimal hintereinander
|
||
ein Bild wählen. Und einmal viermal, damit die Grenze nicht nur im
|
||
Quelltext steht.
|
||
|
||
DREI AUSGÄNGE. Steht Playwright nicht bereit, endet die Prüfung mit
|
||
2 und sagt, was fehlt -- „konnte nicht nachsehen" ist etwas anderes
|
||
als „in Ordnung".
|
||
|
||
Aufrufen mit: node server/pruef-support-bilder.mjs
|
||
===================================================================== */
|
||
import { mkdtempSync, rmSync } from "node:fs";
|
||
import { tmpdir } from "node:os";
|
||
import { join } from "node:path";
|
||
|
||
import { eigenerPort } from "./helfer-port.mjs";
|
||
const PORT = await eigenerPort(import.meta);
|
||
const ordner = mkdtempSync(join(tmpdir(), "ws-supbild-"));
|
||
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
||
process.env.PORT = String(PORT);
|
||
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
||
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
||
|
||
const express = (await import("express")).default;
|
||
const ec = express.response.cookie;
|
||
express.response.cookie = function (n, w, o) {
|
||
return ec.call(this, n, w, { ...(o || {}), secure: false });
|
||
};
|
||
import { notbremse } from "./helfer-notbremse.mjs";
|
||
await import("./index.js");
|
||
notbremse(300_000, "pruef-support-bilder");
|
||
await new Promise((r) => setTimeout(r, 900));
|
||
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
|
||
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
|
||
|
||
const BASIS = `http://127.0.0.1:${PORT}`;
|
||
|
||
let geprueft = 0, fehler = 0;
|
||
function ok(bedingung, text) {
|
||
geprueft++;
|
||
if (bedingung) { console.log(" ok " + text); return true; }
|
||
fehler++; console.log(" FEHL " + text); return false;
|
||
}
|
||
const melde = (t) => console.log(t);
|
||
|
||
/* ---- Zugänge ------------------------------------------------------ */
|
||
const { DatabaseSync } = await import("node:sqlite");
|
||
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
||
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
||
const jetzt = new Date().toISOString();
|
||
const sch = randomBytes(32).toString("hex");
|
||
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
||
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
||
.run("code_kennung_schluessel", sch, jetzt);
|
||
const anlegen = (name, rolle, code) => {
|
||
const s = randomBytes(16).toString("hex");
|
||
const h = scryptSync(code, s, 64,
|
||
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
||
return d.prepare("INSERT INTO personen"
|
||
+ " (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
|
||
+ " VALUES (?,?,?,?,?,?,1,?) RETURNING id")
|
||
.get(name, rolle, h, s, 32768,
|
||
createHmac("sha256", sch).update(code).digest("hex"), jetzt).id;
|
||
};
|
||
anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
||
anlegen("Miss", "modi", "CODE-MODI-0001");
|
||
d.close();
|
||
|
||
async function keksFuer(rolle, code) {
|
||
const an = await fetch(BASIS + "/workspace/api/anmelden", {
|
||
method: "POST", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ rolle, code }),
|
||
});
|
||
return (an.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
|
||
}
|
||
const keksModi = await keksFuer("modi", "CODE-MODI-0001");
|
||
const keksHand = await keksFuer("admin", "CODE-DOGI-0001");
|
||
|
||
/* ---- Playwright, oder der dritte Ausgang -------------------------- */
|
||
let pw = null;
|
||
try {
|
||
pw = await import(
|
||
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||
} catch (f) {
|
||
console.log("KONNTE NICHT NACHSEHEN: Playwright fehlt (" + (f?.message || "") + ").");
|
||
console.log(" Diese Pruefung braucht einen echten Browser -- was VanVan gemeldet hat,");
|
||
console.log(" passiert im Dateifeld und ist ueber die Schnittstelle nicht zu sehen.");
|
||
console.log(" Nachinstallieren in C:/Users/qciga/Documents/Obelix/Analyse:");
|
||
console.log(" npm i -D playwright && npx playwright install chromium");
|
||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||
process.exit(2);
|
||
}
|
||
const browser = await pw.chromium.launch();
|
||
|
||
/** Drei verschiedene Bilder -- verschieden LANG und verschieden GROSS.
|
||
*
|
||
* Drei gleiche waeren nicht zu unterscheiden: Wuerde die Oberflaeche
|
||
* dreimal dasselbe anzeigen oder die Reihenfolge vertauschen, faellt
|
||
* das nur auf, wenn die Bilder sich unterscheiden. */
|
||
const BILDER = [
|
||
{ name: "eins.png", typ: "image/png", b64:
|
||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" },
|
||
{ name: "zwei.gif", typ: "image/gif", b64:
|
||
"R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" },
|
||
{ name: "drei.webp", typ: "image/webp", b64:
|
||
"UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==" },
|
||
];
|
||
|
||
/** Eine Datei in ein `<input type=file>` legen -- SO WIE EIN MENSCH.
|
||
*
|
||
* `setInputFiles` von Playwright ersetzt die ganze Auswahl und
|
||
* feuert `change` -- also genau das, was auch der Dateidialog des
|
||
* Browsers tut. Mehrmals aufgerufen ist es genau VanVans Weg:
|
||
* „nacheinander versucht hinzuzufuegen". */
|
||
async function waehle(seite, wahl, bilder) {
|
||
await seite.setInputFiles(wahl, bilder.map((b) => ({
|
||
name: b.name, mimeType: b.typ, buffer: Buffer.from(b.b64, "base64"),
|
||
})));
|
||
await seite.waitForTimeout(120);
|
||
}
|
||
|
||
async function neueSeite(breite, keks) {
|
||
const kontext = await browser.newContext({
|
||
viewport: { width: breite, height: 900 }, hasTouch: breite <= 860,
|
||
});
|
||
await kontext.addCookies(keks.split("; ").map((k) => {
|
||
const [n, ...r] = k.split("=");
|
||
return { name: n, value: r.join("="), domain: "127.0.0.1", path: "/" };
|
||
}));
|
||
const seite = await kontext.newPage();
|
||
seite.on("pageerror", (f) => { console.log(" JS-FEHLER: " + f.message); fehler++; });
|
||
await seite.goto(BASIS + "/workspace/support.html", { waitUntil: "networkidle" });
|
||
await seite.waitForTimeout(500);
|
||
return { kontext, seite };
|
||
}
|
||
|
||
/* =====================================================================
|
||
1. NACHEINANDER WAEHLEN LEGT DAZU — VanVans Satz, gemessen
|
||
===================================================================== */
|
||
melde("\n=== Nacheinander waehlen (VanVan, Meldung #11, Runde 2) ===");
|
||
{
|
||
const { kontext, seite } = await neueSeite(390, keksModi);
|
||
const zahl = () => seite.locator("#vorschau .s-vorschau__eins").count();
|
||
|
||
ok(await zahl() === 0, `vorher ist nichts gewaehlt (${await zahl()})`);
|
||
|
||
/* ==== UND DIE SEITE SAGT, DASS MEHRERE GEHEN (03.10.2026) ======
|
||
|
||
Gefunden auf Filipes Frage „koennen die leute auch schon mehrere
|
||
fotos schicken?" -- technisch ja, seit dem 02.10. Nur sagte es
|
||
niemand: Der Knopf hiess „Bild anhaengen" (Einzahl), die
|
||
Unterzeile „haeng ein Bild dran", und daneben stand nichts.
|
||
|
||
Eine Moeglichkeit, von der man nichts weiss, gibt es nicht. Wer
|
||
nicht zufaellig ein zweites Mal tippt, schickt eines -- und aus
|
||
seiner Sicht waere VanVans Meldung ungeloest. */
|
||
const sagt = await seite.evaluate(() => ({
|
||
knopf: document.getElementById('bild-waehlen')?.textContent.trim() || '',
|
||
daneben: document.getElementById('bild-name')?.textContent.trim() || '',
|
||
unter: document.getElementById('seiten-unter')?.textContent.replace(/\s+/g, ' ') || '',
|
||
}));
|
||
ok(/Bilder/.test(sagt.knopf), `der Knopf steht in der Mehrzahl („${sagt.knopf}")`);
|
||
ok(/bis zu 3 Bilder/.test(sagt.daneben),
|
||
`und daneben steht, wie viele gehen („${sagt.daneben}")`);
|
||
ok(!/ein Bild dran/.test(sagt.unter),
|
||
`auch die Unterzeile sagt nicht mehr „ein Bild" („${sagt.unter.slice(0, 60)}…")`);
|
||
|
||
await waehle(seite, "#bild", [BILDER[0]]);
|
||
const n1 = await zahl();
|
||
ok(n1 === 1, `nach dem ersten Bild: eine Vorschau (${n1})`);
|
||
/* UND DASS NOCH PLATZ IST. „eins.png · 0 KB" allein sagt nicht,
|
||
dass ein zweites geht -- genau an dieser Stelle hoert jemand
|
||
auf. */
|
||
const nachEinem = await seite.locator('#bild-name').textContent();
|
||
ok(/noch 2 möglich/.test(nachEinem || ''),
|
||
` und daneben steht, dass noch Platz ist („${(nachEinem || '').trim()}")`);
|
||
|
||
/* HIER WAR DER FEHLER. Vorher stand die Auswahl in einer einzigen
|
||
Variablen -- der zweite Griff warf den ersten weg, und genau das
|
||
hat VanVan beschrieben. */
|
||
await waehle(seite, "#bild", [BILDER[1]]);
|
||
const n2 = await zahl();
|
||
ok(n2 === 2, `nach dem ZWEITEN: zwei (${n2}) — vorher wurde hier ersetzt`);
|
||
|
||
await waehle(seite, "#bild", [BILDER[2]]);
|
||
const n3 = await zahl();
|
||
ok(n3 === 3, `nach dem dritten: drei (${n3})`);
|
||
|
||
/* DIE REIHENFOLGE BLEIBT, und es sind wirklich die drei. Abgelesen
|
||
am Alternativtext, in dem der Dateiname steht. */
|
||
const texte = await seite.locator("#vorschau img").evaluateAll(
|
||
(bilder) => bilder.map((b) => b.alt));
|
||
ok(texte.length === 3 && /eins\.png/.test(texte[0])
|
||
&& /zwei\.gif/.test(texte[1]) && /drei\.webp/.test(texte[2]),
|
||
`und zwar in der Reihenfolge des Waehlens (${texte.join(", ")})`);
|
||
|
||
/* ---- DIE GRENZE ----------------------------------------------- */
|
||
await waehle(seite, "#bild", [{ ...BILDER[0], name: "vier.png" }]);
|
||
const n4 = await zahl();
|
||
ok(n4 === 3, `ein viertes kommt nicht dazu (${n4})`);
|
||
const hinweis = (await seite.locator("#melden-fehler").textContent() || "").trim();
|
||
ok(/3 Bilder|drei Bilder/i.test(hinweis) && hinweis.length > 10,
|
||
`und es steht dabei, warum („${hinweis}")`);
|
||
|
||
/* ---- DASSELBE BILD ZWEIMAL IST EINES --------------------------- */
|
||
const vorher = await zahl();
|
||
await waehle(seite, "#bild", [BILDER[0]]);
|
||
ok(await zahl() === vorher,
|
||
`dasselbe Bild noch einmal zaehlt nicht doppelt (${await zahl()})`);
|
||
|
||
/* ==== UND DREI AUF EINEN GRIFF (03.10.2026) ====================
|
||
|
||
VanVans Satz hat ZWEI Haelften, und ich hatte nur eine gemessen:
|
||
|
||
„man kann nicht mehrere Bilder zum hinzufuegen AUSWAEHLEN.
|
||
Und wenn man es NACHEINANDER versucht hinzuzufuegen wird das
|
||
Bild immer nur ersetzt"
|
||
|
||
Oben steht das Nacheinander. Hier steht das Auswaehlen: drei
|
||
Dateien in EINEM Griff, so wie der Dateidialog sie uebergibt,
|
||
wenn man sie mit gedrueckter Taste markiert. Das haengt am
|
||
`multiple` im Feld -- und ohne diese Zeile waere es nur eine
|
||
Behauptung im HTML.
|
||
|
||
ERST ALLES WEG, dann waehlen: Sonst misst die Zeile, was vorher
|
||
schon dastand. */
|
||
await seite.locator('#bild-weg').click();
|
||
await seite.waitForTimeout(120);
|
||
await waehle(seite, "#bild", BILDER);
|
||
const aufEinmal = await seite.locator("#vorschau img").evaluateAll(
|
||
(bilder) => bilder.map((b) => b.alt));
|
||
ok(aufEinmal.length === 3,
|
||
`drei auf einen Griff ausgewaehlt ergeben drei (${aufEinmal.length})`);
|
||
ok(/eins\.png/.test(aufEinmal[0] || "") && /drei\.webp/.test(aufEinmal[2] || ""),
|
||
` und zwar in der Reihenfolge des Dialogs (${aufEinmal.join(", ")})`);
|
||
/* GEGENPROBE ZUM `multiple`: Ohne das Merkmal gaebe der Browser nur
|
||
EINE Datei weiter, egal wie viele man markiert. Gemessen wird
|
||
deshalb das Merkmal selbst -- sonst stuende hier eine Drei, die
|
||
auch aus drei einzelnen Griffen stammen koennte. */
|
||
ok(await seite.locator("#bild").evaluate((f) => f.multiple) === true,
|
||
" das Feld laesst Mehrfachauswahl ausdruecklich zu (multiple)");
|
||
|
||
/* ---- EINZELN WIEDER WEGNEHMEN ---------------------------------- */
|
||
await seite.locator("#vorschau .s-vorschau__eins").nth(1)
|
||
.locator(".s-vorschau__weg").click();
|
||
await seite.waitForTimeout(120);
|
||
const nachWeg = await seite.locator("#vorschau img").evaluateAll(
|
||
(bilder) => bilder.map((b) => b.alt));
|
||
ok(nachWeg.length === 2 && /eins\.png/.test(nachWeg[0]) && /drei\.webp/.test(nachWeg[1]),
|
||
`das mittlere einzeln entfernen laesst die anderen stehen (${nachWeg.join(", ")})`);
|
||
|
||
/* ---- UND „ALLE WEG" NIMMT ALLE ---------------------------------- */
|
||
await seite.locator("#bild-weg").click();
|
||
await seite.waitForTimeout(120);
|
||
ok(await zahl() === 0, `„Alle weg" raeumt auf (${await zahl()})`);
|
||
ok(await seite.locator("#vorschau").isHidden(),
|
||
"und der leere Kasten nimmt keinen Platz mehr ein");
|
||
|
||
await kontext.close();
|
||
}
|
||
|
||
/* =====================================================================
|
||
2. DIE KREUZE SIND ZU TREFFEN — 44 px und nichts davor
|
||
===================================================================== */
|
||
melde("\n=== Die Kreuze auf 390 px ===");
|
||
{
|
||
const { kontext, seite } = await neueSeite(390, keksModi);
|
||
for (const b of BILDER) await waehle(seite, "#bild", [b]);
|
||
|
||
const mass = await seite.evaluate(() => {
|
||
const raus = [];
|
||
for (const k of document.querySelectorAll("#vorschau .s-vorschau__weg")) {
|
||
const r = k.getBoundingClientRect();
|
||
const oben = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2);
|
||
raus.push({
|
||
breite: Math.round(r.width), hoehe: Math.round(r.height),
|
||
frei: k.contains(oben) || k === oben,
|
||
imBild: r.right <= document.documentElement.clientWidth + 0.5 && r.left >= -0.5,
|
||
});
|
||
}
|
||
return raus;
|
||
});
|
||
ok(mass.length === 3, `drei Kreuze da (${mass.length})`);
|
||
const zuKlein = mass.filter((m) => m.breite < 44 || m.hoehe < 44);
|
||
ok(mass.length === 3 && zuKlein.length === 0,
|
||
`jedes misst mindestens 44x44 (${mass.map((m) => `${m.breite}x${m.hoehe}`).join(", ")})`);
|
||
const verdeckt = mass.filter((m) => !m.frei);
|
||
ok(mass.length === 3 && verdeckt.length === 0,
|
||
`und keines liegt unter etwas anderem (${verdeckt.length} verdeckt)`);
|
||
const draussen = mass.filter((m) => !m.imBild);
|
||
ok(mass.length === 3 && draussen.length === 0,
|
||
`alle drei stehen im sichtbaren Bereich (${draussen.length} daneben)`);
|
||
|
||
/* GEGENPROBE: Kann diese Messung ueberhaupt „verdeckt" sagen? Ein
|
||
Deckel ueber das erste Kreuz, und sie MUSS anschlagen -- sonst
|
||
waere der gruene Haken oben wertlos. */
|
||
await seite.evaluate(() => {
|
||
const k = document.querySelector("#vorschau .s-vorschau__weg");
|
||
const r = k.getBoundingClientRect();
|
||
const deckel = document.createElement("div");
|
||
deckel.id = "probe-deckel";
|
||
deckel.style.cssText = `position:fixed;left:${r.left}px;top:${r.top}px;`
|
||
+ `width:${r.width}px;height:${r.height}px;z-index:9999;`;
|
||
document.body.append(deckel);
|
||
});
|
||
const mitDeckel = await seite.evaluate(() => {
|
||
const k = document.querySelector("#vorschau .s-vorschau__weg");
|
||
const r = k.getBoundingClientRect();
|
||
const oben = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2);
|
||
return k.contains(oben) || k === oben;
|
||
});
|
||
ok(mitDeckel === false, "Gegenprobe: ein Deckel darueber wird erkannt");
|
||
await seite.evaluate(() => document.getElementById("probe-deckel")?.remove());
|
||
|
||
/* ---- UND NICHTS RAGT AUS DER SEITE -------------------------- */
|
||
const ueber = await seite.evaluate(() =>
|
||
document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
||
ok(ueber <= 0, `die Seite mit drei Vorschauen ist nicht zu breit (${ueber} px)`);
|
||
|
||
await kontext.close();
|
||
}
|
||
|
||
/* =====================================================================
|
||
3. ABSCHICKEN UND WIEDERSEHEN
|
||
===================================================================== */
|
||
melde("\n=== Abschicken und wiedersehen ===");
|
||
let meldungId = 0;
|
||
{
|
||
const { kontext, seite } = await neueSeite(390, keksModi);
|
||
await seite.fill("#text", "Vorher, der Klick und nachher – drei Bilder dazu.");
|
||
for (const b of BILDER) await waehle(seite, "#bild", [b]);
|
||
await seite.click("#senden");
|
||
await seite.waitForTimeout(1200);
|
||
|
||
const bilder = await seite.locator("#meine-liste .s-karte__bild img").count();
|
||
ok(bilder === 3, `die eigene Karte zeigt alle drei (${bilder})`);
|
||
|
||
/* GELADEN HEISST GELADEN. Ein <img> mit kaputter Adresse steht
|
||
genauso im Baum wie ein gutes -- `naturalWidth` ist der
|
||
Unterschied. Ohne diese Zeile waeren drei 404er gruen. */
|
||
await seite.waitForTimeout(600);
|
||
const echt = await seite.locator("#meine-liste .s-karte__bild img").evaluateAll(
|
||
(bs) => bs.filter((b) => b.complete && b.naturalWidth > 0).length);
|
||
ok(echt === 3, `und alle drei kommen wirklich vom Server (${echt} geladen)`);
|
||
|
||
const wege = await seite.locator("#meine-liste .s-karte__bild a").evaluateAll(
|
||
(as) => as.map((a) => a.getAttribute("href")));
|
||
ok(new Set(wege).size === 3,
|
||
`drei verschiedene Adressen, nicht dreimal dieselbe (${new Set(wege).size})`);
|
||
|
||
const ueber = await seite.evaluate(() =>
|
||
document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
||
ok(ueber <= 0, `die Karte mit drei Bildern ist nicht zu breit (${ueber} px)`);
|
||
|
||
/* Das Feld ist danach leer -- sonst haengt beim naechsten Melden
|
||
noch die alte Auswahl dran. */
|
||
ok(await seite.locator("#vorschau .s-vorschau__eins").count() === 0,
|
||
"nach dem Abschicken ist die Auswahl leer");
|
||
|
||
meldungId = await seite.evaluate(() => {
|
||
const k = document.querySelector("#meine-liste .s-karte");
|
||
return Number((k?.id || "").replace("meldung-", "")) || 0;
|
||
});
|
||
ok(meldungId > 0, `die Meldung hat eine Nummer (#${meldungId})`);
|
||
await kontext.close();
|
||
}
|
||
|
||
/* =====================================================================
|
||
4. UND IM „NOCH NICHT"-FENSTER GENAUSO
|
||
===================================================================== */
|
||
melde("\n=== Bilder in der Rueckfrage ===");
|
||
{
|
||
/* Die Leitung schickt die Meldung zum Melder zurueck -- erst dann
|
||
gibt es die zwei Knoepfe. */
|
||
await fetch(BASIS + `/workspace/api/support/${meldungId}`, {
|
||
method: "PATCH",
|
||
headers: { "Content-Type": "application/json", cookie: keksHand },
|
||
body: JSON.stringify({ stand: "erledigt", antwort: "Sollte jetzt gehen." }),
|
||
});
|
||
|
||
const { kontext, seite } = await neueSeite(390, keksModi);
|
||
await seite.locator("#meine-liste .s-tun--geht").waitFor({ timeout: 8000 });
|
||
await seite.locator("#meine-liste .s-karte__tun .schritt:not(.s-tun--geht)").click();
|
||
await seite.locator("dialog.nachfrage").waitFor({ state: "visible", timeout: 8000 });
|
||
ok(true, "das Fenster „Geht es noch nicht?“ geht auf");
|
||
|
||
const dz = () => seite.locator("dialog.nachfrage .nachfrage__vorschau").count();
|
||
ok(await dz() === 0, `vorher keine Vorschau (${await dz()})`);
|
||
|
||
await waehle(seite, "#nachfrage-bild", [BILDER[0]]);
|
||
ok(await dz() === 1, `ein Bild (${await dz()})`);
|
||
await waehle(seite, "#nachfrage-bild", [BILDER[1]]);
|
||
ok(await dz() === 2, `nacheinander gewaehlt macht zwei (${await dz()}) — auch hier`);
|
||
await waehle(seite, "#nachfrage-bild", [BILDER[2]]);
|
||
await waehle(seite, "#nachfrage-bild", [{ ...BILDER[1], name: "vier.gif" }]);
|
||
ok(await dz() === 3, `und bei drei ist Schluss (${await dz()})`);
|
||
|
||
await seite.fill("#nachfrage-grund", "Geht noch nicht, hier drei Bildschirmfotos.");
|
||
await seite.locator("dialog.nachfrage [data-teil=\"ja\"]").click();
|
||
await seite.waitForTimeout(1500);
|
||
|
||
const imVerlauf = await seite.locator("#meine-liste .s-runde .s-karte__bild img").count();
|
||
ok(imVerlauf === 3, `im Verlauf haengen alle drei an der Runde (${imVerlauf})`);
|
||
const echt = await seite.locator("#meine-liste .s-runde .s-karte__bild img").evaluateAll(
|
||
(bs) => bs.filter((b) => b.complete && b.naturalWidth > 0).length);
|
||
ok(echt === 3, `und alle drei laden (${echt})`);
|
||
|
||
/* DIE BILDER DER MELDUNG BLEIBEN OBEN, die der Antwort unten. Sonst
|
||
waere nach drei Runden nicht mehr zu sehen, was wozu gehoert. */
|
||
const oben = await seite.locator("#meine-liste .s-karte > .s-karte__bild img").count();
|
||
ok(oben === 3, `oben stehen weiterhin die drei vom Melden (${oben})`);
|
||
|
||
const ueber = await seite.evaluate(() =>
|
||
document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
||
ok(ueber <= 0, `und die Seite bleibt schmal genug (${ueber} px)`);
|
||
|
||
await kontext.close();
|
||
}
|
||
|
||
/* =====================================================================
|
||
5. AUF DEM GROSSEN BILDSCHIRM: KEINE LEERE SPALTE
|
||
=====================================================================
|
||
|
||
Ab 760 px steht das Bild der Meldung NEBEN dem Text. Seit die
|
||
Rundenbilder denselben Kasten benutzen, trifft diese Regel ohne den
|
||
Pfeil in `:has(> …)` auch auf eine Karte zu, deren einziges Bild
|
||
tief im Verlauf steckt -- die Karte bekaeme eine zweite Spalte, in
|
||
der nichts steht.
|
||
===================================================================== */
|
||
melde("\n=== Die zweite Spalte nur mit eigenem Bild ===");
|
||
{
|
||
/* Eine Meldung OHNE Bild, aber mit einem Bild in der Antwortrunde. */
|
||
const neu = await fetch(BASIS + "/workspace/api/support", {
|
||
method: "POST",
|
||
headers: { cookie: keksModi, "Content-Type": "text/plain",
|
||
"x-text": encodeURIComponent("Diese Meldung hat selbst kein Bild.") },
|
||
});
|
||
const id2 = (await neu.json())?.meldung?.id;
|
||
await fetch(BASIS + `/workspace/api/support/${id2}`, {
|
||
method: "PATCH",
|
||
headers: { "Content-Type": "application/json", cookie: keksHand },
|
||
body: JSON.stringify({ stand: "erledigt", antwort: "Schau mal." }),
|
||
});
|
||
await fetch(BASIS + `/workspace/api/support/${id2}/rueckmeldung`, {
|
||
method: "POST",
|
||
headers: { cookie: keksModi, "Content-Type": "application/octet-stream",
|
||
"x-geht": "nein", "x-text": encodeURIComponent("Geht noch nicht, siehe Bild.") },
|
||
body: Buffer.from(BILDER[0].b64, "base64"),
|
||
});
|
||
|
||
const { kontext, seite } = await neueSeite(1280, keksModi);
|
||
const mass = await seite.evaluate((nr) => {
|
||
const k = document.getElementById("meldung-" + nr);
|
||
if (!k) return null;
|
||
const st = getComputedStyle(k);
|
||
const text = k.querySelector(".s-karte__text");
|
||
return {
|
||
raster: st.display,
|
||
spalten: st.gridTemplateColumns,
|
||
textBreite: Math.round(text?.getBoundingClientRect().width || 0),
|
||
karteBreite: Math.round(k.getBoundingClientRect().width),
|
||
};
|
||
}, id2);
|
||
ok(!!mass, `die Karte ohne eigenes Bild ist da (#${id2})`);
|
||
/* GEMESSEN WIRD `display`, NICHT DIE TEXTBREITE. Erst stand hier
|
||
„der Text nimmt mehr als 90 % ein" -- und die Gegenprobe darunter
|
||
wurde rot, obwohl alles stimmte: Die Pruefbilder sind 1x1 Bildpunkt
|
||
gross, die Bildspalte damit wenige Pixel breit, und der Text nimmt
|
||
auch MIT zweiter Spalte 93 %. Die Zahl mass die Bildgroesse, nicht
|
||
die Regel. `display` sagt genau das, was die Regel schaltet. */
|
||
ok(mass?.raster !== "grid",
|
||
`und sie bekommt KEINE zweite Spalte (display: ${mass?.raster})`);
|
||
|
||
/* GEGENPROBE: Bei einer Karte MIT eigenem Bild soll die zweite
|
||
Spalte sehr wohl kommen -- sonst waere die Zeile darueber nur
|
||
deshalb gruen, weil die Regel gar nicht mehr wirkt. */
|
||
const mitBild = await seite.evaluate((nr) => {
|
||
const k = document.getElementById("meldung-" + nr);
|
||
if (!k) return null;
|
||
return {
|
||
display: getComputedStyle(k).display,
|
||
spalten: getComputedStyle(k).gridTemplateColumns,
|
||
eigene: k.querySelectorAll(":scope > .s-karte__bild img").length,
|
||
imVerlauf: k.querySelectorAll(".s-runde .s-karte__bild img").length,
|
||
};
|
||
}, meldungId);
|
||
ok(mitBild?.display === "grid" && mitBild.eigene === 3,
|
||
`Gegenprobe: MIT eigenem Bild gibt es die zweite Spalte `
|
||
+ `(${mitBild?.display}, ${mitBild?.eigene} eigene Bilder)`);
|
||
/* UND BEIDE KARTEN HABEN BILDER IM VERLAUF -- die eine mit, die
|
||
andere ohne eigenes. Ohne diese Zeile koennte der Unterschied
|
||
oben auch daher ruehren, dass die zweite Karte ueberhaupt kein
|
||
Bild hat. */
|
||
ok((mitBild?.imVerlauf || 0) === 3,
|
||
` und beide Karten haben Rundenbilder (${mitBild?.imVerlauf})`);
|
||
|
||
await kontext.close();
|
||
}
|
||
|
||
/* =====================================================================
|
||
6. DER VERLAUF: EINE FARBE JE RUNDE (03.10.2026)
|
||
=====================================================================
|
||
|
||
Filipe: „ich will diese komplette seite vom support viel
|
||
uebersichtlicher, profissioneller, jede runde soll auch immer eine
|
||
spezielle farbe haben und anders aufgestellt aufgeteilt sein damit
|
||
man einen viel besseren und krasseren ueberblick hat."
|
||
|
||
WARUM DAS GEPRUEFT WERDEN MUSS und nicht nur auf einem Bild steht:
|
||
Die Farbe kommt aus einer Rechnung (`rundenTon` in support.js) und
|
||
wird im CSS zu `hsl()`. Zwischen diesen beiden Stellen kann sie
|
||
verlorengehen, ohne dass etwas abstuerzt -- dann sind alle Runden
|
||
gleich grau, und auf dem Bildschirm sieht das aus wie Absicht.
|
||
|
||
GEMESSEN WIRD DIE ANGEZEIGTE FARBE, nicht die Zahl: `--ton` kann
|
||
gesetzt sein und das CSS sie trotzdem nicht benutzen.
|
||
===================================================================== */
|
||
melde("\n=== Eine Farbe je Runde ===");
|
||
{
|
||
/* Eine Meldung mit VIER Runden -- so viele, wie es Toene gibt.
|
||
Mit zwei saehe man zwei Farben und wueste nicht, ob es eine
|
||
Reihe ist. */
|
||
/* UEBER `fetch` MIT KEKS -- diese Datei hat keinen `hol`-Helfer wie
|
||
pruef-support.mjs. Mein erster Anlauf rief ihn trotzdem auf und
|
||
stuerzte ab; die Pruefung hat es in derselben Minute gesagt. */
|
||
const ruf = (pfad, keks, o = {}) => fetch(BASIS + pfad,
|
||
{ ...o, headers: { cookie: keks, ...(o.headers || {}) } });
|
||
const neu = await ruf("/workspace/api/support", keksModi, {
|
||
method: "POST", headers: { "Content-Type": "text/plain",
|
||
"x-text": encodeURIComponent("Probe fuer die Rundenfarben.") },
|
||
});
|
||
const vId = (await neu.json())?.meldung?.id;
|
||
for (let i = 0; i < 4; i++) {
|
||
await ruf(`/workspace/api/support/${vId}`, keksHand, {
|
||
method: "PATCH", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ stand: "erledigt", antwort: `Versuch ${i + 1}.` }),
|
||
});
|
||
await ruf(`/workspace/api/support/${vId}/rueckmeldung`, keksModi, {
|
||
method: "POST",
|
||
headers: { "x-geht": "nein",
|
||
"x-text": encodeURIComponent(`Ging noch nicht, Versuch ${i + 1}.`) },
|
||
});
|
||
}
|
||
/* Noch einmal zurueckgeben, damit die letzte Runde eine Antwort
|
||
hat und der Melder wieder dran ist. */
|
||
await ruf(`/workspace/api/support/${vId}`, keksHand, {
|
||
method: "PATCH", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ stand: "erledigt", antwort: "Jetzt aber." }),
|
||
});
|
||
|
||
const { kontext, seite } = await neueSeite(1280, keksModi);
|
||
const karte = `#meldung-${vId}`;
|
||
await seite.locator(karte).waitFor({ timeout: 8000 });
|
||
|
||
const bild = await seite.evaluate((wahl) => {
|
||
const k = document.querySelector(wahl);
|
||
const punkte = [...k.querySelectorAll(".s-rundenleiste__punkt")];
|
||
const offen = [...k.querySelectorAll(".s-runde")];
|
||
const farbe = (e) => getComputedStyle(e).backgroundColor;
|
||
return {
|
||
punkte: punkte.length,
|
||
punktFarben: punkte.map((p) => farbe(p)),
|
||
nummern: punkte.map((p) => p.textContent.trim()),
|
||
/* Die Farbe der Rundennummer -- sie muss sich mit der Runde
|
||
aendern, sonst ist die Achse farbig und die Schrift nicht. */
|
||
nrFarben: offen.map((r) =>
|
||
getComputedStyle(r.querySelector(".s-runde__nr")).color),
|
||
offene: offen.length,
|
||
mehr: (k.querySelector(".s-verlauf__mehr")?.innerText || "")
|
||
.replace(/\s+/g, " ").trim(),
|
||
/* Zwei Sprechrichtungen: Wer geantwortet hat und was daraus
|
||
wurde, muessen verschieden aussehen. */
|
||
schilder: [...k.querySelectorAll(".s-schritt__schild")]
|
||
.map((x) => x.textContent.trim()),
|
||
hoch: Math.round(k.getBoundingClientRect().height),
|
||
};
|
||
}, karte);
|
||
|
||
ok(bild.punkte === 5, `die Leiste zeigt jede Runde (${bild.punkte} Punkte)`);
|
||
ok(bild.nummern.join(",") === "1,2,3,4,5",
|
||
`mit ihrer Nummer (${bild.nummern.join(", ")})`);
|
||
/* DER EIGENTLICHE PUNKT: vier verschiedene Farben. Die fuenfte
|
||
wiederholt die erste -- so ist die Formel gebaut, und das steht
|
||
hier als Aussage, damit es niemand fuer einen Fehler haelt. */
|
||
const verschieden = new Set(bild.punktFarben.slice(0, 4)).size;
|
||
ok(verschieden === 4,
|
||
`die ersten vier Runden haben vier verschiedene Farben (${verschieden})`);
|
||
ok(bild.punktFarben[4] === bild.punktFarben[0],
|
||
"und die fuenfte faengt die Reihe wieder von vorn an — so ist die Formel");
|
||
/* GEGENPROBE: Waere `--ton` nicht angekommen, waeren ALLE gleich.
|
||
Diese Zeile unterscheidet „vier Farben" von „viermal dieselbe". */
|
||
ok(new Set(bild.punktFarben).size > 1,
|
||
`Gegenprobe: sie sind nicht alle gleich (${new Set(bild.punktFarben).size} Farben)`);
|
||
|
||
ok(new Set(bild.nrFarben).size === bild.offene,
|
||
`auch die Rundennummern tragen ihre Farbe (${new Set(bild.nrFarben).size} `
|
||
+ `von ${bild.offene})`);
|
||
|
||
ok(bild.offene === 2, `offen stehen die letzten zwei Runden (${bild.offene})`);
|
||
ok(/3 frühere Runden/.test(bild.mehr),
|
||
`und die aelteren sind einen Griff entfernt („${bild.mehr}")`);
|
||
|
||
ok(bild.schilder.includes("Geantwortet")
|
||
&& bild.schilder.some((t) => /Ging noch nicht|Wartet/.test(t)),
|
||
`jede Runde zeigt beide Seiten (${[...new Set(bild.schilder)].join(" / ")})`);
|
||
|
||
/* AUFKLAPPEN UND WIEDER ZU. */
|
||
await seite.locator(`${karte} .s-verlauf__mehr`).click();
|
||
await seite.waitForTimeout(400);
|
||
const ganz = await seite.evaluate((wahl) => ({
|
||
offene: document.querySelectorAll(`${wahl} .s-runde`).length,
|
||
mehr: (document.querySelector(`${wahl} .s-verlauf__mehr`)?.innerText || "")
|
||
.replace(/\s+/g, " ").trim(),
|
||
}), karte);
|
||
ok(ganz.offene === 5, `aufgeklappt stehen alle da (${ganz.offene})`);
|
||
await seite.locator(`${karte} .s-verlauf__mehr`).click();
|
||
await seite.waitForTimeout(400);
|
||
ok(await seite.locator(`${karte} .s-runde`).count() === 2,
|
||
"und wieder zu — der Knopf geht in beide Richtungen");
|
||
|
||
/* UND NICHTS RAGT HERAUS, auch nicht auf dem Handy. */
|
||
await kontext.close();
|
||
const h = await neueSeite(390, keksModi);
|
||
await h.seite.locator(karte).waitFor({ timeout: 8000 });
|
||
const ueber = await h.seite.evaluate(() =>
|
||
document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
||
ok(ueber <= 0, `auf 390 px ragt nichts heraus (${ueber} px)`);
|
||
/* GEGENPROBE ZUR LEISTE: Bei EINER Runde gibt es sie nicht -- ein
|
||
Punkt neben keinem Punkt sagt nichts. */
|
||
const einzeln = await h.seite.evaluate(() => {
|
||
const k = [...document.querySelectorAll(".s-karte")]
|
||
.find((x) => x.querySelectorAll(".s-runde").length === 1);
|
||
return k ? k.querySelectorAll(".s-rundenleiste").length : -1;
|
||
});
|
||
ok(einzeln === 0 || einzeln === -1,
|
||
`bei einer einzigen Runde gibt es keine Leiste (${einzeln})`);
|
||
await h.kontext.close();
|
||
}
|
||
|
||
await browser.close();
|
||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
||
|
||
console.log("");
|
||
console.log(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
||
console.log(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
||
process.exit(fehler ? 1 : 0);
|