feat: 63 Unit-Tests fuer die ausgelagerten Funktionen
Nodes eingebauter Test-Runner mit --experimental-strip-types: keine neue Abhaengigkeit, keine Browser, keine Datenbank, Laufzeit rund 170 ms. npm test fuehrt sie aus, npm run test:e2e startet weiterhin Playwright. Die Tests haben sofort zwei echte Fehler gefunden, die ich beim Auslagern in v1.117.7 selbst eingebaut hatte: - isAllDayEvent rief parseCalendarDate ohne Import auf - resolveViewDays nutzte clampViewDays ohne Import Beides haette zur Laufzeit einen ReferenceError gegeben. Der Build meldete nichts, weil typescript.ignoreBuildErrors noch aktiv ist -- genau der Fall, gegen den C1 laeuft. isAllDayEvent liegt jetzt in date-utils.ts, wo es thematisch hingehoert; clampViewDays und MAX_VIEW_DAYS in device-settings.ts, DaysSelector re-exportiert sie. Geprueft werden ISO-Kalenderwochen gegen bekannte Kalenderdaten, die Wochenstart-Berechnung inklusive Sonntag-Sonderfall, das Zeitraster, die 12/24-Stunden-Anzeige und der Round-Trip formatDateToISO <-> parseCalendarDate -- die Bruecke zwischen Datenbank und Anzeige, aus der Ein-Tag-Verschiebungen entstehen. Mit npm run build verifiziert (Exit 0). v1.118.0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
cd59c02995
commit
5c5f2a284e
23
CHANGELOG.md
23
CHANGELOG.md
@ -2,6 +2,29 @@
|
|||||||
|
|
||||||
All notable changes to this project are documented in this file.
|
All notable changes to this project are documented in this file.
|
||||||
|
|
||||||
|
## [1.118.0] - 2026-09-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **63 Unit-Tests** in `tests/unit/` für die ausgelagerten reinen Funktionen. Sie laufen in
|
||||||
|
rund 170 ms über Nodes eingebauten Test-Runner — ohne Browser, ohne Datenbank, ohne neue
|
||||||
|
Abhängigkeit. `npm test` führt sie aus, `npm run test:e2e` startet weiterhin Playwright.
|
||||||
|
Geprüft werden ISO-Kalenderwochen, Wochenstart-Berechnung, das Zeitraster, die
|
||||||
|
12/24-Stunden-Anzeige, der Round-Trip zwischen `formatDateToISO` und `parseCalendarDate`
|
||||||
|
sowie Farbinvertierung und Spaltenbegrenzung.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- `isAllDayEvent` rief `parseCalendarDate` auf, ohne es zu importieren — beim Auslagern aus
|
||||||
|
`WeeklyView.tsx` in v1.117.7 übersehen. Der Build meldete nichts, weil
|
||||||
|
`typescript.ignoreBuildErrors` noch aktiv ist; zur Laufzeit hätte der erste ganztägige
|
||||||
|
Termin mit Uhrzeit einen `ReferenceError` ausgelöst. Die Funktion liegt jetzt in
|
||||||
|
`date-utils.ts`, wo sie thematisch hingehört.
|
||||||
|
- `resolveViewDays` nutzte `clampViewDays` ohne Import — derselbe Fehler beim selben Schnitt.
|
||||||
|
`clampViewDays` und `MAX_VIEW_DAYS` sind nach `device-settings.ts` gewandert; `DaysSelector`
|
||||||
|
re-exportiert sie, bestehende Importe bleiben gültig.
|
||||||
|
|
||||||
|
Beide Fehler sind genau die Sorte, die `typescript.ignoreBuildErrors` verdeckt — und beide
|
||||||
|
wären von den neuen Tests sofort gefunden worden.
|
||||||
|
|
||||||
## [1.117.7] - 2026-09-07
|
## [1.117.7] - 2026-09-07
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
8
TODO.md
8
TODO.md
@ -22,9 +22,13 @@ Kein Login-Bypass (der Vergleich schlägt immer fehl), aber unnötige Angriffsfl
|
|||||||
|
|
||||||
### A2. `npm test` reparieren (Audit 6)
|
### A2. `npm test` reparieren (Audit 6)
|
||||||
- [x] Entscheiden: Jest raus oder Jest einrichten
|
- [x] Entscheiden: Jest raus oder Jest einrichten
|
||||||
- [x] `package.json`: `"test"` auf das zeigen lassen, was wirklich existiert (Playwright)
|
- [x] `package.json`: `"test"` auf das zeigen lassen, was wirklich existiert
|
||||||
- [x] `"test:e2e"` für Playwright ergänzen
|
- [x] `"test:e2e"` für Playwright ergänzen
|
||||||
- [x] Debug-Skripte in `tests/` aussortieren (siehe unten)
|
- [x] Debug-Skripte in `tests/` aussortieren
|
||||||
|
- [x] **63 Unit-Tests** für die ausgelagerten reinen Funktionen (`tests/unit/`), laufen in
|
||||||
|
~170 ms ohne Browser und ohne Datenbank über Nodes eingebauten Test-Runner
|
||||||
|
- [ ] E2E-Tests mit echtem Login schreiben (braucht laufende App + Datenbank)
|
||||||
|
- [ ] Testlauf in den nächtlichen Qualitätslauf aufnehmen (`scripts/quality-check.sh`)
|
||||||
|
|
||||||
**Warum:** `"test": "jest"` — aber es gibt **keine Jest-Config und keinen einzigen Jest-Test**.
|
**Warum:** `"test": "jest"` — aber es gibt **keine Jest-Config und keinen einzigen Jest-Test**.
|
||||||
Die 15 Dateien in `tests/` sind Playwright-Specs, und die Namen verraten den Charakter:
|
Die 15 Dateien in `tests/` sind Playwright-Specs, und die Namen verraten den Charakter:
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "my-weekly-todo-list",
|
"name": "my-weekly-todo-list",
|
||||||
"version": "1.117.7",
|
"version": "1.118.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "my-weekly-todo-list",
|
"name": "my-weekly-todo-list",
|
||||||
"version": "1.117.7",
|
"version": "1.118.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/prisma-adapter": "^2.11.1",
|
"@auth/prisma-adapter": "^2.11.1",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "my-weekly-todo-list",
|
"name": "my-weekly-todo-list",
|
||||||
"version": "1.117.7",
|
"version": "1.118.0",
|
||||||
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -9,8 +9,10 @@
|
|||||||
"start": "echo \"\\n ✦ My Weekly ToDo List v$(node -p \"require('./package.json').version\") — production\\n\" && next start",
|
"start": "echo \"\\n ✦ My Weekly ToDo List v$(node -p \"require('./package.json').version\") — production\\n\" && next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
"test": "playwright test",
|
"test": "npm run test:unit",
|
||||||
"test:ui": "playwright test --ui",
|
"test:unit": "node --experimental-strip-types --test tests/unit/*.test.ts",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui",
|
||||||
"test:debug": "playwright test tests/debug",
|
"test:debug": "playwright test tests/debug",
|
||||||
"sonar": "todo-sonar-scan"
|
"sonar": "todo-sonar-scan"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import { defineConfig, devices } from '@playwright/test';
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: './tests',
|
testDir: './tests',
|
||||||
/* tests/debug enthält Wegwerf-Skripte aus der Fehlersuche, keine Regressionstests */
|
/* tests/debug enthält Wegwerf-Skripte aus der Fehlersuche, keine Regressionstests */
|
||||||
testIgnore: '**/debug/**',
|
testIgnore: ['**/debug/**', '**/unit/**'],
|
||||||
/* Run tests in files in parallel */
|
/* Run tests in files in parallel */
|
||||||
fullyParallel: true,
|
fullyParallel: true,
|
||||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { MAX_VIEW_DAYS, clampViewDays } from "@/lib/weekly/device-settings";
|
||||||
|
// Re-Export: WeeklyView importiert clampViewDays historisch von hier
|
||||||
|
export { MAX_VIEW_DAYS, clampViewDays };
|
||||||
|
|
||||||
export const DAY_PRESETS = [1, 2, 3, 5, 7];
|
export const DAY_PRESETS = [1, 2, 3, 5, 7];
|
||||||
export const MAX_VIEW_DAYS = 31;
|
|
||||||
|
|
||||||
export function clampViewDays(n: number): number {
|
|
||||||
if (!Number.isFinite(n)) return 7;
|
|
||||||
return Math.min(MAX_VIEW_DAYS, Math.max(1, Math.round(n)));
|
|
||||||
}
|
|
||||||
|
|
||||||
type Variant = "panel" | "header" | "menu";
|
type Variant = "panel" | "header" | "menu";
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { CellDuration } from "@/lib/weekly/types";
|
import type { CellDuration, CalendarEvent } from "@/lib/weekly/types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Datums-, Wochen- und Zeitraster-Helfer der Wochenansicht.
|
* Datums-, Wochen- und Zeitraster-Helfer der Wochenansicht.
|
||||||
@ -199,3 +199,24 @@ export function getSelectedDay(days: Date[], explicitSelection?: Date | null): D
|
|||||||
d.getFullYear() === today.getFullYear()
|
d.getFullYear() === today.getFullYear()
|
||||||
) ? today : days[0];
|
) ? today : days[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isAllDayEvent = (event: CalendarEvent): boolean => {
|
||||||
|
if (!event.startTime) return false;
|
||||||
|
|
||||||
|
// Date-only format (YYYY-MM-DD)
|
||||||
|
if (!event.startTime.includes("T")) return true;
|
||||||
|
|
||||||
|
const start = parseCalendarDate(event.startTime);
|
||||||
|
const end = parseCalendarDate(event.endTime);
|
||||||
|
const durationHours = (end.getTime() - start.getTime()) / (1000 * 60 * 60);
|
||||||
|
|
||||||
|
// Check if strictly midnight to midnight in local time
|
||||||
|
const isLocalMidnight = start.getHours() === 0 && start.getMinutes() === 0;
|
||||||
|
|
||||||
|
// Check if UTC midnight (common for API-converted date strings)
|
||||||
|
const isUTCMidnight =
|
||||||
|
start.getUTCHours() === 0 && start.getUTCMinutes() === 0;
|
||||||
|
|
||||||
|
// If it's effectively 24h+ and starts at midnight (local or UTC), treat as all-day
|
||||||
|
return durationHours >= 24 && (isLocalMidnight || isUTCMidnight);
|
||||||
|
};
|
||||||
|
|||||||
@ -6,6 +6,16 @@
|
|||||||
* Lagen bis v1.117.7 in WeeklyView.tsx.
|
* Lagen bis v1.117.7 in WeeklyView.tsx.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/** Obergrenze für die Spaltenzahl der Wochenansicht. */
|
||||||
|
export const MAX_VIEW_DAYS = 31;
|
||||||
|
|
||||||
|
/** Hält eine gewünschte Spaltenzahl im gültigen Bereich 1…31. */
|
||||||
|
export function clampViewDays(n: number): number {
|
||||||
|
if (!Number.isFinite(n)) return 7;
|
||||||
|
return Math.min(MAX_VIEW_DAYS, Math.max(1, Math.round(n)));
|
||||||
|
}
|
||||||
|
|
||||||
// Cookie helpers for per-device settings
|
// Cookie helpers for per-device settings
|
||||||
// Keys here bypass the DB and save/load from cookie only — each device keeps its own value
|
// Keys here bypass the DB and save/load from cookie only — each device keeps its own value
|
||||||
export const DEVICE_SETTINGS_KEYS = ["viewDays", "cellDuration", "startHour", "endHour", "fontSize", "showSubHourSlots"];
|
export const DEVICE_SETTINGS_KEYS = ["viewDays", "cellDuration", "startHour", "endHour", "fontSize", "showSubHourSlots"];
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { Task, CalendarEvent } from "@/lib/weekly/types";
|
import type { Task } from "@/lib/weekly/types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Darstellungs-Helfer der Wochenansicht: Text, Farben, Ganztags-Erkennung, Slot-Anzahl.
|
* Darstellungs-Helfer der Wochenansicht: Text, Farben, Ganztags-Erkennung, Slot-Anzahl.
|
||||||
@ -22,27 +22,6 @@ export const getSomedaySlotCount = (tasks: Task[]) => {
|
|||||||
return Math.max(DEFAULT_SOMEDAY_SLOT_COUNT, maxIdx + 2);
|
return Math.max(DEFAULT_SOMEDAY_SLOT_COUNT, maxIdx + 2);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isAllDayEvent = (event: CalendarEvent): boolean => {
|
|
||||||
if (!event.startTime) return false;
|
|
||||||
|
|
||||||
// Date-only format (YYYY-MM-DD)
|
|
||||||
if (!event.startTime.includes("T")) return true;
|
|
||||||
|
|
||||||
const start = parseCalendarDate(event.startTime);
|
|
||||||
const end = parseCalendarDate(event.endTime);
|
|
||||||
const durationHours = (end.getTime() - start.getTime()) / (1000 * 60 * 60);
|
|
||||||
|
|
||||||
// Check if strictly midnight to midnight in local time
|
|
||||||
const isLocalMidnight = start.getHours() === 0 && start.getMinutes() === 0;
|
|
||||||
|
|
||||||
// Check if UTC midnight (common for API-converted date strings)
|
|
||||||
const isUTCMidnight =
|
|
||||||
start.getUTCHours() === 0 && start.getUTCMinutes() === 0;
|
|
||||||
|
|
||||||
// If it's effectively 24h+ and starts at midnight (local or UTC), treat as all-day
|
|
||||||
return durationHours >= 24 && (isLocalMidnight || isUTCMidnight);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function invertColor(hex: string): string {
|
export function invertColor(hex: string): string {
|
||||||
if (!hex) return hex;
|
if (!hex) return hex;
|
||||||
let color = hex.startsWith("#") ? hex.slice(1) : hex;
|
let color = hex.startsWith("#") ? hex.slice(1) : hex;
|
||||||
|
|||||||
230
tests/unit/date-utils.test.ts
Normal file
230
tests/unit/date-utils.test.ts
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
import { test, describe } from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import {
|
||||||
|
getStartOfWeek, getDayName, isSameDay, formatDateToISO, parseCalendarDate,
|
||||||
|
formatHour, getTimeSlots, getHourFromSlot, getWeekNumber, getCWReferenceDate,
|
||||||
|
getSelectedDay,
|
||||||
|
} from "../../src/lib/weekly/date-utils.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit-Tests der Datums- und Zeitraster-Helfer.
|
||||||
|
*
|
||||||
|
* Läuft ohne Browser, ohne Datenbank, ohne laufende App: `npm run test:unit`.
|
||||||
|
*
|
||||||
|
* Die Erwartungen sind bewusst gegen unabhängig überprüfbare Fakten formuliert
|
||||||
|
* (Kalenderdaten, ISO-8601-Regeln) und nicht gegen die Implementierung — ein Test,
|
||||||
|
* der nur nachspricht, was der Code tut, findet keine Fehler.
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe("getWeekNumber (ISO 8601)", () => {
|
||||||
|
// Die Wochenzahl wird prominent in der Kopfzeile angezeigt ("KW 34 | August 2026"),
|
||||||
|
// ein Fehler hier fällt sofort jedem Nutzer auf.
|
||||||
|
test("Donnerstag 01.01.2026 liegt in KW 1", () => {
|
||||||
|
assert.equal(getWeekNumber(new Date(2026, 0, 1)), 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Samstag 22.08.2026 liegt in KW 34", () => {
|
||||||
|
assert.equal(getWeekNumber(new Date(2026, 7, 22)), 34);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Mittwoch 02.09.2026 liegt in KW 36", () => {
|
||||||
|
assert.equal(getWeekNumber(new Date(2026, 8, 2)), 36);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Montag 01.01.2024 liegt in KW 1", () => {
|
||||||
|
assert.equal(getWeekNumber(new Date(2024, 0, 1)), 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Sonntag 01.01.2023 gehört noch zu KW 52 des Vorjahres", () => {
|
||||||
|
// ISO 8601: Die Woche gehört dem Jahr, in dem ihr Donnerstag liegt.
|
||||||
|
assert.equal(getWeekNumber(new Date(2023, 0, 1)), 52);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Freitag 01.01.2021 gehört zu KW 53 des Vorjahres", () => {
|
||||||
|
assert.equal(getWeekNumber(new Date(2021, 0, 1)), 53);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getStartOfWeek", () => {
|
||||||
|
test("Wochenstart Montag: Donnerstag 03.09.2026 → Montag 31.08.2026", () => {
|
||||||
|
const r = getStartOfWeek(new Date(2026, 8, 3), 1);
|
||||||
|
assert.equal(formatDateToISO(r), "2026-08-31");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Wochenstart Montag: Sonntag gehört zur ablaufenden Woche", () => {
|
||||||
|
// Der klassische Stolperstein — Sonntag ist Tag 0, liegt aber am Wochenende.
|
||||||
|
const r = getStartOfWeek(new Date(2026, 8, 6), 1);
|
||||||
|
assert.equal(formatDateToISO(r), "2026-08-31");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Wochenstart Sonntag: Sonntag ist bereits der Wochenstart", () => {
|
||||||
|
const r = getStartOfWeek(new Date(2026, 8, 6), 0);
|
||||||
|
assert.equal(formatDateToISO(r), "2026-09-06");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Ein Tag, der bereits der Wochenstart ist, bleibt unverändert", () => {
|
||||||
|
const r = getStartOfWeek(new Date(2026, 7, 31), 1);
|
||||||
|
assert.equal(formatDateToISO(r), "2026-08-31");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("verändert das übergebene Datum nicht", () => {
|
||||||
|
const input = new Date(2026, 8, 3);
|
||||||
|
getStartOfWeek(input, 1);
|
||||||
|
assert.equal(formatDateToISO(input), "2026-09-03");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("formatDateToISO", () => {
|
||||||
|
test("formatiert mit führenden Nullen", () => {
|
||||||
|
assert.equal(formatDateToISO(new Date(2026, 0, 5)), "2026-01-05");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("nutzt die lokale Zeit, nicht UTC", () => {
|
||||||
|
// Kern der Sache: toISOString() würde bei einem späten Abendtermin in einer
|
||||||
|
// Zeitzone östlich von UTC auf den Folgetag rutschen. Genau daraus entstehen
|
||||||
|
// Termine, die im Kalender einen Tag daneben landen.
|
||||||
|
const spaeterAbend = new Date(2026, 8, 7, 23, 30, 0);
|
||||||
|
assert.equal(formatDateToISO(spaeterAbend), "2026-09-07");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("früher Morgen bleibt am selben Tag", () => {
|
||||||
|
assert.equal(formatDateToISO(new Date(2026, 8, 7, 0, 30, 0)), "2026-09-07");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parseCalendarDate", () => {
|
||||||
|
test("reines Datum wird als lokale Mitternacht gelesen", () => {
|
||||||
|
const d = parseCalendarDate("2026-09-07");
|
||||||
|
assert.equal(d.getFullYear(), 2026);
|
||||||
|
assert.equal(d.getMonth(), 8);
|
||||||
|
assert.equal(d.getDate(), 7);
|
||||||
|
assert.equal(d.getHours(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Round-Trip mit formatDateToISO ist verlustfrei", () => {
|
||||||
|
// Diese beiden Funktionen sind die Brücke zwischen Datenbank und Anzeige.
|
||||||
|
// Wenn sie auseinanderlaufen, verschieben sich Aufgaben um einen Tag.
|
||||||
|
for (const s of ["2026-01-01", "2026-06-15", "2026-12-31", "2026-02-28"]) {
|
||||||
|
assert.equal(formatDateToISO(parseCalendarDate(s)), s);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getTimeSlots", () => {
|
||||||
|
test("30-Minuten-Raster von 8 bis 10 Uhr", () => {
|
||||||
|
assert.deepEqual(getTimeSlots(30, 8, 10), ["08:00", "08:30", "09:00", "09:30"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("15-Minuten-Raster ergibt vier Einträge pro Stunde", () => {
|
||||||
|
assert.deepEqual(getTimeSlots(15, 9, 10), ["09:00", "09:15", "09:30", "09:45"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("volle Stunden über den ganzen Tag ergeben 24 Einträge", () => {
|
||||||
|
const slots = getTimeSlots(60, 0, 24);
|
||||||
|
assert.equal(slots.length, 24);
|
||||||
|
assert.equal(slots[0], "00:00");
|
||||||
|
assert.equal(slots[23], "23:00");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Endstunde ist exklusiv", () => {
|
||||||
|
assert.equal(getTimeSlots(60, 8, 9).length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("leeres Ergebnis, wenn Ende nicht nach Start liegt", () => {
|
||||||
|
assert.deepEqual(getTimeSlots(30, 10, 10), []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("formatHour", () => {
|
||||||
|
test("24h-Format, kurz, ohne Minuten", () => {
|
||||||
|
assert.equal(formatHour(9, 0, "short", "24h"), "9");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("24h-Format mit Minuten wird zweistellig", () => {
|
||||||
|
assert.equal(formatHour(9, 30, "short", "24h"), "09:30");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("12h-Format: Nachmittag", () => {
|
||||||
|
assert.equal(formatHour(13, 0, "short", "12h"), "1 PM");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("12h-Format: Mitternacht ist 12 AM, nicht 0 AM", () => {
|
||||||
|
assert.equal(formatHour(0, 0, "short", "12h"), "12 AM");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("12h-Format: Mittag ist 12 PM", () => {
|
||||||
|
assert.equal(formatHour(12, 0, "short", "12h"), "12 PM");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getHourFromSlot", () => {
|
||||||
|
test("liest die Stunde aus dem Slot", () => {
|
||||||
|
assert.equal(getHourFromSlot("08:30"), 8);
|
||||||
|
assert.equal(getHourFromSlot("23:45"), 23);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("führende Null wird nicht als Oktalzahl gelesen", () => {
|
||||||
|
assert.equal(getHourFromSlot("09:00"), 9);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isSameDay", () => {
|
||||||
|
test("gleicher Tag, verschiedene Uhrzeit", () => {
|
||||||
|
assert.equal(isSameDay(new Date(2026, 8, 7, 8, 0), new Date(2026, 8, 7, 22, 0)), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("aufeinanderfolgende Tage sind nicht gleich", () => {
|
||||||
|
assert.equal(isSameDay(new Date(2026, 8, 7), new Date(2026, 8, 8)), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("gleicher Tag im anderen Jahr ist nicht gleich", () => {
|
||||||
|
assert.equal(isSameDay(new Date(2026, 8, 7), new Date(2025, 8, 7)), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getCWReferenceDate", () => {
|
||||||
|
test("liefert den Montag der Woche des ersten sichtbaren Tages", () => {
|
||||||
|
const days = [new Date(2026, 8, 3), new Date(2026, 8, 4)]; // Do, Fr
|
||||||
|
assert.equal(formatDateToISO(getCWReferenceDate(days)), "2026-08-31");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Sonntag gehört zur ablaufenden Woche, nicht zur kommenden", () => {
|
||||||
|
const days = [new Date(2026, 8, 6)]; // Sonntag
|
||||||
|
assert.equal(formatDateToISO(getCWReferenceDate(days)), "2026-08-31");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("leere Liste liefert ein Datum statt zu werfen", () => {
|
||||||
|
assert.ok(getCWReferenceDate([]) instanceof Date);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getSelectedDay", () => {
|
||||||
|
test("ohne ausdrückliche Auswahl wird ein Tag aus der Liste geliefert", () => {
|
||||||
|
const days = [new Date(2026, 8, 7), new Date(2026, 8, 8)];
|
||||||
|
assert.ok(days.some((d) => isSameDay(d, getSelectedDay(days))));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("eine ausdrückliche Auswahl aus der Liste wird übernommen", () => {
|
||||||
|
const days = [new Date(2026, 8, 7), new Date(2026, 8, 8)];
|
||||||
|
assert.equal(formatDateToISO(getSelectedDay(days, days[1])), "2026-09-08");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getDayName", () => {
|
||||||
|
test("deutscher Wochentag, lang", () => {
|
||||||
|
assert.equal(getDayName(new Date(2026, 8, 7), "de-DE"), "Montag");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("eigene Namen werden nach Wochentag zugeordnet", () => {
|
||||||
|
const namen = "So,Mo,Di,Mi,Do,Fr,Sa";
|
||||||
|
assert.equal(getDayName(new Date(2026, 8, 7), "de-DE", "custom", namen, 0), "Mo");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("eigene Namen mit falscher Anzahl fallen auf das Gebietsschema zurück", () => {
|
||||||
|
assert.equal(getDayName(new Date(2026, 8, 7), "de-DE", "custom", "nur,drei,Namen"), "Montag");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Großschreibung ist einstellbar", () => {
|
||||||
|
assert.equal(getDayName(new Date(2026, 8, 7), "de-DE", undefined, undefined, 0, "uppercase"), "MONTAG");
|
||||||
|
});
|
||||||
|
});
|
||||||
133
tests/unit/format.test.ts
Normal file
133
tests/unit/format.test.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { test, describe } from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import {
|
||||||
|
stripHtml, invertColor, getSomedaySlotCount, DEFAULT_SOMEDAY_SLOT_COUNT,
|
||||||
|
} 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("<p>Hallo <b>Welt</b></p>"), "Hallo Welt");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("schneidet umgebende Leerzeichen ab", () => {
|
||||||
|
assert.equal(stripHtml(" <p> Text </p> "), "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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user