fragebogen.js ersetzt den Stub aus Block B: bestellpruefung (Kriterien (a) bis (h), je Punkt ein Eintrag, nach Schrittfolge sortiert, Abo-Punkt nur bei mitAbo !== false), SCHRITT_TITEL, gesundheitsWarnungen (Stichworte nur am Wortanfang, hoechstens 10 Meldungen) und bestellungAnsicht fuer die Bestellseite. einstellungen.js exportiert sichtbareLeistungen und MELDUNG_PFLICHT. Testhelfer bestellbarerStand und entwurfDoc; der Stub-Test entfaellt, die zwei B-Tests mit dem Stub laufen gegen das echte Gate. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
809 lines
45 KiB
JavaScript
809 lines
45 KiB
JavaScript
import { test } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import { VORLAGEN, vorlagenListe, ausVorlage } from '../server/services/termine/vorlagen.js'
|
||
import {
|
||
SCHRITTE, SCHRITT_TITEL, BRANCHEN, FRAGEBOGEN_MAX, leerFragebogen, normalisiereFragebogen,
|
||
bestellpruefung, gesundheitsWarnungen, bestellungAnsicht,
|
||
} from '../server/services/termine/fragebogen.js'
|
||
import { MELDUNG_PFLICHT, sichtbareLeistungen } from '../server/services/termine/einstellungen.js'
|
||
import { aboMeldung } from '../server/services/featureOrder.js'
|
||
import { oeffentlicheAnsicht, featureHaken, BELEHRUNG_PLATZHALTER } from '../server/services/featureRegistry.js'
|
||
import { config } from '../server/config.js'
|
||
import { bestellbarerStand, entwurfDoc } from './support/fragebogenStand.js'
|
||
|
||
const norm = normalisiereFragebogen
|
||
const ok = (eingabe) => {
|
||
const ergebnis = norm(eingabe)
|
||
assert.equal(ergebnis.fehler, undefined, `unerwarteter Fehler: ${JSON.stringify(ergebnis.fehler)}`)
|
||
return ergebnis.fragebogen
|
||
}
|
||
|
||
// ---------------------------------------------------------------- SCHRITTE und BRANCHEN
|
||
|
||
test('SCHRITTE: genau die acht Schritte in fester Reihenfolge, unveraenderlich', () => {
|
||
assert.deepEqual(SCHRITTE, ['branche', 'wer', 'leistungen', 'zeiten', 'regeln', 'gastfelder', 'benachrichtigung', 'website'])
|
||
assert.ok(Object.isFrozen(SCHRITTE))
|
||
})
|
||
|
||
test('BRANCHEN: Reihenfolge von vorlagenListe(), danach genau der Eintrag Gesundheitsbereich', () => {
|
||
const liste = vorlagenListe()
|
||
assert.equal(BRANCHEN.length, liste.length + 1)
|
||
BRANCHEN.slice(0, liste.length).forEach((branche, i) => {
|
||
assert.deepEqual(branche, { key: liste[i].key, name: liste[i].name, vorlage: liste[i].key, gesundheit: false })
|
||
})
|
||
assert.deepEqual(BRANCHEN.at(-1), { key: 'gesundheit', name: 'Gesundheitsbereich', vorlage: null, gesundheit: true })
|
||
})
|
||
|
||
test('BRANCHEN: jede Vorlage steht in VORLAGEN, alle neun Vorlagen kommen genau einmal vor', () => {
|
||
const mitVorlage = BRANCHEN.filter((b) => b.vorlage !== null)
|
||
assert.ok(mitVorlage.every((b) => Object.hasOwn(VORLAGEN, b.vorlage)))
|
||
assert.equal(Object.keys(VORLAGEN).length, 9)
|
||
assert.deepEqual(mitVorlage.map((b) => b.vorlage).sort(), Object.keys(VORLAGEN).sort())
|
||
assert.equal(new Set(mitVorlage.map((b) => b.vorlage)).size, 9)
|
||
assert.equal(new Set(BRANCHEN.map((b) => b.key)).size, BRANCHEN.length, 'Schluessel eindeutig')
|
||
})
|
||
|
||
test('BRANCHEN: genau ein Eintrag mit gesundheit true und ohne Vorlage; sonstiges bleibt waehlbar (D6)', () => {
|
||
const gesundheit = BRANCHEN.filter((b) => b.gesundheit)
|
||
assert.equal(gesundheit.length, 1)
|
||
assert.equal(gesundheit[0].vorlage, null)
|
||
assert.equal(gesundheit[0].key, 'gesundheit')
|
||
assert.equal(gesundheit[0].name, 'Gesundheitsbereich')
|
||
assert.ok(BRANCHEN.filter((b) => b.vorlage === null).every((b) => b.gesundheit), 'ohne Vorlage nur der Gesundheitsbereich')
|
||
const sonstiges = BRANCHEN.find((b) => b.key === 'sonstiges')
|
||
assert.deepEqual(sonstiges, { key: 'sonstiges', name: VORLAGEN.sonstiges.name, vorlage: 'sonstiges', gesundheit: false })
|
||
assert.equal(ok({ branche: 'sonstiges' }).branche, 'sonstiges')
|
||
})
|
||
|
||
test('BRANCHEN: jeder Eintrag hat genau die vier Felder, die Liste ist unveraenderlich', () => {
|
||
for (const branche of BRANCHEN) {
|
||
assert.deepEqual(Object.keys(branche), ['key', 'name', 'vorlage', 'gesundheit'])
|
||
assert.ok(typeof branche.name === 'string' && branche.name.length > 2)
|
||
assert.equal(typeof branche.gesundheit, 'boolean')
|
||
}
|
||
assert.ok(Object.isFrozen(BRANCHEN))
|
||
assert.ok(BRANCHEN.every((b) => Object.isFrozen(b)))
|
||
})
|
||
|
||
test('FRAGEBOGEN_MAX ist 16000', () => {
|
||
assert.equal(FRAGEBOGEN_MAX, 16000)
|
||
})
|
||
|
||
// ---------------------------------------------------------------- leerFragebogen
|
||
|
||
test('leerFragebogen: Grundform, bei jedem Aufruf ein neues Objekt', () => {
|
||
assert.deepEqual(leerFragebogen(), {
|
||
schema: 1,
|
||
branche: null,
|
||
bestaetigt: [],
|
||
bundeslandGewaehlt: false,
|
||
preiseGeprueft: false,
|
||
einbau: { modus: null, knopfText: '', stelle: '', gestaltung: '', bemerkung: '' },
|
||
})
|
||
const a = leerFragebogen()
|
||
a.bestaetigt.push('wer')
|
||
a.einbau.stelle = 'x'
|
||
assert.deepEqual(leerFragebogen().bestaetigt, [])
|
||
assert.equal(leerFragebogen().einbau.stelle, '')
|
||
})
|
||
|
||
// ---------------------------------------------------------------- normalisiereFragebogen
|
||
|
||
test('normalisiere: {} ergibt leerFragebogen()', () => {
|
||
assert.deepEqual(norm({}), { fragebogen: leerFragebogen() })
|
||
})
|
||
|
||
test('normalisiere: Nicht-Objekt ergibt fehler._ und kein fragebogen', () => {
|
||
for (const eingabe of [null, undefined, 'text', 42, true, [], [{}]]) {
|
||
const ergebnis = norm(eingabe)
|
||
assert.equal(typeof ergebnis.fehler._, 'string', `Eingabe ${JSON.stringify(eingabe)}`)
|
||
assert.deepEqual(Object.keys(ergebnis.fehler), ['_'])
|
||
assert.equal('fragebogen' in ergebnis, false)
|
||
}
|
||
})
|
||
|
||
test('normalisiere: unbekannte Schluessel und __proto__ fliegen raus, ohne den Prototyp anzufassen', () => {
|
||
const eingabe = JSON.parse(
|
||
'{"__proto__":{"vergiftet":1},"fremd":"x","constructor":{"prototype":{"vergiftet":2}},'
|
||
+ '"einbau":{"__proto__":{"vergiftet":3},"fremd":"y","stelle":"Startseite"}}',
|
||
)
|
||
const fragebogen = ok(eingabe)
|
||
assert.deepEqual(fragebogen, { ...leerFragebogen(), einbau: { ...leerFragebogen().einbau, stelle: 'Startseite' } })
|
||
assert.deepEqual(Object.keys(fragebogen), ['schema', 'branche', 'bestaetigt', 'bundeslandGewaehlt', 'preiseGeprueft', 'einbau'])
|
||
assert.deepEqual(Object.keys(fragebogen.einbau), ['modus', 'knopfText', 'stelle', 'gestaltung', 'bemerkung'])
|
||
assert.equal(Object.hasOwn(fragebogen, '__proto__'), false)
|
||
assert.equal(Object.getPrototypeOf(fragebogen), Object.prototype)
|
||
assert.equal({}.vergiftet, undefined)
|
||
assert.equal(fragebogen.vergiftet, undefined)
|
||
})
|
||
|
||
test('normalisiere: schema ist immer 1, egal was kommt', () => {
|
||
assert.equal(ok({ schema: 7 }).schema, 1)
|
||
assert.equal(ok({ schema: 'x' }).schema, 1)
|
||
})
|
||
|
||
test('normalisiere: bestaetigt in Reihenfolge der Schritte, ohne Doppelte und Fremde', () => {
|
||
assert.deepEqual(ok({ bestaetigt: ['website', 'branche', 'branche', 'quatsch'] }).bestaetigt, ['branche', 'website'])
|
||
assert.deepEqual(ok({ bestaetigt: [...SCHRITTE].reverse() }).bestaetigt, SCHRITTE)
|
||
assert.deepEqual(ok({ bestaetigt: [1, null, {}, ['wer'], 'wer'] }).bestaetigt, ['wer'])
|
||
assert.deepEqual(ok({ bestaetigt: [] }).bestaetigt, [])
|
||
assert.deepEqual(ok({ bestaetigt: null }).bestaetigt, [])
|
||
assert.deepEqual(ok({ bestaetigt: ['__proto__', 'constructor', 'toString'] }).bestaetigt, [])
|
||
})
|
||
|
||
test('normalisiere: bestaetigt, das keine Liste ist, ergibt fehler.bestaetigt', () => {
|
||
for (const wert of ['branche', 5, true, { 0: 'branche' }]) {
|
||
const ergebnis = norm({ bestaetigt: wert })
|
||
assert.deepEqual(Object.keys(ergebnis.fehler), ['bestaetigt'], JSON.stringify(wert))
|
||
}
|
||
})
|
||
|
||
test('normalisiere: branche nur aus BRANCHEN oder null', () => {
|
||
for (const branche of BRANCHEN) assert.equal(ok({ branche: branche.key }).branche, branche.key)
|
||
assert.equal(ok({ branche: 'gesundheit' }).branche, 'gesundheit')
|
||
assert.equal(ok({ branche: null }).branche, null)
|
||
assert.equal(ok({}).branche, null)
|
||
for (const wert of ['zahnarzt', '', 'Salon', 'SALON', 'constructor', '__proto__', 7, ['salon'], {}]) {
|
||
assert.deepEqual(Object.keys(norm({ branche: wert }).fehler ?? {}), ['branche'], JSON.stringify(wert))
|
||
}
|
||
})
|
||
|
||
test('normalisiere: knopfText mit 41 Zeichen wird auf 40 gekuerzt, einzeilig', () => {
|
||
assert.equal(ok({ einbau: { knopfText: 'x'.repeat(41) } }).einbau.knopfText, 'x'.repeat(40))
|
||
assert.equal(ok({ einbau: { knopfText: 'x'.repeat(40) } }).einbau.knopfText, 'x'.repeat(40))
|
||
assert.equal(ok({ einbau: { knopfText: '😀'.repeat(41) } }).einbau.knopfText, '😀'.repeat(40), 'Codepoints, nicht UTF-16-Einheiten')
|
||
assert.equal(ok({ einbau: { knopfText: ' Jetzt\nTermin\tbuchen ' } }).einbau.knopfText, 'Jetzt Termin buchen')
|
||
})
|
||
|
||
test('normalisiere: Textlaengen stelle 300, gestaltung 1000, bemerkung 1000', () => {
|
||
const einbau = ok({ einbau: { stelle: 'a'.repeat(301), gestaltung: 'b'.repeat(1001), bemerkung: 'c'.repeat(1001) } }).einbau
|
||
assert.equal(einbau.stelle, 'a'.repeat(300))
|
||
assert.equal(einbau.gestaltung, 'b'.repeat(1000))
|
||
assert.equal(einbau.bemerkung, 'c'.repeat(1000))
|
||
})
|
||
|
||
test('normalisiere: Steuerzeichen verschwinden, Absaetze bleiben (stelle, gestaltung, bemerkung)', () => {
|
||
const roh = 'Farbe\u0000 dunkel, rund\r\n\r\nZweiter Absatz\n\n\n\nDritter'
|
||
const erwartet = 'Farbe dunkel, rund\n\nZweiter Absatz\n\nDritter'
|
||
const einbau = ok({ einbau: { stelle: roh, gestaltung: roh, bemerkung: roh } }).einbau
|
||
assert.equal(einbau.gestaltung, erwartet)
|
||
assert.equal(einbau.stelle, erwartet)
|
||
assert.equal(einbau.bemerkung, erwartet)
|
||
assert.ok(!/[\u0000\r]/.test(einbau.gestaltung))
|
||
})
|
||
|
||
test('normalisiere: Texte werden getrimmt, ein Text nur aus Steuerzeichen wird leer', () => {
|
||
const einbau = ok({ einbau: { knopfText: '\u0000', stelle: ' ', gestaltung: '\n\n', bemerkung: ' x ' } }).einbau
|
||
assert.deepEqual(einbau, { modus: null, knopfText: '', stelle: '', gestaltung: '', bemerkung: 'x' })
|
||
})
|
||
|
||
test('normalisiere: Textfelder, die keine Zeichenkette sind, ergeben einen Fehler am Pfad', () => {
|
||
for (const feld of ['knopfText', 'stelle', 'gestaltung', 'bemerkung']) {
|
||
for (const wert of [5, true, ['a'], {}]) {
|
||
assert.deepEqual(Object.keys(norm({ einbau: { [feld]: wert } }).fehler ?? {}), [`einbau.${feld}`], `${feld}=${JSON.stringify(wert)}`)
|
||
}
|
||
assert.equal(ok({ einbau: { [feld]: null } }).einbau[feld], '')
|
||
}
|
||
})
|
||
|
||
test('normalisiere: einbau.modus nur inline, knopf oder null; popup ergibt fehler[einbau.modus]', () => {
|
||
assert.equal(ok({ einbau: { modus: 'inline' } }).einbau.modus, 'inline')
|
||
assert.equal(ok({ einbau: { modus: 'knopf' } }).einbau.modus, 'knopf')
|
||
assert.equal(ok({ einbau: { modus: null } }).einbau.modus, null)
|
||
assert.equal(ok({ einbau: {} }).einbau.modus, null)
|
||
const ergebnis = norm({ einbau: { modus: 'popup' } })
|
||
assert.deepEqual(Object.keys(ergebnis.fehler), ['einbau.modus'])
|
||
assert.equal(typeof ergebnis.fehler['einbau.modus'], 'string')
|
||
assert.equal('fragebogen' in ergebnis, false)
|
||
for (const wert of ['', 'INLINE', 1, ['inline'], {}]) {
|
||
assert.deepEqual(Object.keys(norm({ einbau: { modus: wert } }).fehler ?? {}), ['einbau.modus'], JSON.stringify(wert))
|
||
}
|
||
})
|
||
|
||
test('normalisiere: einbau, das kein Objekt ist, ergibt fehler.einbau', () => {
|
||
for (const wert of ['inline', 5, [], true]) {
|
||
assert.deepEqual(Object.keys(norm({ einbau: wert }).fehler ?? {}), ['einbau'], JSON.stringify(wert))
|
||
}
|
||
assert.deepEqual(ok({ einbau: null }).einbau, leerFragebogen().einbau)
|
||
})
|
||
|
||
test('normalisiere: bundeslandGewaehlt und preiseGeprueft nur als Wahrheitswert; ja ergibt Fehler', () => {
|
||
assert.equal(ok({ bundeslandGewaehlt: true }).bundeslandGewaehlt, true)
|
||
assert.equal(ok({ preiseGeprueft: true }).preiseGeprueft, true)
|
||
assert.equal(ok({ bundeslandGewaehlt: false, preiseGeprueft: false }).bundeslandGewaehlt, false)
|
||
assert.equal(ok({ bundeslandGewaehlt: null }).bundeslandGewaehlt, false)
|
||
assert.deepEqual(Object.keys(norm({ bundeslandGewaehlt: 'ja' }).fehler), ['bundeslandGewaehlt'])
|
||
assert.deepEqual(Object.keys(norm({ preiseGeprueft: 1 }).fehler), ['preiseGeprueft'])
|
||
for (const wert of ['true', 'ja', 0, 1, [], {}]) {
|
||
assert.deepEqual(Object.keys(norm({ bundeslandGewaehlt: wert }).fehler ?? {}), ['bundeslandGewaehlt'], JSON.stringify(wert))
|
||
}
|
||
})
|
||
|
||
test('normalisiere: mehrere Fehler zugleich, jeder am eigenen Pfad', () => {
|
||
const ergebnis = norm({ branche: 'x', bestaetigt: 3, bundeslandGewaehlt: 'ja', preiseGeprueft: 'nein', einbau: { modus: 'popup', stelle: 4 } })
|
||
assert.deepEqual(Object.keys(ergebnis.fehler).sort(), ['bestaetigt', 'branche', 'bundeslandGewaehlt', 'einbau.modus', 'einbau.stelle', 'preiseGeprueft'])
|
||
assert.ok(Object.values(ergebnis.fehler).every((text) => typeof text === 'string' && text.length > 5))
|
||
})
|
||
|
||
test('normalisiere: das Ergebnis teilt nichts mit der Eingabe', () => {
|
||
const eingabe = { bestaetigt: ['wer'], einbau: { stelle: 'oben' } }
|
||
const fragebogen = ok(eingabe)
|
||
eingabe.bestaetigt.push('zeiten')
|
||
eingabe.einbau.stelle = 'unten'
|
||
assert.deepEqual(fragebogen.bestaetigt, ['wer'])
|
||
assert.equal(fragebogen.einbau.stelle, 'oben')
|
||
assert.notEqual(fragebogen.einbau, eingabe.einbau)
|
||
})
|
||
|
||
test('normalisiere: voll ausgefuellter Fragebogen bleibt unveraendert', () => {
|
||
const voll = {
|
||
schema: 1,
|
||
branche: 'salon',
|
||
bestaetigt: ['branche', 'wer', 'leistungen', 'zeiten', 'regeln', 'gastfelder', 'benachrichtigung', 'website'],
|
||
bundeslandGewaehlt: true,
|
||
preiseGeprueft: true,
|
||
einbau: { modus: 'knopf', knopfText: 'Termin buchen', stelle: 'Unterseite Kontakt', gestaltung: 'Lila Knopf\n\nRund', bemerkung: 'Bitte vor 10 Uhr' },
|
||
}
|
||
assert.deepEqual(norm(voll), { fragebogen: voll })
|
||
})
|
||
|
||
test('normalisiere: idempotent', () => {
|
||
const eingaben = [
|
||
{},
|
||
{ branche: 'gesundheit', bestaetigt: ['website', 'wer', 'wer', 'x'], einbau: { modus: 'inline', knopfText: 'y'.repeat(60), stelle: ' a\u0000\n\n\n\nb ' } },
|
||
{ schema: 3, fremd: 1, bundeslandGewaehlt: true, einbau: { gestaltung: '😀'.repeat(1200), bemerkung: '\r\nzwei\r\n\r\nAbsätze' } },
|
||
JSON.parse('{"__proto__":{"a":1},"branche":"werkstatt"}'),
|
||
]
|
||
for (const eingabe of eingaben) {
|
||
const einmal = ok(eingabe)
|
||
assert.deepEqual(norm(einmal), { fragebogen: einmal })
|
||
}
|
||
})
|
||
|
||
test('normalisiere: groesste gueltige Eingabe bleibt unter FRAGEBOGEN_MAX (auch mit Zeichen, die JSON verdoppelt)', () => {
|
||
// Anfuehrungszeichen werden als \" gespeichert, Absatzumbrueche als \n\n: ungünstigster Fall fuer die Zeichenzahl
|
||
const gross = (zeichen, max) => zeichen.repeat(max)
|
||
for (const zeichen of ['"', '\\', '😀', 'ä', 'a\n\n']) {
|
||
const fragebogen = ok({
|
||
branche: 'werkstatt',
|
||
bestaetigt: SCHRITTE,
|
||
bundeslandGewaehlt: true,
|
||
preiseGeprueft: true,
|
||
einbau: {
|
||
modus: 'knopf',
|
||
knopfText: gross(zeichen, 40),
|
||
stelle: gross(zeichen, 300),
|
||
gestaltung: gross(zeichen, 1000),
|
||
bemerkung: gross(zeichen, 1000),
|
||
},
|
||
})
|
||
const json = JSON.stringify(fragebogen)
|
||
assert.ok(json.length < FRAGEBOGEN_MAX, `${JSON.stringify(zeichen)}: ${json.length} Zeichen`)
|
||
assert.ok(Buffer.byteLength(json, 'utf8') < FRAGEBOGEN_MAX, `${JSON.stringify(zeichen)}: ${Buffer.byteLength(json, 'utf8')} Bytes`)
|
||
}
|
||
})
|
||
|
||
// ---------------------------------------------------------------- Bestell-Gate (bestellpruefung)
|
||
|
||
const T = config.features.termine
|
||
const ABO_OK = { ok: true }
|
||
const pruefe = (stand, extra = {}) => bestellpruefung({ ...stand, feature: T, abo: ABO_OK, ...extra })
|
||
const stand = (aendere) => {
|
||
const s = bestellbarerStand({ now: new Date('2026-10-04T10:00:00.000Z') })
|
||
aendere?.(s)
|
||
return s
|
||
}
|
||
const SIE_FORM = /\b(Sie|Ihnen|Ihr|Ihre|Ihren|Ihrem|Ihrer|Ihres)\b/
|
||
const ohnePflichttexte = (texte) => texte.filter((t) => !Object.values(MELDUNG_PFLICHT).includes(t))
|
||
|
||
test('SCHRITT_TITEL: ein Titel je Schritt und fuer bestellen, unveraenderlich', () => {
|
||
assert.deepEqual(Object.keys(SCHRITT_TITEL), [...SCHRITTE, 'bestellen'])
|
||
assert.ok(Object.values(SCHRITT_TITEL).every((t) => typeof t === 'string' && t.length > 2))
|
||
assert.ok(Object.isFrozen(SCHRITT_TITEL))
|
||
assert.equal(new Set(Object.values(SCHRITT_TITEL)).size, SCHRITTE.length + 1, 'Titel eindeutig')
|
||
})
|
||
|
||
test('bestellbarerStand: Vorlage salon, alle Angaben da, bei jedem Aufruf neue Objekte', () => {
|
||
const a = bestellbarerStand({ now: new Date('2026-10-04T10:00:00.000Z') })
|
||
const b = bestellbarerStand()
|
||
assert.equal(a.einstellung.vorlage, 'salon')
|
||
assert.equal(a.einstellung.betrieb.telefon, '06301 12345')
|
||
assert.ok(a.einstellung.betrieb.adresse && a.einstellung.betrieb.impressumUrl && a.einstellung.betrieb.datenschutzUrl)
|
||
assert.deepEqual(a.fragebogen.bestaetigt, SCHRITTE)
|
||
assert.equal(a.fragebogen.bundeslandGewaehlt, true)
|
||
assert.equal(a.fragebogen.preiseGeprueft, true)
|
||
assert.equal(a.fragebogen.einbau.modus, 'inline')
|
||
assert.equal(a.fragebogen.branche, 'salon')
|
||
assert.notEqual(a.einstellung.ressourcen[0].id, b.einstellung.ressourcen[0].id)
|
||
assert.notEqual(a.fragebogen.bestaetigt, b.fragebogen.bestaetigt)
|
||
assert.deepEqual(norm(a.fragebogen), { fragebogen: a.fragebogen }, 'der Fragebogen ist normalisiert')
|
||
})
|
||
|
||
test('entwurfDoc: termineConfig-Dokument mit Einstellung und Fragebogen als JSON-Text', () => {
|
||
const s = stand()
|
||
const doc = entwurfDoc('f_p1_termine', { customerId: 'c1', projectId: 'p1', stand: s })
|
||
assert.equal(doc.$id, 'f_p1_termine')
|
||
assert.equal(doc.customerId, 'c1')
|
||
assert.equal(doc.projectId, 'p1')
|
||
assert.deepEqual(JSON.parse(doc.einstellung), JSON.parse(JSON.stringify(s.einstellung)))
|
||
assert.deepEqual(JSON.parse(doc.fragebogen), s.fragebogen)
|
||
assert.ok(Number.isInteger(doc.version))
|
||
assert.equal(Object.hasOwn(entwurfDoc('f', { customerId: 'c', projectId: 'p', stand: { einstellung: s.einstellung, fragebogen: null } }), 'fragebogen'), false)
|
||
assert.ok(JSON.parse(entwurfDoc('f', { customerId: 'c', projectId: 'p' }).fragebogen).bestaetigt.length === 8, 'ohne stand: der bestellbare')
|
||
})
|
||
|
||
test('bestellpruefung: bestellbarer Stand mit abo { ok: true } ergibt []', () => {
|
||
const ergebnis = pruefe(stand())
|
||
assert.deepEqual(ergebnis, [])
|
||
assert.ok(Array.isArray(ergebnis), 'ein Array, kein Promise')
|
||
assert.notEqual(pruefe(stand()), pruefe(stand()), 'bei jedem Aufruf ein neues Array')
|
||
})
|
||
|
||
// Tabelle: Kriterium, Aenderung am bestellbaren Stand, erwarteter Schritt, (optional) erwarteter Text
|
||
const GATE_FAELLE = [
|
||
['(a) Telefon leer', (s) => { s.einstellung.betrieb.telefon = '' }, 'website', MELDUNG_PFLICHT['betrieb.telefon']],
|
||
['(a) Telefon fehlt (null)', (s) => { s.einstellung.betrieb.telefon = null }, 'website', MELDUNG_PFLICHT['betrieb.telefon']],
|
||
['(a) Anschrift leer', (s) => { s.einstellung.betrieb.adresse = '' }, 'branche', MELDUNG_PFLICHT['betrieb.adresse']],
|
||
['(a) Anschrift nur Leerzeichen', (s) => { s.einstellung.betrieb.adresse = ' ' }, 'branche', MELDUNG_PFLICHT['betrieb.adresse']],
|
||
['(b) alle Leistungen inaktiv', (s) => { s.einstellung.leistungen.forEach((l) => { l.aktiv = false }) }, 'leistungen'],
|
||
['(b) keine aktive Person', (s) => { s.einstellung.ressourcen.forEach((r) => { r.aktiv = false }) }, 'leistungen'],
|
||
['(b) keine Leistung', (s) => { s.einstellung.leistungen = [] }, 'leistungen'],
|
||
['(c) Schritt branche nicht bestaetigt', (s) => { s.fragebogen.bestaetigt = s.fragebogen.bestaetigt.filter((x) => x !== 'branche') }, 'branche', 'Bitte bestätige den Schritt „Branche und Betrieb“.'],
|
||
['(c) Schritt wer nicht bestaetigt', (s) => { s.fragebogen.bestaetigt = s.fragebogen.bestaetigt.filter((x) => x !== 'wer') }, 'wer', 'Bitte bestätige den Schritt „Wer wird gebucht“.'],
|
||
['(c) Schritt website nicht bestaetigt', (s) => { s.fragebogen.bestaetigt = s.fragebogen.bestaetigt.filter((x) => x !== 'website') }, 'website', 'Bitte bestätige den Schritt „Website“.'],
|
||
['(c) Preise nicht geprueft', (s) => { s.fragebogen.preiseGeprueft = false }, 'leistungen'],
|
||
['(d) Bundesland nicht gewaehlt', (s) => { s.fragebogen.bundeslandGewaehlt = false }, 'zeiten'],
|
||
['(d) bundeslandGewaehlt fehlt', (s) => { delete s.fragebogen.bundeslandGewaehlt }, 'zeiten'],
|
||
['(d) Wahrheitswert als Text zaehlt nicht', (s) => { s.fragebogen.bundeslandGewaehlt = 'true' }, 'zeiten'],
|
||
['(e) einbau.modus fehlt', (s) => { s.fragebogen.einbau.modus = null }, 'website'],
|
||
['(e) einbau fehlt ganz', (s) => { delete s.fragebogen.einbau }, 'website'],
|
||
['(e) Modus knopf ohne Knopftext', (s) => { s.fragebogen.einbau.modus = 'knopf'; s.fragebogen.einbau.knopfText = '' }, 'website'],
|
||
['(e) Modus knopf mit Leerzeichen als Knopftext', (s) => { s.fragebogen.einbau.modus = 'knopf'; s.fragebogen.einbau.knopfText = ' ' }, 'website'],
|
||
['(f) Branche fehlt', (s) => { s.fragebogen.branche = null }, 'branche'],
|
||
['(f) Branche unbekannt', (s) => { s.fragebogen.branche = 'raumschiff' }, 'branche'],
|
||
['(f) Branche Gesundheit', (s) => { s.fragebogen.branche = 'gesundheit' }, 'branche'],
|
||
['(g) Impressum fehlt', (s) => { s.einstellung.betrieb.impressumUrl = null }, 'website'],
|
||
['(g) Datenschutz fehlt', (s) => { s.einstellung.betrieb.datenschutzUrl = null }, 'website'],
|
||
['(g) Datenschutz leer', (s) => { s.einstellung.betrieb.datenschutzUrl = '' }, 'website'],
|
||
]
|
||
|
||
for (const [name, aendere, schritt, erwartetText] of GATE_FAELLE) {
|
||
test(`Gate ${name}: genau ein Punkt im Schritt ${schritt}`, () => {
|
||
const ergebnis = pruefe(stand(aendere))
|
||
assert.equal(ergebnis.length, 1, JSON.stringify(ergebnis))
|
||
assert.equal(ergebnis[0].schritt, schritt)
|
||
assert.ok(typeof ergebnis[0].text === 'string' && ergebnis[0].text.length > 10)
|
||
assert.deepEqual(Object.keys(ergebnis[0]), ['schritt', 'text'])
|
||
if (erwartetText) assert.equal(ergebnis[0].text, erwartetText)
|
||
})
|
||
}
|
||
|
||
test('Gate: Telefon leer ergibt { schritt: website }, Anschrift leer { schritt: branche } (Text aus MELDUNG_PFLICHT)', () => {
|
||
assert.deepEqual(pruefe(stand((s) => { s.einstellung.betrieb.telefon = '' })), [{ schritt: 'website', text: MELDUNG_PFLICHT['betrieb.telefon'] }])
|
||
assert.deepEqual(pruefe(stand((s) => { s.einstellung.betrieb.adresse = '' })), [{ schritt: 'branche', text: MELDUNG_PFLICHT['betrieb.adresse'] }])
|
||
})
|
||
|
||
test('Gate (a): ohne sichtbare Widerrufsbelehrung braucht es die Anschrift nicht (Unternehmer-Gaeste)', () => {
|
||
const ergebnis = pruefe(stand((s) => { s.einstellung.recht.gaesteSind = 'unternehmer'; s.einstellung.betrieb.adresse = '' }))
|
||
assert.deepEqual(ergebnis, [])
|
||
})
|
||
|
||
test('Gate (b): sichtbareLeistungen ist dieselbe Funktion wie in der oeffentlichen Sicht (aktiv und mit aktiver Ressource)', () => {
|
||
const s = stand()
|
||
assert.ok(sichtbareLeistungen(s.einstellung).length >= 1)
|
||
s.einstellung.leistungen.forEach((l) => { l.ressourcen = ['r_gibtesnicht'] })
|
||
assert.equal(sichtbareLeistungen(s.einstellung).length, 0)
|
||
assert.deepEqual(pruefe(s).map((p) => p.schritt), ['leistungen'])
|
||
})
|
||
|
||
test('Gate (c): bestaetigt leer ergibt acht Punkte in Schrittfolge, je Schritt einer', () => {
|
||
const ergebnis = pruefe(stand((s) => { s.fragebogen.bestaetigt = [] }))
|
||
assert.equal(ergebnis.length, 8)
|
||
assert.deepEqual(ergebnis.map((p) => p.schritt), SCHRITTE)
|
||
ergebnis.forEach((p, i) => assert.equal(p.text, `Bitte bestätige den Schritt „${SCHRITT_TITEL[SCHRITTE[i]]}“.`))
|
||
})
|
||
|
||
test('Gate (c): fehlendes bestaetigt (kein Array) zaehlt wie leer', () => {
|
||
const ergebnis = pruefe(stand((s) => { s.fragebogen.bestaetigt = 'alle' }))
|
||
assert.deepEqual(ergebnis.map((p) => p.schritt), SCHRITTE)
|
||
})
|
||
|
||
test('Gate (c): Preis-Punkt nur bei sichtbarer Leistung mit Preis; alle Preise keiner ergibt keinen Preis-Punkt', () => {
|
||
const s = stand((x) => { x.fragebogen.preiseGeprueft = false })
|
||
assert.ok(s.einstellung.leistungen.some((l) => l.preisArt !== 'keiner' && l.preisCent > 0))
|
||
assert.deepEqual(pruefe(s).map((p) => p.schritt), ['leistungen'])
|
||
const keiner = stand((x) => {
|
||
x.fragebogen.preiseGeprueft = false
|
||
x.einstellung.leistungen.forEach((l) => { l.preisArt = 'keiner'; l.preisCent = 0 })
|
||
})
|
||
assert.deepEqual(pruefe(keiner), [])
|
||
// Preis nur an einer unsichtbaren (inaktiven) Leistung ⇒ ebenfalls kein Preis-Punkt
|
||
const verdeckt = stand((x) => {
|
||
x.fragebogen.preiseGeprueft = false
|
||
x.einstellung.leistungen.forEach((l, i) => { if (i > 0) { l.preisArt = 'keiner'; l.preisCent = 0 } })
|
||
x.einstellung.leistungen[0].aktiv = false
|
||
})
|
||
assert.deepEqual(pruefe(verdeckt), [])
|
||
})
|
||
|
||
test('Gate (c): unbestaetigte Leistungen und ungepruefte Preise sind zwei eigene Punkte, beide im Schritt leistungen', () => {
|
||
const ergebnis = pruefe(stand((s) => {
|
||
s.fragebogen.bestaetigt = s.fragebogen.bestaetigt.filter((x) => x !== 'leistungen')
|
||
s.fragebogen.preiseGeprueft = false
|
||
}))
|
||
assert.equal(ergebnis.length, 2)
|
||
assert.deepEqual(ergebnis.map((p) => p.schritt), ['leistungen', 'leistungen'])
|
||
assert.notEqual(ergebnis[0].text, ergebnis[1].text)
|
||
assert.equal(ergebnis[0].text, 'Bitte bestätige den Schritt „Leistungen“.')
|
||
})
|
||
|
||
test('Gate (d): bundeslandGewaehlt false bei einstellung.bundesland RP ist trotzdem ein Punkt (der Serverstandard zaehlt nicht)', () => {
|
||
const s = stand((x) => { x.fragebogen.bundeslandGewaehlt = false })
|
||
assert.equal(s.einstellung.bundesland, 'RP')
|
||
assert.deepEqual(pruefe(s).map((p) => p.schritt), ['zeiten'])
|
||
s.fragebogen.bundeslandGewaehlt = true
|
||
assert.deepEqual(pruefe(s), [])
|
||
})
|
||
|
||
test('Gate (e): beide Modi sind in Ordnung, knopf mit Knopftext auch; ein unbekannter Modus nicht', () => {
|
||
assert.deepEqual(pruefe(stand((s) => { s.fragebogen.einbau.modus = 'knopf'; s.fragebogen.einbau.knopfText = 'Termin buchen' })), [])
|
||
assert.deepEqual(pruefe(stand((s) => { s.fragebogen.einbau.modus = 'popup' })).map((p) => p.schritt), ['website'])
|
||
})
|
||
|
||
test('Gate (e): zwei unterschiedliche Texte fuer fehlenden Modus und fehlenden Knopftext', () => {
|
||
const ohneModus = pruefe(stand((s) => { s.fragebogen.einbau.modus = null }))[0].text
|
||
const ohneText = pruefe(stand((s) => { s.fragebogen.einbau.modus = 'knopf'; s.fragebogen.einbau.knopfText = '' }))[0].text
|
||
assert.notEqual(ohneModus, ohneText)
|
||
})
|
||
|
||
test('Gate (f): Gesundheitstext nennt die Funktion aus dem Register (label.name), nicht die Branche "gesundheit" als Schluessel', () => {
|
||
const [punkt] = pruefe(stand((s) => { s.fragebogen.branche = 'gesundheit' }))
|
||
assert.equal(punkt.schritt, 'branche')
|
||
assert.ok(punkt.text.includes(T.label.name))
|
||
assert.ok(punkt.text.includes('Gesundheitsbereich'))
|
||
const [anders] = pruefe(stand((s) => { s.fragebogen.branche = 'gesundheit' }), { feature: { key: 'x', label: { name: 'Zeitplaner' } } })
|
||
assert.ok(anders.text.includes('Zeitplaner'))
|
||
assert.ok(!anders.text.includes(T.label.name))
|
||
// ohne Register-Eintrag faellt der Name auf "Terminplaner" zurueck
|
||
assert.ok(pruefe(stand((s) => { s.fragebogen.branche = 'gesundheit' }), { feature: undefined })[0].text.includes('Terminplaner'))
|
||
})
|
||
|
||
test('Gate (g): beide URLs fehlen ⇒ zwei Punkte (Impressum, Datenschutz) im Schritt website, Impressum zuerst', () => {
|
||
const ergebnis = pruefe(stand((s) => { s.einstellung.betrieb.impressumUrl = null; s.einstellung.betrieb.datenschutzUrl = null }))
|
||
assert.equal(ergebnis.length, 2)
|
||
assert.deepEqual(ergebnis.map((p) => p.schritt), ['website', 'website'])
|
||
assert.match(ergebnis[0].text, /Impressum/)
|
||
assert.match(ergebnis[1].text, /Datenschutz/)
|
||
})
|
||
|
||
test('Gate (h): abo { ok: false, grund } ergibt einen Punkt in bestellen mit dem Text aus aboMeldung', () => {
|
||
for (const grund of ['kein_abo', 'gekuendigt', 'nicht_aktiv']) {
|
||
const ergebnis = pruefe(stand(), { abo: { ok: false, grund } })
|
||
assert.deepEqual(ergebnis, [{ schritt: 'bestellen', text: aboMeldung(T, grund) }], grund)
|
||
}
|
||
})
|
||
|
||
test('Gate (h): abo fehlt (Lesefehler) ergibt aboMeldung(feature, "unbekannt"); abo ohne gueltigen Grund ebenso', () => {
|
||
const erwartet = [{ schritt: 'bestellen', text: aboMeldung(T, 'unbekannt') }]
|
||
assert.deepEqual(pruefe(stand(), { abo: undefined }), erwartet)
|
||
assert.deepEqual(pruefe(stand(), { abo: null }), erwartet)
|
||
assert.deepEqual(pruefe(stand(), { abo: { ok: false } }), erwartet)
|
||
assert.deepEqual(pruefe(stand(), { abo: { ok: false, grund: 'was_anderes' } }), erwartet)
|
||
assert.deepEqual(bestellpruefung({ ...stand(), feature: T }), erwartet, 'Standard mitAbo true, abo nicht uebergeben')
|
||
})
|
||
|
||
test('Gate (h): nur abo.ok === true gibt frei (truthy genuegt nicht)', () => {
|
||
assert.deepEqual(pruefe(stand(), { abo: { ok: 'ja' } }).map((p) => p.schritt), ['bestellen'])
|
||
assert.deepEqual(pruefe(stand(), { abo: { ok: 1 } }).map((p) => p.schritt), ['bestellen'])
|
||
})
|
||
|
||
test('Gate (h): mitAbo false ergibt keinen Abo-Punkt, auch ohne abo und mit abo { ok: false, grund: kein_abo }', () => {
|
||
assert.deepEqual(pruefe(stand(), { mitAbo: false, abo: undefined }), [])
|
||
assert.deepEqual(pruefe(stand(), { mitAbo: false, abo: { ok: false, grund: 'kein_abo' } }), [])
|
||
assert.deepEqual(bestellpruefung({ ...stand(), feature: T, mitAbo: false }), [])
|
||
// mitAbo true/undefined fragt weiter; nur ein ausdrueckliches false schaltet ab
|
||
assert.equal(pruefe(stand(), { mitAbo: true, abo: { ok: false, grund: 'kein_abo' } }).length, 1)
|
||
assert.equal(pruefe(stand(), { mitAbo: undefined, abo: { ok: false, grund: 'kein_abo' } }).length, 1)
|
||
})
|
||
|
||
test('Gate (h): der Abo-Punkt steht immer zuletzt, nach den Punkten der Schritte', () => {
|
||
const ergebnis = pruefe(stand((s) => { s.fragebogen.bestaetigt = [] }), { abo: { ok: false, grund: 'gekuendigt' } })
|
||
assert.equal(ergebnis.length, 9)
|
||
assert.deepEqual(ergebnis.map((p) => p.schritt), [...SCHRITTE, 'bestellen'])
|
||
})
|
||
|
||
test('Gate: einstellung null ergibt genau einen Punkt branche (auch mit abo, fragebogen null)', () => {
|
||
const ergebnis = bestellpruefung({ einstellung: null, fragebogen: null, feature: T, abo: { ok: false, grund: 'kein_abo' } })
|
||
assert.equal(ergebnis.length, 1)
|
||
assert.equal(ergebnis[0].schritt, 'branche')
|
||
assert.equal(typeof ergebnis[0].text, 'string')
|
||
assert.deepEqual(bestellpruefung({ einstellung: null, fragebogen: null, feature: T, mitAbo: false }).map((p) => p.schritt), ['branche'])
|
||
assert.deepEqual(bestellpruefung({}).map((p) => p.schritt), ['branche'], 'ohne Argumente')
|
||
assert.deepEqual(bestellpruefung().map((p) => p.schritt), ['branche'], 'ganz ohne Argument')
|
||
assert.deepEqual(bestellpruefung({ einstellung: 'kaputt' }).map((p) => p.schritt), ['branche'], 'kein Objekt')
|
||
})
|
||
|
||
test('Gate: fragebogen null bei vorhandener Einstellung ist ein leerer Fragebogen', () => {
|
||
const s = stand()
|
||
const ergebnis = pruefe({ einstellung: s.einstellung, fragebogen: null })
|
||
const schritte = ergebnis.map((p) => p.schritt)
|
||
assert.ok(schritte.filter((x) => SCHRITTE.includes(x)).length >= 8)
|
||
assert.deepEqual(ergebnis, pruefe({ einstellung: s.einstellung, fragebogen: leerFragebogen() }))
|
||
assert.deepEqual(ergebnis, pruefe({ einstellung: s.einstellung, fragebogen: undefined }))
|
||
})
|
||
|
||
test('Gate: Reihenfolge stabil – nach Schrittfolge, innerhalb eines Schritts in der Reihenfolge (a), (e), (g)', () => {
|
||
const kaputt = stand((s) => {
|
||
s.einstellung.betrieb.impressumUrl = null
|
||
s.einstellung.betrieb.telefon = ''
|
||
s.fragebogen.einbau.modus = null
|
||
s.einstellung.betrieb.adresse = ''
|
||
s.fragebogen.branche = null
|
||
s.fragebogen.bundeslandGewaehlt = false
|
||
s.fragebogen.preiseGeprueft = false
|
||
})
|
||
const ergebnis = pruefe(kaputt, { abo: { ok: false, grund: 'nicht_aktiv' } })
|
||
const reihenfolge = ergebnis.map((p) => p.schritt)
|
||
assert.deepEqual(reihenfolge, ['branche', 'branche', 'leistungen', 'zeiten', 'website', 'website', 'website', 'bestellen'])
|
||
const website = ergebnis.filter((p) => p.schritt === 'website').map((p) => p.text)
|
||
assert.equal(website[0], MELDUNG_PFLICHT['betrieb.telefon'], '(a) vor (e) vor (g)')
|
||
assert.match(website[2], /Impressum/)
|
||
assert.equal(ergebnis[0].text, MELDUNG_PFLICHT['betrieb.adresse'], '(a) vor (f) im Schritt branche')
|
||
// zweiter Aufruf mit anders aufgebauten Eingaben: dasselbe Ergebnis
|
||
assert.deepEqual(pruefe(structuredClone(kaputt), { abo: { ok: false, grund: 'nicht_aktiv' } }), ergebnis)
|
||
assert.deepEqual(pruefe(kaputt, { abo: { ok: false, grund: 'nicht_aktiv' } }), ergebnis)
|
||
})
|
||
|
||
test('Gate: reine Funktion – veraendert Eingaben nicht, ohne Gaestedaten und ohne Nebenwirkung', () => {
|
||
const s = stand((x) => { x.fragebogen.bestaetigt = ['wer']; x.einstellung.betrieb.telefon = '' })
|
||
const vorher = structuredClone(s)
|
||
const abo = { ok: false, grund: 'kein_abo' }
|
||
pruefe(s, { abo })
|
||
assert.deepEqual(s, vorher)
|
||
assert.deepEqual(abo, { ok: false, grund: 'kein_abo' })
|
||
})
|
||
|
||
test('Gate: alle Texte in Du-Form (kein Sie/Ihr), ausser den unveraenderten Pflichtangaben-Meldungen aus einstellungen.js', () => {
|
||
const alles = new Set()
|
||
for (const [, aendere] of GATE_FAELLE) pruefe(stand(aendere)).forEach((p) => alles.add(p.text))
|
||
pruefe(stand((s) => { s.fragebogen.bestaetigt = [] })).forEach((p) => alles.add(p.text))
|
||
for (const grund of ['kein_abo', 'gekuendigt', 'nicht_aktiv', undefined]) pruefe(stand(), { abo: { ok: false, grund } }).forEach((p) => alles.add(p.text))
|
||
pruefe({ einstellung: null, fragebogen: null }).forEach((p) => alles.add(p.text))
|
||
pruefe(stand((s) => { s.fragebogen.branche = 'gesundheit'; s.fragebogen.einbau.modus = 'knopf'; s.fragebogen.einbau.knopfText = '' })).forEach((p) => alles.add(p.text))
|
||
assert.ok(alles.size > 15, `nur ${alles.size} verschiedene Texte`)
|
||
for (const text of ohnePflichttexte([...alles])) {
|
||
assert.doesNotMatch(text, SIE_FORM, text)
|
||
assert.ok(text.length <= 300, `zu lang: ${text}`)
|
||
assert.doesNotMatch(text, /[<>]/, 'kein Markup')
|
||
}
|
||
// Die zwei Pflichtangaben-Meldungen bleiben wie in einstellungen.js (Gäste sehen "sie" = die Nummer/Anschrift, keine Anrede)
|
||
assert.ok(Object.values(MELDUNG_PFLICHT).every((m) => /\bdeines? Betriebs\b/.test(m)))
|
||
})
|
||
|
||
// ---------------------------------------------------------------- Gesundheits-Stichwortpruefung
|
||
|
||
const mitName = (name) => ({ betrieb: { name: 'Studio' }, leistungen: [{ name, beschreibung: '', aktiv: true, ressourcen: [] }], gastFelder: { zusatz: [] } })
|
||
const mitBeschreibung = (beschreibung) => ({ betrieb: { name: 'Studio' }, leistungen: [{ name: 'Termin', beschreibung }], gastFelder: { zusatz: [] } })
|
||
const mitLabel = (label) => ({ betrieb: { name: 'Studio' }, leistungen: [], gastFelder: { zusatz: [{ key: 'x', label, typ: 'text', optionen: [] }] } })
|
||
const mitOption = (option) => ({ betrieb: { name: 'Studio' }, leistungen: [], gastFelder: { zusatz: [{ key: 'x', label: 'Anliegen', typ: 'auswahl', optionen: ['Sonstiges', option] }] } })
|
||
const mitBetrieb = (name) => ({ betrieb: { name }, leistungen: [], gastFelder: { zusatz: [] } })
|
||
|
||
// Stichwort ⇒ ein Beispielwort, das den Stamm enthaelt (je Stamm der Liste ein Fall)
|
||
const STICHWORTE = [
|
||
['physio', 'Physiotherapie'], ['therap', 'Therapeutin'], ['arzt', 'Arztbesuch'], ['ärzt', 'Ärztin'],
|
||
['zahnarzt', 'Zahnarzt'], ['zahnärzt', 'Zahnärztin'], ['heilpraktik', 'Heilpraktiker'],
|
||
['krankengymnast', 'Krankengymnastik'], ['osteopath', 'Osteopathie'], ['diagnos', 'Diagnose'],
|
||
['rezept', 'Rezept'], ['beschwerde', 'Beschwerden'], ['behandlung', 'Behandlung'],
|
||
]
|
||
|
||
test('Stichwortpruefung: jedes der 13 Stichworte trifft in Leistungsname, Beschreibung, Zusatzfeld-Beschriftung, Auswahl und Betriebsname', () => {
|
||
assert.equal(STICHWORTE.length, 13)
|
||
for (const [stamm, wort] of STICHWORTE) {
|
||
for (const [ort, einstellung] of [['Name', mitName(wort)], ['Beschreibung', mitBeschreibung(`Termin wegen ${wort} möglich`)], ['Beschriftung', mitLabel(wort)], ['Auswahl', mitOption(wort)], ['Betrieb', mitBetrieb(`${wort} Müller`)]]) {
|
||
const treffer = gesundheitsWarnungen(einstellung)
|
||
assert.equal(treffer.length, 1, `${stamm} (${wort}) in ${ort}: ${JSON.stringify(treffer)}`)
|
||
assert.ok(treffer[0].includes(wort), `${wort} steht in der Meldung: ${treffer[0]}`)
|
||
}
|
||
}
|
||
})
|
||
|
||
test('Stichwortpruefung: Gross-/Kleinschreibung und Umlaute (auch zerlegtes ä)', () => {
|
||
for (const wort of ['PHYSIO', 'physio', 'PhysioTherapie', 'ÄRZTIN', 'ärztin', 'Ärztin', 'ZAHNÄRZTIN', 'zahnärztin', 'HEILPRAKTIKERIN', 'REZEPT', 'BEHANDLUNGEN']) {
|
||
assert.equal(gesundheitsWarnungen(mitName(wort)).length, 1, wort)
|
||
}
|
||
assert.equal(gesundheitsWarnungen(mitName('Arztpraxis')).length, 1)
|
||
})
|
||
|
||
test('Regeltest: Treffer nur am Wortanfang; Gruppe 2 nur mit hoechstens einer Beugungsendung', () => {
|
||
const treffer = ['Rezept', 'Rezepte', 'Behandlungen', 'Beschwerden', 'Physiotherapie', 'Therapiestunde', 'Zahnärztin', 'Krankengymnastik', 'Osteopathie']
|
||
const keine = ['Rezeption', 'Rezeptur', 'Gesichtsbehandlung', 'Beratung', 'Besichtigung']
|
||
for (const wort of treffer) assert.equal(gesundheitsWarnungen(mitName(wort)).length, 1, `Treffer erwartet: ${wort}`)
|
||
for (const wort of keine) assert.deepEqual(gesundheitsWarnungen(mitName(wort)), [], `kein Treffer erwartet: ${wort}`)
|
||
// weitere Randfaelle der Regel
|
||
for (const wort of ['Rezepts', 'Rezeptes', 'Behandlung', 'Behandlungs', 'Beschwerde', 'Beschwerdes', 'Beschwerdenn']) {
|
||
assert.equal(gesundheitsWarnungen(mitName(wort)).length, wort === 'Beschwerdenn' ? 0 : 1, wort)
|
||
}
|
||
for (const wort of ['Behandlungsraum', 'Rezepturen', 'Beschwerdemanagement', 'Hausarzt', 'Kinderphysio', 'Lagerdiagnos', 'Wasserrezeption']) {
|
||
// "Hausarzt", "Kinderphysio", "Lagerdiagnos": kein Buchstabe vor dem Treffer erlaubt – Treffer nur am Wortanfang
|
||
assert.deepEqual(gesundheitsWarnungen(mitName(wort)), [], wort)
|
||
}
|
||
})
|
||
|
||
test('Regeltest: Wortanfang auch nach Bindestrich, Leerzeichen, Ziffer und Satzzeichen (kein Buchstabe davor)', () => {
|
||
for (const text of ['Kinder-Physiotherapie', 'Termin (Rezept abholen)', '3x Therapie', 'Beratung/Behandlung', 'a, Rezepte', 'Ärztliche Beratung']) {
|
||
assert.equal(gesundheitsWarnungen(mitName(text)).length, 1, text)
|
||
}
|
||
assert.deepEqual(gesundheitsWarnungen(mitName('Gesichtsbehandlung und Beratung, Besichtigung')), [])
|
||
})
|
||
|
||
test('Stichwortpruefung: alle neun Vorlagen ergeben [] (schuetzt Gesichtsbehandlung, Beratung, Besichtigung)', () => {
|
||
const keys = Object.keys(VORLAGEN)
|
||
assert.equal(keys.length, 9)
|
||
for (const key of keys) {
|
||
const einstellung = ausVorlage(key, { name: 'Muster GmbH', email: 'a@example.invalid', telefon: '1', adresse: 'Hauptstr. 1' }, new Date('2026-10-04T10:00:00.000Z'))
|
||
assert.deepEqual(gesundheitsWarnungen(einstellung), [], key)
|
||
}
|
||
})
|
||
|
||
test('Stichwortpruefung: null und leere Einstellung ergeben []', () => {
|
||
assert.deepEqual(gesundheitsWarnungen(null), [])
|
||
assert.deepEqual(gesundheitsWarnungen(undefined), [])
|
||
assert.deepEqual(gesundheitsWarnungen({}), [])
|
||
assert.deepEqual(gesundheitsWarnungen('Physiotherapie'), [])
|
||
assert.deepEqual(gesundheitsWarnungen({ betrieb: null, leistungen: 'x', gastFelder: { zusatz: [null, 5, { label: 7, optionen: 'a' }] } }), [])
|
||
})
|
||
|
||
test('Stichwortpruefung: nur Betriebsname, Leistungen und Zusatzfelder werden gelesen – nie E-Mail, Telefon, Anschrift, Storno, Gaeste', () => {
|
||
const e = {
|
||
betrieb: { name: 'Studio Nord', email: 'physio@example.invalid', telefon: 'Arzt-Notruf', adresse: 'Therapiestraße 1', impressumUrl: 'https://rezept.example.invalid' },
|
||
storno: { text: 'Behandlung nur nach Diagnose' },
|
||
leistungen: [{ name: 'Haarschnitt', beschreibung: 'Kurz und gut' }],
|
||
gastFelder: { telefon: 'pflicht', nachricht: 'optional', zusatz: [] },
|
||
}
|
||
assert.deepEqual(gesundheitsWarnungen(e), [])
|
||
})
|
||
|
||
test('Stichwortpruefung: ohne Doppelte (gleiche Meldung nur einmal) und hoechstens 10 Meldungen', () => {
|
||
const doppelt = {
|
||
betrieb: { name: 'Studio' },
|
||
leistungen: [{ name: 'Physiotherapie' }, { name: 'Physiotherapie' }, { name: 'physiotherapie' }],
|
||
gastFelder: { zusatz: [] },
|
||
}
|
||
const treffer = gesundheitsWarnungen(doppelt)
|
||
assert.equal(new Set(treffer).size, treffer.length, 'keine Doppelten')
|
||
assert.ok(treffer.length >= 1 && treffer.length <= 2, JSON.stringify(treffer))
|
||
const viele = { betrieb: { name: 'Studio' }, leistungen: Array.from({ length: 25 }, (_, i) => ({ name: `Therapie${String.fromCharCode(97 + i)}` })), gastFelder: { zusatz: [] } }
|
||
const alle = gesundheitsWarnungen(viele)
|
||
assert.equal(alle.length, 10)
|
||
assert.equal(new Set(alle).size, 10)
|
||
// zusammen mit Zusatzfeldern ebenfalls gedeckelt
|
||
const gemischt = { ...viele, gastFelder: { zusatz: Array.from({ length: 5 }, (_, i) => ({ label: `Rezept${i}`, optionen: ['Diagnose', 'Arzt'] })) } }
|
||
assert.ok(gesundheitsWarnungen(gemischt).length <= 10)
|
||
})
|
||
|
||
test('Stichwortpruefung: Meldungen sind Klartext (Strings), nennen den Fundort und das Wort, kein Markup, begrenzte Laenge', () => {
|
||
const langesWort = `Physio${'x'.repeat(200)}`
|
||
const treffer = gesundheitsWarnungen({ betrieb: { name: '<b>Praxis</b> Therapie' }, leistungen: [{ name: langesWort, beschreibung: 'Rezept <script>' }], gastFelder: { zusatz: [{ label: 'Diagnose?', optionen: ['Beschwerden', 'Nein'] }] } })
|
||
assert.ok(treffer.length >= 4)
|
||
for (const t of treffer) {
|
||
assert.equal(typeof t, 'string')
|
||
assert.ok(t.length <= 200, `${t.length}: ${t}`)
|
||
assert.doesNotMatch(t, /[<>]/, 'Markup bleibt aussen vor')
|
||
}
|
||
assert.ok(treffer.some((t) => /Betriebsname/.test(t)))
|
||
assert.ok(treffer.some((t) => /Leistung/.test(t)))
|
||
assert.ok(treffer.some((t) => /Zusatzfeld/.test(t)))
|
||
})
|
||
|
||
test('Stichwortpruefung: reine Funktion – Eingabe unveraendert, bei jedem Aufruf ein neues Array', () => {
|
||
const e = mitName('Physiotherapie')
|
||
const vorher = structuredClone(e)
|
||
const a = gesundheitsWarnungen(e)
|
||
assert.deepEqual(e, vorher)
|
||
assert.notEqual(a, gesundheitsWarnungen(e))
|
||
assert.deepEqual(a, gesundheitsWarnungen(e))
|
||
})
|
||
|
||
// ---------------------------------------------------------------- bestellungAnsicht
|
||
|
||
test('bestellungAnsicht: Abbildung von oeffentlicheAnsicht – Preis, Hinweis, Einrichtung, Zustimmungen unveraendert', async () => {
|
||
const ansicht = await bestellungAnsicht(T)
|
||
const reg = oeffentlicheAnsicht('termine')
|
||
assert.deepEqual(Object.keys(ansicht).sort(), ['belehrung', 'consent', 'dokumente', 'einrichtung', 'preis', 'preisHinweis'])
|
||
assert.equal(ansicht.preis, reg.priceLabel)
|
||
assert.equal(ansicht.preis, '149 €')
|
||
assert.equal(ansicht.preisHinweis, config.ustHinweis)
|
||
assert.equal(ansicht.einrichtung, reg.setupEstimate)
|
||
assert.deepEqual(ansicht.consent, reg.consent)
|
||
assert.equal(ansicht.consent.knopf, T.consent.knopf, 'der Knopf wörtlich aus dem Register')
|
||
assert.deepEqual(ansicht.consent.items, T.consent.items.map((i) => ({ id: i.id, text: i.text, haken: i.haken === true })))
|
||
assert.ok(ansicht.consent.items.every((i) => typeof i.haken === 'boolean'), 'haken-Flag je Eintrag')
|
||
assert.equal(ansicht.consent.version, T.consent.version)
|
||
})
|
||
|
||
test('bestellungAnsicht: belehrung ist das Ergebnis des Hakens (derselbe Text, den der Nachweis speichert)', async () => {
|
||
const ansicht = await bestellungAnsicht(T)
|
||
assert.equal(ansicht.belehrung, await featureHaken('termine').belehrung())
|
||
assert.equal(ansicht.belehrung, BELEHRUNG_PLATZHALTER)
|
||
// ein Test-Haken im Register ueberschreibt den Standard
|
||
T.belehrung = async () => 'Belehrung Test'
|
||
try {
|
||
assert.equal((await bestellungAnsicht(T)).belehrung, 'Belehrung Test')
|
||
} finally {
|
||
delete T.belehrung
|
||
}
|
||
})
|
||
|
||
test('bestellungAnsicht: dokumente nur mit gesetzter url, mit id, titel, version und url', async () => {
|
||
const merke = { lb: T.leistungsbeschreibung, avv: T.avv, widerruf: T.widerruf }
|
||
try {
|
||
T.leistungsbeschreibung = { version: '2026-10-05', url: '/rechtliches/leistungsbeschreibung-terminplaner-2026-10-05.html' }
|
||
T.avv = { version: '2026-10-05', url: '' }
|
||
T.widerruf = null
|
||
let ansicht = await bestellungAnsicht(T)
|
||
assert.deepEqual(ansicht.dokumente, [{ id: 'leistungsbeschreibung', titel: 'Leistungsbeschreibung', version: '2026-10-05', url: '/rechtliches/leistungsbeschreibung-terminplaner-2026-10-05.html' }])
|
||
T.avv = { version: '2026-10-05', url: '/rechtliches/avv-terminplaner-2026-10-05.html' }
|
||
T.widerruf = { version: '2026-10-05', url: '/rechtliches/widerruf-terminplaner-2026-10-05.html' }
|
||
ansicht = await bestellungAnsicht(T)
|
||
assert.deepEqual(ansicht.dokumente.map((d) => d.id), ['leistungsbeschreibung', 'avv', 'widerruf'])
|
||
for (const d of ansicht.dokumente) {
|
||
assert.deepEqual(Object.keys(d), ['id', 'titel', 'version', 'url'])
|
||
assert.ok(d.titel.length > 3 && d.url.startsWith('/rechtliches/') && d.version === '2026-10-05')
|
||
}
|
||
assert.ok(!('pflichtinfo' in ansicht) && !('widerruf' in ansicht), 'keine Felder pflichtinfo/widerruf')
|
||
} finally {
|
||
T.leistungsbeschreibung = merke.lb
|
||
T.avv = merke.avv
|
||
T.widerruf = merke.widerruf
|
||
}
|
||
})
|
||
|
||
test('bestellungAnsicht: Register ohne Rechtsunterlagen (Entwurf, url leer) ergibt eine leere Liste', async () => {
|
||
const merke = { lb: T.leistungsbeschreibung, avv: T.avv, widerruf: T.widerruf }
|
||
try {
|
||
T.leistungsbeschreibung = { version: 'ENTWURF', url: '' }
|
||
T.avv = { version: 'ENTWURF' }
|
||
T.widerruf = null
|
||
assert.deepEqual((await bestellungAnsicht(T)).dokumente, [])
|
||
} finally {
|
||
T.leistungsbeschreibung = merke.lb
|
||
T.avv = merke.avv
|
||
T.widerruf = merke.widerruf
|
||
}
|
||
})
|
||
|
||
test('bestellungAnsicht: unbekannte Funktion ergibt null, ebenso fehlender Eintrag', async () => {
|
||
assert.equal(await bestellungAnsicht({ key: 'analyse' }), null)
|
||
assert.equal(await bestellungAnsicht({ key: '__proto__' }), null)
|
||
assert.equal(await bestellungAnsicht({ key: 'nope', label: { name: 'x' } }), null)
|
||
assert.equal(await bestellungAnsicht(null), null)
|
||
assert.equal(await bestellungAnsicht(undefined), null)
|
||
assert.equal(await bestellungAnsicht({}), null)
|
||
})
|
||
|
||
test('bestellungAnsicht: enthaelt nie interne Felder (ticket, mails, Funktionen) und ist JSON-sicher', async () => {
|
||
const ansicht = await bestellungAnsicht(T)
|
||
const json = JSON.stringify(ansicht)
|
||
for (const verboten of ['ticket', 'mails', 'topic', 'details', 'formen', 'returnHash', 'product', 'requiresWartungsAbo', 'disabledMessage']) {
|
||
assert.ok(!json.includes(`"${verboten}"`), `kein Feld ${verboten}`)
|
||
}
|
||
assert.deepEqual(JSON.parse(json), ansicht)
|
||
// eine Kopie: Aenderungen an der Ansicht aendern das Register nicht
|
||
ansicht.consent.items[0].text = 'geaendert'
|
||
ansicht.consent.knopf = 'geaendert'
|
||
assert.notEqual(T.consent.items[0].text, 'geaendert')
|
||
assert.notEqual(T.consent.knopf, 'geaendert')
|
||
})
|
||
|
||
test('bestellungAnsicht: News (ohne belehrung-Haken) ergibt belehrung null und den News-Preis', async () => {
|
||
const ansicht = await bestellungAnsicht(config.features.news)
|
||
assert.equal(ansicht.belehrung, null)
|
||
assert.equal(ansicht.preis, config.features.news.priceLabel)
|
||
})
|
||
|
||
test('Der Stub ist weg: keine Marke STUB im Kopf von fragebogen.js', async () => {
|
||
const { readFile } = await import('node:fs/promises')
|
||
const quelle = await readFile(new URL('../server/services/termine/fragebogen.js', import.meta.url), 'utf8')
|
||
assert.doesNotMatch(quelle, /STUB/)
|
||
})
|