481 lines
30 KiB
JavaScript
481 lines
30 KiB
JavaScript
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: ein neuer Name vor einem behaltenen bekommt trotzdem dessen Plan (Vorlage unabhängig von der Reihenfolge)', () => {
|
||
const bisher = [{ id: 'r_anna0001', typ: 'person', name: 'Anna', aktiv: true, plan: { 1: [['09:00', '12:00']] } }]
|
||
const erg = ressourcenAusNamen({ bisher, typ: 'person', namen: ['Ben', 'Anna'], neueId: zaehler() })
|
||
assert.deepEqual(namenVon(erg.ressourcen), ['Ben', 'Anna'])
|
||
const [ben, anna] = erg.ressourcen
|
||
assert.equal(anna.id, 'r_anna0001', 'Anna behält ihre ID')
|
||
assert.deepEqual(anna.plan, { 1: [['09:00', '12:00']] }, 'und ihren Plan')
|
||
assert.equal(ben.id, 'r_test0001')
|
||
assert.deepEqual(ben.plan, { 1: [['09:00', '12:00']] }, 'Ben bekommt Annas Plan, nicht einen leeren')
|
||
assert.notEqual(ben.plan, anna.plan, 'als Kopie')
|
||
ben.plan[1].push(['14:00', '16:00'])
|
||
assert.equal(anna.plan[1].length, 1, 'Änderungen an Ben verändern Anna nicht')
|
||
assert.equal(bisher[0].plan[1].length, 1, 'und nicht den alten Eintrag')
|
||
assert.deepEqual(erg.neu, ['r_test0001'])
|
||
assert.deepEqual(erg.entfernt, [])
|
||
// mehrere neue Namen vor dem behaltenen, und ein umbenannter alter Eintrag liefert die Vorlage ebenso
|
||
const drei = ressourcenAusNamen({ bisher, typ: 'person', namen: ['Ben', 'Cem', 'Anna'], neueId: zaehler() })
|
||
assert.deepEqual(drei.ressourcen.map((r) => r.plan), [bisher[0].plan, bisher[0].plan, bisher[0].plan])
|
||
const umbenannt = ressourcenAusNamen({ bisher, typ: 'person', namen: ['Ben', 'Cem'], neueId: zaehler() })
|
||
assert.deepEqual(idsVon(umbenannt.ressourcen), ['r_anna0001', 'r_test0001'], 'Anna wird zu Ben, Cem ist neu')
|
||
assert.deepEqual(umbenannt.ressourcen.map((r) => r.plan), [bisher[0].plan, bisher[0].plan])
|
||
// ohne bisherigen Eintrag gibt es keine Vorlage: leerer Plan
|
||
assert.deepEqual(ressourcenAusNamen({ bisher: [], typ: 'person', namen: ['Ben', 'Cem'], neueId: zaehler() }).ressourcen.map((r) => r.plan), [{}, {}])
|
||
})
|
||
|
||
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'"])
|
||
})
|