pruef-zeitraum und pruef-highlights bildeten ihren Tagesstempel mit toISOString(). Das ist UTC. Zwischen 22:00 und Mitternacht deutscher Zeit ist das schon der naechste Tag -- die Pruefungen verglichen dann einen Eintrag von heute mit dem Datum von morgen und meldeten einen Fehler, wo keiner war. Der Zeitraum-Filter im Haus rechnet richtig; er benutzt tagLokal() aus helfer-zeit.mjs. Nur die Pruefungen darueber hatten ihre eigene, falsche Rechnung. Jetzt benutzen sie denselben Helfer wie der Code, den sie pruefen -- damit koennen die beiden gar nicht mehr auseinanderlaufen. Co-Authored-By: Claude Opus 5 <[email protected]>
269 lines
14 KiB
JavaScript
269 lines
14 KiB
JavaScript
/* =====================================================================
|
|
pruef-highlights.mjs — Drei Accounts nebeneinander, nach Zeit geteilt
|
|
|
|
Filipe, 20.09.2026: „die highlights sollen in 3 kategorien sein
|
|
nebeneinander, dogfather, hasidog und dogfather clips. und die liste
|
|
soll man auch zu und auf klappen können. die videos will ich auch
|
|
kategorisiert haben nach woche, monat, jahr und ganz oben aktuell
|
|
also heute. und wenn man was hinzufügt soll man auch den account
|
|
auswählen können."
|
|
|
|
---------------------------------------------------------------------
|
|
VIER FRAGEN, UND DIE DRITTE IST DIE, DIE MAN VERGISST
|
|
|
|
1. Stehen die drei Spalten da — in DIESER Reihenfolge?
|
|
2. Landet jedes Highlight in seiner Spalte?
|
|
3. Verschwindet beim Zeitfilter wirklich etwas — und kommt es
|
|
beim Zurückschalten wieder?
|
|
4. Kann man beim Anlegen einen Account wählen — und lehnt der
|
|
Server einen erfundenen ab?
|
|
|
|
Frage 3 braucht die Gegenprobe: Ein Filter, der nichts ausblendet,
|
|
besteht jede Prüfung, die nur zählt, ob danach noch etwas dasteht.
|
|
|
|
AUFRUF: node server/pruef-highlights.mjs
|
|
===================================================================== */
|
|
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { tagLokal } from "./helfer-zeit.mjs";
|
|
import { eigenerPort } from "./helfer-port.mjs";
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
|
|
const PORT = await eigenerPort(import.meta, "pruef-highlights");
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-highlight-"));
|
|
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 }); };
|
|
await import("./index.js");
|
|
notbremse(240_000, "pruef-highlights");
|
|
await new Promise((r) => setTimeout(r, 900));
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
|
|
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
|
|
|
|
let fehler = 0, geprueft = 0;
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
console.log("\n=== Highlights: drei Accounts, vier Zeiträume ===\n");
|
|
|
|
/* ---- Daten ---------------------------------------------------------
|
|
Die Zeitpunkte relativ zu HEUTE, nie fest: Ein festes Datum wird vom
|
|
Kalender eingeholt, und dann schlägt die Prüfung fehl, ohne dass
|
|
etwas kaputt ist (Hausregel 06.09.2026). */
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes } = await import("node:crypto");
|
|
const { KANAELE } = await import("./workspace.js");
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
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, new Date().toISOString());
|
|
|
|
ok(KANAELE.length === 3,
|
|
`der Server kennt drei Accounts (${KANAELE.map((k) => k.name).join(", ")})`);
|
|
|
|
const vorTagen = (n) => {
|
|
const x = new Date();
|
|
x.setHours(9, 0, 0, 0);
|
|
x.setDate(x.getDate() - n);
|
|
return x;
|
|
};
|
|
/* Eines von heute, eines von vor 40 Tagen (also sicher weder heute
|
|
noch diese Woche noch dieser Monat, aber dieses Jahr — sofern es
|
|
nicht Januar ist; deshalb wird unten NICHT auf "jahr" gebaut). */
|
|
const einf = (titel, kanal, wannTage) => d.prepare(
|
|
"INSERT INTO eintraege (bereich, art, titel, text, status, dringlichkeit,"
|
|
+ " datum, quelle_kanal, erstellt_von, erstellt)"
|
|
+ " VALUES (?,?,?,?,?,?,?,?,?,?)")
|
|
.run("highlight", "clip", titel, "", "offen", "mittel",
|
|
/* Das TAGESDATUM ueber den Hausschluessel, nicht ueber UTC:
|
|
`vorTagen` verankert zwar um neun Uhr und waere damit richtig,
|
|
aber das sieht man der Zeile nicht an -- pruef-struktur hat sie
|
|
als UTC-Rechnung gemeldet. Ein Fehlalarm ist schlimmer als keine
|
|
Pruefung. Der volle Zeitstempel eine Zeile tiefer darf bleiben:
|
|
Dort ist UTC richtig, weil er eine ZEIT ist und keinen Tag. */
|
|
tagLokal(-wannTage), kanal, 1,
|
|
vorTagen(wannTage).toISOString());
|
|
|
|
einf("Heute DogFather", "dogfather", 0);
|
|
einf("Heute HasiDog", "hasidog", 0);
|
|
einf("Heute Clips", "clips", 0);
|
|
einf("Alt DogFather", "dogfather", 40);
|
|
einf("Ohne Account dabei", null, 0);
|
|
d.close();
|
|
|
|
const an = await fetch(BASIS + "/workspace/api/anmelden", {
|
|
method: "POST", headers: { "Content-Type": "application/json", Origin: BASIS },
|
|
body: JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" }),
|
|
});
|
|
const keks = (an.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
|
|
ok(an.ok && !!keks, "angemeldet");
|
|
|
|
/* ---- 1. Der Server schickt die Kanäle mit ------------------------- */
|
|
const brett = await (await fetch(BASIS + "/workspace/api/bereich/highlight",
|
|
{ headers: { Cookie: keks } })).json();
|
|
ok(Array.isArray(brett.kanaele) && brett.kanaele.length === 3,
|
|
`die Brett-Auskunft bringt die Accounts mit (${brett.kanaele?.length})`);
|
|
ok(brett.kanaele?.[0]?.wert === "dogfather" && brett.kanaele?.[2]?.wert === "clips",
|
|
"und zwar in der Reihenfolge des Servers — nicht in der der Einträge");
|
|
ok(brett.kanaele?.every((k) => !!k.handle),
|
|
"mit Handle, damit die Spalte sagt, welcher Account gemeint ist");
|
|
|
|
/* GEGENPROBE: Wer nicht zum Team gehört, hat mit den Accounts nichts
|
|
zu tun und darf die Liste nicht bekommen. Ohne diese Probe bestünde
|
|
auch eine Fassung, die sie an jeden schickt. */
|
|
{
|
|
const dd = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const s2 = randomBytes(16).toString("hex");
|
|
const h2 = scryptSync("CODE-GAST-0002", s2, 64,
|
|
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
|
dd.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt)"
|
|
+ " VALUES (?,?,?,?,?,1,?)").run("Gast", "gast", h2, s2, 32768, new Date().toISOString());
|
|
dd.close();
|
|
const ag = await fetch(BASIS + "/workspace/api/anmelden", {
|
|
method: "POST", headers: { "Content-Type": "application/json", Origin: BASIS },
|
|
body: JSON.stringify({ rolle: "gast", code: "CODE-GAST-0002" }),
|
|
});
|
|
const gk = (ag.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
|
|
const gb = await fetch(BASIS + "/workspace/api/bereich/highlight", { headers: { Cookie: gk } });
|
|
const gj = gb.ok ? await gb.json() : null;
|
|
ok(!gj || (gj.kanaele || []).length === 0,
|
|
`Gegenprobe: ein Gast bekommt die Accountliste nicht (${(gj?.kanaele || []).length})`);
|
|
}
|
|
|
|
/* ---- 2. Ein erfundener Account wird abgelehnt --------------------- */
|
|
const anlegen = async (titel, kanal) => fetch(BASIS + "/workspace/api/bereich/highlight", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", Cookie: keks, Origin: BASIS },
|
|
body: JSON.stringify({ titel, art: "bild", text: "", quelle_kanal: kanal }),
|
|
});
|
|
const gut = await anlegen("Ein Bild für HasiDog", "hasidog");
|
|
ok(gut.ok, `ein Bild lässt sich mit gewähltem Account anlegen (${gut.status})`);
|
|
const boese = await anlegen("Erfundener Account", "netflix");
|
|
ok(boese.status === 400,
|
|
`Gegenprobe: ein erfundener Account wird abgelehnt (${boese.status}) — eine Regel, die nur im Formular steht, ist keine Regel`);
|
|
const leer = await anlegen("Noch offen", "");
|
|
ok(leer.ok, `„Noch offen" ist erlaubt — ein falscher Eintrag wäre schlechter als ein leerer (${leer.status})`);
|
|
|
|
/* ---- 3. Am Bildschirm -------------------------------------------- */
|
|
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await pw.chromium.launch();
|
|
try {
|
|
const ctx = await browser.newContext({ viewport: { width: 1400, height: 1000 } });
|
|
const s = await ctx.newPage();
|
|
await s.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
|
await s.click('.rolle[data-rolle="admin"]');
|
|
await s.fill("#code", "CODE-DOGI-0001");
|
|
await s.click("button[type=submit]");
|
|
await s.waitForURL("**/start.html", { timeout: 25000 });
|
|
await s.goto(BASIS + "/workspace/bereich.html?b=highlight", { waitUntil: "networkidle" });
|
|
await s.waitForTimeout(1600);
|
|
|
|
const lies = () => s.evaluate(() => {
|
|
const sp = [...document.querySelectorAll(".kanalspalte")];
|
|
return {
|
|
spalten: sp.map((x) => ({
|
|
kanal: x.dataset.kanal,
|
|
wort: x.querySelector(".kanalspalte__wort")?.firstChild?.textContent?.trim(),
|
|
handle: x.querySelector(".kanalspalte__handle")?.textContent?.trim() || null,
|
|
zahl: x.querySelector(".kanalspalte__zahl")?.textContent,
|
|
karten: x.querySelectorAll(".kanalspalte__inhalt .karte, .kanalspalte__inhalt > *").length,
|
|
titel: [...x.querySelectorAll(".kanalspalte__inhalt")]
|
|
.map((i) => i.textContent.replace(/\s+/g, " ").trim()).join(" "),
|
|
oben: Math.round(x.getBoundingClientRect().top),
|
|
links: Math.round(x.getBoundingClientRect().left),
|
|
})),
|
|
zeitknoepfe: [...document.querySelectorAll(".filter__zeit")]
|
|
.map((b) => ({ zeit: b.dataset.zeit || "", text: b.textContent.trim(),
|
|
aus: b.disabled, an: b.getAttribute("aria-pressed") === "true" })),
|
|
};
|
|
});
|
|
|
|
const a = await lies();
|
|
ok(a.spalten.length >= 3, `die Spalten stehen (${a.spalten.length})`);
|
|
ok(a.spalten[0]?.kanal === "dogfather" && a.spalten[1]?.kanal === "hasidog"
|
|
&& a.spalten[2]?.kanal === "clips",
|
|
`in der Reihenfolge DogFather, HasiDog, Clips (${a.spalten.map((x) => x.kanal).join(", ")})`);
|
|
/* NEBENEINANDER, nicht untereinander — bei 1400 px ist dafür Platz.
|
|
Ohne diese Messung bestünde auch eine Fassung, die drei Blöcke
|
|
stapelt, und genau das war die Bitte. */
|
|
ok(a.spalten[0].oben === a.spalten[1].oben && a.spalten[1].oben === a.spalten[2].oben,
|
|
`sie stehen NEBENeinander (alle bei y=${a.spalten[0].oben})`);
|
|
ok(a.spalten[0].links < a.spalten[1].links && a.spalten[1].links < a.spalten[2].links,
|
|
"und in der richtigen Richtung von links nach rechts");
|
|
/* NUR DIE ECHTEN ACCOUNTS. Die Spalte „Ohne Account" hat
|
|
berechtigterweise kein Handle -- der erste Anlauf verlangte es von
|
|
allen vieren und schlug an einer Stelle an, an der nichts kaputt
|
|
war. Genau die Sorte Fehlalarm, die einen Prüfsatz entwertet. */
|
|
const echte = a.spalten.filter((x) => x.kanal !== "ohne");
|
|
ok(echte.length === 3 && echte.every((x) => /^@/.test(x.handle || "")),
|
|
`jeder echte Account nennt sein Handle (${echte.map((x) => x.handle).join(" ")})`);
|
|
ok(a.spalten.find((x) => x.kanal === "ohne")?.handle === null,
|
|
'und die Spalte ohne Account erfindet sich keines');
|
|
|
|
/* Landet jedes Highlight in SEINER Spalte? */
|
|
ok(/Heute DogFather/.test(a.spalten[0].titel) && !/Heute HasiDog/.test(a.spalten[0].titel),
|
|
"ein DogFather-Clip steht in der DogFather-Spalte und nirgends sonst");
|
|
ok(/Ohne Account/.test(a.spalten.at(-1).titel) && a.spalten.at(-1).kanal === "ohne",
|
|
"was zu keinem Account gehört, bekommt eine eigene Spalte statt zu verschwinden");
|
|
|
|
/* ---- 4. Der Zeitfilter ----------------------------------------- */
|
|
ok(a.zeitknoepfe.length === 5,
|
|
`die Zeitleiste hat Aktuell, Woche, Monat, Jahr und Alles (${a.zeitknoepfe.length})`);
|
|
ok(a.zeitknoepfe[0]?.zeit === "heute" && /Aktuell/.test(a.zeitknoepfe[0]?.text || ""),
|
|
`„Aktuell" steht ganz oben (${a.zeitknoepfe[0]?.text})`);
|
|
ok(a.zeitknoepfe.every((b) => /\d/.test(b.text)),
|
|
"jeder Knopf trägt seine Zahl — sonst klickt man ins Leere und weiß nicht, ob es am Filter lag");
|
|
|
|
const vorher = a.spalten.reduce((n, x) => n + Number(x.zahl || 0), 0);
|
|
await s.click('.filter__zeit[data-zeit="heute"]');
|
|
await s.waitForTimeout(900);
|
|
const b = await lies();
|
|
const nachher = b.spalten.reduce((n, x) => n + Number(x.zahl || 0), 0);
|
|
ok(nachher < vorher,
|
|
`„Aktuell" blendet wirklich etwas aus (${vorher} → ${nachher})`);
|
|
ok(!/Alt DogFather/.test(b.spalten.map((x) => x.titel).join(" ")),
|
|
" der 40 Tage alte Eintrag ist weg");
|
|
ok(/Heute DogFather/.test(b.spalten[0].titel),
|
|
" der von heute steht noch da");
|
|
|
|
/* GEGENPROBE: Zurückschalten muss ihn zurückbringen. Ohne sie
|
|
bestünde auch eine Fassung, die den Eintrag dauerhaft verliert. */
|
|
await s.click('.filter__zeit[data-zeit="heute"]');
|
|
await s.waitForTimeout(900);
|
|
const c = await lies();
|
|
ok(c.spalten.reduce((n, x) => n + Number(x.zahl || 0), 0) === vorher,
|
|
`Gegenprobe: ein zweiter Klick hebt den Filter auf (${vorher} wieder da)`);
|
|
|
|
/* ---- 5. Zuklappen ---------------------------------------------- */
|
|
const zuVorher = await s.evaluate(() =>
|
|
document.querySelector(".kanalspalte__inhalt")?.hidden === true);
|
|
await s.click(".kanalspalte__kopf");
|
|
await s.waitForTimeout(500);
|
|
const zuNachher = await s.evaluate(() => {
|
|
const i = document.querySelector(".kanalspalte__inhalt");
|
|
return { zu: i?.hidden === true || i?.offsetHeight === 0,
|
|
sagt: document.querySelector(".kanalspalte__kopf")?.getAttribute("aria-expanded") };
|
|
});
|
|
ok(zuVorher === false && zuNachher.zu === true,
|
|
"eine Spalte lässt sich zuklappen");
|
|
ok(zuNachher.sagt === "false",
|
|
` und sagt es auch Vorleseprogrammen (aria-expanded=${zuNachher.sagt})`);
|
|
|
|
await ctx.close();
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
|
|
console.log(`\n${fehler ? fehler + " FEHLER" : "Alles in Ordnung"} — ${geprueft} Prüfungen\n`);
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
process.exit(fehler ? 1 : 0);
|