Terminplaner: Kurzfassung der Einbauanleitung fuer das Einrichtungs-Ticket, T16-Gesundheitsblock
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
90692b646b
commit
2db3eca357
9 files changed
+1181
-49
No files matched your search
@@ -6,8 +6,10 @@ import { VORGABE_FARBE, neuerFeedToken } from '../server/services/termine/einste
|
||||
import { ausVorlage } from '../server/services/termine/vorlagen.js'
|
||||
import { RECHTSTEXTE_ENTWURF, datenschutzAbsatz } from '../server/services/termine/rechtstexte.js'
|
||||
import {
|
||||
ENTWURF_BANNER, cssVorschlag, einbauanleitung, einbauSnippet, esc, kontrast, kuerzeListe, luminanz, textFarbeAuf, wochenplanText,
|
||||
ENTWURF_BANNER, cssVorschlag, driftWarnung, einbauanleitung, einbauSnippet, esc, gesundheitsMeldungen, kontrast, kuerzeListe, luminanz, textFarbeAuf,
|
||||
ticketStand, wochenplanText,
|
||||
} from '../server/services/termine/einbauanleitung.js'
|
||||
import { TICKET_DETAILS_MAX, anleitungsLink } from '../server/services/featureRegistry.js'
|
||||
|
||||
// Einbau-Snippet und Farbvorschlag des Terminplaners (TP2 Task D2). Reine Funktionen ohne Appwrite und ohne config.
|
||||
// Das Snippet war bis D2 in server/routes/termine.js; die Aufrufstelle und ihre Antwort-Tests stehen in termineRoutes.test.js.
|
||||
@@ -293,7 +295,8 @@ test('D3a: Ergebnisform, die ersten sechs Abschnitte in fester Reihenfolge, kein
|
||||
}
|
||||
for (const a of r.abschnitte.slice(0, 6)) assert.equal(a.kopierbar, false, a.id)
|
||||
assert.deepEqual(r.warnungen, [], 'ein vollstaendiger Betrieb ohne Entwurf-Kennzeichnung hat keine Warnungen')
|
||||
assert.equal(r.kurz, '', 'die Kurzfassung kommt mit D4')
|
||||
assert.equal(typeof r.kurz, 'string', 'die Kurzfassung steht seit D4 im Ergebnis (Inhalt: Tests ab "D4")')
|
||||
assert.ok(r.kurz.length > 0)
|
||||
// der Volltext fuehrt jeden Abschnitt mit Ueberschrift, die Datenschutz-Fassung der anderen Anrede fehlt (D3b)
|
||||
const ohneDu = r.abschnitte.filter((a) => a.id !== 'datenschutz_du')
|
||||
assert.deepEqual(zeilen(r.text).filter((z) => z.startsWith('## ')), ohneDu.map((a) => `## ${a.titel}`))
|
||||
@@ -1095,7 +1098,7 @@ test('D3b ohne Einstellung: nur der Abschnitt snippet (Modus aus dem Fragebogen)
|
||||
assert.ok(r.abschnitte[0].text.includes('Termine telefonisch vereinbaren.'), 'ohne Einstellung gibt es keine Telefonnummer')
|
||||
assert.equal(r.text, `## ${r.abschnitte[0].titel}\n${r.abschnitte[0].text}`)
|
||||
assert.deepEqual(r.stand, { version: 0, updatedAt: null })
|
||||
assert.equal(r.kurz, '')
|
||||
assert.ok(r.kurz.includes(r.abschnitte[0].text), 'die Kurzfassung (D4) traegt auch hier das Snippet')
|
||||
|
||||
const knopf = erzeuge({ einstellung: null, fragebogen: KNOPF_FRAGEBOGEN, version: 0, entwurf: false })
|
||||
assert.ok(knopf.abschnitte[0].text.includes('data-modus="knopf" data-knopf="Termin sichern"'), knopf.abschnitte[0].text)
|
||||
@@ -1133,3 +1136,360 @@ test('D3b Volltext hat die Abschnitte in der Reihenfolge der Tabelle und nur Tex
|
||||
assert.ok(stellen.every((i) => i >= 0))
|
||||
assert.deepEqual(stellen, [...stellen].sort((x, y) => x - y))
|
||||
})
|
||||
|
||||
// ================================================================ Kurzfassung fuers Ticket, ticketStand, driftWarnung (Task D4)
|
||||
// `kurz` ist Klartext fuer ticket.details (WOMS zeigt pre-wrap): Absaetze durch Leerzeile, erster Absatz = Kopf, hoechstens
|
||||
// TICKET_DETAILS_MAX Zeichen, nie ein Datenschutz-Absatz und nie ein Token.
|
||||
|
||||
const HALBES_PAAR = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/
|
||||
const absaetze = (text) => text.split('\n\n')
|
||||
const absatzMit = (kurz, anfang) => absaetze(kurz).find((a) => a.startsWith(anfang))
|
||||
const LINK = anleitungsLink('c1', 'p1')
|
||||
const GESUNDHEIT_KOPF = 'Gesundheitsbereich prüfen:'
|
||||
|
||||
/** Betrieb mit sechs Warnungen (Banner kommt ueber entwurf: true): Telefon, Anschrift, Impressum, Datenschutz-URL, Kontrast. */
|
||||
function mitSechsWarnungen() {
|
||||
const e = fixtureEinstellung()
|
||||
e.betrieb.telefon = ''
|
||||
e.betrieb.adresse = ''
|
||||
e.betrieb.impressumUrl = ''
|
||||
e.betrieb.datenschutzUrl = ''
|
||||
e.farbe = '#ffff00'
|
||||
return e
|
||||
}
|
||||
|
||||
/** Maximalbefuellung: 50 Leistungen mit 100 Zeichen (die ersten zehn mit Gesundheitswort), 20 Personen, 5 Zusatzfelder, sechs Warnungen. */
|
||||
function maximalEinstellung() {
|
||||
const e = mitSechsWarnungen()
|
||||
const vorlage = e.leistungen[0]
|
||||
e.leistungen = Array.from({ length: 50 }, (_, i) => ({
|
||||
...vorlage,
|
||||
id: `s_max${String(i).padStart(5, '0')}`,
|
||||
name: `${String(i + 1).padStart(2, '0')} ${'L'.repeat(97)}`,
|
||||
aktiv: true,
|
||||
ressourcen: [],
|
||||
beschreibung: i < 10 ? 'Physiotherapie' : '',
|
||||
}))
|
||||
e.ressourcen = Array.from({ length: 20 }, (_, i) => ({
|
||||
id: `r_max${String(i).padStart(5, '0')}`, typ: 'person', name: `Person ${i + 1} ${'P'.repeat(80)}`, aktiv: true, plan: { 1: [['09:00', '12:00']] }, feedToken: neuerFeedToken(),
|
||||
}))
|
||||
e.gastFelder = {
|
||||
telefon: 'pflicht',
|
||||
nachricht: 'optional',
|
||||
zusatz: Array.from({ length: 5 }, (_, i) => ({ key: `z${i}`, label: `Zusatzfeld ${i + 1} ${'Z'.repeat(90)}`, typ: 'text', optionen: [], pflicht: false })),
|
||||
}
|
||||
return e
|
||||
}
|
||||
const MAX_FRAGEBOGEN = { einbau: { modus: 'knopf', knopfText: 'K'.repeat(40), stelle: 'S'.repeat(300), gestaltung: 'G'.repeat(1000), bemerkung: 'B'.repeat(1000) } }
|
||||
|
||||
test('D4 kurz: der erste Absatz ist der Kopf und endet mit der Konfigurationsstand-Zeile', () => {
|
||||
const r = erzeuge({ entwurf: false })
|
||||
const [kopf] = absaetze(r.kurz)
|
||||
assert.deepEqual(kopf.split('\n'), [
|
||||
'Funktion: Terminplaner',
|
||||
'Projekt: Salon Muster Website',
|
||||
'Domain: salon-muster.example',
|
||||
'Kunde: Salon Muster GbR',
|
||||
`siteKey: ${SITE_KEY}`,
|
||||
'Konfigurationsstand: Version 3, 03.10.2026',
|
||||
])
|
||||
assert.equal(ticketStand(r.kurz), 3, 'ticketStand liest die erzeugte Zeile zurueck')
|
||||
})
|
||||
|
||||
test('D4 kurz Kopf: Domain wie createFeatureTicket (liveDomain, sonst previewUrl, sonst subdomain), Projektname faellt auf die subdomain zurueck', () => {
|
||||
const kopf = (project) => absaetze(erzeuge({ project }).kurz)[0].split('\n').slice(1, 3)
|
||||
assert.deepEqual(kopf(PROJEKT), ['Projekt: Salon Muster Website', 'Domain: salon-muster.example'])
|
||||
assert.deepEqual(kopf({ projectName: 'P', subdomain: 's', previewUrl: 'https://s.project.webklar.com' }), ['Projekt: P', 'Domain: https://s.project.webklar.com'])
|
||||
assert.deepEqual(kopf({ projectName: 'P', subdomain: 's' }), ['Projekt: P', 'Domain: s'])
|
||||
assert.deepEqual(kopf({ subdomain: 's' }), ['Projekt: s', 'Domain: s'])
|
||||
assert.deepEqual(kopf(undefined), ['Projekt: ', 'Domain: '])
|
||||
// lange Werte und Umbrueche: eine Zeile, hoechstens 120 Zeichen
|
||||
const lang = kopf({ projectName: `Name\nmit\tUmbruch ${'x'.repeat(200)}`, liveDomain: 'd'.repeat(300) })
|
||||
assert.ok(lang[0].startsWith('Projekt: Name mit Umbruch xxx'), lang[0])
|
||||
assert.ok(lang[0].length <= 'Projekt: '.length + 120 && lang[0].endsWith('…'), lang[0])
|
||||
assert.ok(lang[1].length <= 'Domain: '.length + 120 && lang[1].endsWith('…'), lang[1])
|
||||
})
|
||||
|
||||
test('D4 kurz Konfigurationsstand: Version und Datum, ohne Datum nur die Version, ohne Einstellungen Version 0, unlesbare Einstellungen', () => {
|
||||
const stand = (zusatz) => absaetze(erzeuge(zusatz).kurz)[0].split('\n').at(-1)
|
||||
assert.equal(stand({}), 'Konfigurationsstand: Version 3, 03.10.2026')
|
||||
assert.equal(stand({ updatedAt: null }), 'Konfigurationsstand: Version 3')
|
||||
assert.equal(stand({ updatedAt: 'kein Datum' }), 'Konfigurationsstand: Version 3')
|
||||
assert.equal(stand({ updatedAt: '2026-12-31T23:30:00.000Z' }), 'Konfigurationsstand: Version 3, 01.01.2027', 'Berliner Kalendertag')
|
||||
assert.equal(stand({ einstellung: null, version: 0, updatedAt: null }), 'Konfigurationsstand: Version 0 (noch keine Einstellungen gespeichert)')
|
||||
assert.equal(stand({ einstellung: null, version: 4, updatedAt: '2026-10-03T08:15:00.000Z' }), 'Konfigurationsstand: Version 4 (Einstellungen nicht lesbar)')
|
||||
assert.equal(ticketStand(erzeuge({ einstellung: null, version: 0 }).kurz), 0)
|
||||
})
|
||||
|
||||
test('D4 kurz: Snippet, 6 Schritte in fester Reihenfolge, Verweis; kein Datenschutz-Absatz, kein Token', () => {
|
||||
const e = fixtureEinstellung()
|
||||
const r = erzeuge({ einstellung: e, entwurf: false, feature: PAKET_FEATURE })
|
||||
const snippet = abschnitt(r, 'snippet').text
|
||||
assert.ok(snippet.includes(`data-webklar-termine="${SITE_KEY}"`))
|
||||
assert.ok(r.kurz.includes(`Einbau-Code (Snippet):\n${snippet}`), 'das Snippet steht unveraendert und mit Ueberschrift im Ticket')
|
||||
const schritte = absatzMit(r.kurz, 'Schritte:').split('\n').slice(1)
|
||||
assert.equal(schritte.length, 6, schritte.join('\n'))
|
||||
schritte.forEach((s, i) => assert.ok(s.startsWith(`${i + 1}. `), s))
|
||||
const reihenfolge = [/WOMS/, /Einbau-Code/, /Kontrast/, /Datenschutz-Absatz/, /„Freischalten \(Abnahme\)“.*Probebuchung.*stornier/, /Ticket erledigen.*Zeit.*Nicht auf das Stundenkontingent anrechnen/]
|
||||
reihenfolge.forEach((muster, i) => assert.match(schritte[i], muster, schritte[i]))
|
||||
assert.ok(r.kurz.endsWith(`Verweis: ${LINK}`), 'der Verweis steht zuletzt')
|
||||
assert.ok(!r.kurz.includes('Online-Terminbuchung'), 'der Datenschutz-Absatz gehoert nicht ins Ticket')
|
||||
assert.ok(!r.kurz.includes(datenschutzAbsatz(e, 'sie').slice(0, 80)))
|
||||
for (const x of e.ressourcen) assert.ok(!r.kurz.includes(x.feedToken), 'kein Kalender-Token')
|
||||
assert.ok(!/feedtoken/i.test(r.kurz))
|
||||
assert.ok(!r.kurz.includes(e.betrieb.email), 'die Betriebs-E-Mail steht nur in den Datenschutz-Absaetzen')
|
||||
})
|
||||
|
||||
test('D4 kurz Schritt 4 und 6: Datenschutz-Fassung nach Anrede, Preis nur bei bezahlter Einrichtung, die Pflichtformel steht immer', () => {
|
||||
const schritt = (zusatz, n) => absatzMit(erzeuge(zusatz).kurz, 'Schritte:').split('\n')[n]
|
||||
const du = fixtureEinstellung()
|
||||
du.anrede = 'du'
|
||||
assert.match(schritt({}, 4), /Sie-Form/)
|
||||
assert.match(schritt({ einstellung: du }, 4), /Du-Form/)
|
||||
assert.ok(schritt({}, 4).includes('Absatz per Chat an den Kunden senden'), 'Zwischenregel aus der Checkliste (F10b2 ersetzt sie an einer Stelle)')
|
||||
assert.match(schritt({ feature: PAKET_FEATURE }, 6), /Nicht auf das Stundenkontingent anrechnen \(Einrichtung bezahlt, 149 €\)/)
|
||||
assert.ok(schritt({}, 6).includes('Nicht auf das Stundenkontingent anrechnen'))
|
||||
assert.ok(!schritt({}, 6).includes('bezahlt'), 'ohne Kauf keine Behauptung, die Einrichtung sei bezahlt')
|
||||
for (const unlock of [undefined, null, 1, 'true']) assert.ok(!schritt({ feature: { ...PAKET_FEATURE, unlockedByPurchase: unlock } }, 6).includes('bezahlt'), String(unlock))
|
||||
})
|
||||
|
||||
test('D4 kurz Verweis: anleitungsLink des Projekts, ohne IDs der allgemeine Hinweis', () => {
|
||||
assert.ok(erzeuge().kurz.endsWith(`Verweis: ${LINK}`))
|
||||
assert.equal(LINK, `${config.womsBaseUrl}/customers/c1?tab=funktionen&anleitung=p1`)
|
||||
const ohne = erzeuge({ feature: { $id: 'f1', siteKey: SITE_KEY } })
|
||||
assert.ok(ohne.kurz.endsWith('Verweis: im WOMS unter Kunden → Funktionen, Anleitung des Projekts'), ohne.kurz.slice(-120))
|
||||
assert.ok(!ohne.kurz.includes('?tab='))
|
||||
})
|
||||
|
||||
test('D4 kurz Kundenwunsch: Modus, Knopf-Text, Stelle, Gestaltung und Bemerkung als Text des Kunden (eingerueckt), ohne Fragebogen der Standard', () => {
|
||||
const mit = erzeuge({ fragebogen: KNOPF_FRAGEBOGEN })
|
||||
const block = absatzMit(mit.kurz, 'Kundenwunsch')
|
||||
assert.deepEqual(block.split('\n'), [
|
||||
'Kundenwunsch (Text des Kunden):',
|
||||
' Modus: knopf',
|
||||
' Knopf-Text: Termin sichern',
|
||||
' Stelle: Startseite unter dem Hero',
|
||||
' Gestaltung: Schlicht, wie die Seite',
|
||||
' Bemerkung: <script>alert(1)</script>',
|
||||
])
|
||||
assert.ok(mit.kurz.includes('data-modus="knopf" data-knopf="Termin sichern"'))
|
||||
assert.equal(mit.kurz.split('alert(1)').length - 1, 1, 'der Wunsch steht nur im Kundenwunsch-Block, nie im Snippet')
|
||||
|
||||
const ohne = absatzMit(erzeuge({ fragebogen: null }).kurz, 'Kundenwunsch').split('\n')
|
||||
assert.deepEqual(ohne, ['Kundenwunsch (Text des Kunden):', ' Modus: inline (Standard)', ' Sonst keine Angaben im Fragebogen'])
|
||||
|
||||
// mehrzeilig: weitere Zeilen eingerueckt, Leerzeilen fallen weg (nie ein Absatzumbruch im Kundentext), keine Ueberschrift, kein falscher Stand
|
||||
const mehrKurz = erzeuge({ fragebogen: { einbau: { modus: 'inline', bemerkung: 'Eins\n\n\nZwei\nKonfigurationsstand: Version 99' } } }).kurz
|
||||
assert.deepEqual(absatzMit(mehrKurz, 'Kundenwunsch').split('\n').slice(2), [' Bemerkung: Eins', ' Zwei', ' Konfigurationsstand: Version 99'])
|
||||
assert.ok(!/^Konfigurationsstand: Version 99/m.test(mehrKurz), 'der Wunsch kann den Stand nicht faelschen')
|
||||
assert.equal(ticketStand(mehrKurz), 3)
|
||||
assert.equal(absaetze(mehrKurz).length, absaetze(erzeuge().kurz).length, 'die Absatzzahl aendert sich durch den Wunsch nicht')
|
||||
})
|
||||
|
||||
test('D4 kurz Zaehlungen: Anzahl, aktive und die ersten 8 Namen, danach "und N weitere"', () => {
|
||||
const umfang = absatzMit(erzeuge().kurz, 'Umfang:').split('\n')
|
||||
assert.deepEqual(umfang, [
|
||||
'Umfang:',
|
||||
'- 4 Leistungen (3 aktiv): Herrenschnitt, Damenschnitt, Kinderschnitt, Färben',
|
||||
'- 3 Personen und Plätze (2 aktiv): Anna, Ben, Stuhl 2',
|
||||
'- 2 Zusatzfelder: Haarlänge, Wunschfrisur',
|
||||
])
|
||||
const e = fixtureEinstellung()
|
||||
e.leistungen = Array.from({ length: 12 }, (_, i) => ({ ...e.leistungen[0], id: `s_z${i}`, name: `Leistung ${i + 1}`, aktiv: i !== 3 }))
|
||||
const zeile12 = absatzMit(erzeuge({ einstellung: e }).kurz, 'Umfang:').split('\n')[1]
|
||||
assert.equal(zeile12, '- 12 Leistungen (11 aktiv): Leistung 1, Leistung 2, Leistung 3, Leistung 4, Leistung 5, Leistung 6, Leistung 7, Leistung 8, … und 4 weitere')
|
||||
e.leistungen = e.leistungen.slice(0, 8)
|
||||
assert.ok(!absatzMit(erzeuge({ einstellung: e }).kurz, 'Umfang:').includes('weitere'), 'genau 8: nichts uebrig')
|
||||
e.leistungen = e.leistungen.slice(0, 1)
|
||||
e.ressourcen = []
|
||||
e.gastFelder = { zusatz: [] }
|
||||
assert.deepEqual(absatzMit(erzeuge({ einstellung: e }).kurz, 'Umfang:').split('\n').slice(1), ['- 1 Leistung (1 aktiv): Leistung 1', '- 0 Personen und Plätze', '- 0 Zusatzfelder'])
|
||||
// sehr lange Namen: je Name hoechstens 60 Zeichen, Umbrueche werden zu Leerzeichen
|
||||
e.leistungen = [{ ...e.leistungen[0], name: `${'N'.repeat(100)}\nzwei` }]
|
||||
const lang = absatzMit(erzeuge({ einstellung: e }).kurz, 'Umfang:').split('\n')[1]
|
||||
assert.ok(lang.endsWith(`${'N'.repeat(59)}…`), lang)
|
||||
assert.ok(!lang.includes('zwei') && !lang.includes('N'.repeat(60)))
|
||||
})
|
||||
|
||||
test('D4 kurz Warnungen: hoechstens 5 in der Reihenfolge von warnungen, das Banner nur einmal, ohne Warnung kein Absatz', () => {
|
||||
const r = erzeuge({ einstellung: mitSechsWarnungen(), entwurf: true })
|
||||
assert.equal(r.warnungen.length, 6, r.warnungen.join('\n'))
|
||||
const block = absatzMit(r.kurz, 'Warnungen:').split('\n')
|
||||
assert.deepEqual(block, ['Warnungen:', ...r.warnungen.slice(0, 5).map((w) => `- ${w}`)], 'die ersten fuenf, die sechste (Kontrast) bleibt dem Panel')
|
||||
assert.equal(r.kurz.split(ENTWURF_BANNER).length - 1, 1, 'der Banner-Text steht genau einmal im Ticket')
|
||||
assert.equal(block[1], `- ${ENTWURF_BANNER}`)
|
||||
const ohne = erzeuge({ entwurf: false })
|
||||
assert.deepEqual(ohne.warnungen, [])
|
||||
assert.ok(!ohne.kurz.includes('Warnungen:'))
|
||||
assert.equal(erzeuge({ einstellung: mitSechsWarnungen(), entwurf: false }).warnungen.length, 5)
|
||||
assert.equal(absatzMit(erzeuge({ einstellung: mitSechsWarnungen(), entwurf: false }).kurz, 'Warnungen:').split('\n').length, 6)
|
||||
})
|
||||
|
||||
test('D4 kurz Gesundheit: eigener Absatz, zaehlt nicht zu den 5 Warnungen, wohlgeformt, einzeilig und als Klartext', () => {
|
||||
const e = mitSechsWarnungen()
|
||||
e.leistungen[0].name = 'Physiotherapie'
|
||||
e.leistungen[1].name = `${'x'.repeat(38)}😀${'y'.repeat(10)}`
|
||||
e.leistungen[1].beschreibung = 'Therapie'
|
||||
e.leistungen[2].name = 'Zeile1\n## Zeile2 <b>x</b>'
|
||||
e.leistungen[2].beschreibung = 'Physiotherapie'
|
||||
const r = erzeuge({ einstellung: e, entwurf: true })
|
||||
const gesundheit = absatzMit(r.kurz, GESUNDHEIT_KOPF).split('\n')
|
||||
const meldungen = gesundheitsMeldungen(e)
|
||||
assert.equal(meldungen.length, 3, meldungen.join('\n'))
|
||||
assert.deepEqual(gesundheit, [GESUNDHEIT_KOPF, ...meldungen.map((m) => `- ${m}`)])
|
||||
assert.equal(absatzMit(r.kurz, 'Warnungen:').split('\n').length, 6, 'die fuenf Warnungen bleiben voll, die Gesundheit kommt dazu')
|
||||
for (const m of meldungen) {
|
||||
assert.ok(m.isWellFormed() && !/[\r\n]/.test(m), JSON.stringify(m))
|
||||
assert.ok(!r.warnungen.every((w) => w !== m), 'dieselben Meldungen stehen auch in warnungen')
|
||||
}
|
||||
assert.ok(meldungen.some((m) => m.includes('<b>x</b>')), 'Klartext, nichts wird maskiert')
|
||||
assert.ok(r.kurz.isWellFormed() && !HALBES_PAAR.test(r.kurz))
|
||||
// keine Treffer: kein Absatz
|
||||
assert.ok(!erzeuge().kurz.includes(GESUNDHEIT_KOPF))
|
||||
// Reihenfolge der Absaetze
|
||||
const stellen = ['Funktion:', 'Kundenwunsch', 'Umfang:', 'Warnungen:', GESUNDHEIT_KOPF, 'Einbau-Code (Snippet):', 'Schritte:', 'Verweis:'].map((a) => absaetze(erzeuge({ einstellung: e, entwurf: true, fragebogen: KNOPF_FRAGEBOGEN }).kurz).findIndex((x) => x.startsWith(a)))
|
||||
assert.ok(stellen.every((i) => i >= 0), JSON.stringify(stellen))
|
||||
assert.deepEqual(stellen, [...stellen].sort((a, b) => a - b))
|
||||
})
|
||||
|
||||
test('D4 gesundheitsMeldungen: ohne Treffer und ohne Einstellung leer, sonst die bereinigten Meldungen der Pruefung aus C', () => {
|
||||
assert.deepEqual(gesundheitsMeldungen(null), [])
|
||||
assert.deepEqual(gesundheitsMeldungen(undefined), [])
|
||||
assert.deepEqual(gesundheitsMeldungen(fixtureEinstellung()), [])
|
||||
const e = fixtureEinstellung()
|
||||
e.leistungen[0].name = 'Physiotherapie'
|
||||
assert.deepEqual(gesundheitsMeldungen(e), ['Leistungsname enthält „Physiotherapie“'])
|
||||
const halb = fixtureEinstellung()
|
||||
halb.leistungen[0].name = `${'x'.repeat(38)}😀${'y'.repeat(10)}`
|
||||
halb.leistungen[0].beschreibung = 'Physiotherapie'
|
||||
for (const m of gesundheitsMeldungen(halb)) assert.ok(m.isWellFormed(), JSON.stringify(m))
|
||||
})
|
||||
|
||||
test('D4 kurz ohne Einstellungen: Kopf mit Version 0, Warnung, Snippet, Schritte und Verweis', () => {
|
||||
const r = erzeuge({ einstellung: null, version: 0, updatedAt: null, entwurf: true })
|
||||
const [kopf] = absaetze(r.kurz)
|
||||
assert.ok(kopf.endsWith('Konfigurationsstand: Version 0 (noch keine Einstellungen gespeichert)'), kopf)
|
||||
assert.ok(r.kurz.includes('noch keine Einstellungen'))
|
||||
assert.deepEqual(absatzMit(r.kurz, 'Warnungen:').split('\n'), ['Warnungen:', `- ${ENTWURF_BANNER}`, '- Keine Einstellungen gespeichert'])
|
||||
assert.ok(r.kurz.includes(`Einbau-Code (Snippet):\n${r.abschnitte[0].text}`))
|
||||
assert.ok(!r.kurz.includes('Umfang:') && !r.kurz.includes(GESUNDHEIT_KOPF))
|
||||
assert.equal(absatzMit(r.kurz, 'Schritte:').split('\n').length, 7)
|
||||
assert.ok(r.kurz.endsWith(`Verweis: ${LINK}`))
|
||||
assert.ok(r.kurz.length <= TICKET_DETAILS_MAX)
|
||||
// der Wunsch des Kunden (Fragebogen) steht auch ohne Einstellungen im Ticket
|
||||
assert.ok(erzeuge({ einstellung: null, version: 0, fragebogen: KNOPF_FRAGEBOGEN }).kurz.includes(' Knopf-Text: Termin sichern'))
|
||||
})
|
||||
|
||||
test('D4 kurz Maximalbefuellung: hoechstens TICKET_DETAILS_MAX, Kopf, Snippet, Verweis, Stundenkontingent und alle 10 Gesundheitswarnungen bleiben', () => {
|
||||
const e = maximalEinstellung()
|
||||
const meldungen = gesundheitsMeldungen(e)
|
||||
assert.equal(meldungen.length, 10, 'die Pruefung aus C liefert hoechstens 10')
|
||||
const r = erzeuge({ einstellung: e, fragebogen: MAX_FRAGEBOGEN, feature: PAKET_FEATURE, entwurf: true })
|
||||
assert.ok(r.kurz.length <= TICKET_DETAILS_MAX, `kurz ${r.kurz.length}`)
|
||||
assert.ok(r.kurz.length > 3000, 'es steht etwas Sinnvolles da, nicht nur ein Rumpf')
|
||||
assert.equal(absaetze(r.kurz)[0].split('\n').at(-1), 'Konfigurationsstand: Version 3, 03.10.2026')
|
||||
assert.ok(r.kurz.includes(abschnitt(r, 'snippet').text))
|
||||
assert.ok(r.kurz.includes('Nicht auf das Stundenkontingent anrechnen'))
|
||||
assert.ok(r.kurz.endsWith(`Verweis: ${LINK}`))
|
||||
assert.deepEqual(absatzMit(r.kurz, GESUNDHEIT_KOPF).split('\n'), [GESUNDHEIT_KOPF, ...meldungen.map((m) => `- ${m}`)])
|
||||
assert.ok(!r.kurz.includes('Online-Terminbuchung'))
|
||||
for (const x of e.ressourcen) assert.ok(!r.kurz.includes(x.feedToken))
|
||||
assert.ok(r.kurz.isWellFormed())
|
||||
assert.equal(r.kurz.split(ENTWURF_BANNER).length - 1, 1)
|
||||
})
|
||||
|
||||
test('D4 kurz Platzdruck: erst fallen die Zaehlungen, dann die Warnungen, dann der Kundenwunsch weg; Pflichtteile und Gesundheit nie', () => {
|
||||
const gesehen = new Set()
|
||||
let zuletzt = null
|
||||
// das Snippet waechst mit dem (hier absichtlich nicht normalisierten) Telefon und treibt die Pflichtteile nach oben
|
||||
for (let laenge = 1; laenge <= 3200; laenge += 8) {
|
||||
const e = maximalEinstellung()
|
||||
e.betrieb.telefon = '0'.repeat(laenge)
|
||||
e.betrieb.adresse = ''
|
||||
const r = erzeuge({ einstellung: e, fragebogen: MAX_FRAGEBOGEN, feature: PAKET_FEATURE, entwurf: true })
|
||||
const k = r.kurz
|
||||
assert.ok(k.length <= TICKET_DETAILS_MAX, `Telefon ${laenge}: kurz ${k.length}`)
|
||||
// Pflichtteile
|
||||
assert.equal(absaetze(k)[0].split('\n').at(-1), 'Konfigurationsstand: Version 3, 03.10.2026')
|
||||
assert.ok(k.includes(abschnitt(r, 'snippet').text), `Telefon ${laenge}: Snippet`)
|
||||
assert.ok(k.includes('Nicht auf das Stundenkontingent anrechnen') && k.endsWith(`Verweis: ${LINK}`))
|
||||
assert.deepEqual(absatzMit(k, GESUNDHEIT_KOPF).split('\n').slice(1), gesundheitsMeldungen(e).map((m) => `- ${m}`), `Telefon ${laenge}: Gesundheit`)
|
||||
// Stufen
|
||||
const umfang = k.includes('\n\nUmfang:')
|
||||
const warnungen = k.includes('\n\nWarnungen:')
|
||||
const wunsch = k.includes('\n\nKundenwunsch (Text des Kunden):')
|
||||
if (umfang) assert.ok(warnungen && wunsch, `Telefon ${laenge}: Zaehlungen nur mit Warnungen und Wunsch`)
|
||||
if (warnungen) assert.ok(wunsch, `Telefon ${laenge}: Warnungen nur mit Wunsch`)
|
||||
if (warnungen) {
|
||||
const anzahl = absatzMit(k, 'Warnungen:').split('\n').length - 1
|
||||
assert.deepEqual(absatzMit(k, 'Warnungen:').split('\n').slice(1), r.warnungen.filter((w) => !gesundheitsMeldungen(e).includes(w)).slice(0, anzahl).map((w) => `- ${w}`), 'es fallen die letzten Warnungen zuerst weg')
|
||||
}
|
||||
gesehen.add(`${umfang ? 'Umfang' : '-'}/${warnungen ? 'Warnungen' : '-'}/${wunsch ? 'Wunsch' : '-'}`)
|
||||
zuletzt = k
|
||||
}
|
||||
assert.ok(gesehen.has('Umfang/Warnungen/Wunsch'), [...gesehen].join(' '))
|
||||
assert.ok(gesehen.has('-/Warnungen/Wunsch'), [...gesehen].join(' '))
|
||||
assert.ok(gesehen.has('-/-/Wunsch'), [...gesehen].join(' '))
|
||||
assert.ok(gesehen.has('-/-/-'), [...gesehen].join(' '))
|
||||
assert.ok(zuletzt.length > 0)
|
||||
})
|
||||
|
||||
test('D4 kurz: selbst ein riesiges Snippet sprengt die Grenze nicht, die Gesundheit steht vor dem Schnitt', () => {
|
||||
const e = maximalEinstellung()
|
||||
e.betrieb.telefon = '9'.repeat(20000)
|
||||
const k = erzeuge({ einstellung: e, fragebogen: MAX_FRAGEBOGEN, entwurf: true }).kurz
|
||||
assert.ok(k.length <= TICKET_DETAILS_MAX, `kurz ${k.length}`)
|
||||
assert.ok(k.isWellFormed() && !HALBES_PAAR.test(k))
|
||||
assert.ok(k.includes(GESUNDHEIT_KOPF))
|
||||
assert.ok(k.endsWith('… gekürzt, vollständig im Ticketsystem unter Kunden → Funktionen'))
|
||||
assert.equal(absaetze(k)[0].split('\n').at(-1), 'Konfigurationsstand: Version 3, 03.10.2026')
|
||||
})
|
||||
|
||||
test('D4 kurz ist wohlgeformt: halbe Zeichenpaare in Namen, Wuenschen, Projekt und Kunde werden ersetzt, Emojis bleiben', () => {
|
||||
const e = fixtureEinstellung()
|
||||
e.leistungen[0].name = `Schnitt \uD83D${'x'.repeat(10)}😀`
|
||||
e.ressourcen[0].name = `Anna\uDE00`
|
||||
const fb = { einbau: { modus: 'inline', bemerkung: `halb \uD83D und ganz 😀`, stelle: '\uDE00' } }
|
||||
const r = erzeuge({ einstellung: e, fragebogen: fb, project: { projectName: `Salon \uD83D`, liveDomain: 'x.de' }, kunde: { name: `K\uDE00unde` } })
|
||||
assert.ok(r.kurz.isWellFormed() && !HALBES_PAAR.test(r.kurz), JSON.stringify(r.kurz.match(HALBES_PAAR)))
|
||||
assert.ok(r.kurz.includes('😀'), 'ganze Zeichenpaare bleiben')
|
||||
assert.ok(JSON.parse(JSON.stringify(r.kurz)) === r.kurz)
|
||||
})
|
||||
|
||||
test('D4 kurz ist deterministisch, unabhaengig von now und aendert die Eingaben nicht', () => {
|
||||
const e = maximalEinstellung()
|
||||
const fb = structuredClone(MAX_FRAGEBOGEN)
|
||||
const vorher = structuredClone({ e, fb })
|
||||
const a = erzeuge({ einstellung: e, fragebogen: fb })
|
||||
assert.equal(erzeuge({ einstellung: e, fragebogen: fb }).kurz, a.kurz)
|
||||
assert.equal(erzeuge({ einstellung: e, fragebogen: fb, now: SPAETER }).kurz, a.kurz, 'in der Kurzfassung steht kein Erzeugungsdatum')
|
||||
assert.deepEqual(structuredClone({ e, fb }), vorher)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- ticketStand, driftWarnung
|
||||
|
||||
test('D4 ticketStand liest Konfigurationsstand: Version N am Zeilenanfang, sonst null', () => {
|
||||
assert.equal(ticketStand('Funktion: Terminplaner\nKonfigurationsstand: Version 7, 03.10.2026\n\nrest'), 7)
|
||||
assert.equal(ticketStand('Konfigurationsstand: Version 7, 03.10.2026'), 7)
|
||||
assert.equal(ticketStand('Konfigurationsstand: Version 0 (noch keine Einstellungen gespeichert)'), 0)
|
||||
assert.equal(ticketStand('x\r\nKonfigurationsstand: Version 12'), 12)
|
||||
assert.equal(ticketStand('Konfigurationsstand: Version 999999999'), 999999999, 'neun Stellen sind erlaubt')
|
||||
assert.equal(ticketStand('Konfigurationsstand: Version 99999999999'), null, 'elf Stellen: kein Stand')
|
||||
assert.equal(ticketStand('Konfigurationsstand: Version 1234567890'), null, 'zehn Stellen: kein Stand')
|
||||
for (const ohne of ['', 'Funktion: Terminplaner', 'Konfigurationsstand: Version', 'Konfigurationsstand: Version x', ' Konfigurationsstand: Version 7', 'Der Konfigurationsstand: Version 7', 'konfigurationsstand: Version 7', 'Konfigurationsstand: Version -7']) {
|
||||
assert.equal(ticketStand(ohne), null, JSON.stringify(ohne))
|
||||
}
|
||||
for (const nichtText of [undefined, null, 7, {}, ['Konfigurationsstand: Version 7']]) assert.equal(ticketStand(nichtText), null, String(nichtText))
|
||||
})
|
||||
|
||||
test('D4 driftWarnung: nur wenn die Einstellungen jetzt eine hoehere Version haben als das Ticket', () => {
|
||||
assert.equal(driftWarnung(7, 9), 'Einstellungen seit der Bestellung geändert (Ticket Version 7, jetzt 9).')
|
||||
assert.equal(driftWarnung(0, 1), 'Einstellungen seit der Bestellung geändert (Ticket Version 0, jetzt 1).', 'Version 0 im Ticket ist ein gueltiger Stand')
|
||||
assert.equal(driftWarnung(9, 9), null)
|
||||
assert.equal(driftWarnung(9, 7), null)
|
||||
assert.equal(driftWarnung(null, 9), null)
|
||||
assert.equal(driftWarnung(7, null), null)
|
||||
assert.equal(driftWarnung(undefined, 9), null)
|
||||
assert.equal(driftWarnung('7', 9), null)
|
||||
assert.equal(driftWarnung(7.5, 9), null)
|
||||
})
|
||||
Reference in new issue
Block a user