Terminplaner: Portal-Ansicht Leistungen, Team, Einstellungen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
650919b022
commit
0b75b31009
15 files changed
+2479
-79
No files matched your search
+278
-11
@@ -5,7 +5,18 @@ import {
|
||||
FILTER, ABSAGE_GRUENDE, anfragenText, bereichFuer, datumKurz, datumLang, endeZeit, fensterParsen, fensterText, filterStatus, gruppiereNachTag,
|
||||
fehltText, fensterAufteilen, heuteBerlin, istSperre, istTag, monatVon, moeglicheAktionen, ressourcenWort, statusLabel, statusTon, tagPlus, tagUeberschrift, verschiebeTag, wocheVon, zeitNormal, zeitraumTitel,
|
||||
} from '../public/termine-view.js'
|
||||
import {
|
||||
MAX, BUNDESLAENDER, ERINNERUNG_STUNDEN, FENSTER_FEHLER, GAST_FELD_WAHL, GESUNDHEIT_HINWEIS, LIMITS, RASTER_MIN, WOCHENTAGE_PLAN, ZAHLARTEN,
|
||||
aktivSeitText, centZuEuro, eindeutigeId, euroZuCent, ganzzahl, neueId, optionenAusText, optionenText, planAusTexten, planTexte, preisAnzeige, verschiebeEintrag, zusatzSchluessel,
|
||||
} from '../public/termine-view.js'
|
||||
import { ANREDE_WAHL, stornoMuster } from '../public/termine-gaestetexte.js'
|
||||
import { exportUrl, request } from '../public/termine-dom.js'
|
||||
import { beschrifteFehler } from '../public/termine-speichern.js'
|
||||
import { BUNDESLAENDER as SERVER_BUNDESLAENDER } from '../server/services/termine/feiertage.js'
|
||||
import { normalisiereEinstellung, preisText, zahlungText } from '../server/services/termine/einstellungen.js'
|
||||
import { VORLAGEN, ausVorlage } from '../server/services/termine/vorlagen.js'
|
||||
|
||||
const JETZT_SERVER = new Date('2026-10-05T08:00:00.000Z') // feste Uhr für Aufrufe der Server-Prüfung
|
||||
|
||||
// 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.
|
||||
@@ -81,11 +92,10 @@ 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-04' }, 'ab heute bis zum Ende des Buchungszeitraums (Vorgabe 60 Tage)')
|
||||
assert.deepEqual(bereichFuer('anfragen', '2026-10-07', '2026-10-05', 180), { von: '2026-10-05', bis: '2027-04-03' }, 'Buchungszeitraum der Einstellung')
|
||||
assert.deepEqual(bereichFuer('anfragen', '2026-10-07', '2026-10-05', 365), { von: '2026-10-05', bis: '2027-10-05' })
|
||||
assert.equal(bereichFuer('anfragen', '2026-10-07', '2026-10-05', 9999).bis, '2027-10-05', 'höchstens 365 Tage')
|
||||
for (const kaputt of [0, -5, NaN, 'x', null, undefined]) assert.equal(bereichFuer('anfragen', '2026-10-07', '2026-10-05', kaputt).bis, '2026-12-04', `ungültiger Zeitraum ${String(kaputt)} ⇒ Vorgabe`)
|
||||
// Ruling R50: Offene Anfragen immer ab heute 365 Tage (so lange kann eine Anfrage nicht in der Zukunft liegen), unabhängig vom Buchungszeitraum
|
||||
assert.deepEqual(bereichFuer('anfragen', '2026-10-07', '2026-10-05'), { von: '2026-10-05', bis: '2027-10-05' }, 'ab heute 365 Tage')
|
||||
assert.deepEqual(bereichFuer('anfragen', '2026-10-07', '2026-10-05', 30), { von: '2026-10-05', bis: '2027-10-05' }, 'ein kurzer Buchungszeitraum begrenzt die Liste nicht')
|
||||
assert.deepEqual(bereichFuer('anfragen', '2026-10-07', '2028-02-28'), { von: '2028-02-28', bis: '2029-02-27' }, 'Schaltjahr: 365 Kalendertage')
|
||||
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')
|
||||
@@ -261,33 +271,65 @@ test('Portal: Ansicht #termine ist verdrahtet, funktionen.js behält seinen eine
|
||||
|
||||
test('Ansicht-Module (public/termine*.js): Fremdtext nur als Text, keine Browser-Dialoge, Du-Form, ein einziger Link', () => {
|
||||
const dateien = readdirSync(new URL('../public/', import.meta.url)).filter((f) => /^termine.*\.js$/.test(f)).sort()
|
||||
assert.deepEqual(dateien, ['termine-aktionen.js', 'termine-dom.js', 'termine-formulare.js', 'termine-tab-termine.js', 'termine-view.js', 'termine.js'])
|
||||
assert.deepEqual(dateien, [
|
||||
'termine-aktionen.js', 'termine-dom.js', 'termine-formulare.js', 'termine-gaestetexte.js', 'termine-speichern.js', 'termine-tab-einstellungen.js',
|
||||
'termine-tab-leistungen.js', 'termine-tab-team.js', 'termine-tab-termine.js', 'termine-team-ausnahmen.js', 'termine-view.js', 'termine.js',
|
||||
])
|
||||
const ohneKommentare = (text) => text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|\s)\/\/.*$/gm, '')
|
||||
const quellen = dateien.map((f) => [f, ohneKommentare(lies(`public/${f}`))])
|
||||
// Nur termine-gaestetexte.js schreibt für Gäste (Mustertext, Auswahl der Anrede) und darf deshalb die Sie-Form enthalten
|
||||
const GAESTETEXTE = 'termine-gaestetexte.js'
|
||||
for (const [datei, quelle] of quellen) {
|
||||
assert.doesNotMatch(quelle, /innerHTML|insertAdjacentHTML|outerHTML|document\.write/, `${datei}: Daten nie als HTML`)
|
||||
assert.doesNotMatch(quelle, /\bconfirm\(|\balert\(|\bprompt\(/, `${datei}: Rückfragen stehen in der Seite`)
|
||||
assert.doesNotMatch(quelle, /\bSie\b|\bIhr(e[nmrs]?)?\b/, `${datei}: Portaltexte in der Du-Form`)
|
||||
if (datei !== GAESTETEXTE) assert.doesNotMatch(quelle, /\bSie\b|\bIhr(e[nmrs]?)?\b/, `${datei}: Portaltexte in der Du-Form`)
|
||||
assert.doesNotMatch(quelle, /localStorage|sessionStorage|document\.cookie/, `${datei}: kein Browserspeicher`)
|
||||
}
|
||||
assert.match(quellen.find(([f]) => f === GAESTETEXTE)[1], /\bSie\b/, 'die Ausnahme hat einen Grund')
|
||||
// Links nur auf die eigene Export-Schnittstelle (exportUrl), nie auf Kundentext
|
||||
const hrefs = quellen.flatMap(([datei, quelle]) => (quelle.match(/href: [^,}]+|setAttribute\('href'/g) || []).map((m) => `${datei}: ${m}`))
|
||||
assert.deepEqual(hrefs, ['termine.js: href: exportUrl(projectId)'])
|
||||
assert.deepEqual(hrefs, ['termine-dom.js: href: exportUrl(projectId)'])
|
||||
const dom = quellen.find(([f]) => f === 'termine-dom.js')[1]
|
||||
assert.ok(dom.includes('`/api/termine/export.csv?projectId=${encodeURIComponent(projectId)}`'), 'exportUrl codiert die Projekt-ID')
|
||||
// Speichern der Einstellungen gibt es genau einmal (termine-speichern.js), nicht in jedem Reiter
|
||||
assert.deepEqual(quellen.filter(([, quelle]) => /'PATCH'/.test(quelle)).map(([datei]) => datei), ['termine-speichern.js'])
|
||||
})
|
||||
|
||||
test('Ansicht-Module: Reiter sind Module mit render(panel, ctx), die Huelle kennt sie nur ueber REITER', () => {
|
||||
const huelle = lies('public/termine.js')
|
||||
assert.match(huelle, /import \{ render as renderTermine \} from '\.\/termine-tab-termine\.js'/)
|
||||
for (const tab of ['termine', 'leistungen', 'team', 'einstellungen']) {
|
||||
assert.match(huelle, new RegExp(`import \\{ render as render${tab[0].toUpperCase()}${tab.slice(1)} \\} from '\\./termine-tab-${tab}\\.js'`))
|
||||
assert.match(lies(`public/termine-tab-${tab}.js`), /export function render\(panel, ctx\)/)
|
||||
}
|
||||
assert.match(huelle, /const REITER = \[/)
|
||||
assert.match(lies('public/termine-tab-termine.js'), /export function render\(panel, ctx\)/)
|
||||
assert.deepEqual([...huelle.matchAll(/\{ key: '(\w+)', label: '([^']+)', render: (\w+) \}/g)].map((m) => [m[1], m[2], m[3]]), [
|
||||
['termine', 'Termine', 'renderTermine'], ['leistungen', 'Leistungen', 'renderLeistungen'], ['team', 'Team & Plätze', 'renderTeam'], ['einstellungen', 'Einstellungen', 'renderEinstellungen'],
|
||||
])
|
||||
// Reiter lesen Zustand nur aus ctx, nicht aus der Huelle
|
||||
for (const datei of ['termine-tab-termine.js', 'termine-aktionen.js', 'termine-formulare.js']) {
|
||||
for (const datei of ['termine-tab-termine.js', 'termine-tab-leistungen.js', 'termine-tab-team.js', 'termine-tab-einstellungen.js', 'termine-team-ausnahmen.js', 'termine-speichern.js', 'termine-aktionen.js', 'termine-formulare.js']) {
|
||||
assert.doesNotMatch(lies(`public/${datei}`), /from '\.\/termine\.js'/, `${datei} importiert die Huelle nicht`)
|
||||
}
|
||||
})
|
||||
|
||||
test('beschrifteFehler: Fehler ohne Platz am Feld nennen den Eintrag', () => {
|
||||
const quellen = [{
|
||||
leistungen: [{ id: 's_aaaaaaaa', name: 'Herrenschnitt' }, { id: 's_bbbbbbbb', name: 'Färben' }],
|
||||
ressourcen: [{ id: 'r_aaaaaaaa', name: 'Anna', typ: 'person' }, { id: 'r_bbbbbbbb', name: 'Kabine 1', typ: 'platz' }],
|
||||
abwesenheiten: [{ id: 'a_aaaaaaaa', titel: 'Betriebsurlaub' }],
|
||||
}]
|
||||
assert.equal(beschrifteFehler('leistungen.1.ressourcen', 'Eine aktive Leistung braucht …', quellen), 'Leistung „Färben“: Eine aktive Leistung braucht …', 'nach Stelle')
|
||||
assert.equal(beschrifteFehler('leistungen.s_aaaaaaaa', 'Für diese Leistung gibt es noch 1 künftigen Termin.', quellen), 'Leistung „Herrenschnitt“: Für diese Leistung gibt es noch 1 künftigen Termin.', 'nach ID')
|
||||
assert.equal(beschrifteFehler('ressourcen.r_bbbbbbbb', 'Für diesen Platz …', quellen), 'Platz „Kabine 1“: Für diesen Platz …')
|
||||
assert.equal(beschrifteFehler('ressourcen.0.plan.3', 'Zeitfenster überlappen sich.', quellen), 'Person „Anna“: Zeitfenster überlappen sich.')
|
||||
assert.equal(beschrifteFehler('abwesenheiten.0.bis', 'Das Ende …', quellen), 'Abwesenheit „Betriebsurlaub“: Das Ende …')
|
||||
assert.equal(beschrifteFehler('leistungen.7.name', 'Bitte gib einen Namen an.', quellen), 'Bitte gib einen Namen an.', 'unbekannter Eintrag: Text unverändert')
|
||||
assert.equal(beschrifteFehler('regeln.raster', 'Das Raster …', quellen), 'Das Raster …')
|
||||
assert.equal(beschrifteFehler('betrieb.adresse', 'Bitte gib die Anschrift an.', quellen), 'Bitte gib die Anschrift an. Das änderst du im Reiter „Einstellungen“.', 'Betriebsangaben stehen woanders')
|
||||
assert.equal(beschrifteFehler('_', 'Zu umfangreich.', quellen), 'Zu umfangreich.')
|
||||
// gesucht wird in allen Quellen (der alte Stand kennt Einträge, die der Entwurf nicht mehr hat)
|
||||
assert.equal(beschrifteFehler('leistungen.s_cccccccc', 'x', [{ leistungen: [] }, { leistungen: [{ id: 's_cccccccc', name: 'Alt' }] }]), 'Leistung „Alt“: x')
|
||||
})
|
||||
|
||||
test('request: Netzwerkfehler auf Deutsch, Fehler der Schnittstelle mit Feldern und Code', async () => {
|
||||
const vorher = globalThis.fetch
|
||||
try {
|
||||
@@ -306,3 +348,228 @@ test('exportUrl: Projekt-ID wird codiert', () => {
|
||||
assert.equal(exportUrl('p1'), '/api/termine/export.csv?projectId=p1')
|
||||
assert.equal(exportUrl('a&b=c d'), '/api/termine/export.csv?projectId=a%26b%3Dc%20d')
|
||||
})
|
||||
|
||||
// ---- Einstellungen bearbeiten (Aufgabe 21b): Wochenplan, IDs, Preise, Spiegel der Server-Grenzen
|
||||
test('Wochenplan: Text je Wochentag ⇔ Plan der Einstellung', () => {
|
||||
const plan = { 1: [['09:00', '12:30'], ['13:30', '18:00']], 6: [['09:00', '13:00']] }
|
||||
assert.deepEqual(WOCHENTAGE_PLAN.map(([nr, name]) => `${nr}${name}`), ['1Montag', '2Dienstag', '3Mittwoch', '4Donnerstag', '5Freitag', '6Samstag', '7Sonntag'])
|
||||
assert.deepEqual(planTexte(plan), { 1: '09:00–12:30, 13:30–18:00', 2: '', 3: '', 4: '', 5: '', 6: '09:00–13:00', 7: '' })
|
||||
assert.deepEqual(planTexte(undefined), { 1: '', 2: '', 3: '', 4: '', 5: '', 6: '', 7: '' })
|
||||
assert.deepEqual(planAusTexten(planTexte(plan)), { plan }, 'Umkehrung')
|
||||
assert.deepEqual(planAusTexten({ 1: ' ', 2: '8:00-12:00 und 14:00 bis 24:00' }), { plan: { 2: [['08:00', '12:00'], ['14:00', '24:00']] } }, 'leer = geschlossen, Schreibweisen werden vereinheitlicht')
|
||||
assert.deepEqual(planAusTexten({}), { plan: {} })
|
||||
assert.deepEqual(planAusTexten(undefined), { plan: {} })
|
||||
assert.deepEqual(planAusTexten({ 3: '9-12', 5: '09:00–09:00', 1: '09:00–10:00' }), { fehler: { 3: FENSTER_FEHLER, 5: FENSTER_FEHLER } }, 'Fehler je Tag, kein Teilplan')
|
||||
assert.match(FENSTER_FEHLER, /09:00–12:30, 13:30–18:00/)
|
||||
})
|
||||
|
||||
test('Wochenplan aus dem Formular nimmt der Server an (5-Minuten-Raster, aufsteigend, bis 24:00)', () => {
|
||||
const e = ausVorlage('salon', { name: 'Salon', email: 'a@b.de', telefon: '1', adresse: 'Str. 1' }, JETZT_SERVER)
|
||||
e.ressourcen[0].plan = planAusTexten({ 1: '07:05–12:00, 13:00–24:00', 7: '10:00–11:00' }).plan
|
||||
const erg = normalisiereEinstellung(e, { now: JETZT_SERVER })
|
||||
assert.equal(erg.fehler, undefined, JSON.stringify(erg.fehler))
|
||||
assert.deepEqual(erg.einstellung.ressourcen[0].plan, { 1: [['07:05', '12:00'], ['13:00', '24:00']], 7: [['10:00', '11:00']] })
|
||||
})
|
||||
|
||||
test('neueId: Präfix + 8 Zeichen a–z0–9, mit crypto.getRandomValues (Ruling R8)', () => {
|
||||
for (const p of ['r', 's', 'a']) for (let i = 0; i < 50; i += 1) assert.match(neueId(p), new RegExp(`^${p}_[a-z0-9]{8}$`))
|
||||
assert.equal(new Set(Array.from({ length: 2000 }, () => neueId('s'))).size, 2000, 'Zufall, keine Wiederholung')
|
||||
assert.throws(() => neueId('x'), /Präfix/)
|
||||
// Bytes ab 252 werden verworfen (252 = 7 · 36: sonst wären die ersten Zeichen häufiger); der Rest wird modulo 36 abgebildet
|
||||
assert.equal(neueId('r', () => Uint8Array.from([255, 252, 253, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])), 'r_abcdefgh')
|
||||
assert.equal(neueId('s', () => Uint8Array.from([35, 36, 71, 251, 250, 249, 248, 247, 0, 0, 0, 0, 0, 0, 0, 0])), 's_9a998765')
|
||||
// zu wenige brauchbare Bytes ⇒ es wird nachgezogen
|
||||
let aufrufe = 0
|
||||
const nurSpaeterBrauchbar = () => {
|
||||
aufrufe += 1
|
||||
return Uint8Array.from({ length: 16 }, (_, i) => (aufrufe === 1 ? 255 : i))
|
||||
}
|
||||
assert.equal(neueId('a', nurSpaeterBrauchbar), 'a_abcdefgh')
|
||||
assert.equal(aufrufe, 2)
|
||||
})
|
||||
|
||||
test('eindeutigeId: nie eine ID, die es in der Liste schon gibt', () => {
|
||||
const folgen = [Array.from({ length: 16 }, (_, i) => i), Array.from({ length: 16 }, (_, i) => i + 8)]
|
||||
let k = 0
|
||||
const bytes = () => Uint8Array.from(folgen[k++])
|
||||
assert.equal(eindeutigeId('r', ['r_abcdefgh'], bytes), 'r_ijklmnop')
|
||||
assert.equal(k, 2)
|
||||
assert.match(eindeutigeId('s', []), /^s_[a-z0-9]{8}$/)
|
||||
})
|
||||
|
||||
test('Preis: Euro-Text ⇔ Cent (deutsche Schreibweise), Anzeige wie beim Server', () => {
|
||||
const gut = { '45': 4500, '45,00': 4500, '45,5': 4550, '45.50': 4550, '1.234,50': 123450, '1.234': 123400, '1234,5': 123450, ' 19,99 € ': 1999, '0': 0, '0,05': 5, '10000': 1000000, '10000,01': 1000001, '12.5': 1250 }
|
||||
for (const [text, cent] of Object.entries(gut)) assert.equal(euroZuCent(text), cent, text)
|
||||
for (const schlecht of ['', ' ', 'abc', ',50', '12,345', '1,2,3', '-5', '1.2.3', '1e3', '99999999999', '4 5', null, undefined, 45]) assert.equal(euroZuCent(schlecht), null, String(schlecht))
|
||||
assert.equal(centZuEuro(4500), '45,00')
|
||||
assert.equal(centZuEuro(5), '0,05')
|
||||
assert.equal(centZuEuro(123450), '1234,50')
|
||||
assert.equal(centZuEuro(0), '0,00')
|
||||
assert.equal(centZuEuro(-1), '')
|
||||
assert.equal(centZuEuro(1.5), '')
|
||||
assert.equal(centZuEuro(undefined), '')
|
||||
assert.equal(euroZuCent(centZuEuro(123456)), 123456, 'Hin und zurück')
|
||||
for (const l of [{ preisArt: 'fest', preisCent: 4500 }, { preisArt: 'ab', preisCent: 4500 }, { preisArt: 'fest', preisCent: 123450 }, { preisArt: 'ab', preisCent: 5 }]) {
|
||||
assert.equal(preisAnzeige(l), preisText(l), `wie preisText des Servers: ${JSON.stringify(l)}`)
|
||||
}
|
||||
assert.equal(preisAnzeige({ preisArt: 'keiner', preisCent: 0 }), 'ohne Preis')
|
||||
assert.equal(preisAnzeige({ preisArt: 'fest', preisCent: 0 }), 'ohne Preis')
|
||||
assert.equal(preisAnzeige(undefined), 'ohne Preis')
|
||||
})
|
||||
|
||||
test('ganzzahl: nur ganze Zahlen aus Feldern, sonst null', () => {
|
||||
assert.equal(ganzzahl('45'), 45)
|
||||
assert.equal(ganzzahl(' 45 '), 45)
|
||||
assert.equal(ganzzahl('0'), 0)
|
||||
assert.equal(ganzzahl('-5'), -5)
|
||||
for (const schlecht of ['', ' ', '4.5', '4,5', 'abc', '1e3', '9999999999', null, undefined, 5]) assert.equal(ganzzahl(schlecht), null, String(schlecht))
|
||||
})
|
||||
|
||||
test('zusatzSchluessel: Schlüssel aus der Beschriftung, eindeutig, vom Server erlaubt', () => {
|
||||
assert.equal(zusatzSchluessel('Wunschfarbe', []), 'wunschfarbe')
|
||||
assert.equal(zusatzSchluessel('Größe in cm', []), 'groesse_in_cm')
|
||||
assert.equal(zusatzSchluessel(' Haar-Länge (Straße) ', []), 'haar_laenge_strasse')
|
||||
assert.equal(zusatzSchluessel('Haarlänge', ['haarlaenge']), 'haarlaenge_2')
|
||||
assert.equal(zusatzSchluessel('Haarlänge', ['haarlaenge', 'haarlaenge_2']), 'haarlaenge_3')
|
||||
assert.equal(zusatzSchluessel('!!!', []), 'feld')
|
||||
assert.equal(zusatzSchluessel('', ['feld']), 'feld_2')
|
||||
const lang = zusatzSchluessel('Eine sehr lange Beschriftung für das Feld', [])
|
||||
assert.match(lang, /^[a-z0-9_]{1,20}$/)
|
||||
const langZwei = zusatzSchluessel('Eine sehr lange Beschriftung für das Feld', [lang])
|
||||
assert.match(langZwei, /^[a-z0-9_]{1,20}$/)
|
||||
assert.notEqual(langZwei, lang)
|
||||
for (const reserviert of ['constructor', 'prototype', '__proto__']) {
|
||||
const key = zusatzSchluessel(reserviert, [])
|
||||
assert.match(key, /^[a-z0-9_]{1,20}$/)
|
||||
assert.ok(!['__proto__', 'constructor', 'prototype'].includes(key), `${reserviert} ⇒ ${key}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('Auswahlmöglichkeiten: eine je Zeile', () => {
|
||||
assert.deepEqual(optionenAusText('Rot\n Blau \r\n\nGrün\n'), ['Rot', 'Blau', 'Grün'])
|
||||
assert.deepEqual(optionenAusText(''), [])
|
||||
assert.deepEqual(optionenAusText(undefined), [])
|
||||
assert.equal(optionenText(['Rot', 'Blau']), 'Rot\nBlau')
|
||||
assert.equal(optionenText(undefined), '')
|
||||
})
|
||||
|
||||
test('verschiebeEintrag: Reihenfolge ändern, ohne die Liste zu verändern', () => {
|
||||
const liste = ['a', 'b', 'c']
|
||||
assert.deepEqual(verschiebeEintrag(liste, 1, -1), ['b', 'a', 'c'])
|
||||
assert.deepEqual(verschiebeEintrag(liste, 0, 1), ['b', 'a', 'c'])
|
||||
assert.deepEqual(verschiebeEintrag(liste, 2, 1), ['a', 'b', 'c'], 'am Ende nach unten: unverändert')
|
||||
assert.deepEqual(verschiebeEintrag(liste, 0, -1), ['a', 'b', 'c'], 'am Anfang nach oben: unverändert')
|
||||
assert.deepEqual(verschiebeEintrag(liste, 7, 1), ['a', 'b', 'c'], 'unbekannte Stelle')
|
||||
assert.deepEqual(liste, ['a', 'b', 'c'])
|
||||
assert.notEqual(verschiebeEintrag(liste, 0, -1), liste, 'immer eine Kopie')
|
||||
})
|
||||
|
||||
test('aktivSeitText: Datum nach Berliner Kalender', () => {
|
||||
assert.equal(aktivSeitText('2026-10-01T09:00:00.000Z'), 'Aktiv seit 01.10.2026')
|
||||
assert.equal(aktivSeitText('2026-09-30T22:30:00.000Z'), 'Aktiv seit 01.10.2026', 'nach Mitternacht in Berlin')
|
||||
assert.equal(aktivSeitText('2026-09-30T21:30:00.000Z'), 'Aktiv seit 30.09.2026')
|
||||
for (const schlecht of ['', 'gestern', null, undefined, 0]) assert.equal(aktivSeitText(schlecht), '', String(schlecht))
|
||||
})
|
||||
|
||||
test('Hinweis zu Gesundheitsangaben: Wortlaut wie bei den Gästen', () => {
|
||||
assert.equal(GESUNDHEIT_HINWEIS, 'Bitte keine Gesundheitsangaben.')
|
||||
})
|
||||
|
||||
// ---- Spiegel der Auswahl und Grenzen des Servers (einstellungen.js): driftet eine Seite, wird dieser Test rot
|
||||
const basisEinstellung = () => ausVorlage('salon', { name: 'Salon', email: 'a@b.de', telefon: '06301 1', adresse: 'Hauptstr. 1' }, JETZT_SERVER)
|
||||
/** Fehler des Servers für eine geänderte Einstellung ({} = gültig). */
|
||||
const serverFehler = (aendere) => {
|
||||
const e = basisEinstellung()
|
||||
aendere(e)
|
||||
return normalisiereEinstellung(e, { now: JETZT_SERVER }).fehler || {}
|
||||
}
|
||||
|
||||
test('Spiegel: Auswahllisten sind die des Servers', () => {
|
||||
assert.deepEqual(RASTER_MIN, [5, 10, 15, 20, 30, 60])
|
||||
for (const r of RASTER_MIN) assert.equal(serverFehler((e) => { e.regeln.raster = r })['regeln.raster'], undefined, `Raster ${r}`)
|
||||
for (const r of [0, 7, 45, 90]) assert.ok(serverFehler((e) => { e.regeln.raster = r })['regeln.raster'], `Raster ${r} gibt es nicht`)
|
||||
assert.deepEqual(ERINNERUNG_STUNDEN, [2, 12, 24, 48])
|
||||
for (const n of ERINNERUNG_STUNDEN) assert.equal(serverFehler((e) => { e.erinnerung.stundenVorher = n })['erinnerung.stundenVorher'], undefined, `${n} Stunden`)
|
||||
assert.ok(serverFehler((e) => { e.erinnerung.stundenVorher = 3 })['erinnerung.stundenVorher'])
|
||||
assert.deepEqual(GAST_FELD_WAHL.map(([key]) => key), ['aus', 'optional', 'pflicht'])
|
||||
for (const [key] of GAST_FELD_WAHL) {
|
||||
assert.deepEqual(serverFehler((e) => { e.gastFelder.telefon = key; e.gastFelder.nachricht = key }), {}, key)
|
||||
}
|
||||
assert.deepEqual(new Set(BUNDESLAENDER.map(([key]) => key)), new Set(SERVER_BUNDESLAENDER), 'alle 16 Bundesländer, keines zu viel')
|
||||
assert.equal(BUNDESLAENDER.length, 16)
|
||||
assert.deepEqual(BUNDESLAENDER.map(([, name]) => name), [...BUNDESLAENDER.map(([, name]) => name)].sort((a, b) => a.localeCompare(b, 'de')), 'nach Namen sortiert')
|
||||
for (const [key] of BUNDESLAENDER) assert.deepEqual(serverFehler((e) => { e.bundesland = key }), {}, key)
|
||||
assert.deepEqual(ANREDE_WAHL.map(([key]) => key), ['sie', 'du'])
|
||||
for (const [key] of ANREDE_WAHL) assert.deepEqual(serverFehler((e) => { e.anrede = key }), {}, key)
|
||||
})
|
||||
|
||||
test('Spiegel: Zahlarten und ihre Namen sind die des Servers', () => {
|
||||
const keys = ZAHLARTEN.map(([key]) => key)
|
||||
assert.deepEqual(keys, ['bar', 'ec', 'kreditkarte', 'ueberweisung', 'paypal'])
|
||||
const e = basisEinstellung()
|
||||
e.zahlung.vorOrtArten = keys
|
||||
const erg = normalisiereEinstellung(e, { now: JETZT_SERVER })
|
||||
assert.equal(erg.fehler, undefined)
|
||||
assert.deepEqual(erg.einstellung.zahlung.vorOrtArten, keys, 'Reihenfolge wie beim Server')
|
||||
assert.equal(zahlungText(erg.einstellung), `Bezahlung vor Ort: ${ZAHLARTEN.map(([, name]) => name).join(', ')}`)
|
||||
assert.ok(serverFehler((x) => { x.zahlung.vorOrtArten = ['bitcoin'] })['zahlung.vorOrtArten'])
|
||||
})
|
||||
|
||||
test('Spiegel: Obergrenzen der Listen sind die des Servers', () => {
|
||||
assert.deepEqual(LIMITS, { ressourcen: 20, leistungen: 50, abwesenheiten: 100, sonderoeffnungen: 200, zusatzfelder: 5, optionen: 10 })
|
||||
const tag = (i) => new Date(Date.UTC(2027, 0, 1 + i)).toISOString().slice(0, 10)
|
||||
const faelle = {
|
||||
ressourcen: [LIMITS.ressourcen, 'ressourcen', (e, n) => { e.leistungen = []; e.abwesenheiten = []; e.ressourcen = Array.from({ length: n }, (_, i) => ({ id: neueId('r'), typ: 'person', name: `P${i}`, aktiv: true, plan: {} })) }],
|
||||
leistungen: [LIMITS.leistungen, 'leistungen', (e, n) => { e.leistungen = Array.from({ length: n }, (_, i) => ({ ...e.leistungen[0], id: neueId('s'), name: `L${i}` })) }],
|
||||
abwesenheiten: [LIMITS.abwesenheiten, 'abwesenheiten', (e, n) => { e.abwesenheiten = Array.from({ length: n }, (_, i) => ({ id: neueId('a'), ressourceId: null, von: tag(i), bis: tag(i), titel: '' })) }],
|
||||
sonderoeffnungen: [LIMITS.sonderoeffnungen, 'sonderoeffnungen', (e, n) => { e.sonderoeffnungen = Object.fromEntries(Array.from({ length: n }, (_, i) => [tag(i), []])) }],
|
||||
zusatzfelder: [LIMITS.zusatzfelder, 'gastFelder.zusatz', (e, n) => { e.gastFelder.zusatz = Array.from({ length: n }, (_, i) => ({ key: `f${i}`, label: `F${i}`, typ: 'text', optionen: [], pflicht: false })) }],
|
||||
optionen: [LIMITS.optionen, 'gastFelder.zusatz.0.optionen', (e, n) => { e.gastFelder.zusatz = [{ key: 'wahl', label: 'Wahl', typ: 'auswahl', optionen: Array.from({ length: n }, (_, i) => `O${i}`), pflicht: false }] }],
|
||||
}
|
||||
for (const [name, [grenze, pfad, baue]] of Object.entries(faelle)) {
|
||||
assert.equal(serverFehler((e) => baue(e, grenze))[pfad], undefined, `${name}: ${grenze} sind erlaubt`)
|
||||
assert.ok(serverFehler((e) => baue(e, grenze + 1))[pfad], `${name}: ${grenze + 1} sind zu viele`)
|
||||
}
|
||||
})
|
||||
|
||||
test('Spiegel: Längen der Eingabefelder (maxlength) kürzt der Server nicht mehr', () => {
|
||||
const lang = (n) => 'x'.repeat(n)
|
||||
const lies = (e, was) => was(normalisiereEinstellung(e, { now: JETZT_SERVER }).einstellung)
|
||||
const faelle = [
|
||||
['betriebName', (e, t) => { e.betrieb.name = t }, (x) => x.betrieb.name],
|
||||
['telefon', (e, t) => { e.betrieb.telefon = t }, (x) => x.betrieb.telefon],
|
||||
['adresse', (e, t) => { e.betrieb.adresse = t }, (x) => x.betrieb.adresse],
|
||||
['leistungName', (e, t) => { e.leistungen[0].name = t }, (x) => x.leistungen[0].name],
|
||||
['beschreibung', (e, t) => { e.leistungen[0].beschreibung = t }, (x) => x.leistungen[0].beschreibung],
|
||||
['ressourcenName', (e, t) => { e.ressourcen[0].name = t }, (x) => x.ressourcen[0].name],
|
||||
['abwesenheitTitel', (e, t) => { e.abwesenheiten = [{ id: 'a_abcd1234', ressourceId: null, von: '2027-01-01', bis: '2027-01-02', titel: t }] }, (x) => x.abwesenheiten[0].titel],
|
||||
['zusatzLabel', (e, t) => { e.gastFelder.zusatz = [{ key: 'k', label: t, typ: 'text', optionen: [], pflicht: false }] }, (x) => x.gastFelder.zusatz[0].label],
|
||||
['option', (e, t) => { e.gastFelder.zusatz = [{ key: 'k', label: 'K', typ: 'auswahl', optionen: [t], pflicht: false }] }, (x) => x.gastFelder.zusatz[0].optionen[0]],
|
||||
['storno', (e, t) => { e.storno = { text: t } }, (x) => x.storno.text],
|
||||
]
|
||||
for (const [name, setze, lese] of faelle) {
|
||||
const grenze = MAX[name === 'telefon' ? 'telefon' : name]
|
||||
assert.ok(Number.isInteger(grenze), `MAX.${name}`)
|
||||
const e = basisEinstellung()
|
||||
setze(e, lang(grenze))
|
||||
assert.equal(lies(e, lese).length, grenze, `${name}: ${grenze} Zeichen bleiben ganz`)
|
||||
const zuLang = basisEinstellung()
|
||||
setze(zuLang, lang(grenze + 1))
|
||||
assert.equal(lies(zuLang, lese).length, grenze, `${name}: mehr als ${grenze} würden gekürzt`)
|
||||
}
|
||||
assert.equal(MAX.url, 300)
|
||||
const url = (n) => `https://example.org/${'a'.repeat(n - 'https://example.org/'.length)}`
|
||||
assert.equal(serverFehler((e) => { e.betrieb.impressumUrl = url(300) })['betrieb.impressumUrl'], undefined)
|
||||
assert.ok(serverFehler((e) => { e.betrieb.impressumUrl = url(301) })['betrieb.impressumUrl'])
|
||||
})
|
||||
|
||||
test('Mustertext Storno (Gästetext): wie die Vorlagen des Servers, Frist einsetzbar', () => {
|
||||
assert.equal(stornoMuster(24), VORLAGEN.salon.storno, 'derselbe Wortlaut wie in den Vorlagen')
|
||||
assert.ok(stornoMuster(48).includes('bis 48 Stunden vorher'))
|
||||
assert.ok(stornoMuster(48).includes('50 % des Preises'))
|
||||
for (const unbrauchbar of [0, -1, 1.5, NaN, 'x', null, undefined]) assert.equal(stornoMuster(unbrauchbar), stornoMuster(24), String(unbrauchbar))
|
||||
const e = basisEinstellung()
|
||||
e.storno = { text: stornoMuster(36) }
|
||||
assert.equal(normalisiereEinstellung(e, { now: JETZT_SERVER }).einstellung.storno.text, stornoMuster(36), 'der Server ändert den Text nicht')
|
||||
assert.deepEqual(ANREDE_WAHL.map(([, name]) => name), ['Sie (förmlich)', 'Du (persönlich)'])
|
||||
})
|
||||
Reference in new issue
Block a user