Files
dogfather-universe/server/pruef-teilen.mjs
T
DogFatherGitandClaude Opus 5 aa8842e72e Zwei Pruefungen waren rot, weil Namen sich geaendert hatten
Beide meldeten seit Tagen einen Fehler, den es nicht gab. Eine rote
Pruefung, die rot BLEIBT, gewoehnt einem das Hinsehen ab -- ab da
uebersieht man auch die echte.

--- pruef-teilen: 14 ok / 3 Fehler -> 27 ok / 0 --------------------

Sie suchte `.teilen__brett`. Die Seite baut `.tl-brett`; die Klassen
wurden irgendwann umbenannt, und die Pruefung suchte seitdem nach
Elementen, die es nicht gibt. Gemeldet hat sie das sogar richtig --
"drei Ziele zur Auswahl (0)" --, nur hat niemand die Klammer gelesen.
Danach lief sie in einen Klick-Zeitablauf und brach ab, weswegen
DREIZEHN weitere Pruefungen nie stattfanden.

--- pruef-crew-wand-bild: 41 ok / 4 Fehler -> 45 ok / 0 ------------

Zwei feste Erwartungen aus einer Zeit, die vorbei ist:

1. `/Team Dogi/` im Reiter. Die App heisst seit dem Treff-Umzug
   "DogFather Universe". Statt den neuen Namen einzutragen -- und
   damit dieselbe Zeitbombe mit neuem Datum zu stellen -- kommt er
   jetzt aus `crew.webmanifest`. Benennt jemand die App um, folgt der
   Reiter, und beides bleibt gruen. Weichen sie auseinander, sieht der
   Mensch im Reiter einen anderen Namen als auf seinem
   Startbildschirm, und genau das soll die Pruefung finden.

2. `kacheln === 3`, seit Filipe am 10.09. sagte "3 rollen. dogfather.
   rechte hand und modis". Am 19.09. zog der Treff in Team Dogi ein
   und die Community kam als vierte dazu -- die Zahl war ab da falsch,
   die Wand richtig. Verglichen wird jetzt die MENGE der Rollen, nicht
   ihre Anzahl: Eine Zahl sagt beim Scheitern "3 erwartet, 4
   gefunden" und verschweigt, WELCHE.

Beide Male war die Seite richtig und die Pruefung veraltet -- das ist
dieselbe Krankheit wie bei den Portnummern und bei pruef-entwicklung:
ein Name oder eine Zahl, abgeschrieben an einer zweiten Stelle, die
niemand mitpflegt.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-20 22:55:41 +02:00

