Zwei Fehler, nicht einer. Der sichtbare: Die Kachel stand nur am Anfangstag. Wer im Monatsraster auf den Mittwoch sah, sah nichts -- obwohl die Aktion von Montag bis Freitag lief. Der unsichtbare, und der ist der schlimmere: Der SERVER suchte Eintraege, deren ANFANG im sichtbaren Fenster liegt. Eine Aktion vom 28.09. bis zum 05.10. kam im Oktober deshalb ueberhaupt nicht an -- nicht "nur am ersten Tag markiert", sondern gar nicht da. Wer im Oktober plante, sah eine freie Woche, die belegt war. Jetzt entscheidet die UEBERSCHNEIDUNG, nicht der Anfang. Gebaut wurde es in nachTag() -- der einzigen Stelle, an der Eintraege auf Tage verteilt werden. Monat, Woche, Liste und Zeitstrahl holen sich alle dort; vier Ansichten einzeln nachzuruesten waeren vier Stellen, an denen die fuenfte vergessen wird. Das Ende wird abgeleitet, nicht gepflegt: aus event_ende ODER aus Uhrzeit plus Dauer. Ein Live von 22:00 ueber vier Stunden endet um 02:00 am naechsten Tag -- das stand bisher nirgends, obwohl die Zahlen da waren. Der Tagesdialog filterte selbst auf den Anfangstag. Im Raster war der Mittwoch markiert, tippte man ihn an, stand da "An diesem Tag steht nichts." Jetzt fragt er dieselbe Stelle wie das Raster. pruef-zeitraum.mjs: 16 Pruefungen, beide Fehler einzeln, mit drei Gegenproben (vor dem Anfang, nach dem Ende, Punkttermin). pruef-terminregel.mjs weiterhin 35/0. Co-Authored-By: Claude Opus 5 <[email protected]>
219 lines
9.9 KiB
JavaScript
219 lines
9.9 KiB
JavaScript
/* =====================================================================
|
||
pruef-zeitraum.mjs — Ein Termin von–bis belegt jeden Tag dazwischen
|
||
|
||
Filipe, 20.09.2026: „wenn ich termine eintrage mit von bis, will ich
|
||
dass die ganze zeit markiert ist und nicht nur der anfang."
|
||
|
||
---------------------------------------------------------------------
|
||
ZWEI FEHLER, NICHT EINER
|
||
|
||
Der sichtbare: Die Kachel stand nur am Anfangstag. Wer im
|
||
Monatsraster auf den Mittwoch sah, sah nichts — obwohl die Aktion
|
||
von Montag bis Freitag lief.
|
||
|
||
Der unsichtbare, und der ist der schlimmere: Der SERVER suchte
|
||
Einträge, deren ANFANG im sichtbaren Fenster liegt. Eine Aktion vom
|
||
28.09. bis zum 05.10. kam im Oktober deshalb überhaupt nicht an —
|
||
nicht „nur am ersten Tag markiert", sondern gar nicht da. Wer im
|
||
Oktober plante, sah eine freie Woche, die belegt war.
|
||
|
||
Deshalb wird hier BEIDES gemessen, und der zweite Fall zuerst.
|
||
|
||
AUFRUF: node server/pruef-zeitraum.mjs
|
||
===================================================================== */
|
||
|
||
import { mkdtempSync, rmSync } from "node:fs";
|
||
import { tmpdir } from "node:os";
|
||
import { join } from "node:path";
|
||
import { portMussFreiSein } from "./helfer-port.mjs";
|
||
import { notbremse } from "./helfer-notbremse.mjs";
|
||
|
||
const PORT = await portMussFreiSein(4437, "pruef-zeitraum");
|
||
const ordner = mkdtempSync(join(tmpdir(), "ws-zeitraum-"));
|
||
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(230_000, "pruef-zeitraum");
|
||
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=== Termine über mehrere Tage ===\n");
|
||
|
||
/* ---- Daten anlegen ------------------------------------------------
|
||
RELATIV ZU HEUTE, nie mit einem festen Datum: Ein fest eingetragener
|
||
Termin 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 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());
|
||
|
||
const tagPlus = (n) => {
|
||
const x = new Date();
|
||
x.setHours(12, 0, 0, 0);
|
||
x.setDate(x.getDate() + n);
|
||
return x.toISOString().slice(0, 10);
|
||
};
|
||
/* Eine Aktion über fünf Tage, die GESTERN angefangen hat — genau der
|
||
Fall, den der Server vorher verschluckt hat. */
|
||
const LAUF_START = tagPlus(-1), LAUF_ENDE = tagPlus(3);
|
||
/* Und ein Punkttermin als Gegenprobe. */
|
||
const EINZEL = tagPlus(5);
|
||
|
||
const spalten = new Set(d.prepare("PRAGMA table_info(eintraege)").all().map((z) => z.name));
|
||
ok(spalten.has("event_ende") && spalten.has("geplant"),
|
||
"die Tabelle kennt Anfang und Ende (event_ende, geplant)");
|
||
|
||
const einf = (titel, von, bis) => d.prepare(
|
||
"INSERT INTO eintraege (bereich, art, titel, text, status, dringlichkeit,"
|
||
+ " datum, geplant, event_ende, erstellt_von, erstellt)"
|
||
+ " VALUES (?,?,?,?,?,?,?,?,?,?,?)")
|
||
.run("live", "idee", titel, "", "offen", "mittel", von, von, bis, 1,
|
||
new Date().toISOString());
|
||
einf("Wochenaktion quer", LAUF_START, LAUF_ENDE);
|
||
einf("Nur ein Tag", EINZEL, null);
|
||
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: Überschneidung statt Anfang ------------------- */
|
||
const hole = async (von, tage) => {
|
||
const a = await fetch(`${BASIS}/workspace/api/termine?von=${von}&tage=${tage}`,
|
||
{ headers: { Cookie: keks } });
|
||
return a.ok ? a.json() : null;
|
||
};
|
||
const titel = (j) => (j?.brettsachen || []).map((b) => b.titel);
|
||
|
||
/* Ein Fenster, das MITTEN im Zeitraum liegt — der Anfang ist davor. */
|
||
const mitten = await hole(tagPlus(1), 2);
|
||
ok(titel(mitten).includes("Wochenaktion quer"),
|
||
"ein laufender Zeitraum kommt mit, auch wenn sein Anfang vor dem Fenster liegt");
|
||
console.log(` Fenster ${tagPlus(1)} + 2 Tage, Aktion läuft ${LAUF_START}–${LAUF_ENDE}`);
|
||
|
||
/* GEGENPROBE: Ein Fenster NACH dem Ende darf ihn nicht bringen. Ohne
|
||
diese Probe würde auch ein Server bestehen, der einfach alles
|
||
schickt — und dann stünde jeder je getippte Zettel im Kalender. */
|
||
const danach = await hole(tagPlus(10), 5);
|
||
ok(!titel(danach).includes("Wochenaktion quer"),
|
||
"Gegenprobe: nach dem Ende kommt er nicht mehr — es wird nicht einfach alles geschickt");
|
||
const davor = await hole(tagPlus(-30), 20);
|
||
ok(!titel(davor).includes("Wochenaktion quer"),
|
||
"Gegenprobe: und vor dem Anfang auch nicht");
|
||
|
||
/* Der Punkttermin muss sich genauso verhalten wie vorher. */
|
||
const beimEinzel = await hole(EINZEL, 1);
|
||
ok(titel(beimEinzel).includes("Nur ein Tag"),
|
||
"der Punkttermin kommt weiterhin an seinem Tag");
|
||
|
||
/* ---- 2. DER BILDSCHIRM: jeder Tag ist markiert -------------------- */
|
||
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/kalender.html", { waitUntil: "networkidle" });
|
||
await s.waitForTimeout(1800);
|
||
|
||
/* Wie viele Tageszellen tragen die Aktion? */
|
||
const lage = await s.evaluate(() => {
|
||
const zellen = [...document.querySelectorAll(".k-tag")];
|
||
const treffer = [];
|
||
for (const z of zellen) {
|
||
for (const p of z.querySelectorAll(".k-pille")) {
|
||
if (/Wochenaktion quer/.test(p.textContent)) {
|
||
treffer.push({
|
||
zahl: z.querySelector(".k-tag__zahl")?.textContent || "?",
|
||
spanne: p.dataset.spanne || null,
|
||
text: p.textContent.replace(/\s+/g, " ").trim(),
|
||
breit: Math.round(p.getBoundingClientRect().width),
|
||
});
|
||
}
|
||
}
|
||
}
|
||
const einzel = [...document.querySelectorAll(".k-pille")]
|
||
.filter((p) => /Nur ein Tag/.test(p.textContent));
|
||
return { zellen: zellen.length, treffer,
|
||
einzel: einzel.length, einzelSpanne: einzel[0]?.dataset.spanne ?? null };
|
||
});
|
||
|
||
ok(lage.zellen > 0, `das Monatsraster steht (${lage.zellen} Tageszellen)`);
|
||
ok(lage.treffer.length === 5,
|
||
`die Aktion ist an allen fünf Tagen markiert (${lage.treffer.length})`);
|
||
console.log(" " + lage.treffer.map((t) => `${t.zahl}.[${t.spanne}]`).join(" "));
|
||
|
||
const pos = lage.treffer.map((t) => t.spanne);
|
||
ok(pos[0] === "start" && pos.at(-1) === "ende",
|
||
`Anfang und Ende sind als solche erkennbar (${pos[0]} … ${pos.at(-1)})`);
|
||
ok(pos.slice(1, -1).every((p) => p === "mitte"),
|
||
"dazwischen steht überall 'mitte' — die Form sagt, dass es weitergeht");
|
||
|
||
/* Auf dem ersten Tag steht die Uhrzeit-Spalte, danach die Lage. Eine
|
||
Uhrzeit am Mittwoch zu wiederholen behauptet, es ginge am Mittwoch
|
||
wieder los. */
|
||
ok(/Tag 2\/5/.test(lage.treffer[1]?.text || ""),
|
||
`der zweite Tag sagt, der wievielte er ist ("${lage.treffer[1]?.text.slice(0, 34)}")`);
|
||
ok(!/Tag \d+\/\d+/.test(lage.treffer[0]?.text || ""),
|
||
"am ersten Tag steht das NICHT — dort gehört die Startzeit hin");
|
||
|
||
/* GEGENPROBE AM BILDSCHIRM: Der Punkttermin darf sich nicht
|
||
angesteckt haben. Ohne sie bestünde auch eine Fassung, die jede
|
||
Kachel über den halben Monat zieht. */
|
||
ok(lage.einzel === 1,
|
||
`Gegenprobe: der Punkttermin steht an genau einem Tag (${lage.einzel})`);
|
||
ok(lage.einzelSpanne === null,
|
||
"Gegenprobe: und trägt keine Spannen-Kennzeichnung");
|
||
|
||
/* ---- 3. Der Tagesdialog widerspricht dem Raster nicht ----------- */
|
||
/* Vorher filterte er selbst auf den Anfangstag: Im Raster war der
|
||
Mittwoch markiert, tippte man ihn an, stand da „An diesem Tag
|
||
steht nichts." */
|
||
const mitteTag = lage.treffer[2]?.zahl;
|
||
await s.evaluate((zahl) => {
|
||
for (const z of document.querySelectorAll(".k-tag")) {
|
||
if (z.querySelector(".k-tag__zahl")?.textContent === zahl) { z.click(); return; }
|
||
}
|
||
}, mitteTag);
|
||
await s.waitForTimeout(700);
|
||
const dialog = await s.evaluate(() => {
|
||
const dl = document.querySelector("#tagdialog[open]");
|
||
return dl ? dl.textContent.replace(/\s+/g, " ").trim() : null;
|
||
});
|
||
ok(!!dialog && /Wochenaktion quer/.test(dialog),
|
||
`der Tagesdialog am ${mitteTag}. zeigt die Aktion auch`);
|
||
ok(!/An diesem Tag steht nichts/.test(dialog || ""),
|
||
" und sagt nicht mehr 'An diesem Tag steht nichts'");
|
||
|
||
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);
|