/* ===================================================================== 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); /* ===== 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)`); 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);