Filipe: "zuerst will ich dass du die kategorien und die kacheln viel
krasser geiler machst."
Beim Nachmessen stand fest, warum die Pipeline flach aussah: Drei
Regeln in scouting.css kamen gar nicht an, seit es den Baukasten
(module.css) gibt -- und module.css NENNT zwei davon im eigenen Text.
--ton war an JEDER Karte #db4b66, dem Rot der Seite.
Daraus nimmt der Baukasten Kantenlicht, die
drei Eckwinkel und das Licht beim Zeigen. Sechs
Stufenfarben in der Datei, eine auf dem Schirm.
.kk::before (3px) gemessen 560px breit -- das ist das Kantenlicht
des Baukastens, das dasselbe Pseudoelement
belegt. Die farbige Pipeline-Kante gab es nicht.
.kk { border } /:hover module.css setzt `border: 0`, gleich stark und
spaeter im Ladeweg.
Das Haus hatte die Lehre schon: aufgaben.css setzt `--ton` an fuenf
Spalten mit genau dieser Begruendung. Hierher uebertragen hat es nie
jemand.
WAS JETZT ANDERS IST
* Jede Stufe traegt ihren Ton -- Eckwinkel, Kantenlicht und Hover
laufen damit von kuehl (neu entdeckt) zu gruen (uebergeben).
* Die Farbkante der Karte liegt im HINTERGRUND statt auf einem
Pseudoelement (beide gehoeren dem Baukasten). 6px, weil das
Kantenlicht 1,6px davon verdeckt -- bei 3px blieb nichts Sichtbares.
* Etappennummern 01-05, abgeleitet aus WEITER statt daneben
geschrieben. "Abgelehnt" bekommt keine: keine Etappe nach vorn.
* Stufentitel in der eigenen Farbe (vorher einheitlich --akzent),
mit Weiss aufgehellt -- 4,3:1 waeren bei fett-versal 12,5px zu wenig.
* Faellig-Saum als inset-Schatten wie bei .kachel[data-warn], mit
(0,3,0) gegen die Modulliste; vorher border-color und damit wirkungslos.
* Karte: Plattform als Schild, Handle in gleichbreiter Schrift,
Haarlinie zwischen "wer" und "lohnt sich", Reichweite deutlicher.
DAS MESSGERAET, DAS GEFEHLT HAT: module.css notiert "NICHT
MITREPARIERT, weil es kein Messgeraet dafuer gibt ... Hover laesst sich
nicht pruefen." Doch -- pruef-scouting-felder faehrt den Zeiger jetzt
auf die Karte und liest nach (6px -> 9px).
DREI FUNDE IN DEN EIGENEN WERKZEUGEN, alle von Hauswachen oder der
eigenen Messung:
* bild-arten hatte keine Notbremse; page.hover() blieb haengen und
liess beim Abbrechen einen Server auf Port 4336 zurueck. Jetzt
notbremse(240s) und mouse.move statt hover().
* color-mix(in srgb, var(--rand)) -- mit EINER Farbe ungueltig; der
ganze Verlauf fiel aus, uebrig blieb Abstand ohne Linie.
* Im Pruefausdruck steckte ein Backspace-Byte (0x08), das die
Kommandozeile aus einer Zeichenfolge gemacht hatte. Sah im
Quelltext richtig aus, traf nie. Jetzt startsWith.
pruef-scouting-felder: 64 -> 72 Pruefungen, gruen. Gegenprobe gefahren
(einer Stufe den Ton genommen -> rot, mit #db4b66 als Beleg). Statt des
226-Sekunden-Hauslaufs pruef-handy misst die Datei jetzt selbst auf
320px: 97 Teile, keines ueber dem Rand, kein seitliches Schieben.
Dazu gruen: css-klassen, lesbarkeit, tippziele, fingermass,
haus-trennung, leerzustand, deutsche-texte, zeichen (der eine Rest dort
ist bereich.css und aelter).
Co-Authored-By: Claude Opus 5 <[email protected]>
630 lines
32 KiB
JavaScript
630 lines
32 KiB
JavaScript
/* =====================================================================
|
||
DIE SCOUT-PIPELINE — die sieben neuen Felder und die Trennung.
|
||
|
||
Filipe (11.09.2026), zur Pipeline: "perfektionnier diese kategorien
|
||
wie die aussehen und was man da noch alles immer in jeder kategorie
|
||
eintragen kann. weil man kan da nichts machen ... und das soll
|
||
getrennt von der team dogi seite sein."
|
||
|
||
Drei Behauptungen stehen darin, und jede wird hier gemessen:
|
||
|
||
1. Man kann mehr eintragen -> die sieben Felder kommen an
|
||
2. Der Server nimmt keinen Unsinn -> Land, Netzwerk, Follower
|
||
3. Getrennt von Team Dogi -> auf crew. gibt es keine Leads
|
||
|
||
WARUM AUCH DIE OBERFLAECHE GEMESSEN WIRD: "man kann nichts machen"
|
||
war ja gerade KEIN Serverproblem -- der Editor lag hinter einem Knopf
|
||
namens "Details" und wurde nicht gefunden. Ein gruener Servertest
|
||
haette das nie bemerkt.
|
||
|
||
Aufruf: node server/pruef-scouting-felder.mjs
|
||
===================================================================== */
|
||
import { mkdtempSync, rmSync } from "node:fs";
|
||
import { tmpdir } from "node:os";
|
||
import { join, dirname } from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
import { spawn } from "node:child_process";
|
||
import { DatabaseSync } from "node:sqlite";
|
||
import { scryptSync, randomBytes } from "node:crypto";
|
||
import { notbremse } from "./helfer-notbremse.mjs";
|
||
|
||
/* MILLISEKUNDEN, nicht Sekunden. Beim ersten Anlauf stand hier 240 --
|
||
die Pruefung brach nach einer Viertelsekunde mit "HING" ab, obwohl
|
||
sie noch gar nicht angefangen hatte. Die Meldung war richtig, die
|
||
Zahl war es nicht. */
|
||
notbremse(240_000, "pruef-scouting-felder");
|
||
|
||
const HIER = dirname(fileURLToPath(import.meta.url));
|
||
const PORT = 4387;
|
||
const BASIS = `http://127.0.0.1:${PORT}`;
|
||
const ordner = mkdtempSync(join(tmpdir(), "ws-scoutf-"));
|
||
const DB = join(ordner, "workspace.db");
|
||
const jetzt = new Date().toISOString();
|
||
|
||
let gemacht = 0, fehler = 0;
|
||
const ok = (b, text) => {
|
||
gemacht++;
|
||
if (b) console.log(` ok ${text}`);
|
||
else { fehler++; console.log(` FEHL ${text}`); }
|
||
};
|
||
|
||
let kind = null;
|
||
async function starten() {
|
||
kind = spawn(process.execPath, [join(HIER, "index.js")], {
|
||
env: { ...process.env, WORKSPACE_DB: DB, PORT: String(PORT),
|
||
SITE_ACCESS_SECRET: "lokaler-test",
|
||
SITE_PUBLIC_LAUNCH_AT: "2020-01-01T00:00:00+01:00" },
|
||
stdio: ["ignore", "pipe", "pipe"] });
|
||
let aus = "";
|
||
kind.stdout.on("data", (d) => { aus += d; });
|
||
kind.stderr.on("data", (d) => { aus += d; });
|
||
for (let i = 0; i < 80; i++) {
|
||
await new Promise((r) => setTimeout(r, 250));
|
||
if (kind.exitCode !== null) break;
|
||
try { const a = await fetch(BASIS + "/workspace/", { redirect: "manual" });
|
||
if (a.status < 500) return; } catch { /* noch nicht oben */ }
|
||
}
|
||
throw new Error("Server startete nicht:\n" + aus.slice(-1500));
|
||
}
|
||
await starten();
|
||
kind.kill();
|
||
await new Promise((r) => setTimeout(r, 600));
|
||
|
||
function anlegen(d, name, rolle, code) {
|
||
const salz = randomBytes(16).toString("hex");
|
||
const hash = scryptSync(code, salz, 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, salz, 32768, jetzt);
|
||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||
}
|
||
{
|
||
const d = new DatabaseSync(DB);
|
||
anlegen(d, "Filipe", "admin", "CODE-DOGI-0001");
|
||
anlegen(d, "Sam", "scout", "CODE-SAM-00001");
|
||
d.close();
|
||
}
|
||
await starten();
|
||
|
||
const anmelden = async (rolle, code) => {
|
||
const a = await fetch(BASIS + "/workspace/api/anmelden", {
|
||
method: "POST", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ rolle, code }) });
|
||
const keks = (a.headers.getSetCookie?.() || []).map((k) => k.split(";")[0]).join("; ");
|
||
if (!keks) throw new Error(`Anmeldung ${rolle} fehlgeschlagen (${a.status})`);
|
||
return keks;
|
||
};
|
||
const keksScout = await anmelden("scout", "CODE-SAM-00001");
|
||
|
||
const ruf = async (art, weg, keks, koerper) => {
|
||
const a = await fetch(BASIS + weg, { method: art,
|
||
headers: { Cookie: keks, "Content-Type": "application/json", Origin: BASIS },
|
||
body: koerper ? JSON.stringify(koerper) : undefined });
|
||
let daten = null; try { daten = await a.json(); } catch { /* leer ist erlaubt */ }
|
||
return { status: a.status, daten };
|
||
};
|
||
|
||
/* =======================================================================
|
||
1. Die sieben Felder kommen an -- und wieder heraus
|
||
======================================================================= */
|
||
console.log("=== Was ein Scout jetzt eintragen kann ===");
|
||
let leadId = null;
|
||
{
|
||
const { status, daten } = await ruf("POST", "/workspace/api/leads", keksScout, {
|
||
name: "Tamy", plattform: "TikTok", handle: "@coolemama1991",
|
||
land: "CH", netzwerk: "nein", follower: "12,4k",
|
||
woher: "LIVE entdeckt", kontaktweg: "TikTok-DM",
|
||
live_zeiten: "Mo–Fr ab 21 Uhr",
|
||
});
|
||
ok(status === 201, `ein Kontakt mit allen Feldern entsteht (HTTP ${status})`);
|
||
leadId = daten?.id;
|
||
}
|
||
{
|
||
const { daten } = await ruf("GET", "/workspace/api/leads", keksScout);
|
||
const l = (daten?.leads || []).find((x) => x.id === leadId);
|
||
ok(!!l, "und steht in der Liste");
|
||
ok(l?.land === "CH", `Land kommt zurueck (${l?.land})`);
|
||
ok(l?.netzwerk === "nein", `Netzwerk kommt zurueck (${l?.netzwerk})`);
|
||
/* DIE UMRECHNUNG IST DER PUNKT: Wer "12,4k" aus TikTok kopiert, soll
|
||
12400 gespeichert bekommen und nicht 12. */
|
||
ok(l?.follower === 12400, `"12,4k" wurde zu 12400 (${l?.follower})`);
|
||
ok(l?.woher === "LIVE entdeckt", `Wie gefunden (${l?.woher})`);
|
||
ok(l?.kontaktweg === "TikTok-DM", `Angeschrieben ueber (${l?.kontaktweg})`);
|
||
ok(l?.live_zeiten === "Mo–Fr ab 21 Uhr", `LIVE-Zeiten (${l?.live_zeiten})`);
|
||
/* Und die Auswahllisten kommen mit, damit die Oberflaeche keine
|
||
eigenen fuehrt. */
|
||
ok(Array.isArray(daten?.laender) && daten.laender.includes("CH") && daten.laender.includes("LU"),
|
||
`die Laenderliste kommt vom Server (${(daten?.laender || []).join(", ")})`);
|
||
ok(Array.isArray(daten?.netzwerk) && daten.netzwerk.length === 3,
|
||
`und die Netzwerk-Liste (${(daten?.netzwerk || []).join(", ")})`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
2. Aendern geht auch -- das war der eigentliche Mangel
|
||
======================================================================= */
|
||
console.log("\n=== Nachtraeglich aendern ===");
|
||
{
|
||
const { status } = await ruf("PATCH", `/workspace/api/leads/${leadId}`, keksScout, {
|
||
aktivitaet: "Vier Abende die Woche, Just Chatting",
|
||
potenzial: "Sehr ruhige Moderation, treue Zuschauer",
|
||
netzwerk: "ja",
|
||
});
|
||
ok(status === 200, `Aktivitaet und Potenzial lassen sich aendern (HTTP ${status})`);
|
||
const { daten } = await ruf("GET", "/workspace/api/leads", keksScout);
|
||
const l = (daten?.leads || []).find((x) => x.id === leadId);
|
||
ok(l?.aktivitaet?.startsWith("Vier Abende"), `und stehen danach da (${l?.aktivitaet?.slice(0, 20)}…)`);
|
||
ok(l?.netzwerk === "ja", "die Netzwerk-Angabe ebenfalls");
|
||
}
|
||
|
||
/* =======================================================================
|
||
3. Der Server nimmt keinen Unsinn an
|
||
======================================================================= */
|
||
console.log("\n=== Gegenproben: was NICHT angenommen wird ===");
|
||
for (const [feld, wert, was] of [
|
||
["land", "Schweiz", "ein Land ausserhalb der Liste"],
|
||
["netzwerk", "vielleicht", "eine erfundene Netzwerk-Angabe"],
|
||
["follower", "ganz viele", "eine Follower-Zahl, die keine ist"],
|
||
["follower", "-5", "eine negative Follower-Zahl"],
|
||
]) {
|
||
const { status } = await ruf("PATCH", `/workspace/api/leads/${leadId}`, keksScout, { [feld]: wert });
|
||
ok(status === 400, `${was} wird abgelehnt (HTTP ${status})`);
|
||
}
|
||
{
|
||
/* Und die Gegenprobe zur Gegenprobe: Das Richtige geht durch. Ohne
|
||
sie hiesse "alles wird abgelehnt" auch "alles wird abgelehnt". */
|
||
const { status } = await ruf("PATCH", `/workspace/api/leads/${leadId}`, keksScout, { land: "LU" });
|
||
ok(status === 200, `Luxemburg dagegen geht durch (HTTP ${status})`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
4. Getrennt von Team Dogi
|
||
======================================================================= */
|
||
console.log("\n=== Die Pipeline gibt es auf der crew-Adresse nicht ===");
|
||
{
|
||
/* DogFather sieht im Workspace alles -- das ist der Vergleichswert. */
|
||
const keksDogi = await anmelden("admin", "CODE-DOGI-0001");
|
||
const drin = await ruf("GET", "/workspace/api/leads", keksDogi);
|
||
ok(drin.status === 200 && (drin.daten?.leads || []).length >= 1,
|
||
`DogFather sieht die Pipeline im Workspace (${(drin.daten?.leads || []).length} Kontakte)`);
|
||
|
||
/* Dieselbe Sitzung, dieselbe Adresse -- nur der Host-Kopf sagt crew.
|
||
Genau so kommt die Anfrage auf der echten Adresse an.
|
||
|
||
MIT node:http UND NICHT MIT fetch: `Host` ist ein verbotener
|
||
Kopf, den fetch stillschweigend wegwirft. Der erste Anlauf dieser
|
||
Pruefung hat deshalb 200 gemeldet und sah wie ein Befund aus --
|
||
tatsaechlich war die Anfrage nie auf der crew-Adresse. Ein
|
||
Messgeraet, das die Bedingung gar nicht herstellen kann, misst
|
||
nichts. */
|
||
const { request } = await import("node:http");
|
||
const status = await new Promise((fertig, schief) => {
|
||
const r = request({ host: "127.0.0.1", port: PORT, path: "/workspace/api/leads",
|
||
headers: { Cookie: keksDogi, Host: "crew.dogfather-universe.com" } },
|
||
(antwort) => { antwort.resume(); fertig(antwort.statusCode); });
|
||
r.on("error", schief);
|
||
r.end();
|
||
});
|
||
ok(status === 404,
|
||
`auf crew. gibt es sie nicht -- auch fuer DogFather (HTTP ${status})`);
|
||
|
||
/* GEGENPROBE ZUM MESSGERAET: Derselbe Weg mit dem normalen Host muss
|
||
200 liefern. Sonst hiesse "404 auf crew" nur, dass node:http hier
|
||
grundsaetzlich scheitert. */
|
||
const statusNormal = await new Promise((fertig, schief) => {
|
||
const r = request({ host: "127.0.0.1", port: PORT, path: "/workspace/api/leads",
|
||
headers: { Cookie: keksDogi } },
|
||
(antwort) => { antwort.resume(); fertig(antwort.statusCode); });
|
||
r.on("error", schief);
|
||
r.end();
|
||
});
|
||
ok(statusNormal === 200,
|
||
`derselbe Weg ohne crew-Kopf liefert weiterhin die Liste (HTTP ${statusNormal})`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
5. Normal oder Premium -- die beiden Arten, die TikTok kennt
|
||
======================================================================= */
|
||
console.log("\n=== Normal und Premium ===");
|
||
let premiumId = null, uebergebenId = null;
|
||
{
|
||
/* Die Liste kommt vom Server, damit die Oberflaeche keine eigene
|
||
fuehrt -- und die REIHENFOLGE ist die der Spalten: links, rechts. */
|
||
const { daten } = await ruf("GET", "/workspace/api/leads", keksScout);
|
||
ok(Array.isArray(daten?.arten) && daten.arten.length === 2
|
||
&& daten.arten[0] === "normal" && daten.arten[1] === "premium",
|
||
`die Artenliste kommt vom Server, in der Reihenfolge der Spalten (${(daten?.arten || []).join(", ")})`);
|
||
ok(daten?.art_name?.premium === "Premium",
|
||
`und ihre Namen (${daten?.art_name?.premium})`);
|
||
|
||
/* DER LEAD VON OBEN HAT NIE EINE ART BEKOMMEN. Er muss trotzdem in
|
||
einer der beiden Spalten liegen, sonst waere er unsichtbar. */
|
||
const alt = (daten?.leads || []).find((x) => x.id === leadId);
|
||
ok(alt?.creator_art === "normal",
|
||
`ein Kontakt ohne Angabe gilt als "normal" (${alt?.creator_art})`);
|
||
}
|
||
{
|
||
const { status, daten } = await ruf("POST", "/workspace/api/leads", keksScout, {
|
||
name: "Nala", plattform: "TikTok", handle: "@nala",
|
||
creator_art: "premium", status: "angesprochen",
|
||
});
|
||
ok(status === 201, `ein Premium-Kontakt laesst sich anlegen (HTTP ${status})`);
|
||
premiumId = daten?.id;
|
||
const liste = await ruf("GET", "/workspace/api/leads", keksScout);
|
||
const l = (liste.daten?.leads || []).find((x) => x.id === premiumId);
|
||
ok(l?.creator_art === "premium", `und bleibt Premium (${l?.creator_art})`);
|
||
}
|
||
{
|
||
/* GEGENPROBEN. Eine erfundene Art wird NICHT stillschweigend zu
|
||
"normal" gemacht -- wer sich vertippt, soll es erfahren, statt die
|
||
Karte in der falschen Spalte zu suchen. */
|
||
const falsch = await ruf("PATCH", `/workspace/api/leads/${premiumId}`, keksScout,
|
||
{ creator_art: "vip" });
|
||
ok(falsch.status === 400, `eine erfundene Art wird abgelehnt (HTTP ${falsch.status})`);
|
||
|
||
/* Und die Gegenprobe zur Gegenprobe: Umstellen geht. Ohne sie hiesse
|
||
"wird abgelehnt" auch "wird immer abgelehnt". */
|
||
const zurueck = await ruf("PATCH", `/workspace/api/leads/${premiumId}`, keksScout,
|
||
{ creator_art: "normal" });
|
||
ok(zurueck.status === 200, `auf "normal" umstellen geht (HTTP ${zurueck.status})`);
|
||
const wieder = await ruf("PATCH", `/workspace/api/leads/${premiumId}`, keksScout,
|
||
{ creator_art: "premium" });
|
||
ok(wieder.status === 200, `und wieder zurueck auf "premium" (HTTP ${wieder.status})`);
|
||
const liste = await ruf("GET", "/workspace/api/leads", keksScout);
|
||
const l = (liste.daten?.leads || []).find((x) => x.id === premiumId);
|
||
ok(l?.creator_art === "premium", `der Wert steht danach richtig da (${l?.creator_art})`);
|
||
}
|
||
|
||
/* =======================================================================
|
||
6. Die Art ueberlebt die Uebergabe
|
||
======================================================================= */
|
||
console.log("\n=== Vom Lead zum Creator ===");
|
||
{
|
||
const keksDogi = await anmelden("admin", "CODE-DOGI-0001");
|
||
const neu = await ruf("POST", "/workspace/api/leads", keksScout, {
|
||
name: "Nova", plattform: "TikTok", handle: "@nova",
|
||
creator_art: "premium", status: "uebergeben",
|
||
});
|
||
uebergebenId = neu.daten?.id;
|
||
ok(neu.status === 201, `ein uebergebener Premium-Lead entsteht (HTTP ${neu.status})`);
|
||
|
||
const ob = await ruf("POST", `/workspace/api/leads/${uebergebenId}/onboarding`, keksDogi,
|
||
{ name: "Nova" });
|
||
ok(ob.status === 201, `das Onboarding legt den Creator an (HTTP ${ob.status})`);
|
||
const creatorId = ob.daten?.id;
|
||
|
||
const liste = await ruf("GET", "/workspace/api/verwaltung/personen", keksDogi);
|
||
const person = (liste.daten?.personen || []).find((x) => x.id === creatorId);
|
||
/* DAS IST DER PUNKT DIESES ABSCHNITTS: Ohne diese Zeile endet die
|
||
Angabe genau dort, wo sie zum ersten Mal vertraglich zaehlt. */
|
||
ok(person?.creator_art === "premium",
|
||
`und die Art geht mit auf den Creator (${person?.creator_art})`);
|
||
ok(Array.isArray(liste.daten?.arten) && liste.daten.arten.length === 2,
|
||
`die Personenliste bringt dieselbe Artenliste mit (${(liste.daten?.arten || []).join(", ")})`);
|
||
|
||
/* AENDERBAR, nicht nur gesetzt. Ein Wert, den man einmal setzt und nie
|
||
wieder, war am 11.09.2026 der eigentliche Mangel der Pipeline. */
|
||
const um = await ruf("PUT", `/workspace/api/verwaltung/personen/${creatorId}/creator-art`,
|
||
keksDogi, { creator_art: "normal" });
|
||
ok(um.status === 200, `die Art laesst sich am Creator aendern (HTTP ${um.status})`);
|
||
const danach = await ruf("GET", "/workspace/api/verwaltung/personen", keksDogi);
|
||
const p2 = (danach.daten?.personen || []).find((x) => x.id === creatorId);
|
||
ok(p2?.creator_art === "normal", `und steht danach auf normal (${p2?.creator_art})`);
|
||
|
||
/* Gegenproben: Unsinn und falsche Rolle. */
|
||
const unsinn = await ruf("PUT", `/workspace/api/verwaltung/personen/${creatorId}/creator-art`,
|
||
keksDogi, { creator_art: "deluxe" });
|
||
ok(unsinn.status === 400, `eine erfundene Art wird auch hier abgelehnt (HTTP ${unsinn.status})`);
|
||
|
||
const scoutPerson = (danach.daten?.personen || []).find((x) => x.rolle === "scout");
|
||
const falscheRolle = await ruf("PUT",
|
||
`/workspace/api/verwaltung/personen/${scoutPerson?.id}/creator-art`, keksDogi,
|
||
{ creator_art: "premium" });
|
||
ok(falscheRolle.status === 404,
|
||
`an einem Scout gibt es keine Creator-Art (HTTP ${falscheRolle.status})`);
|
||
|
||
/* Zurueck auf Premium -- die Oberflaechenpruefung unten erwartet es so. */
|
||
await ruf("PUT", `/workspace/api/verwaltung/personen/${creatorId}/creator-art`,
|
||
keksDogi, { creator_art: "premium" });
|
||
}
|
||
|
||
/* =======================================================================
|
||
7. Und die Oberflaeche zeigt es auch
|
||
======================================================================= */
|
||
console.log("\n=== Die Seite selbst ===");
|
||
{
|
||
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
||
const browser = await pw.chromium.launch();
|
||
const ctx = await browser.newContext({ viewport: { width: 1400, height: 1000 } });
|
||
const seite = await ctx.newPage();
|
||
const skriptfehler = [];
|
||
seite.on("pageerror", (e) => skriptfehler.push(String(e).slice(0, 140)));
|
||
await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
||
await seite.click('.rolle[data-rolle="scout"]');
|
||
await seite.fill("#code", "CODE-SAM-00001");
|
||
await seite.click("button[type=submit]");
|
||
await seite.waitForURL("**/start.html", { timeout: 25000 });
|
||
await seite.goto(BASIS + "/workspace/scouting.html", { waitUntil: "networkidle" });
|
||
await seite.waitForTimeout(1600);
|
||
|
||
ok(skriptfehler.length === 0, `kein Skriptfehler (${skriptfehler.join(" | ") || "keiner"})`);
|
||
|
||
/* Der Knopf heisst, was er tut. */
|
||
const knopfText = await seite.$eval(".knopf-still--bearbeiten", (e) => e.textContent.trim())
|
||
.catch(() => null);
|
||
ok(knopfText === "Bearbeiten", `der Knopf heisst "Bearbeiten" (${knopfText})`);
|
||
|
||
/* Die Stufe erklaert sich. */
|
||
const was = await seite.$eval(".stufe__was", (e) => e.textContent.trim()).catch(() => null);
|
||
ok(!!was && was.length > 10, `die Stufe sagt, was sie bedeutet ("${was}")`);
|
||
|
||
/* Die harten Zahlen stehen auf der Karte, ohne Klick. */
|
||
const fakten = await seite.$$eval(".kk__fakt", (ks) => ks.map((k) => k.textContent.trim()));
|
||
ok(fakten.some((f) => f.includes("Luxemburg")), `das Land steht auf der Karte (${fakten.join(", ")})`);
|
||
ok(fakten.some((f) => f.includes("Follower")), "die Reichweite ebenfalls");
|
||
|
||
/* Und die Warnung, wenn jemand schon vergeben ist. */
|
||
const warn = await seite.$eval('.marke-punkt[data-warn="ja"]', (e) => e.textContent.trim())
|
||
.catch(() => null);
|
||
ok(warn === "Schon im Netzwerk", `die Netzwerk-Warnung ist zu sehen (${warn})`);
|
||
|
||
/* ===== LINKS NORMAL, RECHTS PREMIUM =============================
|
||
|
||
Filipe: "ich will dass es da in der mitte eine trennung gibt."
|
||
|
||
Gemessen wird nicht, ob die Klassen da sind, sondern wo die Kaesten
|
||
WIRKLICH liegen -- links/rechts, auf gleicher Hoehe, mit einer
|
||
Trennlinie dazwischen. Eine Pruefung auf Klassennamen waere auch
|
||
dann gruen, wenn das CSS gar nicht geladen haette. */
|
||
const spaltenLesen = (wo, stufe) => wo.$$eval(
|
||
`.stufe[data-stufe="${stufe}"] .art-spalte`, (ks) => ks.map((k) => {
|
||
const r = k.getBoundingClientRect();
|
||
const stil = getComputedStyle(k);
|
||
return {
|
||
art: k.dataset.art,
|
||
titel: k.querySelector(".art-spalte__titel")?.textContent.trim() || null,
|
||
zahl: k.querySelector(".art-spalte__zahl")?.textContent.trim() || null,
|
||
/* Wie weit steht die Zahl von ihrem Wort entfernt? Siehe den
|
||
Fund vom 06.10.2026 in scouting.css: Eine fremde Regel aus
|
||
`aufgaben.css` hat sie ans andere Ende der Spalte gezogen,
|
||
und keine Pruefung wurde rot. */
|
||
abstand: (() => {
|
||
const t = k.querySelector(".art-spalte__titel");
|
||
const z = k.querySelector(".art-spalte__zahl");
|
||
if (!t || !z) return null;
|
||
return Math.round(z.getBoundingClientRect().left - t.getBoundingClientRect().right);
|
||
})(),
|
||
x: Math.round(r.x), y: Math.round(r.y), breite: Math.round(r.width),
|
||
randLinks: parseFloat(stil.borderLeftWidth) || 0,
|
||
namen: [...k.querySelectorAll(".kk__name")].map((n) => n.textContent.trim()),
|
||
leer: k.querySelector(".art-spalte__leer")?.textContent.trim() || null,
|
||
};
|
||
}));
|
||
|
||
const neuSpalten = await spaltenLesen(seite, "neu");
|
||
ok(neuSpalten.length === 2, `die Stufe "Neu" hat zwei Spalten (${neuSpalten.length})`);
|
||
ok(neuSpalten[0]?.art === "normal" && neuSpalten[0]?.titel === "Normal",
|
||
`links steht "Normal" (${neuSpalten[0]?.titel})`);
|
||
ok(neuSpalten[1]?.art === "premium" && neuSpalten[1]?.titel === "Premium",
|
||
`rechts steht "Premium" (${neuSpalten[1]?.titel})`);
|
||
/* LINKS IST WIRKLICH LINKS -- in Pixeln, nicht in der Reihenfolge im
|
||
Quelltext. `order` oder `direction` koennten sie drehen, ohne dass
|
||
am HTML etwas auffaellt. */
|
||
ok(neuSpalten.length === 2 && neuSpalten[1].x > neuSpalten[0].x + 100,
|
||
`und zwar rechts DANEBEN (x ${neuSpalten[0]?.x} -> ${neuSpalten[1]?.x})`);
|
||
ok(neuSpalten.length === 2 && Math.abs(neuSpalten[0].y - neuSpalten[1].y) <= 4,
|
||
`beide beginnen auf gleicher Hoehe (y ${neuSpalten[0]?.y} / ${neuSpalten[1]?.y})`);
|
||
ok((neuSpalten[1]?.randLinks || 0) >= 1,
|
||
`die Trennung in der Mitte ist eine echte Linie (${neuSpalten[1]?.randLinks} px)`);
|
||
/* DIE ZAHL GEHOERT NEBEN IHR WORT. Nicht Geschmack: Eine Zahl am
|
||
anderen Ende einer 580 px breiten Spalte gehoert optisch zu nichts
|
||
mehr. Gefunden wurde das nur im Bildschirmfoto -- diese Messung
|
||
holt es in die Zahlen. */
|
||
ok(neuSpalten.every((k) => k.abstand !== null && k.abstand >= 0 && k.abstand <= 20),
|
||
`die Zahl steht neben ihrem Wort (${neuSpalten.map((k) => k.abstand + " px").join(" / ")})`);
|
||
|
||
/* UND DIE KARTEN LIEGEN AUCH DORT, WO SIE HINGEHOEREN. */
|
||
ok(neuSpalten[0]?.namen.includes("Tamy"),
|
||
`der normale Kontakt steht links (${(neuSpalten[0]?.namen || []).join(", ") || "nichts"})`);
|
||
ok(neuSpalten[1]?.leer === "noch niemand",
|
||
`die leere Premium-Spalte bleibt stehen und sagt es (${neuSpalten[1]?.leer})`);
|
||
|
||
const angSpalten = await spaltenLesen(seite, "angesprochen");
|
||
ok(angSpalten[1]?.namen.includes("Nala"),
|
||
`der Premium-Kontakt steht rechts (${(angSpalten[1]?.namen || []).join(", ") || "nichts"})`);
|
||
ok(angSpalten[0]?.leer === "noch niemand",
|
||
`und links steht dort "noch niemand" (${angSpalten[0]?.leer})`);
|
||
|
||
/* Die Marke auf der Karte -- nur bei Premium, nirgends sonst. */
|
||
const premiumMarken = await seite.$$eval('.kk .marke-punkt[data-art="premium"]',
|
||
(ks) => ks.map((k) => k.closest(".kk")?.querySelector(".kk__name")?.textContent.trim()));
|
||
ok(premiumMarken.length === 2 && premiumMarken.includes("Nala") && premiumMarken.includes("Nova"),
|
||
`genau die zwei Premium-Karten tragen die Marke (${premiumMarken.join(", ")})`);
|
||
|
||
/* AUFKLAPPEN UND ZAEHLEN: Wie viele Felder stehen wirklich zur
|
||
Verfuegung? Das ist Filipes Frage ("was man da noch alles eintragen
|
||
kann") als Zahl. Vorher waren es fuenf. */
|
||
await seite.click(".knopf-still--bearbeiten");
|
||
await seite.waitForTimeout(700);
|
||
const felder = await seite.$$eval(".kk__detail input, .kk__detail select, .kk__detail textarea",
|
||
(ks) => ks.length);
|
||
ok(felder >= 13, `im Editor stehen ${felder} Felder (vorher 5, mit der Art 13)`);
|
||
|
||
/* Die Art ist im Editor dabei -- sonst waere sie beim Anlegen zu
|
||
setzen und danach nie wieder zu aendern. */
|
||
const artImEditor = await seite.$$eval(".kk__detail select",
|
||
(ks) => ks.map((k) => [...k.options].map((o) => o.value).join(",")));
|
||
ok(artImEditor.includes("normal,premium"),
|
||
`im Editor steht die Art zur Auswahl (${artImEditor.join(" | ")})`);
|
||
|
||
/* Und beim ANLEGEN -- das war der zweite Teil von Filipes Wunsch. */
|
||
await seite.click("#neu-auf");
|
||
await seite.waitForTimeout(300);
|
||
const neuArt = await seite.$$eval("#n-art option",
|
||
(os) => os.map((o) => `${o.value}:${o.textContent.trim()}`));
|
||
ok(neuArt.length === 2 && neuArt[0] === "normal:Normal" && neuArt[1] === "premium:Premium",
|
||
`im Formular "Neuer Kontakt" kann man zwischen beiden waehlen (${neuArt.join(", ")})`);
|
||
await seite.click("#neu-zu");
|
||
await seite.waitForTimeout(250);
|
||
|
||
/* ===== DIE OPTIK DER STUFE KOMMT WIRKLICH AN (06.10.2026) ========
|
||
|
||
module.css notiert selbst: „`.kk:hover` in scouting.css ... setzt
|
||
border-color -- und die Grundregel oben setzt `border: 0`, gleich
|
||
stark und spaeter im Ladeweg. Diese vier Regeln tun also vermutlich
|
||
nichts." Und: „NICHT MITREPARIERT, weil es kein Messgeraet dafuer
|
||
gibt ... Hover laesst sich nicht pruefen."
|
||
|
||
DOCH -- es laesst sich pruefen. Hier steht das Messgeraet.
|
||
|
||
ERST DEN ZEIGER WEGSETZEN. Beim ersten Anlauf meldete diese
|
||
Messung „die Kante ist 9px statt 6" und „unter dem Zeiger waechst
|
||
sie nicht (9 -> 9)" -- beides stimmte, und beides war kein Befund
|
||
am Programm: Der Klick auf „Bearbeiten" weiter oben hatte die Maus
|
||
auf der ersten Karte stehen lassen. Gemessen wurde also zweimal der
|
||
Zustand UNTER dem Zeiger. Eine Messung, die ihren Ausgangszustand
|
||
nicht herstellt, misst den Rest des Tests mit. */
|
||
await seite.mouse.move(2, 2);
|
||
await seite.waitForTimeout(300);
|
||
const optik = await seite.$$eval(".stufe", (gs) => gs.map((g) => {
|
||
const k = g.querySelector(".kk");
|
||
const s2 = k ? getComputedStyle(k) : null;
|
||
return {
|
||
stufe: g.dataset.stufe,
|
||
ton: getComputedStyle(g).getPropertyValue("--ton").trim(),
|
||
farbe: getComputedStyle(g).getPropertyValue("--stufe-farbe").trim(),
|
||
kante: s2 ? s2.getPropertyValue("--kante").trim() : null,
|
||
kantenbreite: s2 ? s2.backgroundSize.split(",")[0].trim() : null,
|
||
nr: g.querySelector(".stufe__nr")?.textContent.trim() || null,
|
||
};
|
||
}));
|
||
/* JEDE Stufe traegt IHREN Ton -- nicht den der Seite. Gezaehlt wird
|
||
in der Bedingung, nicht nur im Text: Ein `every()` auf einer leeren
|
||
Liste waere `true` und wuerde nichts beweisen. */
|
||
ok(optik.length >= 3 && optik.every((o) => o.ton && o.ton === o.farbe),
|
||
`jede der ${optik.length} Stufen traegt ihren eigenen Ton `
|
||
+ `(${optik.map((o) => o.stufe + "=" + o.ton).join(", ")})`);
|
||
/* Und die Toene sind verschieden -- sonst waere „jede traegt ihren
|
||
eigenen" auch dann wahr, wenn alle denselben haetten. */
|
||
ok(new Set(optik.map((o) => o.ton)).size === optik.length,
|
||
`und zwar jede eine andere (${new Set(optik.map((o) => o.ton)).size} Toene auf ${optik.length} Stufen)`);
|
||
/* `backgroundSize` liefert BEIDE Masse der ersten Ebene ("6px 100%").
|
||
Der erste Anlauf verglich mit "6px" und meldete rot, obwohl ueberall
|
||
6px standen -- ein Messfehler, der wie ein Befund aussah.
|
||
|
||
DER ZWEITE ANLAUF MELDETE DASSELBE, und die Ursache war eine
|
||
andere: Beim Schreiben ueber die Kommandozeile wurde aus der
|
||
Zeichenfolge fuer eine Wortgrenze ein echtes Steuerzeichen (0x08,
|
||
Backspace) mitten im Ausdruck. Ein Ausdruck mit einem Backspace
|
||
darin trifft natuerlich nie. Im Quelltext sah die Zeile voellig
|
||
richtig aus -- sichtbar wurde das Byte erst mit `cat -v`. Genau
|
||
davor warnt pruef-zeichen ("Serverdateien mit Steuerzeichen"), und
|
||
genau sie haette es gefunden.
|
||
|
||
Deshalb jetzt `startsWith` statt eines Ausdrucks: Daran gibt es
|
||
nichts, was beim Schreiben verstuemmelt werden koennte. */
|
||
ok(optik.length >= 3 && optik.every((o) => (o.kantenbreite || "").startsWith("6px")),
|
||
`die Farbkante der Karte ist 6px breit (${optik.map((o) => o.kantenbreite).join(", ")})`);
|
||
/* Die Etappennummer wird aus WEITER abgeleitet: 01 bis 05, und
|
||
„abgelehnt" bekommt keine. */
|
||
const nummern = Object.fromEntries(optik.map((o) => [o.stufe, o.nr]));
|
||
ok(nummern.neu === "01" && nummern.angesprochen === "02",
|
||
`die Stufen sind nummeriert (neu=${nummern.neu}, angesprochen=${nummern.angesprochen})`);
|
||
|
||
/* HOVER -- gemessen mit dem Zeiger, nicht geglaubt.
|
||
|
||
`mouse.move` und nicht `page.hover()`: Letzteres wartet auf
|
||
„bedienbar" und blieb beim ersten Anlauf haengen. Gemessen werden
|
||
soll hier die Farbe, nicht die Bedienbarkeit. */
|
||
const vorher = await seite.$eval(".kk", (k) => getComputedStyle(k).getPropertyValue("--kante").trim());
|
||
const mitte = await seite.$eval(".kk", (k) => {
|
||
const r = k.getBoundingClientRect();
|
||
return { x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) };
|
||
});
|
||
await seite.mouse.move(mitte.x, mitte.y);
|
||
await seite.waitForTimeout(350);
|
||
const nachher = await seite.$eval(".kk", (k) => getComputedStyle(k).getPropertyValue("--kante").trim());
|
||
ok(vorher === "6px" && nachher === "9px",
|
||
`unter dem Zeiger waechst die Kante (${vorher} -> ${nachher})`);
|
||
await seite.mouse.move(0, 0);
|
||
await seite.waitForTimeout(200);
|
||
|
||
/* Die Haarlinie in der Karte. Ein ungueltiger Farbwert laesst
|
||
`border-image` lautlos ausfallen -- „none" heisst, sie fehlt. */
|
||
const haar = await seite.$eval(".kk__info",
|
||
(k) => getComputedStyle(k).borderImageSource);
|
||
ok(haar.includes("gradient"),
|
||
`die Haarlinie zwischen Kopf und Zahlen ist da (${haar.slice(0, 28)}…)`);
|
||
|
||
/* ===== GEGENPROBE: AUF DEM HANDY LIEGEN SIE UNTEREINANDER ========
|
||
|
||
Ohne diese Messung wuesste niemand, ob die Trennung ueberhaupt
|
||
umschaltet -- eine Spaltenregel, die bei 390 px zwei Spalten zu je
|
||
170 px macht, waere unbedienbar und waere oben trotzdem gruen
|
||
gewesen. Gleichzeitig beweist sie, dass die erste Messung etwas
|
||
messen KANN: Dieselben Kaesten, dieselbe Abfrage, anderes Ergebnis.
|
||
|
||
MIT EIGENEM KONTEXT UND MIT FINGER (hasTouch), nicht mit
|
||
`setViewportSize` auf derselben Seite. `hasTouch` gehoert zum
|
||
KONTEXT und laesst sich danach nicht nachtragen -- ohne ihn meldet
|
||
der Browser `pointer: fine`, und keine einzige Regel aus
|
||
`@media (pointer: coarse)` greift. Eine Messung mit Mauszeiger auf
|
||
390 px vermisst eine Seite, die es auf keinem Telefon gibt, und
|
||
meldet dabei "in Ordnung". Genau das hat `pruef-fingermass` hier
|
||
beim ersten Anlauf gefunden -- die Hauswache, die es dafuer gibt. */
|
||
const handyKontext = await browser.newContext({
|
||
viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true,
|
||
});
|
||
await handyKontext.addCookies(await ctx.cookies());
|
||
const handy = await handyKontext.newPage();
|
||
await handy.goto(seite.url(), { waitUntil: "networkidle" });
|
||
await handy.waitForTimeout(900);
|
||
const schmal = await spaltenLesen(handy, "neu");
|
||
ok(schmal.length === 2, `auf dem Handy sind es immer noch zwei Spalten (${schmal.length})`);
|
||
ok(schmal.length === 2 && schmal[1].y > schmal[0].y + 40,
|
||
`aber untereinander statt nebeneinander (y ${schmal[0]?.y} -> ${schmal[1]?.y})`);
|
||
ok(schmal.length === 2 && Math.abs(schmal[0].x - schmal[1].x) <= 2,
|
||
`und beide gleich weit links (x ${schmal[0]?.x} / ${schmal[1]?.x})`);
|
||
ok((schmal[1]?.randLinks || 0) === 0,
|
||
`die senkrechte Trennlinie faellt dabei weg (${schmal[1]?.randLinks} px)`);
|
||
ok(schmal.every((k) => k.breite >= 300),
|
||
`keine Spalte wird gequetscht (${schmal.map((k) => k.breite).join(" / ")} px)`);
|
||
|
||
/* ===== UND AUF DEM SCHMALSTEN GERAET, DAS ES GIBT (320 px) =======
|
||
|
||
Die Karte hat links 26px Polster bekommen (davor 21), dazu eine
|
||
6px-Kante. Was auf 390px passt, muss auf 320px nicht passen -- und
|
||
ein Element, das ueber den Rand steht, erzeugt seitliches Scrollen
|
||
auf der ganzen Seite.
|
||
|
||
GEMESSEN STATT DEN HAUSLAUF GESTARTET: `pruef-handy` prueft 186
|
||
Dinge auf vielen Seiten und braucht 226 Sekunden. Die Frage hier
|
||
ist eine einzige und in vier Sekunden beantwortet. */
|
||
const engKontext = await browser.newContext({
|
||
viewport: { width: 320, height: 720 }, hasTouch: true, isMobile: true,
|
||
});
|
||
await engKontext.addCookies(await ctx.cookies());
|
||
const eng = await engKontext.newPage();
|
||
await eng.goto(seite.url(), { waitUntil: "networkidle" });
|
||
await eng.waitForTimeout(900);
|
||
const ueber = await eng.$$eval(".stufe *", (ks) => ks
|
||
.filter((k) => k.getBoundingClientRect().right > window.innerWidth + 1)
|
||
.slice(0, 4)
|
||
.map((k) => `${k.className || k.tagName} bis ${Math.round(k.getBoundingClientRect().right)}px`));
|
||
const gemessen = await eng.$$eval(".stufe *", (ks) => ks.length);
|
||
ok(gemessen > 20 && ueber.length === 0,
|
||
`auf 320px steht keines der ${gemessen} Teile ueber den Rand (${ueber.join(", ") || "keins"})`);
|
||
const quer = await eng.evaluate(() =>
|
||
document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
||
ok(quer <= 1, `und die Seite laesst sich nicht seitlich schieben (${quer} px)`);
|
||
|
||
await browser.close();
|
||
}
|
||
|
||
kind.kill();
|
||
await new Promise((r) => setTimeout(r, 400));
|
||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Zugabe */ }
|
||
|
||
console.log(`\n${gemacht} Pruefungen`);
|
||
console.log(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`);
|
||
process.exit(fehler ? 1 : 0);
|