import { test, describe } from "node:test"; import assert from "node:assert/strict"; import { stripHtml, invertColor, getSomedaySlotCount, DEFAULT_SOMEDAY_SLOT_COUNT, splitTextAndUrls, } from "../../src/lib/weekly/format.ts"; import { isAllDayEvent } from "../../src/lib/weekly/date-utils.ts"; import { clampViewDays, MAX_VIEW_DAYS } from "../../src/lib/weekly/device-settings.ts"; import type { Task, CalendarEvent } from "../../src/lib/weekly/types.ts"; /** * Unit-Tests der Darstellungs-Helfer und der Spaltenbegrenzung. * Läuft ohne Browser und ohne Datenbank: `npm run test:unit`. */ // Minimale Aufgabe — nur die Felder, die die getestete Funktion liest. const task = (somedaySlotIndex: number | null): Task => ({ id: "t", title: "t", completed: false, updatedAt: new Date(), order: 0, userId: "u", somedaySlotIndex, }); describe("stripHtml", () => { test("entfernt Tags, behält den Text", () => { assert.equal(stripHtml("

Hallo Welt

"), "Hallo Welt"); }); test("schneidet umgebende Leerzeichen ab", () => { assert.equal(stripHtml("

Text

"), "Text"); }); test("lässt reinen Text unverändert", () => { assert.equal(stripHtml("Einkaufen gehen"), "Einkaufen gehen"); }); test("kommt mit leerer Eingabe zurecht", () => { assert.equal(stripHtml(""), ""); }); }); describe("invertColor", () => { test("invertiert Weiß zu Schwarz", () => { assert.equal(invertColor("#ffffff"), "#000000"); }); test("invertiert Schwarz zu Weiß", () => { assert.equal(invertColor("#000000"), "#ffffff"); }); test("Kurzform wird ausgeschrieben", () => { assert.equal(invertColor("#fff"), "#000000"); }); test("Rautezeichen ist optional", () => { assert.equal(invertColor("ffffff"), "#000000"); }); test("zweimal invertieren ergibt die Ausgangsfarbe", () => { assert.equal(invertColor(invertColor("#3b82f6")), "#3b82f6"); }); test("ungültige Eingaben werden unverändert zurückgegeben", () => { assert.equal(invertColor("keine-farbe"), "keine-farbe"); assert.equal(invertColor(""), ""); }); }); describe("getSomedaySlotCount", () => { test("leere Liste ergibt die Grundanzahl", () => { assert.equal(getSomedaySlotCount([]), DEFAULT_SOMEDAY_SLOT_COUNT); }); test("hält unter der Grundanzahl immer die Grundanzahl", () => { assert.equal(getSomedaySlotCount([task(0), task(1)]), DEFAULT_SOMEDAY_SLOT_COUNT); }); test("wächst mit, sobald der letzte Platz belegt ist", () => { // Anspruch laut Kommentar im Code: unten bleibt immer ein freier Platz. assert.equal(getSomedaySlotCount([task(5)]), 7); }); test("Aufgaben ohne Platznummer verändern nichts", () => { assert.equal(getSomedaySlotCount([task(null), task(null)]), DEFAULT_SOMEDAY_SLOT_COUNT); }); }); describe("isAllDayEvent", () => { // Diese Funktion rief parseCalendarDate auf, ohne es zu importieren — beim Auslagern // aus WeeklyView.tsx übersehen. Der Build meldete nichts, weil ignoreBuildErrors // aktiv war; zur Laufzeit hätte es einen ReferenceError gegeben. Dieser Test hätte // den Fehler sofort gefunden. const ev = (startTime: string, endTime: string): CalendarEvent => ({ id: "e", title: "e", startTime, endTime, source: "google" }); test("reines Datum ohne Uhrzeit gilt als ganztägig", () => { assert.equal(isAllDayEvent(ev("2026-09-07", "2026-09-08")), true); }); test("Termin mit Uhrzeit über eine Stunde ist nicht ganztägig", () => { assert.equal(isAllDayEvent(ev("2026-09-07T10:00:00", "2026-09-07T11:00:00")), false); }); test("Mitternacht bis Mitternacht gilt als ganztägig", () => { assert.equal(isAllDayEvent(ev("2026-09-07T00:00:00", "2026-09-08T00:00:00")), true); }); test("ohne Startzeit nicht ganztägig", () => { assert.equal(isAllDayEvent(ev("", "")), false); }); }); describe("clampViewDays", () => { test("gültige Werte bleiben unverändert", () => { for (const n of [1, 7, 14, 21, 31]) assert.equal(clampViewDays(n), n); }); test("begrenzt nach oben", () => { assert.equal(clampViewDays(999), MAX_VIEW_DAYS); }); test("begrenzt nach unten", () => { assert.equal(clampViewDays(0), 1); assert.equal(clampViewDays(-5), 1); }); test("rundet Kommazahlen", () => { assert.equal(clampViewDays(7.4), 7); assert.equal(clampViewDays(7.6), 8); }); test("fällt bei Unsinn auf 7 zurück", () => { assert.equal(clampViewDays(NaN), 7); assert.equal(clampViewDays(Infinity), 7); }); }); /** * URLs in Aufgabentiteln erkennen (damit sie als Link statt als reiner Text angezeigt * werden können, siehe LinkifiedText.tsx). Erwartetes Verhalten: eine getippte Adresse * im Titel wird als eigenes Segment erkannt, umgebender Text bleibt unverändert, und * Satzzeichen, die grammatisch zum Satz gehören, werden nicht mit in den Link gezogen. */ describe("splitTextAndUrls", () => { test("Titel ohne URL bleibt ein einzelnes Textsegment", () => { assert.deepEqual(splitTextAndUrls("Einkaufen gehen"), [ { type: "text", value: "Einkaufen gehen" }, ]); }); test("leerer Titel ergibt keine Segmente", () => { assert.deepEqual(splitTextAndUrls(""), []); }); test("Titel, der nur aus einer URL besteht", () => { assert.deepEqual(splitTextAndUrls("https://example.com"), [ { type: "url", value: "https://example.com" }, ]); }); test("URL mit Text davor und danach", () => { assert.deepEqual( splitTextAndUrls("Rechnung siehe https://example.com/invoice prüfen"), [ { type: "text", value: "Rechnung siehe " }, { type: "url", value: "https://example.com/invoice" }, { type: "text", value: " prüfen" }, ], ); }); test("www.-Adresse ohne Schema wird ebenfalls erkannt", () => { assert.deepEqual(splitTextAndUrls("siehe www.example.com"), [ { type: "text", value: "siehe " }, { type: "url", value: "www.example.com" }, ]); }); test("abschließender Satzpunkt gehört zum Text, nicht zur URL", () => { const segments = splitTextAndUrls("Link: https://example.com."); assert.deepEqual(segments, [ { type: "text", value: "Link: " }, { type: "url", value: "https://example.com" }, { type: "text", value: "." }, ]); }); test("URL in Klammern: die schließende Klammer bleibt Text", () => { assert.deepEqual(splitTextAndUrls("Details (https://example.com/x) nachlesen"), [ { type: "text", value: "Details (" }, { type: "url", value: "https://example.com/x" }, { type: "text", value: ") nachlesen" }, ]); }); test("zwei URLs im selben Titel", () => { assert.deepEqual( splitTextAndUrls("https://a.example.com und https://b.example.com"), [ { type: "url", value: "https://a.example.com" }, { type: "text", value: " und " }, { type: "url", value: "https://b.example.com" }, ], ); }); });