Files
Webklar-Kundenbereich/test/termineView.test.js
T
ClaudeandClaude Sonnet 5.5 43e4826ae3 Terminplaner: Portal-Ansicht, Kachel, Termine
Kachel in Funktionen (Kommt bald / Aus / Aktiv mit Anfragen-Zahl / Abgeschaltet),
Ansicht #termine als Unterseite von Funktionen: Projektauswahl, Vorlagenauswahl
beim ersten Oeffnen, Reiter-Geruest (Platzhalter fuer Leistungen, Team & Plaetze,
Einstellungen) und der Reiter Termine mit Tag/Woche/Monat, Filtern, Aktionen mit
Rueckfragen in der Seite, Termin eintragen und Zeit sperren.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 08:17:44 +00:00

243 lines
15 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.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import {
FILTER, ABSAGE_GRUENDE, anfragenText, bereichFuer, datumKurz, datumLang, endeZeit, fensterParsen, fensterText, filterStatus, gruppiereNachTag,
fehltText, heuteBerlin, istSperre, monatVon, moeglicheAktionen, ressourcenWort, statusLabel, statusTon, tagPlus, tagUeberschrift, verschiebeTag, wocheVon, zeitNormal, zeitraumTitel,
} from '../public/termine-view.js'
// Reine Helfer der Portal-Ansicht Terminplaner (public/termine-view.js): Tage sind Berliner Kalendertage 'JJJJ-MM-TT', gerechnet wird
// nur mit dem Kalender (kein Browser-Datum), die Uhrzeit des Endes kommt aus endsAt in Berliner Zeit.
test('fensterParsen: Zeitfenster aus Text, normalisiert', () => {
assert.deepEqual(fensterParsen('09:00–12:30, 13:30–18:00'), [['09:00', '12:30'], ['13:30', '18:00']])
assert.deepEqual(fensterParsen('9:00 - 12:30; 13:30 bis 18:00'), [['09:00', '12:30'], ['13:30', '18:00']])
assert.deepEqual(fensterParsen('9.00—24:00'), [['09:00', '24:00']])
assert.deepEqual(fensterParsen('08:00-12:00 und 14:00-18:00'), [['08:00', '12:00'], ['14:00', '18:00']])
assert.deepEqual(fensterParsen(' '), [], 'leer = an diesem Tag geschlossen')
assert.deepEqual(fensterParsen(''), [])
})
test('fensterParsen: ungültige Eingaben ⇒ null', () => {
for (const schlecht of [
'abc', '09:00', '09:00–', '–12:00', '12:00–09:00', '09:00–09:00', '09:07–12:00', '25:00–26:00', '24:00–24:00',
'09:00–12:00, 11:00–13:00', '13:00–14:00, 09:00–10:00', '09:00–12:00,', '09:00–12:00 13:00–14:00', null, undefined, 5,
]) {
assert.equal(fensterParsen(schlecht), null, String(schlecht))
}
})
test('fensterText ist die Umkehrung von fensterParsen', () => {
assert.equal(fensterText([['09:00', '12:30'], ['13:30', '18:00']]), '09:00–12:30, 13:30–18:00')
assert.equal(fensterText([]), '')
assert.equal(fensterText(undefined), '')
assert.deepEqual(fensterParsen(fensterText([['08:00', '24:00']])), [['08:00', '24:00']])
})
test('zeitNormal: HH:MM, Ende darf 24:00 sein', () => {
assert.equal(zeitNormal('9:05'), '09:05')
assert.equal(zeitNormal(' 09:00 '), '09:00')
assert.equal(zeitNormal('17.30'), '17:30')
assert.equal(zeitNormal('24:00'), null)
assert.equal(zeitNormal('24:00', { ende: true }), '24:00')
assert.equal(zeitNormal('24:05', { ende: true }), null)
assert.equal(zeitNormal('12:07'), '12:07', 'Zeit ohne 5er-Raster wird nur normalisiert, der Server prüft das Raster')
assert.equal(zeitNormal('12:60'), null)
assert.equal(zeitNormal('abc'), null)
assert.equal(zeitNormal(''), null)
assert.equal(zeitNormal(undefined), null)
})
test('statusLabel: alle acht Status deutsch, Unbekanntes nie roh', () => {
assert.deepEqual(
['gehalten', 'angefragt', 'bestaetigt', 'erledigt', 'nichterschienen', 'abgesagt', 'abgelehnt', 'verfallen'].map(statusLabel),
['Reserviert', 'Angefragt', 'Bestätigt', 'Erledigt', 'Nicht erschienen', 'Abgesagt', 'Abgelehnt', 'Verfallen'],
)
assert.equal(statusLabel('geheim<b>'), 'Unbekannt')
assert.equal(statusLabel(undefined), 'Unbekannt')
for (const s of ['gehalten', 'angefragt', 'bestaetigt', 'erledigt', 'nichterschienen', 'abgesagt', 'abgelehnt', 'verfallen']) {
assert.match(statusTon(s), /^(ok|wait|off|bad)$/, s)
}
assert.equal(statusTon('bestaetigt'), 'ok')
assert.equal(statusTon('angefragt'), 'wait')
})
test('Kalender: wocheVon (Montag bis Sonntag), monatVon, tagPlus über Monats- und Jahresgrenzen', () => {
assert.deepEqual(wocheVon('2026-10-05'), { von: '2026-10-05', bis: '2026-10-11' }, 'Montag')
assert.deepEqual(wocheVon('2026-10-11'), { von: '2026-10-05', bis: '2026-10-11' }, 'Sonntag gehört zur Woche davor')
assert.deepEqual(wocheVon('2026-10-07'), { von: '2026-10-05', bis: '2026-10-11' })
assert.deepEqual(wocheVon('2026-12-31'), { von: '2026-12-28', bis: '2027-01-03' }, 'Jahreswechsel')
assert.deepEqual(monatVon('2026-10-15'), { von: '2026-10-01', bis: '2026-10-31' })
assert.deepEqual(monatVon('2028-02-10'), { von: '2028-02-01', bis: '2028-02-29' }, 'Schaltjahr')
assert.deepEqual(monatVon('2026-12-01'), { von: '2026-12-01', bis: '2026-12-31' })
assert.equal(tagPlus('2026-10-31', 1), '2026-11-01')
assert.equal(tagPlus('2026-01-01', -1), '2025-12-31')
assert.equal(tagPlus('2026-03-29', 1), '2026-03-30', 'Zeitumstellung spielt beim Kalender keine Rolle')
assert.equal(tagPlus('2026-10-25', 1), '2026-10-26')
})
test('bereichFuer und verschiebeTag je Ansicht', () => {
assert.deepEqual(bereichFuer('tag', '2026-10-07', '2026-10-05'), { von: '2026-10-07', bis: '2026-10-07' })
assert.deepEqual(bereichFuer('woche', '2026-10-07', '2026-10-05'), { von: '2026-10-05', bis: '2026-10-11' })
assert.deepEqual(bereichFuer('monat', '2026-10-07', '2026-10-05'), { von: '2026-10-01', bis: '2026-10-31' })
assert.deepEqual(bereichFuer('anfragen', '2026-10-07', '2026-10-05'), { von: '2026-10-05', bis: '2026-12-06' }, 'ab heute, 62 Tage (Grenze der Schnittstelle)')
assert.equal(verschiebeTag('tag', '2026-10-31', 1), '2026-11-01')
assert.equal(verschiebeTag('tag', '2026-10-01', -1), '2026-09-30')
assert.equal(verschiebeTag('woche', '2026-10-05', 1), '2026-10-12')
assert.equal(verschiebeTag('woche', '2026-10-05', -1), '2026-09-28')
assert.equal(verschiebeTag('monat', '2026-10-17', 1), '2026-11-01')
assert.equal(verschiebeTag('monat', '2026-01-17', -1), '2025-12-01')
assert.equal(verschiebeTag('monat', '2026-12-31', 1), '2027-01-01')
})
test('Daten: datumLang, datumKurz, tagUeberschrift, zeitraumTitel', () => {
assert.equal(datumLang('2026-10-05'), 'Montag, 5. Oktober 2026')
assert.equal(datumLang('2026-03-01'), 'Sonntag, 1. März 2026')
assert.equal(datumKurz('2026-10-05'), '05.10.2026')
assert.equal(tagUeberschrift('2026-10-05', '2026-10-05'), 'Heute · Montag, 5. Oktober')
assert.equal(tagUeberschrift('2026-10-06', '2026-10-05'), 'Morgen · Dienstag, 6. Oktober')
assert.equal(tagUeberschrift('2026-10-04', '2026-10-05'), 'Gestern · Sonntag, 4. Oktober')
assert.equal(tagUeberschrift('2026-10-12', '2026-10-05'), 'Montag, 12. Oktober')
assert.equal(tagUeberschrift('2027-01-04', '2026-10-05'), 'Montag, 4. Januar 2027', 'anderes Jahr nennt das Jahr')
assert.equal(zeitraumTitel('tag', '2026-10-05'), 'Montag, 5. Oktober 2026')
assert.equal(zeitraumTitel('woche', '2026-10-07'), '5.–11. Oktober 2026')
assert.equal(zeitraumTitel('woche', '2026-10-28'), '26. Oktober – 1. November 2026')
assert.equal(zeitraumTitel('woche', '2026-12-30'), '28. Dezember 2026 – 3. Januar 2027')
assert.equal(zeitraumTitel('monat', '2026-10-17'), 'Oktober 2026')
assert.equal(zeitraumTitel('anfragen', '2026-10-17'), 'Offene Anfragen')
})
test('heuteBerlin: der Berliner Kalendertag, nicht der des Browsers', () => {
assert.equal(heuteBerlin(new Date('2026-10-04T22:30:00Z')), '2026-10-05', '00:30 Berlin (MESZ) ist schon Montag')
assert.equal(heuteBerlin(new Date('2026-10-05T21:59:00Z')), '2026-10-05')
assert.equal(heuteBerlin(new Date('2026-12-31T23:30:00Z')), '2027-01-01', 'MEZ')
assert.match(heuteBerlin(), /^\d{4}-\d{2}-\d{2}$/)
})
test('endeZeit: Ende des Termins in Berliner Wanduhr aus endsAt', () => {
assert.equal(endeZeit({ endsAt: '2026-10-05T08:45:00.000Z' }), '10:45')
assert.equal(endeZeit({ endsAt: '2026-12-01T08:45:00.000Z' }), '09:45', 'Winterzeit')
assert.equal(endeZeit({ endsAt: '2026-10-05T22:00:00.000Z' }), '00:00')
assert.equal(endeZeit({ endsAt: 'kaputt' }), '')
assert.equal(endeZeit({}), '')
})
const B = (extra = {}) => ({
id: 'b1', status: 'bestaetigt', quelle: 'online', startsAt: '2026-10-05T08:00:00.000Z', endsAt: '2026-10-05T08:45:00.000Z', datum: '2026-10-05', zeit: '10:00',
gastName: 'Max', gastEmail: 'max@example.com', gastTelefon: '', gastNachricht: '', ...extra,
})
test('gruppiereNachTag: nach Berliner Tag, Tage und Termine aufsteigend, Termine der Liste bleiben vollständig', () => {
const liste = [
B({ id: 'c', datum: '2026-10-06', zeit: '09:00', startsAt: '2026-10-06T07:00:00.000Z' }),
B({ id: 'b', datum: '2026-10-05', zeit: '11:00', startsAt: '2026-10-05T09:00:00.000Z' }),
B({ id: 'a', datum: '2026-10-05', zeit: '10:00', startsAt: '2026-10-05T08:00:00.000Z' }),
]
const gruppen = gruppiereNachTag(liste)
assert.deepEqual(gruppen.map((g) => g.tag), ['2026-10-05', '2026-10-06'])
assert.deepEqual(gruppen[0].buchungen.map((b) => b.id), ['a', 'b'])
assert.deepEqual(gruppen[1].buchungen.map((b) => b.id), ['c'])
assert.deepEqual(gruppiereNachTag([]), [])
assert.deepEqual(gruppiereNachTag(undefined), [])
assert.deepEqual(liste.map((b) => b.id), ['c', 'b', 'a'], 'Eingabe bleibt unverändert')
})
test('istSperre: nur quelle sperre', () => {
assert.equal(istSperre({ quelle: 'sperre' }), true)
assert.equal(istSperre({ quelle: 'manuell' }), false)
assert.equal(istSperre({ quelle: 'online' }), false)
assert.equal(istSperre({}), false)
})
test('moeglicheAktionen: je Status und Zeit; Sperren nie mit Gast-Aktionen', () => {
const vorher = Date.parse('2026-10-05T07:00:00Z') // vor Beginn
const mitten = Date.parse('2026-10-05T08:10:00Z') // nach Beginn
const danach = Date.parse('2026-10-05T09:00:00Z') // nach Ende
assert.deepEqual(moeglicheAktionen(B({ status: 'angefragt' }), vorher), ['annehmen', 'ablehnen', 'verschieben', 'anonymisieren'])
assert.deepEqual(moeglicheAktionen(B({ status: 'angefragt' }), mitten), ['ablehnen', 'anonymisieren'], 'begonnene Anfrage lässt sich nicht mehr annehmen')
assert.deepEqual(moeglicheAktionen(B({ status: 'bestaetigt' }), vorher), ['verschieben', 'absagen', 'anonymisieren'])
assert.deepEqual(moeglicheAktionen(B({ status: 'bestaetigt' }), mitten), ['nichterschienen', 'anonymisieren'])
assert.deepEqual(moeglicheAktionen(B({ status: 'bestaetigt' }), danach), ['nichterschienen', 'anonymisieren'])
assert.deepEqual(moeglicheAktionen(B({ status: 'erledigt' }), danach), ['nichterschienen', 'anonymisieren'])
for (const s of ['nichterschienen', 'abgesagt', 'abgelehnt', 'verfallen']) assert.deepEqual(moeglicheAktionen(B({ status: s }), danach), ['anonymisieren'], s)
assert.deepEqual(moeglicheAktionen(B({ status: 'gehalten' }), vorher), [], 'Reservierung wartet auf den Gast')
// Gastdaten schon gelöscht oder nie erfasst: nichts zu löschen
assert.deepEqual(moeglicheAktionen(B({ status: 'abgesagt', gastName: 'Gelöscht', gastEmail: '' }), danach), [])
assert.deepEqual(moeglicheAktionen(B({ status: 'abgesagt', gastName: '', gastEmail: '' }), danach), [])
assert.deepEqual(moeglicheAktionen(B({ status: 'abgesagt', gastName: 'Gelöscht', gastEmail: 'x@y.de' }), danach), ['anonymisieren'])
// Sperre: nur aufheben, solange sie noch nicht vorbei ist
assert.deepEqual(moeglicheAktionen(B({ quelle: 'sperre', status: 'bestaetigt', gastName: '' }), vorher), ['entsperren'])
assert.deepEqual(moeglicheAktionen(B({ quelle: 'sperre', status: 'bestaetigt', gastName: '' }), mitten), ['entsperren'])
assert.deepEqual(moeglicheAktionen(B({ quelle: 'sperre', status: 'erledigt', gastName: '' }), danach), [])
})
test('Filter: Statuslisten für die Schnittstelle', () => {
assert.deepEqual(FILTER.map((f) => f.key), ['alle', 'offen', 'angefragt', 'bestaetigt', 'abgesagt'])
assert.equal(filterStatus('alle'), '')
assert.equal(filterStatus('offen'), 'angefragt,bestaetigt')
assert.equal(filterStatus('angefragt'), 'angefragt')
assert.equal(filterStatus('bestaetigt'), 'bestaetigt')
assert.equal(filterStatus('abgesagt'), 'abgesagt,abgelehnt,verfallen')
assert.equal(filterStatus('gibtsnicht'), '')
})
test('anfragenText: Einzahl und Mehrzahl', () => {
assert.equal(anfragenText(0), '')
assert.equal(anfragenText(1), '1 Anfrage wartet')
assert.equal(anfragenText(2), '2 Anfragen warten')
assert.equal(anfragenText(17), '17 Anfragen warten')
assert.equal(anfragenText(undefined), '')
})
test('Absagegründe: Auswahl mit kurzen Texten (Server: höchstens 100 Zeichen)', () => {
assert.ok(ABSAGE_GRUENDE.length >= 4)
for (const g of ABSAGE_GRUENDE) assert.ok(g.length > 0 && [...g].length <= 100, g)
assert.equal(new Set(ABSAGE_GRUENDE).size, ABSAGE_GRUENDE.length)
})
test('ressourcenWort: Person, Platz oder beides', () => {
assert.equal(ressourcenWort([{ typ: 'person' }, { typ: 'person' }]), 'Person')
assert.equal(ressourcenWort([{ typ: 'platz' }]), 'Platz')
assert.equal(ressourcenWort([{ typ: 'person' }, { typ: 'platz' }]), 'Person/Platz')
assert.equal(ressourcenWort([]), 'Person/Platz')
assert.equal(ressourcenWort(undefined), 'Person/Platz')
})
test('fehltText: Pflichtangaben für Gäste in Worten', () => {
assert.equal(fehltText(['betrieb.telefon']), 'deine Telefonnummer')
assert.equal(fehltText(['betrieb.telefon', 'betrieb.adresse']), 'deine Telefonnummer und deine Anschrift')
assert.equal(fehltText(['betrieb.adresse']), 'deine Anschrift')
assert.equal(fehltText([]), '')
assert.equal(fehltText(['unbekannt']), '', 'Unbekanntes nie roh')
assert.equal(fehltText(undefined), '')
})
// ---- Verdrahtung (Portal-Ansicht #termine)
const lies = (pfad) => readFileSync(new URL(`../${pfad}`, import.meta.url), 'utf8')
test('Portal: Ansicht #termine ist verdrahtet, funktionen.js behält seinen einen absoluten Import', () => {
const app = lies('public/app.js')
assert.match(app, /VIEW_HASHES = \{[^}]*termine: 'termine'/)
assert.match(app, /APP_PARENT = \{ news: 'funktionen', analyse: 'funktionen', termine: 'funktionen'/)
assert.match(app, /FEATURE_LABELS = \{[^}]*termine: 'Terminplaner'/)
assert.match(app, /termine: \(\) => import\('\/termine\.js'\)\.then\(\(m\) => m\.initTermine\(document\.getElementById\('termine-body'\)\)\)/)
const html = lies('public/dashboard.html')
assert.match(html, /<section class="kb-view" id="view-termine">[\s\S]*<h1>Terminplaner<\/h1>[\s\S]*<div id="termine-body">/)
assert.match(html, /<link rel="stylesheet" href="\/termine\.css">/)
// R4: funktionen.js lädt nichts Neues (die Kachel steht inline); test/funktionenTiles.test.js biegt genau diesen Import um
const fn = lies('public/funktionen.js')
assert.deepEqual(fn.match(/from '\/[^']+'/g), ["from '/analyse-view.js'"])
assert.doesNotMatch(fn, /import\s*\(/, 'auch kein dynamischer Import')
})
test('termine.js: Fremdtext nur als Text, keine Browser-Dialoge, Du-Form', () => {
const quelle = lies('public/termine.js').replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|\s)\/\/.*$/gm, '') // ohne Kommentare
assert.doesNotMatch(quelle, /innerHTML|insertAdjacentHTML|outerHTML|document\.write/, 'Daten nie als HTML')
assert.doesNotMatch(quelle, /\bconfirm\(|\balert\(|\bprompt\(/, 'Rückfragen stehen in der Seite')
assert.doesNotMatch(quelle, /\bSie\b|\bIhr(e[nmrs]?)?\b/, 'Portaltexte in der Du-Form')
// Links nur auf die eigene Export-Schnittstelle, nie auf Kundentext
const hrefs = quelle.match(/href: `[^`]*`/g) || []
assert.deepEqual(hrefs, ['href: `/api/termine/export.csv?projectId=${encodeURIComponent(projectId)}`'])
assert.doesNotMatch(quelle, /href: [^`]|setAttribute\('href'/, 'sonst kein href')
})