291 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
AUS „TEILEN" WIRD EIN TIPP (17.09.2026) — Schritt 4 des Video-Plans
Filipe postet ein Video auf dem Handy. Bisher: Link kopieren, App
öffnen, Bereich suchen, Feld finden, einfügen. Fünf Schritte, von
denen jeder der Grund sein kann, es „später" zu machen.
---------------------------------------------------------------------
DER FALL, DER DIESE PRÜFUNG RECHTFERTIGT
Das Teilen-Ziel kennt drei Felder: `title`, `text`, `url`. Man baut
es und probiert es mit `?url=https://…` aus — das geht, und man ist
fertig.
IM ECHTEN LEBEN KOMMT DER LINK ABER IN `text`. Android-Apps,
TikTok eingeschlossen, teilen als reinen Text: „Sieh dir das an!
https://vm.tiktok.com/xyz/" steht dann in `text`, und `url` ist
leer. Wer nur `url` liest, hat eine Seite gebaut, die im Test
funktioniert und auf dem Handy eine leere Maske zeigt.
Abschnitt 2 prüft genau diesen Fall — und zwar mit einem Satz
drumherum, nicht mit einer nackten Adresse.
Aufrufen mit: node server/pruef-teilen.mjs
===================================================================== */
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createServer } from "node:http";
import { eigenerPort } from "./helfer-port.mjs";
const PORT = await eigenerPort(import.meta, "pruef-teilen");
const TIKTOK = await eigenerPort(import.meta, "nachgebautes TikTok", 1);
const ordner = mkdtempSync(join(tmpdir(), "ws-teilen-"));
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";
process.env.TIKTOK_OEMBED_BASIS = `http://127.0.0.1:${TIKTOK}/oembed`;
const PNG = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
"base64");
const tiktok = createServer((q, a) => {
const u = new URL(q.url, `http://127.0.0.1:${TIKTOK}`);
if (u.pathname === "/oembed") {
const wer = ((u.searchParams.get("url") || "").match(/@([^/?]+)/) || [])[1] || "";
if (!wer) { a.writeHead(404); a.end("{}"); return; }
a.writeHead(200, { "content-type": "application/json" });
a.end(JSON.stringify({
version: "1.0", type: "video",
title: "Casper erwischt den Postboten",
author_url: `https://www.tiktok.com/@${wer}`,
thumbnail_url: `http://127.0.0.1:${TIKTOK}/cover.jpg`,
}));
return;
}
if (u.pathname === "/cover.jpg") {
a.writeHead(200, { "content-type": "image/jpeg" }); a.end(PNG); return;
}
a.writeHead(404); a.end("{}");
});
await new Promise((r) => tiktok.listen(TIKTOK, "127.0.0.1", r));
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(240_000, "pruef-teilen");
await new Promise((r) => setTimeout(r, 900));
const BASIS = `http://127.0.0.1:${PORT}`;
let fehler = 0, geprueft = 0;
const melde = (t) => console.log(t);
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes } = await import("node:crypto");
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const j = new Date().toISOString();
const salt = randomBytes(16).toString("hex");
const hash = scryptSync("CODE-DOGI-0001", salt, 64,
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d.prepare("INSERT INTO personen (name,rolle,code_hash,code_salt,code_n,aktiv,erstellt) VALUES (?,?,?,?,?,1,?)")
.run("Filipe", "admin", hash, salt, 32768, j);
d.close();
const an = await fetch(BASIS + "/workspace/api/anmelden", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" }),
});
const keks = (an.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await pw.chromium.launch();
/* AUF HANDYGRÖSSE. Diese Seite wird fast ausschliesslich dort
geoeffnet -- eine Messung auf 1400 px misst etwas, das niemand
sieht. */
const ctx = await browser.newContext({
viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true,
});
await ctx.addCookies(keks.split("; ").map((k) => {
const [name, ...rest] = k.split("=");
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
}));
const seite = await ctx.newPage();
const konsolenfehler = [];
seite.on("console", (m) => { if (m.type() === "error") konsolenfehler.push(m.text()); });
seite.on("pageerror", (f) => konsolenfehler.push(String(f)));
const VIDEO = `http://127.0.0.1:${TIKTOK}/@dogfather0804/video/4711`;
const oeffne = async (frage) => {
await seite.goto(`${BASIS}/workspace/teilen.html?${frage}`, { waitUntil: "networkidle" });
await seite.waitForTimeout(900);
};
const gezeigterLink = () => seite.$eval("#url-text", (n) => n.textContent.trim()).catch(() => "");
const zahlIm = async (bereich) => {
const a = await fetch(`${BASIS}/workspace/api/bereich/${bereich}`, { headers: { cookie: keks } });
const x = await a.json().catch(() => null);
return (x?.eintraege || []).length;
};
/* =======================================================================
1. DAS SAUBERE FELD
======================================================================= */
melde("");
melde("=== 1. Wenn der Link in „url“ steht ===");
{
await oeffne(`url=${encodeURIComponent(VIDEO)}`);
ok(await gezeigterLink() === VIDEO, "der Link wird erkannt und angezeigt");
ok(!(await seite.$("#haendisch:not([hidden])")),
"und das Feld zum Einfügen bleibt weg – es gibt ja nichts einzufügen");
/* DIE KLASSEN HEISSEN `tl-…`, NICHT `teilen__…` (berichtigt
20.09.2026). Sie wurden irgendwann umbenannt, und diese Pruefung
suchte seitdem nach Elementen, die es nicht gibt: Sie meldete
"drei Ziele zur Auswahl (0)" -- also rot mit der richtigen Zahl
in Klammern, und niemand hat die Klammer gelesen. Ein Selektor
ist ein Name, und Namen altern: Sie stehen in zwei Dateien und
werden nur in einer geaendert. */
const bretter = await seite.$$eval(".tl-brett", (ns) => ns.map((n) => ({
name: n.querySelector(".tl-brett-name")?.textContent,
gewaehlt: n.getAttribute("aria-pressed"),
})));
ok(bretter.length === 3, `drei Ziele zur Auswahl (${bretter.length})`);
ok(bretter[0]?.gewaehlt === "true" && /Highlights/.test(bretter[0]?.name || ""),
"Highlights ist vorausgewählt – dort gehören Videos hin");
await seite.screenshot({ path: "server/bild-teilen.png", fullPage: true });
}
/* =======================================================================
2. DER ECHTE FALL: ein Satz mit Link darin ← der eigentliche Punkt
======================================================================= */
melde("");
melde("=== 2. Wenn TikTok einen Satz teilt ===");
{
/* GENAU SO kommt es von einem Android-Handy an: alles in `text`,
`url` leer, und die Adresse steckt mitten im Satz. */
const satz = `Sieh dir das an! ${VIDEO} #fyp`;
await oeffne(`text=${encodeURIComponent(satz)}`);
ok(await gezeigterLink() === VIDEO,
`die Adresse wird aus dem Satz herausgeholt (${await gezeigterLink()})`);
/* UND MIT EINEM PUNKT DAHINTER. „…/4711." ist keine Adresse mit
Punkt, sondern ein Satzende. */
await oeffne(`text=${encodeURIComponent(`Schau mal: ${VIDEO}.`)}`);
ok(await gezeigterLink() === VIDEO,
`ein Satzzeichen am Ende gehört nicht zur Adresse (${await gezeigterLink()})`);
/* Und im Titel, falls eine App es dort ablegt. */
await oeffne(`title=${encodeURIComponent(VIDEO)}&text=`);
ok(await gezeigterLink() === VIDEO, "auch aus dem Titel");
}
/* =======================================================================
3. GAR KEIN LINK
======================================================================= */
melde("");
melde("=== 3. Wenn nichts dabei ist ===");
{
await oeffne("text=nur%20ein%20Text%20ohne%20Adresse");
ok(!!(await seite.$("#haendisch:not([hidden])")),
"dann erscheint ein Feld zum Einfügen – kein Fehler, ein Angebot");
ok(!(await seite.$("#fund:not([hidden])")),
"und es wird keine Adresse behauptet, die es nicht gibt");
}
/* =======================================================================
4. UND ES TRÄGT WIRKLICH EIN
======================================================================= */
melde("");
melde("=== 4. Der Tipp auf „Übernehmen“ ===");
{
const vorher = await zahlIm("highlight");
const satz = `Neues Video! ${VIDEO}`;
await oeffne(`text=${encodeURIComponent(satz)}`);
/* NICHT VON SELBST. Eine Seite, die beim Öffnen einträgt, kann nicht
wissen, ob der Griff in die Tasche gemeint war. */
ok(await zahlIm("highlight") === vorher,
`allein vom Öffnen passiert nichts (${vorher})`);
await seite.click("#uebernehmen");
await seite.waitForTimeout(2500);
const nachher = await zahlIm("highlight");
ok(nachher === vorher + 1, `nach dem Tipp liegt es da (${vorher} → ${nachher})`);
const fertig = await seite.$eval("#fertig-satz", (n) => n.textContent.trim()).catch(() => "");
ok(/Angekommen/.test(fertig), `und die Seite sagt es (\u201e${fertig.slice(0, 55)}\u201c)`);
ok(/Casper erwischt den Postboten/.test(fertig),
"mitsamt dem Titel, den TikTok geliefert hat");
/* DER WEG DORTHIN steht als Knopf da -- wer teilt, will meistens
gleich sehen, wie es aussieht. */
const hin = await seite.$eval("#hin", (n) => n.getAttribute("href")).catch(() => "");
ok(/b=highlight/.test(hin), `der Knopf führt zum richtigen Brett (${hin})`);
/* ---- Und derselbe Link ein zweites Mal ---------------------------- */
await oeffne(`text=${encodeURIComponent(satz)}`);
await seite.click("#uebernehmen");
await seite.waitForTimeout(2500);
ok(await zahlIm("highlight") === nachher,
`zweimal geteilt ergibt kein zweites Video (${await zahlIm("highlight")})`);
const zweiterSatz = await seite.$eval("#fertig-satz", (n) => n.textContent.trim()).catch(() => "");
ok(/lag schon/.test(zweiterSatz), `und es steht auch da (\u201e${zweiterSatz.slice(0, 45)}\u201c)`);
}
/* =======================================================================
5. AUF EIN ANDERES BRETT
======================================================================= */
melde("");
melde("=== 5. Wohin ist eine Frage ===");
{
const vorher = await zahlIm("treff");
await oeffne(`url=${encodeURIComponent(`http://127.0.0.1:${TIKTOK}/@hasidog00804/video/99`)}`);
await seite.click('.tl-brett[data-brett="treff"]');
await seite.waitForTimeout(300);
const gewaehlt = await seite.$$eval(".tl-brett",
(ns) => ns.filter((n) => n.getAttribute("aria-pressed") === "true").map((n) => n.dataset.brett));
ok(gewaehlt.length === 1 && gewaehlt[0] === "treff",
`genau ein Ziel ist gewählt (${gewaehlt.join(", ")})`);
await seite.click("#uebernehmen");
await seite.waitForTimeout(2500);
ok(await zahlIm("treff") === vorher + 1,
`und es landet dort und nicht bei den Highlights (${vorher} → ${await zahlIm("treff")})`);
}
/* =======================================================================
6. DAS TEILEN-ZIEL STEHT IM MANIFEST
======================================================================= */
melde("");
melde("=== 6. Damit das Handy es überhaupt anbietet ===");
{
/* OHNE DIESEN EINTRAG erscheint der Workspace gar nicht erst im
Teilen-Menü -- die ganze Seite wäre dann unerreichbar, und die
fünf Abschnitte darüber hätten es nicht gemerkt. */
for (const datei of ["crew.webmanifest", "app.webmanifest"]) {
const m = JSON.parse(readFileSync(new URL(`../workspace/${datei}`, import.meta.url), "utf8"));
const z = m.share_target;
ok(!!z && z.action === "/workspace/teilen.html",
`${datei}: Teilen-Ziel zeigt auf die Seite (${z?.action})`);
ok(z?.method === "GET",
`${datei}: per GET – POST bräuchte einen Service Worker dazwischen (${z?.method})`);
/* ALLE DREI FELDER. Fehlte `text`, wäre genau der Fall aus
Abschnitt 2 nicht abgedeckt -- und das ist der häufigste. */
ok(z?.params?.text === "text" && z?.params?.url === "url" && z?.params?.title === "title",
`${datei}: title, text UND url werden entgegengenommen`);
}
/* UND DIE SEITE IST IN DER RECHTETAFEL. Ohne Eintrag ist sie seit
dem 11.09. für jede Rolle zu -- ein fehlender Eintrag heißt nein. */
const { SEITEN } = await import("./rechte.js");
const rollen = SEITEN["/workspace/teilen.html"] || [];
ok(rollen.length > 0, `die Seite steht in der Rechtetafel (${rollen.join(", ")})`);
ok(!rollen.includes("gast"),
"und die Community kommt nicht hin – sie darf auch keine Videos eintragen");
}
melde("");
ok(konsolenfehler.length === 0,
konsolenfehler.length ? konsolenfehler.join(" | ") : "keine Fehler in der Browserkonsole");
melde("");
melde(`${geprueft} Prüfungen, ${fehler} Fehler`);
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
await browser.close();
await new Promise((r) => tiktok.close(r));
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
process.exit(fehler ? 1 : 0);