Files
Webklar-Kundenbereich/test/termineFragebogenView.test.js
T
ClaudeandClaude Sonnet 5.5 edaefd4237 Terminplaner TP2: Fragebogen-Helfer im Client
Reine Helfer in public/termine-view.js: FRAGEBOGEN_SCHRITTE (Spiegel von
SCHRITTE und SCHRITT_TITEL), schrittStatus, ersterOffenerSchritt,
ressourcenAusNamen, buchungsartFuerAlle, buchungsartLage und
zusammenfassung (acht Gruppen, nur Text, Bundesland nur bei ausdruecklicher
Wahl, nie feedToken). Tests in test/termineFragebogenView.test.js, Spiegel-Test
in test/termineView.test.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 20:48:10 +00:00

456 lines
28 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 * as ansicht from '../public/termine-view.js'
import {
FRAGEBOGEN_SCHRITTE, LIMITS, MAX, buchungsartFuerAlle, buchungsartLage, ersterOffenerSchritt, ressourcenAusNamen, schrittStatus, zusammenfassung,
} from '../public/termine-view.js'
import { BRANCHEN, SCHRITTE, SCHRITT_TITEL, leerFragebogen } from '../server/services/termine/fragebogen.js'
import { normalisiereEinstellung } from '../server/services/termine/einstellungen.js'
import { ausVorlage } from '../server/services/termine/vorlagen.js'
// Reine Helfer des Fragebogens in public/termine-view.js (TP2, Task C7a): Schrittstand, Namen der Personen/Plaetze, sofort/Anfrage fuer alle
// und die Zusammenfassung auf der Bestellseite. Kein DOM; der Spiegel zum Server (SCHRITTE) steht in test/termineView.test.js.
const JETZT = new Date('2026-10-05T08:00:00.000Z')
const BETRIEB = {
name: 'Salon Muster',
email: 'salon@example.invalid',
telefon: '06301 12345',
adresse: 'Hauptstraße 1, 67655 Kaiserslautern',
impressumUrl: 'https://salon.example.invalid/impressum',
datenschutzUrl: 'https://salon.example.invalid/datenschutz',
bundesland: 'RP',
}
const salon = () => ausVorlage('salon', BETRIEB, JETZT)
/** Zaehlende neueId: r_test0001, r_test0002 … (bekommt die belegten IDs, braucht sie hier nicht). */
const zaehler = () => {
let n = 0
return () => `r_test${String(++n).padStart(4, '0')}`
}
/** Zwei Ressourcen mit verschiedenen Plaenen, wie sie ein Entwurf haben kann. */
const zweiRessourcen = () => [
{ id: 'r_anna0001', typ: 'person', name: 'Anna', aktiv: true, plan: { 1: [['09:00', '12:00']] }, feedToken: 'a'.repeat(24) },
{ id: 'r_ben00002', typ: 'person', name: 'Ben', aktiv: true, plan: { 2: [['13:00', '17:00']] }, feedToken: 'b'.repeat(24) },
]
const namenVon = (ressourcen) => ressourcen.map((r) => r.name)
const idsVon = (ressourcen) => ressourcen.map((r) => r.id)
// ---------------------------------------------------------------- Schritte
test('FRAGEBOGEN_SCHRITTE: acht Paare [Schlüssel, Titel], Schlüssel und Titel wie beim Server', () => {
assert.equal(FRAGEBOGEN_SCHRITTE.length, 8)
for (const eintrag of FRAGEBOGEN_SCHRITTE) {
assert.equal(eintrag.length, 2)
assert.equal(typeof eintrag[0], 'string')
assert.equal(typeof eintrag[1], 'string')
assert.ok(eintrag[1].length > 0)
}
assert.deepEqual(FRAGEBOGEN_SCHRITTE.map(([key]) => key), [...SCHRITTE])
assert.deepEqual(FRAGEBOGEN_SCHRITTE.map(([, titel]) => titel), SCHRITTE.map((key) => SCHRITT_TITEL[key]))
})
test('schrittStatus: leerer Fragebogen ⇒ nichts erledigt, Reihenfolge und Titel wie FRAGEBOGEN_SCHRITTE', () => {
const erwartet = FRAGEBOGEN_SCHRITTE.map(([key, titel]) => ({ key, titel, erledigt: false }))
assert.deepEqual(schrittStatus(leerFragebogen()), erwartet)
assert.deepEqual(schrittStatus(null), erwartet, 'noch kein Fragebogen gespeichert')
assert.deepEqual(schrittStatus(undefined), erwartet)
assert.deepEqual(schrittStatus({ bestaetigt: 'wer' }), erwartet, 'kein Array ⇒ nichts erledigt')
assert.deepEqual(schrittStatus({}), erwartet)
})
test('schrittStatus: teilweise bestätigt, Fremdes zählt nicht', () => {
const status = schrittStatus({ bestaetigt: ['zeiten', 'branche', 'wer', 'bestellen', 'unbekannt'] })
assert.deepEqual(status.map((s) => s.key), [...SCHRITTE])
assert.deepEqual(status.filter((s) => s.erledigt).map((s) => s.key), ['branche', 'wer', 'zeiten'])
assert.equal(status.length, 8, '„bestellen“ ist kein Schritt des Fragebogens')
})
test('schrittStatus: vollständig bestätigt ⇒ alles erledigt', () => {
assert.ok(schrittStatus({ bestaetigt: [...SCHRITTE] }).every((s) => s.erledigt === true))
})
test('ersterOffenerSchritt: leer ⇒ branche, teilweise ⇒ der erste Schritt ohne Haken, vollständig ⇒ bestellen', () => {
assert.equal(ersterOffenerSchritt(leerFragebogen()), 'branche')
assert.equal(ersterOffenerSchritt(null), 'branche')
assert.equal(ersterOffenerSchritt(undefined), 'branche')
assert.equal(ersterOffenerSchritt({ bestaetigt: ['branche', 'wer'] }), 'leistungen')
assert.equal(ersterOffenerSchritt({ bestaetigt: ['branche', 'leistungen'] }), 'wer', 'die erste Lücke zählt, nicht die Zahl der Haken')
assert.equal(ersterOffenerSchritt({ bestaetigt: SCHRITTE.slice(0, 7) }), 'website')
assert.equal(ersterOffenerSchritt({ bestaetigt: [...SCHRITTE] }), 'bestellen')
})
// ---------------------------------------------------------------- ressourcenAusNamen
test('ressourcenAusNamen: „Stylist/in“ → „Anna“ behält ID und Plan, der zweite Name bekommt neue ID und den Plan von Anna', () => {
const bisher = salon().ressourcen
assert.equal(bisher.length, 1)
assert.equal(bisher[0].name, 'Stylist/in')
const erg = ressourcenAusNamen({ bisher, typ: 'person', namen: ['Anna', 'Ben'], neueId: zaehler() })
assert.deepEqual(namenVon(erg.ressourcen), ['Anna', 'Ben'])
const [anna, ben] = erg.ressourcen
assert.equal(anna.id, bisher[0].id, 'Anna behält die ID')
assert.deepEqual(anna.plan, bisher[0].plan, 'und den Plan')
assert.equal(ben.id, 'r_test0001')
assert.deepEqual(ben.plan, anna.plan, 'Ben bekommt den Plan von Anna …')
assert.notEqual(ben.plan, anna.plan, '… als Kopie')
const laenge = anna.plan[2].length
ben.plan[2].push(['20:00', '21:00'])
assert.equal(anna.plan[2].length, laenge, 'Änderungen an Ben verändern Anna nicht')
assert.equal(bisher[0].plan[2].length, laenge, 'und nicht den alten Eintrag')
assert.deepEqual(erg.neu, ['r_test0001'])
assert.deepEqual(erg.entfernt, [])
assert.ok(erg.ressourcen.every((r) => r.aktiv === true && r.typ === 'person'))
})
test('ressourcenAusNamen: gleiche Namen behalten ID und Plan, auch in anderer Reihenfolge und Schreibweise', () => {
const bisher = zweiRessourcen()
const erg = ressourcenAusNamen({ bisher, typ: 'person', namen: ['ben', 'Anna'], neueId: zaehler() })
assert.deepEqual(idsVon(erg.ressourcen), ['r_ben00002', 'r_anna0001'], 'Reihenfolge der Namen, IDs bleiben')
assert.deepEqual(namenVon(erg.ressourcen), ['ben', 'Anna'], 'die eingegebene Schreibweise gilt')
assert.deepEqual(erg.ressourcen[0].plan, bisher[1].plan)
assert.deepEqual(erg.ressourcen[1].plan, bisher[0].plan)
assert.deepEqual(erg.neu, [])
assert.deepEqual(erg.entfernt, [])
})
test('ressourcenAusNamen: Entfernen liefert die IDs der weggefallenen Einträge', () => {
const bisher = [...zweiRessourcen(), { id: 'r_cem00003', typ: 'person', name: 'Cem', aktiv: true, plan: {} }]
const erg = ressourcenAusNamen({ bisher, typ: 'person', namen: ['Ben'], neueId: zaehler() })
assert.deepEqual(idsVon(erg.ressourcen), ['r_ben00002'])
assert.deepEqual(erg.entfernt, ['r_anna0001', 'r_cem00003'])
assert.deepEqual(erg.neu, [])
const alle = ressourcenAusNamen({ bisher, typ: 'person', namen: [], neueId: zaehler() })
assert.deepEqual(alle.ressourcen, [])
assert.deepEqual(alle.entfernt, ['r_anna0001', 'r_ben00002', 'r_cem00003'])
})
test('ressourcenAusNamen: übrige alte Einträge werden der Reihe nach umbenannt', () => {
const bisher = zweiRessourcen()
const umbenannt = ressourcenAusNamen({ bisher, typ: 'person', namen: ['Xaver', 'Yvonne'], neueId: zaehler() })
assert.deepEqual(idsVon(umbenannt.ressourcen), ['r_anna0001', 'r_ben00002'])
assert.deepEqual(umbenannt.ressourcen.map((r) => r.plan), [bisher[0].plan, bisher[1].plan])
assert.deepEqual([umbenannt.neu, umbenannt.entfernt], [[], []])
// Ben bleibt, Dora übernimmt Annas Platz in der Liste, Cem fällt weg
const mitCem = [...bisher, { id: 'r_cem00003', typ: 'person', name: 'Cem', aktiv: true, plan: { 3: [['08:00', '09:00']] } }]
const gemischt = ressourcenAusNamen({ bisher: mitCem, typ: 'person', namen: ['Ben', 'Dora'], neueId: zaehler() })
assert.deepEqual(namenVon(gemischt.ressourcen), ['Ben', 'Dora'])
assert.deepEqual(idsVon(gemischt.ressourcen), ['r_ben00002', 'r_anna0001'])
assert.deepEqual(gemischt.ressourcen[1].plan, bisher[0].plan)
assert.deepEqual(gemischt.entfernt, ['r_cem00003'])
assert.deepEqual(gemischt.neu, [])
})
test('ressourcenAusNamen: trimmt, entfernt Leere und Doppelte ohne Rücksicht auf Groß-/Kleinschreibung', () => {
const erg = ressourcenAusNamen({ bisher: [], typ: 'person', namen: [' Anna ', '', ' ', 'anna', 'ANNA', 'Ben', ' Ben', null, undefined, 5], neueId: zaehler() })
assert.deepEqual(namenVon(erg.ressourcen), ['Anna', 'Ben'], 'die erste Schreibweise gilt')
assert.equal(idsVon(erg.ressourcen).length, 2)
assert.deepEqual(namenVon(ressourcenAusNamen({ bisher: [], typ: 'person', namen: ['Anna Müller', 'anna müller'] }).ressourcen), ['Anna Müller'], 'mehrfache Leerzeichen zählen als eines (wie beim Server)')
})
test('ressourcenAusNamen: höchstens LIMITS.ressourcen Einträge', () => {
const namen = Array.from({ length: LIMITS.ressourcen + 5 }, (_, i) => `Person ${i + 1}`)
const erg = ressourcenAusNamen({ bisher: [], typ: 'person', namen, neueId: zaehler() })
assert.equal(LIMITS.ressourcen, 20)
assert.equal(erg.ressourcen.length, 20)
assert.deepEqual(namenVon(erg.ressourcen), namen.slice(0, 20), 'die ersten 20 bleiben')
assert.equal(erg.neu.length, 20)
// Leere und Doppelte zählen nicht mit
const mitLuecken = ['', ...namen.slice(0, 20).flatMap((n) => [n, n.toUpperCase()]), 'Überzählig']
assert.deepEqual(namenVon(ressourcenAusNamen({ bisher: [], typ: 'person', namen: mitLuecken }).ressourcen), namen.slice(0, 20))
// genau LIMITS.ressourcen bleibt ganz
assert.equal(ressourcenAusNamen({ bisher: [], typ: 'person', namen: namen.slice(0, 20) }).ressourcen.length, 20)
})
test('ressourcenAusNamen: Namen werden auf MAX.ressourcenName Zeichen gekürzt', () => {
const erg = ressourcenAusNamen({ bisher: [], typ: 'person', namen: ['x'.repeat(MAX.ressourcenName + 20)] })
assert.equal([...erg.ressourcen[0].name].length, MAX.ressourcenName)
})
test('ressourcenAusNamen: typ gilt für alle, auch für behaltene Einträge', () => {
const bisher = zweiRessourcen()
const erg = ressourcenAusNamen({ bisher, typ: 'platz', namen: ['Anna', 'Platz 2', 'Platz 3'], neueId: zaehler() })
assert.ok(erg.ressourcen.every((r) => r.typ === 'platz'))
assert.equal(erg.ressourcen[0].id, 'r_anna0001')
assert.deepEqual(bisher.map((r) => r.typ), ['person', 'person'], 'bisher bleibt unverändert')
assert.ok(ressourcenAusNamen({ bisher, typ: 'person', namen: ['A'] }).ressourcen.every((r) => r.typ === 'person'))
assert.equal(ressourcenAusNamen({ bisher: [], typ: 'unsinn', namen: ['A'] }).ressourcen[0].typ, 'person', 'unbekannter Typ ⇒ Person')
})
test('ressourcenAusNamen: ändert die Eingaben nicht, liefert nur Felder, die der Server kennt (kein feedToken)', () => {
const bisher = zweiRessourcen()
const vorher = JSON.stringify(bisher)
const namen = ['Ben', ' Anna ', 'Cem']
const namenVorher = [...namen]
const erg = ressourcenAusNamen({ bisher, typ: 'platz', namen, neueId: zaehler() })
assert.equal(JSON.stringify(bisher), vorher)
assert.deepEqual(namen, namenVorher)
for (const r of erg.ressourcen) assert.deepEqual(Object.keys(r).sort(), ['aktiv', 'id', 'name', 'plan', 'typ'])
assert.ok(!JSON.stringify(erg).includes('feedToken'))
assert.deepEqual(ressourcenAusNamen({ bisher: null, typ: 'person', namen: ['A'], neueId: zaehler() }).entfernt, [], 'ohne bisher: nichts zu entfernen')
assert.deepEqual(ressourcenAusNamen({ bisher, typ: 'person', namen: 'Anna', neueId: zaehler() }).ressourcen, [], 'keine Liste ⇒ keine Namen')
})
test('ressourcenAusNamen: neue IDs sind gültig und kollidieren nicht (Vorgabe ohne neueId, Zähler ohne Treffer)', () => {
const bisher = zweiRessourcen()
const namen = Array.from({ length: 15 }, (_, i) => `Neu ${i}`)
const erg = ressourcenAusNamen({ bisher, typ: 'person', namen })
const ids = idsVon(erg.ressourcen)
assert.equal(new Set(ids).size, ids.length)
assert.ok(ids.every((id) => /^r_[a-z0-9]{8}$/.test(id)))
assert.equal(erg.neu.length, 13, 'zwei der Namen übernehmen die alten Einträge')
assert.ok(erg.neu.every((id) => ids.includes(id) && !bisher.some((r) => r.id === id)))
// neueId bekommt die belegten IDs (Set) und darf nur unbelegte liefern
const gesehen = []
ressourcenAusNamen({ bisher, typ: 'person', namen: ['Anna', 'Ben', 'Cem', 'Dora'], neueId: (belegt) => { gesehen.push([...belegt]); return `r_zzzz000${gesehen.length}` } })
assert.equal(gesehen.length, 2)
assert.ok(gesehen[0].includes('r_anna0001') && gesehen[0].includes('r_ben00002'), 'behaltene IDs sind belegt')
assert.ok(gesehen[1].includes('r_zzzz0001'), 'und die schon vergebenen neuen')
// liefert neueId eine belegte ID, ist das ein Fehler (der Server würde doppelte IDs ablehnen)
assert.throws(() => ressourcenAusNamen({ bisher, typ: 'person', namen: ['Anna', 'Ben', 'Cem'], neueId: () => 'r_anna0001' }), /ID/)
})
test('ressourcenAusNamen: das Ergebnis besteht normalisiereEinstellung (Leistungen allen zugeordnet), für Personen und Plätze', () => {
for (const [typ, namen] of [['person', ['Anna', 'Ben', 'Cem']], ['platz', ['Platz 1', 'Platz 2']], ['person', ['Nur Ich']]]) {
const e = salon()
const erg = ressourcenAusNamen({ bisher: e.ressourcen, typ, namen })
e.ressourcen = erg.ressourcen
e.leistungen = e.leistungen.map((l) => ({ ...l, ressourcen: idsVon(erg.ressourcen) }))
const geprueft = normalisiereEinstellung(e, { now: JETZT })
assert.equal(geprueft.fehler, undefined, `${typ}: ${JSON.stringify(geprueft.fehler)}`)
assert.deepEqual(namenVon(geprueft.einstellung.ressourcen), namen)
assert.ok(geprueft.einstellung.ressourcen.every((r) => r.typ === typ && r.aktiv === true))
assert.ok(geprueft.einstellung.ressourcen.every((r) => typeof r.feedToken === 'string' && r.feedToken.length > 0), 'der Server vergibt die Feed-Token')
}
})
// ---------------------------------------------------------------- Buchungsart
test('buchungsartFuerAlle: ändert nur buchungsart, an einer Kopie', () => {
const leistungen = salon().leistungen
const vorher = JSON.stringify(leistungen)
const anfrage = buchungsartFuerAlle(leistungen, 'anfrage')
assert.equal(JSON.stringify(leistungen), vorher, 'Eingabe unverändert')
assert.equal(anfrage.length, leistungen.length)
assert.ok(anfrage.every((l) => l.buchungsart === 'anfrage'))
assert.deepEqual(anfrage.map(({ buchungsart, ...rest }) => rest), leistungen.map(({ buchungsart, ...rest }) => rest), 'alles andere bleibt')
assert.notEqual(anfrage[0], leistungen[0])
const zurueck = buchungsartFuerAlle(anfrage, 'sofort')
assert.deepEqual(zurueck, leistungen)
})
test('buchungsartFuerAlle: inaktive Leistungen auch, unbekannte Art und Nicht-Liste ändern nichts', () => {
const leistungen = [{ id: 's_aaaaaaaa', name: 'A', aktiv: false, buchungsart: 'sofort' }, { id: 's_bbbbbbbb', name: 'B', aktiv: true, buchungsart: 'anfrage' }]
assert.deepEqual(buchungsartFuerAlle(leistungen, 'anfrage').map((l) => l.buchungsart), ['anfrage', 'anfrage'])
assert.deepEqual(buchungsartFuerAlle(leistungen, 'bar'), leistungen)
assert.deepEqual(buchungsartFuerAlle(leistungen, undefined), leistungen)
assert.deepEqual(buchungsartFuerAlle(null, 'sofort'), [])
assert.deepEqual(buchungsartFuerAlle(undefined, 'sofort'), [])
})
test('buchungsartLage: sofort, anfrage oder gemischt', () => {
const l = (...arten) => arten.map((buchungsart, i) => ({ id: `s_${i}`, buchungsart }))
assert.equal(buchungsartLage(l('sofort', 'sofort')), 'sofort')
assert.equal(buchungsartLage(l('anfrage', 'anfrage', 'anfrage')), 'anfrage')
assert.equal(buchungsartLage(l('sofort', 'anfrage')), 'gemischt')
assert.equal(buchungsartLage(l('anfrage')), 'anfrage')
assert.equal(buchungsartLage([]), 'sofort', 'ohne Leistungen gilt die Vorgabe des Servers')
assert.equal(buchungsartLage(null), 'sofort')
assert.equal(buchungsartLage([{ name: 'ohne Angabe' }, { buchungsart: 'anfrage' }]), 'gemischt', 'fehlende Angabe = sofort (Vorgabe des Servers)')
assert.equal(buchungsartLage(ausVorlage('handwerk', BETRIEB, JETZT).leistungen), 'anfrage', 'Vorlage mit Anfragen')
assert.equal(buchungsartLage(salon().leistungen), 'sofort')
})
test('buchungsartLage nach buchungsartFuerAlle: zeigt genau die gesetzte Art', () => {
const leistungen = [{ buchungsart: 'sofort' }, { buchungsart: 'anfrage' }]
assert.equal(buchungsartLage(leistungen), 'gemischt')
assert.equal(buchungsartLage(buchungsartFuerAlle(leistungen, 'anfrage')), 'anfrage')
assert.equal(buchungsartLage(buchungsartFuerAlle(leistungen, 'sofort')), 'sofort')
})
// ---------------------------------------------------------------- zusammenfassung
const stand = (aendere = () => {}) => {
const einstellung = salon()
const fragebogen = { ...leerFragebogen(), branche: 'salon', bestaetigt: [...SCHRITTE], bundeslandGewaehlt: true, preiseGeprueft: true, einbau: { ...leerFragebogen().einbau, modus: 'inline' } }
aendere(einstellung, fragebogen)
return { einstellung, fragebogen }
}
const gruppe = (ergebnis, schritt) => ergebnis.find((g) => g.schritt === schritt)
test('zusammenfassung: acht Gruppen in der Reihenfolge der Schritte, nur Text', () => {
const { einstellung, fragebogen } = stand()
const erg = zusammenfassung(einstellung, fragebogen, BRANCHEN)
assert.deepEqual(erg.map((g) => g.schritt), [...SCHRITTE])
assert.deepEqual(erg.map((g) => g.titel), FRAGEBOGEN_SCHRITTE.map(([, titel]) => titel))
for (const g of erg) {
assert.deepEqual(Object.keys(g).sort(), ['schritt', 'titel', 'zeilen'])
assert.ok(Array.isArray(g.zeilen) && g.zeilen.length > 0, `${g.schritt}: Zeilen`)
assert.ok(g.zeilen.every((z) => typeof z === 'string' && z.trim() !== ''), `${g.schritt}: nur nicht-leere Texte`)
}
})
test('zusammenfassung (Vorlage salon): Name, Branche und Anschrift', () => {
const { einstellung, fragebogen } = stand()
const zeilen = gruppe(zusammenfassung(einstellung, fragebogen, BRANCHEN), 'branche').zeilen
assert.ok(zeilen.includes('Branche: Friseur und Salon'))
assert.ok(zeilen.includes('Betrieb: Salon Muster'))
assert.ok(zeilen.includes('Anschrift: Hauptstraße 1, 67655 Kaiserslautern'))
})
test('zusammenfassung (Vorlage salon): Leistungen mit Preis', () => {
const { einstellung, fragebogen } = stand()
const zeilen = gruppe(zusammenfassung(einstellung, fragebogen, BRANCHEN), 'leistungen').zeilen
assert.ok(zeilen.includes('Herrenschnitt: 30 Min., 28,00 €'), zeilen.join(' | '))
assert.ok(zeilen.includes('Damenschnitt: 45 Min., ab 45,00 €'))
assert.ok(zeilen.includes('Kinderschnitt: 30 Min., 18,00 €'))
assert.ok(zeilen.includes('Färben: 120 Min., ab 75,00 €'))
assert.ok(zeilen.includes('Preise geprüft: ja'))
const offen = stand((e, f) => { f.preiseGeprueft = false })
assert.ok(gruppe(zusammenfassung(offen.einstellung, offen.fragebogen, BRANCHEN), 'leistungen').zeilen.includes('Preise geprüft: nein'))
const ohnePreis = stand((e) => { e.leistungen[0] = { ...e.leistungen[0], preisArt: 'keiner', preisCent: 0, aktiv: false } })
const z = gruppe(zusammenfassung(ohnePreis.einstellung, ohnePreis.fragebogen, BRANCHEN), 'leistungen').zeilen
assert.ok(z.includes('Herrenschnitt: 30 Min., ohne Preis (nicht aktiv)'), z.join(' | '))
})
test('zusammenfassung (Vorlage salon): Wochenplan, Tage mit gleichen Zeiten zusammengefasst, Ruhetage', () => {
const { einstellung, fragebogen } = stand()
const zeilen = gruppe(zusammenfassung(einstellung, fragebogen, BRANCHEN), 'zeiten').zeilen
assert.ok(zeilen.includes('Dienstag–Freitag: 09:00–12:30, 13:30–18:00'), zeilen.join(' | '))
assert.ok(zeilen.includes('Samstag: 09:00–13:00'))
assert.ok(zeilen.includes('Geschlossen: Montag, Sonntag'))
const mehrere = stand((e) => { e.ressourcen = [{ ...e.ressourcen[0], name: 'Anna' }, { ...e.ressourcen[0], id: 'r_zweite01', name: 'Ben', plan: { 1: [['10:00', '11:00']] } }] })
const z = gruppe(zusammenfassung(mehrere.einstellung, mehrere.fragebogen, BRANCHEN), 'zeiten').zeilen
assert.ok(z.includes('Anna: Dienstag–Freitag: 09:00–12:30, 13:30–18:00'), z.join(' | '))
assert.ok(z.includes('Ben: Montag: 10:00–11:00'))
assert.ok(z.includes('Ben: Geschlossen: Dienstag–Sonntag'))
})
test('zusammenfassung: wer wird gebucht, Zeitraum-Angaben (Feiertage, Urlaub, besondere Tage)', () => {
const { einstellung, fragebogen } = stand((e) => {
e.ressourcen = [{ id: 'r_aaaaaaaa', typ: 'person', name: 'Anna', aktiv: true, plan: {} }, { id: 'r_bbbbbbbb', typ: 'person', name: 'Ben', aktiv: false, plan: {} }]
e.abwesenheiten = [{ id: 'a_aaaaaaaa', ressourceId: null, von: '2027-01-01', bis: '2027-01-02', titel: '' }]
e.sonderoeffnungen = { '2026-12-24': [['09:00', '12:00']], '2026-12-31': [] }
e.feiertageGeschlossen = true
})
const erg = zusammenfassung(einstellung, fragebogen, BRANCHEN)
assert.deepEqual(gruppe(erg, 'wer').zeilen, ['Personen: Anna, Ben (nicht aktiv)'])
const zeiten = gruppe(erg, 'zeiten').zeilen
assert.ok(zeiten.includes('Feiertage: geschlossen'))
assert.ok(zeiten.includes('Abwesenheiten: 1 eingetragen'))
assert.ok(zeiten.includes('Besondere Öffnungstage: 2'))
const plaetze = stand((e) => { e.ressourcen = [{ id: 'r_aaaaaaaa', typ: 'platz', name: 'Platz 1', aktiv: true, plan: {} }, { id: 'r_cccccccc', typ: 'person', name: 'Cem', aktiv: true, plan: {} }] })
assert.deepEqual(gruppe(zusammenfassung(plaetze.einstellung, plaetze.fragebogen, BRANCHEN), 'wer').zeilen, ['Plätze: Platz 1', 'Personen: Cem'])
})
test('zusammenfassung: Bundesland nur, wenn der Kunde es ausdrücklich gewählt hat (der Serverstandard RP zählt nicht)', () => {
const nichtGewaehlt = stand((e, f) => { f.bundeslandGewaehlt = false })
assert.equal(nichtGewaehlt.einstellung.bundesland, 'RP', 'Serverstandard')
const ohne = JSON.stringify(zusammenfassung(nichtGewaehlt.einstellung, nichtGewaehlt.fragebogen, BRANCHEN))
assert.ok(!ohne.includes('Rheinland-Pfalz') && !ohne.includes('Bundesland'), ohne)
const gewaehlt = stand((e) => { e.bundesland = 'HE' })
const zeilen = gruppe(zusammenfassung(gewaehlt.einstellung, gewaehlt.fragebogen, BRANCHEN), 'zeiten').zeilen
assert.ok(zeilen.includes('Bundesland: Hessen'), zeilen.join(' | '))
const rp = stand()
assert.ok(gruppe(zusammenfassung(rp.einstellung, rp.fragebogen, BRANCHEN), 'zeiten').zeilen.includes('Bundesland: Rheinland-Pfalz'), 'RP ausdrücklich gewählt')
})
test('zusammenfassung: Regeln, Gastfelder, Benachrichtigung und Website', () => {
const { einstellung, fragebogen } = stand((e, f) => {
e.gastFelder = { telefon: 'pflicht', nachricht: 'optional', zusatz: [{ key: 'kennzeichen', label: 'Kennzeichen', typ: 'text', optionen: [], pflicht: true }, { key: 'wahl', label: 'Wunsch', typ: 'auswahl', optionen: ['a'], pflicht: false }] }
e.anrede = 'du'
e.erinnerung = { anbieten: true, stundenVorher: 12 }
e.betrieb.websiteUrl = 'https://salon.example.invalid'
f.einbau = { modus: 'knopf', knopfText: 'Termin buchen', stelle: 'Unter dem Text auf der Startseite', gestaltung: 'Dunkelblau', bemerkung: 'Bitte vor dem Wochenende' }
})
const erg = zusammenfassung(einstellung, fragebogen, BRANCHEN)
const regeln = gruppe(erg, 'regeln').zeilen
assert.ok(regeln.includes('Buchungsart: sofort buchbar'), regeln.join(' | '))
assert.ok(regeln.includes('Raster: 15 Minuten'))
assert.ok(regeln.includes('Vorlaufzeit: 2 Stunden'))
assert.ok(regeln.includes('Buchungszeitraum: 60 Tage im Voraus'))
assert.ok(regeln.includes('Absage durch Gäste: bis 24 Stunden vorher'))
assert.ok(regeln.includes('Deine Gäste sind: Privatpersonen'))
assert.ok(regeln.includes('Zahlung: vor Ort (bar, EC-Karte, Kreditkarte)'))
const gast = gruppe(erg, 'gastfelder').zeilen
assert.ok(gast.includes('Telefonnummer des Gastes: Pflicht') && gast.includes('Nachricht des Gastes: Optional'), gast.join(' | '))
assert.ok(gast.includes('Zusatzfeld „Kennzeichen“: Pflicht') && gast.includes('Zusatzfeld „Wunsch“: Optional (Auswahl)'))
const benachrichtigung = gruppe(erg, 'benachrichtigung').zeilen
assert.ok(benachrichtigung.includes('Benachrichtigungen an: salon@example.invalid'), benachrichtigung.join(' | '))
assert.ok(benachrichtigung.includes('Anrede der Gäste: Du (persönlich)'))
assert.ok(benachrichtigung.includes('E-Mail-Erinnerung für Gäste: angeboten, 12 Stunden vorher'))
const website = gruppe(erg, 'website').zeilen
assert.ok(website.includes('Telefon: 06301 12345'), website.join(' | '))
assert.ok(website.includes('Impressum: https://salon.example.invalid/impressum'))
assert.ok(website.includes('Datenschutzerklärung: https://salon.example.invalid/datenschutz'))
assert.ok(website.includes('Website: https://salon.example.invalid'))
assert.ok(website.includes('Darstellung: hinter einem Knopf „Termin buchen“'))
assert.ok(website.includes('Stelle: Unter dem Text auf der Startseite'))
assert.ok(website.includes('Gestaltungswunsch: Dunkelblau'))
assert.ok(website.includes('Bemerkung: Bitte vor dem Wochenende'))
const inline = stand()
assert.ok(gruppe(zusammenfassung(inline.einstellung, inline.fragebogen, BRANCHEN), 'website').zeilen.includes('Darstellung: direkt auf deiner Seite'))
})
test('zusammenfassung: Anfrage, gemischte Gäste, Freizeitangebote, keine Erinnerung', () => {
const { einstellung, fragebogen } = stand((e) => {
e.leistungen = e.leistungen.map((l, i) => ({ ...l, buchungsart: i === 0 ? 'anfrage' : 'sofort' }))
e.recht = { gaesteSind: 'gemischt', freizeit: true }
e.erinnerung = { anbieten: false, stundenVorher: 24 }
})
const erg = zusammenfassung(einstellung, fragebogen, BRANCHEN)
const regeln = gruppe(erg, 'regeln').zeilen
assert.ok(regeln.includes('Buchungsart: gemischt, je Leistung'), regeln.join(' | '))
assert.ok(regeln.includes('Deine Gäste sind: Privatpersonen und Unternehmen'))
assert.ok(regeln.includes('Leistungen sind Freizeitangebote mit festem Termin'))
assert.ok(gruppe(erg, 'benachrichtigung').zeilen.includes('E-Mail-Erinnerung für Gäste: aus'))
const anfrage = stand((e) => { e.leistungen = ausVorlage('handwerk', BETRIEB, JETZT).leistungen })
assert.ok(gruppe(zusammenfassung(anfrage.einstellung, anfrage.fragebogen, BRANCHEN), 'regeln').zeilen.includes('Buchungsart: auf Anfrage'))
})
test('zusammenfassung: nie feedToken, nie Zeichen aus der Einstellung außerhalb der genannten Felder', () => {
const { einstellung, fragebogen } = stand()
assert.ok(einstellung.ressourcen.every((r) => typeof r.feedToken === 'string' && r.feedToken.length > 0), 'die Einstellung trägt Token')
const text = JSON.stringify(zusammenfassung(einstellung, fragebogen, BRANCHEN))
assert.ok(!text.includes('feedToken'))
for (const r of einstellung.ressourcen) assert.ok(!text.includes(r.feedToken))
assert.ok(!text.includes(einstellung.ressourcen[0].id), 'auch keine IDs')
})
test('zusammenfassung: Kundentext bleibt Text (nichts entschärft, nichts verändert), lange Wünsche werden gekürzt', () => {
const frech = '<img src=x onerror=alert(1)> & "Anführung"'
const { einstellung, fragebogen } = stand((e, f) => {
e.betrieb.name = frech
f.einbau = { modus: 'inline', knopfText: '', stelle: '', gestaltung: 'g'.repeat(1000), bemerkung: '' }
})
const erg = zusammenfassung(einstellung, fragebogen, BRANCHEN)
assert.ok(gruppe(erg, 'branche').zeilen.includes(`Betrieb: ${frech}`))
const wunsch = gruppe(erg, 'website').zeilen.find((z) => z.startsWith('Gestaltungswunsch: '))
assert.ok(wunsch.length <= 'Gestaltungswunsch: '.length + 200, 'gekürzt')
assert.ok(wunsch.endsWith('…'))
})
test('zusammenfassung: ohne Einstellung und Fragebogen weiterhin acht Gruppen, ohne Fehler und ohne erfundene Angaben', () => {
for (const [e, f, b] of [[null, null, null], [undefined, undefined, undefined], [{}, {}, []], ['x', 5, 'y']]) {
const erg = zusammenfassung(e, f, b)
assert.deepEqual(erg.map((g) => g.schritt), [...SCHRITTE])
assert.ok(erg.every((g) => Array.isArray(g.zeilen) && g.zeilen.every((z) => typeof z === 'string')))
const text = JSON.stringify(erg)
assert.ok(!/undefined|null|NaN|Rheinland/.test(text), text)
}
const unbekannt = zusammenfassung(salon(), { ...leerFragebogen(), branche: 'unbekannt' }, BRANCHEN)
assert.ok(!gruppe(unbekannt, 'branche').zeilen.some((z) => z.startsWith('Branche:')), 'unbekannte Branche ⇒ keine Zeile')
})
test('Löschdatum und Abschalt-Texte gehören nicht in die Helfer des Fragebogens (E11: abschaltText)', () => {
assert.equal(ansicht.loeschDatum, undefined)
assert.equal(typeof ansicht.abschaltText, 'function')
})
test('termine-view.js bleibt rein: kein DOM, kein Netz, nur der Import der Anrede-Auswahl', () => {
const quelle = readFileSync(new URL('../public/termine-view.js', import.meta.url), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|\s)\/\/.*$/gm, '')
assert.doesNotMatch(quelle, /\b(document|window|navigator|fetch|XMLHttpRequest|localStorage|sessionStorage)\b/)
assert.deepEqual(quelle.match(/^import .*$/gm), ["import { ANREDE_WAHL } from './termine-gaestetexte.js'"])
})