Öffnungszeiten: Register und Texte im Ticketsystem
Funktionen-Reiter kennt "Öffnungszeiten live" (zeiten) hinter dem Terminplaner: Bestätigungstexte, Site-Key, Aktionen je Zustand (Abnahme nur ohne Hindernis "zeiten_fehlen"). Neues Modul zeitenView.js: Statustext (Einbau läuft / wartet auf die Zeiten des Kunden / Abo-Ende mit Frist), navSelektorFehler (Spiegel der Portal-Regel) und teamBody für PATCH .../zeiten/team. Protokoll-Label für zeiten_gespeichert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
72eedeb59c
commit
994347408d
6 files changed
+326
-13
No files matched your search
@@ -0,0 +1,154 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { loadModule } from './load.mjs'
|
||||
|
||||
// Öffnungszeiten live im Funktionen-Reiter: Statustexte, Prüfung des Navigations-Selektors (Spiegel des Portals) und Rumpf der Team-Einstellungen
|
||||
const { zeitenStatusText, navSelektorFehler, NAV_SELEKTOR_MAX, NAV_SELEKTOR_FEHLER, teamBody } = await loadModule('src/lib/zeitenView.js')
|
||||
|
||||
const EINBAU = 'Leisten live – Einbau läuft'
|
||||
const WARTET = 'wartet auf die Zeiten des Kunden'
|
||||
|
||||
test('zeitenStatusText: Einrichtung ohne Hindernis – Leisten sind live, der Einbau läuft', () => {
|
||||
assert.equal(zeitenStatusText({ state: 'setup', hindernis: null }), EINBAU)
|
||||
assert.equal(zeitenStatusText({ state: 'setup' }), EINBAU, 'ohne Feld hindernis')
|
||||
assert.equal(zeitenStatusText({ state: 'setup', hindernis: '' }), EINBAU)
|
||||
})
|
||||
|
||||
test('zeitenStatusText: Einrichtung mit hindernis "zeiten_fehlen" – wartet auf die Zeiten des Kunden', () => {
|
||||
assert.equal(zeitenStatusText({ state: 'setup', hindernis: 'zeiten_fehlen' }), WARTET)
|
||||
})
|
||||
|
||||
test('zeitenStatusText: unbekanntes Hindernis zeigt nichts Erfundenes, sondern den Einbau-Text', () => {
|
||||
assert.equal(zeitenStatusText({ state: 'setup', hindernis: 'irgendwas_neues' }), EINBAU)
|
||||
})
|
||||
|
||||
test('zeitenStatusText: Abo-Ende nennt die Frist (Berliner Tag des Abo-Endes plus 30 Tage)', () => {
|
||||
const abo = (state, aboEndeAt) => zeitenStatusText({ state, hindernis: null, aboEndeAt })
|
||||
const hinweis = (datum) => `Abo beendet – Abschaltung am ${datum}, falls der Kunde die Funktion nicht behält`
|
||||
assert.equal(abo('active', '2026-10-04T10:00:00.000Z'), hinweis('03.11.2026'))
|
||||
// 22:30 UTC ist in Berlin schon der nächste Tag (Sommerzeit, +2 h): 05.10. -> 04.11.
|
||||
assert.equal(abo('active', '2026-10-04T22:30:00.000Z'), hinweis('04.11.2026'))
|
||||
// Monats- und Jahreswechsel, Zeitumstellung am 25.10. spielt bei Kalendertagen keine Rolle
|
||||
assert.equal(abo('active', '2026-10-20T12:00:00.000Z'), hinweis('19.11.2026'))
|
||||
assert.equal(abo('active', '2026-12-15T12:00:00.000Z'), hinweis('14.01.2027'))
|
||||
// Appwrite liefert auch +00:00
|
||||
assert.equal(abo('active', '2026-10-04T10:00:00.000+00:00'), hinweis('03.11.2026'))
|
||||
})
|
||||
|
||||
test('zeitenStatusText: Abo-Ende in der Einrichtung steht zusätzlich zum Einbau- bzw. Warte-Text', () => {
|
||||
const aboEndeAt = '2026-10-04T10:00:00.000Z'
|
||||
const frist = 'Abo beendet – Abschaltung am 03.11.2026, falls der Kunde die Funktion nicht behält'
|
||||
assert.equal(zeitenStatusText({ state: 'setup', hindernis: null, aboEndeAt }), `${EINBAU} · ${frist}`)
|
||||
assert.equal(zeitenStatusText({ state: 'setup', hindernis: 'zeiten_fehlen', aboEndeAt }), `${WARTET} · ${frist}`)
|
||||
})
|
||||
|
||||
test('zeitenStatusText: Abo-Ende mit unlesbarem Datum nennt 30 Tage statt eines erfundenen Tages', () => {
|
||||
const text = zeitenStatusText({ state: 'active', aboEndeAt: 'kein-datum' })
|
||||
assert.equal(text, 'Abo beendet – Abschaltung nach 30 Tagen, falls der Kunde die Funktion nicht behält')
|
||||
})
|
||||
|
||||
test('zeitenStatusText: aktiv ohne Abo-Ende, aus, bestellt, abgeschaltet, ohne Ansicht – leer', () => {
|
||||
assert.equal(zeitenStatusText({ state: 'active', hindernis: null, aboEndeAt: null }), '')
|
||||
assert.equal(zeitenStatusText({ state: 'active' }), '')
|
||||
assert.equal(zeitenStatusText({ state: 'off' }), '')
|
||||
assert.equal(zeitenStatusText({ state: 'ordered' }), '')
|
||||
// abgeschaltet: Grund und Löschdatum zeigt abgeschaltetText, nichts doppelt
|
||||
assert.equal(zeitenStatusText({ state: 'disabled', disabledReason: 'abo_ende', loeschungAm: '2026-11-04', aboEndeAt: '2026-10-04T10:00:00.000Z' }), '')
|
||||
assert.equal(zeitenStatusText({ state: 'off', aboEndeAt: '2026-10-04T10:00:00.000Z' }), '')
|
||||
assert.equal(zeitenStatusText(null), '')
|
||||
assert.equal(zeitenStatusText(undefined), '')
|
||||
})
|
||||
|
||||
test('zeitenStatusText: kein Text nennt "24 Uhr"', () => {
|
||||
for (const feature of [
|
||||
{ state: 'setup' },
|
||||
{ state: 'setup', hindernis: 'zeiten_fehlen' },
|
||||
{ state: 'active', aboEndeAt: '2026-10-04T10:00:00.000Z' },
|
||||
{ state: 'setup', hindernis: 'zeiten_fehlen', aboEndeAt: '2026-10-04T22:30:00.000Z' },
|
||||
]) {
|
||||
assert.doesNotMatch(zeitenStatusText(feature), /24 Uhr/)
|
||||
}
|
||||
})
|
||||
|
||||
test('navSelektorFehler: leer ist erlaubt (kein Selektor)', () => {
|
||||
for (const wert of [undefined, null, '', ' ', '\n\t ']) {
|
||||
assert.equal(navSelektorFehler(wert), null, JSON.stringify(wert))
|
||||
}
|
||||
})
|
||||
|
||||
test('navSelektorFehler: übliche Selektoren sind gültig', () => {
|
||||
for (const wert of [
|
||||
'header',
|
||||
'#nav',
|
||||
'.site-header',
|
||||
'.site-header > nav',
|
||||
'header#main-nav.fixed',
|
||||
'nav[role="navigation"]',
|
||||
"a[href='x']",
|
||||
'ul.menu:first-child',
|
||||
'div_wrap .a-b',
|
||||
'ABC xyz 019',
|
||||
' header ',
|
||||
]) {
|
||||
assert.equal(navSelektorFehler(wert), null, wert)
|
||||
}
|
||||
})
|
||||
|
||||
test('navSelektorFehler: erlaubt sind nur A–Z a–z 0–9, Leerzeichen und # . - _ > : [ ] = " \'', () => {
|
||||
const zeichen = 'AZaz09 #.-_>:[]="\''
|
||||
assert.equal(navSelektorFehler(zeichen), null)
|
||||
assert.equal(navSelektorFehler('a'.repeat(NAV_SELEKTOR_MAX)), null)
|
||||
})
|
||||
|
||||
test('navSelektorFehler: < { } ; und alle übrigen Zeichen werden abgelehnt', () => {
|
||||
for (const zeichen of ['<', '{', '}', ';', '(', ')', ',', '*', '+', '~', '/', '\\', '@', '!', '|', '$', '%', '^', '&', '?', '`', 'ä', 'ß', '€', '\n', '\t']) {
|
||||
assert.equal(navSelektorFehler(`a${zeichen}b`), NAV_SELEKTOR_FEHLER, JSON.stringify(zeichen))
|
||||
}
|
||||
assert.equal(navSelektorFehler('header { display: none }'), NAV_SELEKTOR_FEHLER)
|
||||
assert.equal(navSelektorFehler('</style><script>'), NAV_SELEKTOR_FEHLER)
|
||||
assert.equal(navSelektorFehler('a\nb'), NAV_SELEKTOR_FEHLER, 'Zeilenumbruch mitten im Text')
|
||||
})
|
||||
|
||||
test('navSelektorFehler: höchstens 120 Zeichen, gezählt nach dem Trimmen', () => {
|
||||
assert.equal(NAV_SELEKTOR_MAX, 120)
|
||||
assert.equal(navSelektorFehler('a'.repeat(120)), null)
|
||||
assert.equal(navSelektorFehler(` ${'a'.repeat(120)} `), null, 'Leerraum außen zählt nicht')
|
||||
assert.equal(navSelektorFehler('a'.repeat(121)), NAV_SELEKTOR_FEHLER)
|
||||
assert.equal(navSelektorFehler(`${'a'.repeat(60)} ${'b'.repeat(60)}`), NAV_SELEKTOR_FEHLER, '121 mit Leerzeichen innen')
|
||||
})
|
||||
|
||||
test('navSelektorFehler: kein Text wird abgelehnt wie auf dem Server (Zahl, Objekt, Wahrheitswert)', () => {
|
||||
for (const wert of [5, true, {}, []]) {
|
||||
assert.equal(navSelektorFehler(wert), NAV_SELEKTOR_FEHLER, JSON.stringify(wert))
|
||||
}
|
||||
})
|
||||
|
||||
test('NAV_SELEKTOR_FEHLER: wörtlich wie die Meldung des Portals', () => {
|
||||
assert.equal(NAV_SELEKTOR_FEHLER, 'Der Selektor darf höchstens 120 Zeichen lang sein und nur A–Z, 0–9, Leerzeichen und # . - _ > : [ ] = " \' enthalten.')
|
||||
})
|
||||
|
||||
test('teamBody: genau die fünf Felder der Team-Route, Selektor getrimmt, leer wird null', () => {
|
||||
assert.deepEqual(
|
||||
teamBody({ projectId: 'p1', teamVersion: 3, navSelektor: ' header > nav ', kontaktleisteAus: true, hinweisleisteAus: false }),
|
||||
{ projectId: 'p1', teamVersion: 3, navSelektor: 'header > nav', kontaktleisteAus: true, hinweisleisteAus: false }
|
||||
)
|
||||
for (const leer of ['', ' ', null, undefined]) {
|
||||
assert.equal(teamBody({ projectId: 'p1', teamVersion: 0, navSelektor: leer, kontaktleisteAus: false, hinweisleisteAus: false }).navSelektor, null, JSON.stringify(leer))
|
||||
}
|
||||
})
|
||||
|
||||
test('teamBody: Schalter sind immer echte Wahrheitswerte, nur true gilt als an', () => {
|
||||
const body = (kontaktleisteAus, hinweisleisteAus) => teamBody({ projectId: 'p1', teamVersion: 1, navSelektor: '', kontaktleisteAus, hinweisleisteAus })
|
||||
assert.deepEqual(body(true, true), { projectId: 'p1', teamVersion: 1, navSelektor: null, kontaktleisteAus: true, hinweisleisteAus: true })
|
||||
assert.deepEqual(body(undefined, undefined), { projectId: 'p1', teamVersion: 1, navSelektor: null, kontaktleisteAus: false, hinweisleisteAus: false })
|
||||
assert.deepEqual(body('true', 1), { projectId: 'p1', teamVersion: 1, navSelektor: null, kontaktleisteAus: false, hinweisleisteAus: false })
|
||||
})
|
||||
|
||||
test('teamBody: die Version wird unverändert durchgereicht (das Portal prüft sie), auch 0', () => {
|
||||
assert.equal(teamBody({ projectId: 'p1', teamVersion: 0, navSelektor: '', kontaktleisteAus: false, hinweisleisteAus: false }).teamVersion, 0)
|
||||
assert.equal(teamBody({ projectId: 'p1', teamVersion: 41, navSelektor: '', kontaktleisteAus: false, hinweisleisteAus: false }).teamVersion, 41)
|
||||
})
|
||||
|
||||
test('teamBody: ein ungültiger Selektor wird nicht "repariert", sondern unverändert gesendet (das Formular sperrt vorher, das Portal prüft noch einmal)', () => {
|
||||
assert.equal(teamBody({ projectId: 'p1', teamVersion: 1, navSelektor: 'a{b}', kontaktleisteAus: false, hinweisleisteAus: false }).navSelektor, 'a{b}')
|
||||
})
|
||||
Reference in new issue
Block a user