Terminplaner TP2: Fragebogen-Schritte 4 bis 8 und Bestellseite

- public/termine-fragebogen.js: Schritt 4 Zeiten (je Person/Platz wochenplanFeld,
  Bundesland ohne Vorauswahl mit leerer erster Option, "An Feiertagen geschlossen" im
  PATCH, danach PUT mit bundeslandGewaehlt; darunter abwesenheiten und sonderoeffnungen
  mit eigenem Container und eigenem zeichne, damit ungespeicherte Zeiten bleiben),
  Schritt 5 Regeln (Sammelknopf Alle sofort buchbar / Alle nur als Anfrage mit
  buchungsartFuerAlle und buchungsartLage, regeln, storno mit Muster nach Absagefrist,
  rechtliches, zahlung), Schritt 6 Gastfelder, Schritt 7 mit gemeinsamem
  leseAbschnitte, Schritt 8 Website (betrieb nur Telefon und vier Links, Einbauwuensche
  Modus/Knopftext/Stelle/Gestaltung/Bemerkung im PUT, Hinweis auf Mehrwuensche nach
  Stunden); danach renderBestellen. Platzhalter-Schritt entfernt.
- termine.css: Klassen der Schritte 4 bis 8
- Tests: termineView (Importe, PATCH/PUT nur an erlaubten Stellen, Bundesland ohne
  Vorauswahl im Quelltext), termineFragebogenView (Verhalten mit Fake-DOM fuer die
  Schritte 4 bis 8, Schluesselmengen, Bestellseite nach Schritt 8)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 22:18:44 +00:00
1 parent 9bf5ec09f8
commit 8d628ef906
4 files changed
+939 -34

No files matched your search

+654 -11
View File
@@ -20,6 +20,7 @@ import * as fragebogenAnsicht from '../public/termine-fragebogen.js'
import { FENSTER_FEHLER } from '../public/termine-view.js'
import { h, request } from '../public/termine-dom.js'
import { neuerSpeicher } from '../public/termine-speichern.js'
import { stornoMuster } from '../public/termine-gaestetexte.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.
@@ -1674,16 +1675,6 @@ test('renderFragebogen: Kundenansicht zeigt den Hinweis, sonst nicht', mitDom(un
assert.equal(fx.finde(fbSeite(fbDaten()).wurzel, '.tm-box[role="note"]'), null)
}))
test('Schritte ohne Inhalt (4, 5, 6, 8 kommen mit C9b): Titel, Hinweis, Zurück, kein Weiter und kein Aufruf', mitDom(undefined, async (fx) => {
const { wurzel } = fbSeite(fbDaten({ fragebogen: fbBis(3) }))
assert.equal(fbSchritt(fx, wurzel), 'zeiten')
assert.equal(fx.text(fx.finde(wurzel, 'h3')), 'Zeiten')
assert.equal(fbWeiter(fx, wurzel), null)
await fx.klick(fx.finde(wurzel, '[data-aktion="zurueck"]'))
assert.equal(fbSchritt(fx, wurzel), 'leistungen')
assert.deepEqual(fx.fetchLog, [])
}))
// ------------------------------------------------------- Schritt 1: Branche und Betrieb, ohne Entwurf
test('Schritt 1 ohne Entwurf: Radiogruppe aus daten.branchen, Betrieb mit Vorschlag, Knopf „Mit dieser Branche starten“ gesperrt bis zur Wahl', mitDom(undefined, async (fx) => {
const { wurzel } = fbSeite(fbStart())
@@ -2295,12 +2286,658 @@ test('Texte der Kunden stehen nur als Text im Baum (Namen, Vorschlag)', mitDom(u
assert.equal(fbFeld(fx, start, 'betrieb.name').value, boese)
}))
// ------------------------------------------------------- Fragebogen-Oberfläche: Schritte 4 bis 8 und Bestellseite (TP2, Task C9b)
// Die Schritte bauen auf den Abschnitten der Reiter auf (wochenplanFeld aus termine-tab-team.js, abwesenheiten/sonderoeffnungen aus termine-team-ausnahmen.js,
// betrieb/darstellung/regeln/gastFelder/erinnerung/storno/rechtliches/zahlung aus termine-tab-einstellungen.js); hier steht, was der Fragebogen daraus macht.
const fbZeitenSeite = (aenderung = {}, optionen) => fbSeite(fbDaten({ fragebogen: fbBis(3), ...aenderung }), optionen)
const fbRegelnSeite = (aenderung = {}) => fbSeite(fbDaten({ fragebogen: fbBis(4, { bundeslandGewaehlt: true }), ...aenderung }))
const fbGastSeite = (aenderung = {}) => fbSeite(fbDaten({ fragebogen: fbBis(5, { bundeslandGewaehlt: true }), ...aenderung }))
const fbWebsiteSeite = (aenderung = {}) => fbSeite(fbDaten({ fragebogen: fbBis(7, { bundeslandGewaehlt: true }), ...aenderung }))
const EINBAU_LEER = { modus: null, knopfText: '', stelle: '', gestaltung: '', bemerkung: '' }
/** Entwurf mit zwei Personen (Anna behält den Plan der Vorlage, Ben hat einen eigenen) und einem Platz. */
function dreiEintraege() {
const e = salon()
const [anna] = e.ressourcen
e.ressourcen = [
{ ...anna, name: 'Anna' },
{ id: 'r_ben00002', typ: 'person', name: 'Ben', aktiv: true, plan: { 2: [['13:00', '17:00']] } },
{ id: 'r_stuhl003', typ: 'platz', name: 'Stuhl 3', aktiv: false, plan: {} },
]
return e
}
test('alle acht Schritte haben Inhalt: ein Formular mit „Weiter“ (außer Schritt 1 ohne Entwurf), keinen Platzhalter', mitDom(undefined, async (fx) => {
const { wurzel } = fbSeite(fbDaten({ fragebogen: fbBis(0) }))
for (const [key, titel] of FRAGEBOGEN_SCHRITTE) {
await fbGehe(fx, wurzel, key)
assert.equal(fbSchritt(fx, wurzel), key)
assert.equal(fx.text(fx.finde(wurzel, 'h3')), titel)
assert.ok(fx.finde(wurzel, 'form'), `${key}: ein Formular`)
assert.ok(fbWeiter(fx, wurzel), `${key}: „Weiter“`)
assert.doesNotMatch(fx.text(wurzel), /noch nicht verfügbar/, `${key}: kein Platzhalter`)
}
assert.deepEqual(fx.fetchLog, [], 'das Anzeigen allein ruft nichts auf')
}))
// ------------------------------------------------------- Schritt 4: Zeiten
test('Schritt 4: je Person und Platz ein Wochenplan (wochenplanFeld, Felder ressourcen.n.plan.1 bis 7), vorbelegt aus dem Plan', mitDom(undefined, (fx) => {
const { wurzel } = fbZeitenSeite({ einstellung: dreiEintraege() })
assert.equal(fbSchritt(fx, wurzel), 'zeiten')
const gruppen = fx.alle(wurzel, '[data-ressource]')
assert.deepEqual(gruppen.map((g) => fx.text(fx.finde(g, 'h4'))), ['Anna', 'Ben', 'Stuhl 3 (nicht aktiv)'])
assert.equal(fx.alle(wurzel, 'fieldset.tm-wochenplan').length, 3, 'drei Wochenpläne')
const plaene = felderVon(fx, wurzel).filter((n) => n.includes('.plan.'))
assert.deepEqual(plaene, [0, 1, 2].flatMap((i) => [1, 2, 3, 4, 5, 6, 7].map((nr) => `ressourcen.${i}.plan.${nr}`)))
assert.equal(fbFeld(fx, wurzel, 'ressourcen.0.plan.2').value, '09:00–12:30, 13:30–18:00')
assert.equal(fbFeld(fx, wurzel, 'ressourcen.0.plan.1').value, '', 'Montag geschlossen')
assert.equal(fbFeld(fx, wurzel, 'ressourcen.1.plan.2').value, '13:00–17:00')
assert.deepEqual(fbKnoepfe(fx, wurzel).filter((t) => /Zurück|Weiter/.test(t)), ['Zurück', 'Weiter'])
}))
test('Schritt 4 (a): Bundesland ohne Vorauswahl – die erste Option hat value "", keine Option ist gewählt, obwohl der Entwurf RP trägt', mitDom(undefined, (fx) => {
const { wurzel, ctx } = fbZeitenSeite()
assert.equal(ctx.daten.einstellung.bundesland, 'RP', 'der Serverstandard steht im Entwurf …')
const land = fbFeld(fx, wurzel, 'bundesland')
assert.equal(land.tagName, 'SELECT')
const optionen = fx.alle(land, 'option')
assert.equal(optionen.length, ansicht.BUNDESLAENDER.length + 1, 'alle 16 Länder und die leere Option')
assert.equal(optionen[0].value, '', 'die erste Option ist leer')
assert.ok(optionen.every((o) => !o.selected && !o.hasAttribute('selected')), 'keine Option ist gewählt')
assert.equal(land.value, '', '… die Auswahl zeigt trotzdem nichts')
assert.deepEqual(optionen.slice(1).map((o) => [o.value, fx.text(o)]), ansicht.BUNDESLAENDER)
const haken = fbFeld(fx, wurzel, 'feiertageGeschlossen')
assert.equal(haken.checked, true, '„An Feiertagen geschlossen“ steht für alte und neue Entwürfe auf ja')
assert.match(fx.text(wurzel), /An Feiertagen geschlossen/)
}))
test('Schritt 4: hat der Kunde das Bundesland schon gewählt (bundeslandGewaehlt), steht seine Wahl da – und nur sie', mitDom(undefined, (fx) => {
const e = salon()
e.bundesland = 'BY'
e.feiertageGeschlossen = false
const { wurzel } = fbZeitenSeite({ einstellung: e, fragebogen: fbBis(3, { bundeslandGewaehlt: true }) })
const land = fbFeld(fx, wurzel, 'bundesland')
assert.equal(land.value, 'BY')
assert.deepEqual(fx.alle(land, 'option').filter((o) => o.selected).map((o) => o.value), ['BY'])
assert.equal(fbFeld(fx, wurzel, 'feiertageGeschlossen').checked, false)
}))
test('Schritt 4 (a): „Weiter“ ohne Bundesland ⇒ Fehler am Feld, kein Aufruf, der Schritt bleibt', mitDom(undefined, async (fx) => {
const { wurzel } = fbZeitenSeite()
await fx.klick(fbWeiter(fx, wurzel))
assert.match(fbFehler(fx, wurzel, 'bundesland'), /Bundesland/)
assert.equal(fbFeld(fx, wurzel, 'bundesland').getAttribute('aria-invalid'), 'true')
assert.deepEqual(fx.fetchLog, [], 'weder PATCH noch PUT')
assert.equal(fbSchritt(fx, wurzel), 'zeiten')
assert.equal(fbWeiter(fx, wurzel).disabled, false, 'wieder frei')
}))
test('Schritt 4 (a): mit Wahl RP ein PATCH (bundesland, Wochenplan aus wochenplanFeld), danach der PUT mit bundeslandGewaehlt und zeiten', mitDom({
antworten: [fbPatchAntwort(4), fbAntwort(4, { bundeslandGewaehlt: true })],
}, async (fx) => {
const { wurzel, ctx } = fbZeitenSeite()
const plan = structuredClone(ctx.daten.einstellung.ressourcen[0].plan)
const ressourceId = ctx.daten.einstellung.ressourcen[0].id
await fx.setze(fbFeld(fx, wurzel, 'bundesland'), 'RP')
await fx.setze(fbFeld(fx, wurzel, 'ressourcen.0.plan.1'), '10:00–14:00')
await fx.setze(fbFeld(fx, wurzel, 'ressourcen.0.plan.6'), '') // Samstag zu
await fx.setze(fbFeld(fx, wurzel, 'feiertageGeschlossen'), false)
await fx.klick(fbWeiter(fx, wurzel))
assert.deepEqual(fbAufrufe(fx), [FB_PATCH, FB_PUT], 'genau ein PATCH, danach der PUT')
const [patch, put] = fx.fetchLog
assert.equal(patch.body.version, 3)
assert.equal(patch.body.einstellung.bundesland, 'RP')
assert.equal(patch.body.einstellung.feiertageGeschlossen, false)
const erwartet = { ...plan, 1: [['10:00', '14:00']] }
delete erwartet[6]
assert.deepEqual(patch.body.einstellung.ressourcen[0].plan, erwartet, 'der Plan kommt aus wochenplanFeld')
assert.equal(patch.body.einstellung.ressourcen[0].id, ressourceId, 'dieselbe Person')
assert.equal(put.body.projectId, 'p1')
assert.equal(put.body.fragebogen.bundeslandGewaehlt, true)
assert.deepEqual(put.body.fragebogen.bestaetigt, [...SCHRITTE.slice(0, 3), 'zeiten'])
assert.equal(fbSchritt(fx, wurzel), 'regeln')
assert.ok(document.activeElement === fx.finde(wurzel, 'h3'), 'Fokus auf der Überschrift des nächsten Schritts')
}))
test('Schritt 4: mehrere Personen und ein Platz – jeder Plan wird einzeln gelesen, unberührte Pläne bleiben', mitDom({
antworten: [fbPatchAntwort(4), fbAntwort(4, { bundeslandGewaehlt: true })],
}, async (fx) => {
const e = dreiEintraege()
const { wurzel } = fbZeitenSeite({ einstellung: e })
await fx.setze(fbFeld(fx, wurzel, 'bundesland'), 'HE')
await fx.setze(fbFeld(fx, wurzel, 'ressourcen.1.plan.4'), '08:00–12:00, 14:00–16:00')
await fx.setze(fbFeld(fx, wurzel, 'ressourcen.2.plan.7'), '10:00–12:00')
await fx.klick(fbWeiter(fx, wurzel))
const ressourcen = fx.fetchLog[0].body.einstellung.ressourcen
assert.deepEqual(ressourcen.map((r) => r.id), e.ressourcen.map((r) => r.id), 'Reihenfolge und IDs bleiben')
assert.deepEqual(ressourcen[0].plan, e.ressourcen[0].plan, 'Anna unberührt')
assert.deepEqual(ressourcen[1].plan, { 2: [['13:00', '17:00']], 4: [['08:00', '12:00'], ['14:00', '16:00']] })
assert.deepEqual(ressourcen[2].plan, { 7: [['10:00', '12:00']] })
assert.equal(ressourcen[2].aktiv, false, 'andere Angaben der Einträge bleiben')
assert.equal(fx.fetchLog[0].body.einstellung.bundesland, 'HE')
}))
test('Schritt 4: ein Zeitfenster, das nicht zu lesen ist, steht am Feld seines Tages; es geht nichts raus', mitDom(undefined, async (fx) => {
const { wurzel } = fbZeitenSeite({ einstellung: dreiEintraege() })
await fx.setze(fbFeld(fx, wurzel, 'bundesland'), 'RP')
await fx.setze(fbFeld(fx, wurzel, 'ressourcen.1.plan.3'), 'nachmittags')
await fx.setze(fbFeld(fx, wurzel, 'ressourcen.0.plan.5'), 'abends')
await fx.klick(fbWeiter(fx, wurzel))
assert.equal(fbFehler(fx, wurzel, 'ressourcen.1.plan.3'), ansicht.FENSTER_FEHLER)
assert.equal(fbFehler(fx, wurzel, 'ressourcen.0.plan.5'), ansicht.FENSTER_FEHLER, 'alle Fehler auf einmal')
assert.equal(fbFehler(fx, wurzel, 'bundesland'), '', 'das Land ist gewählt')
assert.equal(fbFeld(fx, wurzel, 'ressourcen.1.plan.3').getAttribute('aria-invalid'), 'true')
assert.deepEqual(fx.fetchLog, [])
}))
test('Schritt 4: ohne Wahl und mit unlesbarem Plan zeigt „Weiter“ beide Fehler zugleich', mitDom(undefined, async (fx) => {
const { wurzel } = fbZeitenSeite()
await fx.setze(fbFeld(fx, wurzel, 'ressourcen.0.plan.1'), 'xx')
await fx.klick(fbWeiter(fx, wurzel))
assert.ok(fbFehler(fx, wurzel, 'bundesland') !== '')
assert.equal(fbFehler(fx, wurzel, 'ressourcen.0.plan.1'), ansicht.FENSTER_FEHLER)
assert.deepEqual(fx.fetchLog, [])
}))
test('Schritt 4: antwortet der PATCH mit 400 und einem Fehler am Plan, steht er am Feld und der PUT bleibt aus', mitDom({
antworten: [{ status: 400, body: { error: 'Zeiten überlappen.', fehler: { 'ressourcen.0.plan.2': 'Die Zeitfenster überlappen sich.' } } }],
}, async (fx) => {
const { wurzel } = fbZeitenSeite()
await fx.setze(fbFeld(fx, wurzel, 'bundesland'), 'RP')
await fx.klick(fbWeiter(fx, wurzel))
assert.deepEqual(fbAufrufe(fx), [FB_PATCH])
assert.equal(fbFehler(fx, wurzel, 'ressourcen.0.plan.2'), 'Die Zeitfenster überlappen sich.')
assert.equal(fbSchritt(fx, wurzel), 'zeiten')
}))
test('Schritt 4: darunter Abwesenheiten und Sonderöffnungen mit eigenem Container; Bundesland und Feiertage gibt es nur im Formular des Schritts', mitDom(undefined, (fx) => {
const { wurzel } = fbZeitenSeite()
const ausnahmen = fx.finde(wurzel, '.tm-fb-ausnahmen')
assert.ok(ausnahmen, 'eigener Container')
const form = fx.finde(wurzel, 'form.tm-fb-form')
assert.ok(!form.contains(ausnahmen), 'nicht im Formular des Schritts (keine verschachtelten Formulare)')
assert.ok(fx.finde(ausnahmen, '[data-werkzeug="abwesenheit-neu"]'))
assert.ok(fx.finde(ausnahmen, '[data-werkzeug="sonder-neu"]'))
assert.deepEqual(fx.alle(ausnahmen, 'h3').map((el) => fx.text(el)), ['Abwesenheiten und Urlaub', 'Sonderöffnungen'])
assert.equal(fx.alle(wurzel, 'form[aria-label="Feiertage"]').length, 0, 'kein zweites Speichern für die Feiertage')
assert.equal(fx.alle(wurzel, '[data-feld="bundesland"]').length, 1)
const alle = fx.alle(wurzel, '*')
assert.ok(alle.indexOf(form) < alle.indexOf(ausnahmen), 'die Ausnahmen stehen unter dem Formular')
}))
test('Schritt 4: eine Abwesenheit speichert sofort und lässt die noch nicht gespeicherten Zeiten oben stehen; „Weiter“ nimmt beides mit', mitDom({
antworten: [
(aufruf) => ({ body: { einstellung: { ...aufruf.body.einstellung, abwesenheiten: [{ id: 'a_urlaub01', ressourceId: null, von: '2099-12-24', bis: '2099-12-31', titel: 'Betriebsurlaub' }] }, version: 4, fehlt: [] } }),
fbPatchAntwort(5),
fbAntwort(4, { bundeslandGewaehlt: true }),
],
}, async (fx) => {
const { wurzel, ctx } = fbZeitenSeite()
const plan = structuredClone(ctx.daten.einstellung.ressourcen[0].plan)
await fx.setze(fbFeld(fx, wurzel, 'ressourcen.0.plan.1'), '10:00–14:00') // nicht gespeichert
await fx.setze(fbFeld(fx, wurzel, 'bundesland'), 'RP')
await fx.klick(fx.finde(wurzel, '[data-werkzeug="abwesenheit-neu"]'))
await fx.setze(fbFeld(fx, wurzel, 'abwesenheiten.0.von'), '2099-12-24')
await fx.setze(fbFeld(fx, wurzel, 'abwesenheiten.0.bis'), '2099-12-31')
await fx.setze(fbFeld(fx, wurzel, 'abwesenheiten.0.titel'), 'Betriebsurlaub')
await fx.klick(fx.finde(wurzel, 'form[aria-label="Abwesenheit eintragen"] button[type="submit"]'))
assert.deepEqual(fbAufrufe(fx), [FB_PATCH], 'die Abwesenheit speichert selbst – ohne PUT')
const erster = fx.fetchLog[0].body
assert.equal(erster.einstellung.abwesenheiten.length, 1)
assert.deepEqual(erster.einstellung.abwesenheiten[0], { id: erster.einstellung.abwesenheiten[0].id, ressourceId: null, von: '2099-12-24', bis: '2099-12-31', titel: 'Betriebsurlaub' })
assert.deepEqual(erster.einstellung.ressourcen[0].plan, plan, 'die noch ungespeicherten Zeiten gehen in diesen PATCH nicht mit')
assert.equal(erster.einstellung.bundesland, 'RP', 'und das Bundesland ist der Stand des Entwurfs')
assert.equal(fbFeld(fx, wurzel, 'ressourcen.0.plan.1').value, '10:00–14:00', 'die Eingabe oben bleibt stehen')
assert.equal(fbFeld(fx, wurzel, 'bundesland').value, 'RP')
assert.equal(fx.alle(wurzel, 'ul[aria-label="Abwesenheiten"] li').length, 1, 'der Container unten ist neu gezeichnet')
assert.equal(fbSchritt(fx, wurzel), 'zeiten')
await fx.klick(fbWeiter(fx, wurzel))
assert.deepEqual(fbAufrufe(fx), [FB_PATCH, FB_PATCH, FB_PUT])
const zweiter = fx.fetchLog[1].body
assert.equal(zweiter.version, 4, 'der zweite PATCH baut auf der Antwort des ersten auf')
assert.equal(zweiter.einstellung.abwesenheiten.length, 1, 'die gespeicherte Abwesenheit bleibt')
assert.deepEqual(zweiter.einstellung.ressourcen[0].plan[1], [['10:00', '14:00']], 'jetzt sind die Zeiten dabei')
}))
test('Schritt 4: eine Sonderöffnung speichert selbst und lässt das Formular des Schritts unberührt', mitDom({
antworten: [(aufruf) => ({ body: { einstellung: aufruf.body.einstellung, version: 4, fehlt: [] } })],
}, async (fx) => {
const { wurzel } = fbZeitenSeite()
await fx.setze(fbFeld(fx, wurzel, 'ressourcen.0.plan.7'), '10:00–12:00')
await fx.klick(fx.finde(wurzel, '[data-werkzeug="sonder-neu"]'))
await fx.setze(fbFeld(fx, wurzel, 'datum'), '2099-12-24')
await fx.setze(fbFeld(fx, wurzel, 'zeiten'), '09:00–13:00')
await fx.klick(fx.finde(wurzel, 'form[aria-label="Sonderöffnung eintragen"] button[type="submit"]'))
assert.deepEqual(fbAufrufe(fx), [FB_PATCH])
assert.deepEqual(fx.fetchLog[0].body.einstellung.sonderoeffnungen['2099-12-24'], [['09:00', '13:00']])
assert.equal(fbFeld(fx, wurzel, 'ressourcen.0.plan.7').value, '10:00–12:00')
assert.equal(fx.alle(wurzel, 'ul[aria-label="Sonderöffnungen"] li').length, 1)
}))
test('Schritt 4 (d): in der Kundenansicht geht der PATCH raus, ein PUT nicht, der Schritt führt trotzdem weiter', mitDom({ antworten: [fbPatchAntwort(4)] }, async (fx) => {
const { wurzel } = fbZeitenSeite({ kundenansicht: true })
await fx.setze(fbFeld(fx, wurzel, 'bundesland'), 'RP')
await fx.klick(fbWeiter(fx, wurzel))
assert.deepEqual(fbAufrufe(fx), [FB_PATCH])
assert.equal(fbSchritt(fx, wurzel), 'regeln')
}))
// ------------------------------------------------------- Schritt 5: Regeln
const lageVon = (fx, wurzel) => fx.finde(wurzel, '[data-lage]')?.dataset.lage
const sammel = (fx, wurzel, art) => fx.finde(wurzel, `[data-aktion="alle-${art}"]`)
test('Schritt 5: Sammelknopf, Regeln, Stornobedingungen, Rechtliches und Bezahlung in dieser Reihenfolge; Zurück und Weiter', mitDom(undefined, (fx) => {
const { wurzel } = fbRegelnSeite()
assert.equal(fbSchritt(fx, wurzel), 'regeln')
assert.deepEqual(fx.alle(fx.finde(wurzel, 'form.tm-fb-form'), 'h3').map((el) => fx.text(el)), ['Buchungsart', 'Regeln fürs Buchen', 'Stornobedingungen', 'Rechtliches', 'Bezahlung'])
assert.deepEqual(felderVon(fx, wurzel), [
'regeln.raster', 'regeln.vorlaufMin', 'regeln.horizontTage', 'regeln.absageBisStd', 'regeln.maxOffeneJeEmail', 'regeln.personWaehlbar',
'storno.text', 'recht.gaesteSind', 'recht.freizeit', 'zahlung.vorOrtArten',
])
assert.equal(fx.text(sammel(fx, wurzel, 'sofort')), 'Alle sofort buchbar')
assert.equal(fx.text(sammel(fx, wurzel, 'anfrage')), 'Alle nur als Anfrage')
assert.deepEqual(fbKnoepfe(fx, wurzel).filter((t) => /Zurück|Weiter/.test(t)), ['Zurück', 'Weiter'])
}))
test('Schritt 5: der Sammelknopf zeigt die Lage (buchungsartLage) und ändert nichts, bis „Weiter“ gedrückt wird', mitDom({
antworten: [fbPatchAntwort(5), fbAntwort(5, { bundeslandGewaehlt: true })],
}, async (fx) => {
const e = salon()
e.leistungen[1].buchungsart = 'anfrage'
assert.equal(buchungsartLage(e.leistungen), 'gemischt')
const { wurzel } = fbRegelnSeite({ einstellung: e })
assert.equal(lageVon(fx, wurzel), 'gemischt')
assert.match(fx.text(fx.finde(wurzel, '[data-lage]')), /je Leistung/)
assert.equal(sammel(fx, wurzel, 'sofort').getAttribute('aria-pressed'), 'false')
await fx.klick(sammel(fx, wurzel, 'anfrage'))
assert.equal(lageVon(fx, wurzel), 'anfrage')
assert.equal(sammel(fx, wurzel, 'anfrage').getAttribute('aria-pressed'), 'true')
assert.equal(sammel(fx, wurzel, 'sofort').getAttribute('aria-pressed'), 'false')
await fx.klick(sammel(fx, wurzel, 'sofort'))
assert.equal(lageVon(fx, wurzel), 'sofort')
assert.equal(sammel(fx, wurzel, 'anfrage').getAttribute('aria-pressed'), 'false')
await fx.klick(sammel(fx, wurzel, 'anfrage'))
assert.deepEqual(fx.fetchLog, [], 'ein Klick auf den Sammelknopf ruft nichts auf')
await fx.klick(fbWeiter(fx, wurzel))
assert.deepEqual(fbAufrufe(fx), [FB_PATCH, FB_PUT])
const leistungen = fx.fetchLog[0].body.einstellung.leistungen
assert.equal(leistungen.length, e.leistungen.length)
assert.ok(leistungen.every((l) => l.buchungsart === 'anfrage'), 'jede Leistung nur als Anfrage')
assert.deepEqual(leistungen.map((l) => l.id), e.leistungen.map((l) => l.id))
assert.deepEqual(fx.fetchLog[1].body.fragebogen.bestaetigt, [...SCHRITTE.slice(0, 4), 'regeln'])
}))
test('Schritt 5: ohne Klick auf den Sammelknopf bleiben die Buchungsarten der Leistungen, wie sie sind', mitDom({
antworten: [fbPatchAntwort(5), fbAntwort(5)],
}, async (fx) => {
const e = salon()
e.leistungen[1].buchungsart = 'anfrage'
const { wurzel } = fbRegelnSeite({ einstellung: e })
await fx.klick(fbWeiter(fx, wurzel))
assert.deepEqual(fx.fetchLog[0].body.einstellung.leistungen.map((l) => l.buchungsart), e.leistungen.map((l) => l.buchungsart))
}))
test('Schritt 5: „Alle sofort buchbar“ setzt auch inaktive Leistungen und nimmt andere Angaben der Leistung nicht mit', mitDom({
antworten: [fbPatchAntwort(5), fbAntwort(5)],
}, async (fx) => {
const e = salon()
e.leistungen[0].buchungsart = 'anfrage'
e.leistungen[2].aktiv = false
e.leistungen[2].buchungsart = 'anfrage'
const { wurzel } = fbRegelnSeite({ einstellung: e })
await fx.klick(sammel(fx, wurzel, 'sofort'))
await fx.klick(fbWeiter(fx, wurzel))
const leistungen = fx.fetchLog[0].body.einstellung.leistungen
assert.ok(leistungen.every((l) => l.buchungsart === 'sofort'))
assert.deepEqual(leistungen.map((l) => ({ ...l, buchungsart: null })), e.leistungen.map((l) => ({ ...l, buchungsart: null })), 'sonst ändert sich nichts')
}))
test('Schritt 5: das Muster der Stornobedingungen folgt der Absagefrist, auch ungespeichert', mitDom(undefined, async (fx) => {
const { wurzel, ctx } = fbRegelnSeite()
const muster = fx.finde(wurzel, '.tm-muster-text')
assert.equal(fx.text(muster), stornoMuster(ctx.daten.einstellung.regeln.absageBisStd))
await fx.setze(fbFeld(fx, wurzel, 'regeln.absageBisStd'), '48')
assert.equal(fx.text(muster), stornoMuster(48))
assert.match(fx.text(muster), /bis 48 Stunden vorher/)
await fx.klick(fx.finde(wurzel, '[data-aktion="muster-uebernehmen"]'))
assert.equal(fbFeld(fx, wurzel, 'storno.text').value, stornoMuster(48), 'der Knopf übernimmt es ins Textfeld')
assert.deepEqual(fx.fetchLog, [])
}))
test('Schritt 5: „Weiter“ liest Regeln, Storno, Rechtliches und Bezahlung in genau einen PATCH, danach der PUT', mitDom({
antworten: [fbPatchAntwort(5), fbAntwort(5)],
}, async (fx) => {
const { wurzel } = fbRegelnSeite()
await fx.setze(fbFeld(fx, wurzel, 'regeln.vorlaufMin'), '180')
await fx.setze(fbFeld(fx, wurzel, 'regeln.absageBisStd'), '48')
await fx.setze(fbFeld(fx, wurzel, 'regeln.personWaehlbar'), false)
await fx.setze(fbFeld(fx, wurzel, 'storno.text'), 'Absagen bis 48 Stunden vorher.')
await fx.setze(fbFeld(fx, wurzel, 'recht.gaesteSind'), 'gemischt')
await fx.setze(fbFeld(fx, wurzel, 'recht.freizeit'), true)
const arten = fx.alle(fbFeld(fx, wurzel, 'zahlung.vorOrtArten'), 'input[type="checkbox"]')
await fx.setze(arten[1], false) // EC-Karte ab
await fx.klick(fbWeiter(fx, wurzel))
assert.deepEqual(fbAufrufe(fx), [FB_PATCH, FB_PUT])
const e = fx.fetchLog[0].body.einstellung
assert.equal(e.regeln.vorlaufMin, 180)
assert.equal(e.regeln.absageBisStd, 48)
assert.equal(e.regeln.personWaehlbar, false)
assert.equal(e.regeln.raster, 15, 'unberührte Regeln bleiben')
assert.equal(e.storno.text, 'Absagen bis 48 Stunden vorher.')
assert.deepEqual(e.recht, { gaesteSind: 'gemischt', freizeit: true })
assert.deepEqual(e.zahlung, { modus: 'vorort', vorOrtArten: ['bar', 'kreditkarte'] })
assert.equal(fbSchritt(fx, wurzel), 'gastfelder')
}))
test('Schritt 5: eine Zahl, die keine ist, steht am Feld; es geht nichts raus', mitDom(undefined, async (fx) => {
const { wurzel } = fbRegelnSeite()
await fx.setze(fbFeld(fx, wurzel, 'regeln.horizontTage'), 'viel')
await fx.klick(fbWeiter(fx, wurzel))
assert.ok(fbFehler(fx, wurzel, 'regeln.horizontTage') !== '')
assert.deepEqual(fx.fetchLog, [])
assert.equal(fbSchritt(fx, wurzel), 'regeln')
}))
test('Schritt 5: Fehler des Servers stehen am Feld, der PUT bleibt aus; die Stornofrist-Eingabe bleibt stehen', mitDom({
antworten: [{ status: 400, body: { error: 'x', fehler: { 'regeln.vorlaufMin': 'Der Vorlauf darf höchstens 20160 Minuten betragen.', 'zahlung.vorOrtArten': 'Bitte wähle mindestens eine Zahlart.' } } }],
}, async (fx) => {
const { wurzel } = fbRegelnSeite()
await fx.setze(fbFeld(fx, wurzel, 'regeln.vorlaufMin'), '99999')
await fx.klick(fbWeiter(fx, wurzel))
assert.deepEqual(fbAufrufe(fx), [FB_PATCH])
assert.equal(fbFehler(fx, wurzel, 'regeln.vorlaufMin'), 'Der Vorlauf darf höchstens 20160 Minuten betragen.')
assert.equal(fbFehler(fx, wurzel, 'zahlung.vorOrtArten'), 'Bitte wähle mindestens eine Zahlart.')
assert.equal(fbFeld(fx, wurzel, 'regeln.vorlaufMin').value, '99999')
assert.equal(fbSchritt(fx, wurzel), 'regeln')
}))
// ------------------------------------------------------- Schritt 6: Gastfelder
test('Schritt 6: gastFelder (Telefon, Nachricht, Zusatzfelder); „Weiter“ = ein PATCH, dann der PUT mit gastfelder', mitDom({
antworten: [fbPatchAntwort(5), fbAntwort(6, { bundeslandGewaehlt: true })],
}, async (fx) => {
const { wurzel } = fbGastSeite()
assert.equal(fbSchritt(fx, wurzel), 'gastfelder')
assert.deepEqual(felderVon(fx, wurzel), ['gastFelder.telefon', 'gastFelder.nachricht'])
await fx.setze(fbFeld(fx, wurzel, 'gastFelder.telefon'), 'pflicht')
await fx.setze(fbFeld(fx, wurzel, 'gastFelder.nachricht'), 'optional')
await fx.klick(fx.finde(wurzel, '[data-aktion="zusatz-neu"]'))
await fx.setze(fbFeld(fx, wurzel, 'gastFelder.zusatz.0.label'), 'Haarlänge')
await fx.klick(fbWeiter(fx, wurzel))
assert.deepEqual(fbAufrufe(fx), [FB_PATCH, FB_PUT])
const g = fx.fetchLog[0].body.einstellung.gastFelder
assert.equal(g.telefon, 'pflicht')
assert.equal(g.nachricht, 'optional')
assert.equal(g.zusatz.length, 1)
assert.deepEqual({ ...g.zusatz[0], key: typeof g.zusatz[0].key }, { key: 'string', label: 'Haarlänge', typ: 'text', optionen: [], pflicht: false })
assert.ok(g.zusatz[0].key.length > 0)
assert.deepEqual(fx.fetchLog[1].body.fragebogen.bestaetigt, [...SCHRITTE.slice(0, 5), 'gastfelder'])
assert.equal(fbSchritt(fx, wurzel), 'benachrichtigung')
}))
test('Schritt 6: Fehler des Servers am Zusatzfeld stehen am Feld, es folgt kein PUT', mitDom({
antworten: [{ status: 400, body: { error: 'x', fehler: { 'gastFelder.zusatz.0.label': 'Bitte gib eine Beschriftung an.' } } }],
}, async (fx) => {
const { wurzel } = fbGastSeite()
await fx.klick(fx.finde(wurzel, '[data-aktion="zusatz-neu"]'))
await fx.klick(fbWeiter(fx, wurzel))
assert.deepEqual(fbAufrufe(fx), [FB_PATCH])
assert.equal(fbFehler(fx, wurzel, 'gastFelder.zusatz.0.label'), 'Bitte gib eine Beschriftung an.')
assert.equal(fbSchritt(fx, wurzel), 'gastfelder')
}))
// ------------------------------------------------------- Schritt 7 (C9b): welche Angaben gelesen werden
const BETRIEB_ANDERS = {
name: 'Anderer Name', adresse: 'Neue Straße 2, 67655 Kaiserslautern', telefon: '0800 111', kleinunternehmer: true,
impressumUrl: 'https://neu.example.invalid/impressum', datenschutzUrl: 'https://neu.example.invalid/datenschutz', agbUrl: 'https://neu.example.invalid/agb', websiteUrl: 'https://neu.example.invalid',
}
test('Schritt 7 (b): aus dem Betrieb wird nur die E-Mail-Adresse gelesen (Schlüsselmenge), alles andere bleibt, wie es zuletzt gespeichert war', mitDom({
antworten: [fbPatchAntwort(4), fbAntwort(7)],
}, async (fx) => {
const { wurzel, ctx } = fbSiebenSeite()
assert.deepEqual(felderVon(fx, wurzel).filter((n) => n.startsWith('betrieb.')), ['betrieb.email'])
// inzwischen anderswo gespeichert (zweiter Tab, Schritt 8): die Felder dieses Schritts kennen die Werte nicht
Object.assign(ctx.daten.einstellung.betrieb, BETRIEB_ANDERS)
const vorher = structuredClone(ctx.daten.einstellung.betrieb)
await fx.setze(fbFeld(fx, wurzel, 'betrieb.email'), 'neu@example.invalid')
await fx.klick(fbWeiter(fx, wurzel))
const betrieb = fx.fetchLog[0].body.einstellung.betrieb
assert.deepEqual(betrieb, { ...vorher, email: 'neu@example.invalid' })
assert.deepEqual(Object.keys(betrieb).filter((k) => betrieb[k] !== vorher[k]), ['email'])
}))
// ------------------------------------------------------- Schritt 8: Website
const WEBSITE_FELDER = ['betrieb.telefon', 'betrieb.impressumUrl', 'betrieb.datenschutzUrl', 'betrieb.agbUrl', 'betrieb.websiteUrl']
const modusRadio = (fx, wurzel, wert) => fbRadio(fx, wurzel, wert)
const einbauFeld = (fx, wurzel, name) => fbFeld(fx, wurzel, `einbau.${name}`)
test('Schritt 8: Telefon und Links aus dem Betrieb, dazu die Einbauwünsche; der Knopftext erscheint erst bei „hinter einem Knopf“', mitDom(undefined, async (fx) => {
const { wurzel } = fbWebsiteSeite()
assert.equal(fbSchritt(fx, wurzel), 'website')
assert.deepEqual(felderVon(fx, wurzel).filter((n) => n.startsWith('betrieb.')), WEBSITE_FELDER)
assert.equal(fbFeld(fx, wurzel, 'betrieb.telefon').value, '06301 12345')
const radios = fx.alle(wurzel, 'input[name="termine-einbau-modus"]')
assert.deepEqual(radios.map((r) => r.value), ['inline', 'knopf'])
assert.ok(radios.every((r) => !r.checked), 'keine Vorauswahl: der Kunde entscheidet')
assert.match(fx.text(wurzel), /direkt auf der Seite/i)
assert.match(fx.text(wurzel), /hinter einem Knopf/i)
const knopfFeld = einbauFeld(fx, wurzel, 'knopfText').closest('.tm-feld')
assert.equal(knopfFeld.hidden, true, 'ohne Knopf kein Knopftext')
await fx.klick(modusRadio(fx, wurzel, 'knopf'))
assert.equal(knopfFeld.hidden, false)
await fx.klick(modusRadio(fx, wurzel, 'inline'))
assert.equal(knopfFeld.hidden, true)
for (const name of ['stelle', 'gestaltung', 'bemerkung']) assert.ok(einbauFeld(fx, wurzel, name), `${name}: ein Feld`)
assert.deepEqual(fbKnoepfe(fx, wurzel).filter((t) => /Zurück|Weiter/.test(t)), ['Zurück', 'Weiter'])
}))
test('Schritt 8: der Hinweis nennt, was nach Stunden geht – zweites Widget, Knöpfe je Leistung, Sonderdesign', mitDom(undefined, (fx) => {
const { wurzel } = fbWebsiteSeite()
const hinweis = fx.text(fx.finde(wurzel, '[data-hinweis="einbau-mehr"]'))
assert.match(hinweis, /zweites Widget/)
assert.match(hinweis, /Knöpfe für einzelne Leistungen/)
assert.match(hinweis, /Sonderdesign/)
assert.match(hinweis, /nach Stunden/)
}))
test('Schritt 8: vorbelegt aus dem gespeicherten Fragebogen', mitDom(undefined, (fx) => {
const einbau = { modus: 'knopf', knopfText: 'Jetzt Termin buchen', stelle: 'Unter dem Text „Kontakt“', gestaltung: 'Farben wie das Logo', bemerkung: 'Bitte vorher anrufen' }
const { wurzel } = fbWebsiteSeite({ fragebogen: fbBis(7, { bundeslandGewaehlt: true, einbau }) })
assert.equal(modusRadio(fx, wurzel, 'knopf').checked, true)
assert.equal(modusRadio(fx, wurzel, 'inline').checked, false)
assert.equal(einbauFeld(fx, wurzel, 'knopfText').closest('.tm-feld').hidden, false)
assert.equal(einbauFeld(fx, wurzel, 'knopfText').value, einbau.knopfText)
assert.equal(einbauFeld(fx, wurzel, 'stelle').value, einbau.stelle)
assert.equal(einbauFeld(fx, wurzel, 'gestaltung').value, einbau.gestaltung)
assert.equal(einbauFeld(fx, wurzel, 'bemerkung').value, einbau.bemerkung)
}))
test('Schritt 8 (b): der PATCH enthält aus dem Betrieb nur Telefon und die vier Links (Schlüsselmenge); der PUT trägt einbau mit allen fünf Angaben', mitDom({
antworten: [fbPatchAntwort(4), fbAntwort(8, { bundeslandGewaehlt: true })],
}, async (fx) => {
const { wurzel, ctx } = fbWebsiteSeite()
Object.assign(ctx.daten.einstellung.betrieb, { name: 'Anderer Name', adresse: 'Neue Straße 2', email: 'andere@example.invalid', kleinunternehmer: true })
const vorher = structuredClone(ctx.daten.einstellung.betrieb)
const neu = {
telefon: '0800 222', impressumUrl: 'https://salon.example.invalid/impr', datenschutzUrl: 'https://salon.example.invalid/ds', agbUrl: 'https://salon.example.invalid/agb', websiteUrl: 'https://salon.example.invalid',
}
for (const [name, wert] of Object.entries(neu)) await fx.setze(fbFeld(fx, wurzel, `betrieb.${name}`), wert)
await fx.klick(modusRadio(fx, wurzel, 'inline'))
await fx.setze(einbauFeld(fx, wurzel, 'stelle'), 'Unter dem Text „Kontakt“')
await fx.setze(einbauFeld(fx, wurzel, 'gestaltung'), 'Farben wie das Logo')
await fx.setze(einbauFeld(fx, wurzel, 'bemerkung'), 'Bitte vorher anrufen')
await fx.klick(fbWeiter(fx, wurzel))
assert.deepEqual(fbAufrufe(fx), [FB_PATCH, FB_PUT])
const [patch, put] = fx.fetchLog
assert.deepEqual(patch.body.einstellung.betrieb, { ...vorher, ...neu })
assert.deepEqual(Object.keys(patch.body.einstellung.betrieb).filter((k) => patch.body.einstellung.betrieb[k] !== vorher[k]).sort(), Object.keys(neu).sort(), 'geändert sind genau diese fünf')
assert.deepEqual(put.body.fragebogen.einbau, { modus: 'inline', knopfText: '', stelle: 'Unter dem Text „Kontakt“', gestaltung: 'Farben wie das Logo', bemerkung: 'Bitte vorher anrufen' })
assert.deepEqual(Object.keys(put.body.fragebogen.einbau).sort(), ['bemerkung', 'gestaltung', 'knopfText', 'modus', 'stelle'])
assert.deepEqual(put.body.fragebogen.bestaetigt, [...SCHRITTE])
assert.equal(put.body.fragebogen.bundeslandGewaehlt, true, 'der übrige Fragebogen bleibt')
}))
test('Schritt 8: hinter einem Knopf geht der Knopftext in den PUT', mitDom({
antworten: [fbPatchAntwort(4), fbAntwort(8)],
}, async (fx) => {
const { wurzel } = fbWebsiteSeite()
await fx.klick(modusRadio(fx, wurzel, 'knopf'))
await fx.setze(einbauFeld(fx, wurzel, 'knopfText'), 'Termin buchen')
await fx.klick(fbWeiter(fx, wurzel))
assert.deepEqual(fx.fetchLog[1].body.fragebogen.einbau, { modus: 'knopf', knopfText: 'Termin buchen', stelle: '', gestaltung: '', bemerkung: '' })
}))
test('Schritt 8: wer von „Knopf“ auf „direkt auf der Seite“ zurückwechselt, schickt keinen Knopftext mit', mitDom({
antworten: [fbPatchAntwort(4), fbAntwort(8)],
}, async (fx) => {
const { wurzel } = fbWebsiteSeite()
await fx.klick(modusRadio(fx, wurzel, 'knopf'))
await fx.setze(einbauFeld(fx, wurzel, 'knopfText'), 'Termin buchen')
await fx.klick(modusRadio(fx, wurzel, 'inline'))
await fx.klick(fbWeiter(fx, wurzel))
assert.equal(fx.fetchLog[1].body.fragebogen.einbau.modus, 'inline')
assert.equal(fx.fetchLog[1].body.fragebogen.einbau.knopfText, '')
}))
test('Schritt 8 (b): „hinter einem Knopf“ ohne Knopftext ⇒ Fehler am Feld und kein Aufruf', mitDom(undefined, async (fx) => {
const { wurzel } = fbWebsiteSeite()
await fx.klick(modusRadio(fx, wurzel, 'knopf'))
await fx.klick(fbWeiter(fx, wurzel))
assert.match(fbFehler(fx, wurzel, 'einbau.knopfText'), /Knopf/)
assert.equal(einbauFeld(fx, wurzel, 'knopfText').getAttribute('aria-invalid'), 'true')
assert.deepEqual(fx.fetchLog, [], 'weder PATCH noch PUT')
assert.equal(fbSchritt(fx, wurzel), 'website')
await fx.setze(einbauFeld(fx, wurzel, 'knopfText'), ' ')
await fx.klick(fbWeiter(fx, wurzel))
assert.match(fbFehler(fx, wurzel, 'einbau.knopfText'), /Knopf/, 'Leerzeichen allein sind kein Text')
assert.deepEqual(fx.fetchLog, [])
}))
test('Schritt 8: ohne Wahl des Modus ⇒ Fehler an der Gruppe und kein Aufruf', mitDom(undefined, async (fx) => {
const { wurzel } = fbWebsiteSeite()
await fx.klick(fbWeiter(fx, wurzel))
assert.match(fbFehler(fx, wurzel, 'einbau.modus'), /direkt auf deiner Seite oder hinter einem Knopf/)
assert.equal(fbFeld(fx, wurzel, 'einbau.modus').getAttribute('aria-invalid'), 'true')
assert.deepEqual(fx.fetchLog, [])
}))
test('Schritt 8: Fehler des Servers (PATCH am Link, PUT am Einbau) stehen am Feld; nach dem PATCH-Fehler gibt es keinen PUT', mitDom({
antworten: [
{ status: 400, body: { error: 'x', fehler: { 'betrieb.impressumUrl': 'Bitte gib eine vollständige Adresse mit https:// an.' } } },
fbPatchAntwort(4),
{ status: 400, body: { error: 'x', fehler: { 'einbau.stelle': 'Der Text ist ungültig.' } } },
],
}, async (fx) => {
const { wurzel } = fbWebsiteSeite()
await fx.klick(modusRadio(fx, wurzel, 'inline'))
await fx.setze(fbFeld(fx, wurzel, 'betrieb.impressumUrl'), 'impressum')
await fx.klick(fbWeiter(fx, wurzel))
assert.deepEqual(fbAufrufe(fx), [FB_PATCH])
assert.equal(fbFehler(fx, wurzel, 'betrieb.impressumUrl'), 'Bitte gib eine vollständige Adresse mit https:// an.')
await fx.klick(fbWeiter(fx, wurzel))
assert.deepEqual(fbAufrufe(fx), [FB_PATCH, FB_PATCH, FB_PUT])
assert.equal(fbFehler(fx, wurzel, 'einbau.stelle'), 'Der Text ist ungültig.')
assert.equal(fbSchritt(fx, wurzel), 'website')
}))
test('Schritt 8: Texte des Kunden stehen nur als Text im Baum', mitDom(undefined, (fx) => {
const boese = '<img src=x onerror=alert(1)>'
const { wurzel } = fbWebsiteSeite({ fragebogen: fbBis(7, { einbau: { ...EINBAU_LEER, modus: 'knopf', knopfText: boese, stelle: boese, gestaltung: boese, bemerkung: boese } }) })
assert.equal(fx.alle(wurzel, 'img').length, 0)
for (const name of ['knopfText', 'stelle', 'gestaltung', 'bemerkung']) assert.equal(einbauFeld(fx, wurzel, name).value, boese)
}))
test('Schritt 8 (d): in der Kundenansicht geht der PATCH raus, ein PUT nicht; die Bestellseite bleibt gesperrt', mitDom({ antworten: [fbPatchAntwort(4)] }, async (fx) => {
const bestellung = await bestellungTest()
const { wurzel } = fbWebsiteSeite({ kundenansicht: true, bestellung })
await fx.klick(modusRadio(fx, wurzel, 'inline'))
await fx.klick(fbWeiter(fx, wurzel))
assert.deepEqual(fbAufrufe(fx), [FB_PATCH])
assert.equal(fbSchritt(fx, wurzel), 'bestellen')
assert.equal(bestellKnopf(fx, wurzel).disabled, true)
}))
// ------------------------------------------------------- nach Schritt 8: die Bestellseite
test('Nach Schritt 8 (c) zeigt renderBestellen die Bestellseite; die Knopfbeschriftung ist bestellung.consent.knopf', mitDom({
antworten: [fbPatchAntwort(4), fbAntwort(8, { bundeslandGewaehlt: true, einbau: { ...EINBAU_LEER, modus: 'inline' } })],
}, async (fx) => {
const bestellung = await bestellungTest()
const { wurzel } = fbWebsiteSeite({ bestellung })
await fx.klick(modusRadio(fx, wurzel, 'inline'))
await fx.klick(fbWeiter(fx, wurzel))
assert.equal(fbSchritt(fx, wurzel), 'bestellen')
assert.ok(fx.finde(wurzel, '.tm-fb-bestellen'), 'die Bestellseite steht in der Hülle des Fragebogens')
const knopf = bestellKnopf(fx, wurzel)
assert.ok(knopf)
assert.equal(fx.text(knopf), bestellung.consent.knopf)
assert.equal(fx.text(knopf), featureConfig('termine').consent.knopf)
assert.ok(document.activeElement === fx.finde(wurzel, 'h3'), 'Fokus auf der Überschrift')
assert.equal(fx.fetchLog.length, 2, 'das Zeigen der Bestellseite ruft nichts auf')
}))
test('Nach Schritt 8: die offenen Punkte aus der Antwort des PUT stehen auf der Bestellseite, jeder mit „Zum Schritt“', mitDom({
antworten: [fbPatchAntwort(4), { body: { fragebogen: fbBis(8), offen: [{ schritt: 'zeiten', text: 'Bitte wähle das Bundesland deines Betriebs. Danach richten sich die Feiertage.' }] } }],
}, async (fx) => {
const { wurzel } = fbWebsiteSeite({ bestellung: await bestellungTest() })
await fx.klick(modusRadio(fx, wurzel, 'inline'))
await fx.klick(fbWeiter(fx, wurzel))
assert.match(fx.text(fx.finde(wurzel, '.tm-bestellen-offen')), /Bundesland/)
assert.equal(bestellKnopf(fx, wurzel).disabled, true)
await fx.klick(fx.finde(wurzel, '[data-aktion="zum-schritt"][data-schritt="zeiten"]'))
assert.equal(fbSchritt(fx, wurzel), 'zeiten', 'zurück in den Schritt')
}))
test('Durchlauf: von Schritt 4 bis zur Bestellseite, jeder Schritt mit genau einem PATCH und einem PUT', mitDom({
antworten: [
fbPatchAntwort(4), fbAntwort(4, { bundeslandGewaehlt: true }),
fbPatchAntwort(5), fbAntwort(5, { bundeslandGewaehlt: true }),
fbPatchAntwort(6), fbAntwort(6, { bundeslandGewaehlt: true }),
fbPatchAntwort(7), fbAntwort(7, { bundeslandGewaehlt: true }),
fbPatchAntwort(8), fbAntwort(8, { bundeslandGewaehlt: true, einbau: { ...EINBAU_LEER, modus: 'inline' } }),
],
}, async (fx) => {
const { wurzel } = fbZeitenSeite({ bestellung: await bestellungTest() })
await fx.setze(fbFeld(fx, wurzel, 'bundesland'), 'RP')
await fx.klick(fbWeiter(fx, wurzel))
assert.equal(fbSchritt(fx, wurzel), 'regeln')
await fx.klick(fbWeiter(fx, wurzel))
assert.equal(fbSchritt(fx, wurzel), 'gastfelder')
await fx.klick(fbWeiter(fx, wurzel))
assert.equal(fbSchritt(fx, wurzel), 'benachrichtigung')
await fx.klick(fbWeiter(fx, wurzel))
assert.equal(fbSchritt(fx, wurzel), 'website')
await fx.klick(modusRadio(fx, wurzel, 'inline'))
await fx.klick(fbWeiter(fx, wurzel))
assert.equal(fbSchritt(fx, wurzel), 'bestellen')
assert.deepEqual(fbAufrufe(fx), [FB_PATCH, FB_PUT, FB_PATCH, FB_PUT, FB_PATCH, FB_PUT, FB_PATCH, FB_PUT, FB_PATCH, FB_PUT])
assert.deepEqual(fx.fetchLog.filter((a) => a.method === 'PUT').map((a) => a.body.fragebogen.bestaetigt.at(-1)), ['zeiten', 'regeln', 'gastfelder', 'benachrichtigung', 'website'])
assert.equal(fx.text(bestellKnopf(fx, wurzel)), featureConfig('termine').consent.knopf)
}))
test('Texte der Kunden in den Schritten 4 bis 6 stehen nur als Text im Baum (Namen, Storno, Zusatzfeld)', mitDom(undefined, (fx) => {
const boese = '<img src=x onerror=alert(1)>'
const e = salon()
e.ressourcen[0].name = boese
e.storno = { ...e.storno, text: boese }
e.gastFelder = { ...e.gastFelder, zusatz: [{ key: 'haar', label: boese, typ: 'text', optionen: [], pflicht: false }] }
const zeiten = fbZeitenSeite({ einstellung: e }).wurzel
assert.equal(fx.alle(zeiten, 'img').length, 0)
assert.equal(fx.text(fx.finde(zeiten, '[data-ressource] h4')), boese)
const regelnSeite = fbRegelnSeite({ einstellung: e }).wurzel
assert.equal(fx.alle(regelnSeite, 'img').length, 0)
assert.equal(fbFeld(fx, regelnSeite, 'storno.text').value, boese)
const gast = fbGastSeite({ einstellung: e }).wurzel
assert.equal(fx.alle(gast, 'img').length, 0)
assert.equal(fbFeld(fx, gast, 'gastFelder.zusatz.0.label').value, boese)
}))
// ------------------------------------------------------- Quelltext und Styles
test('termine-fragebogen.js statisch: nur diese Importe, ein einziger PUT, kein PATCH, kein Browserspeicher, Du-Form', () => {
const code = ohneKommentare(fbQuelleText())
assert.ok(code.length > 3000, 'die Datei ist gefüllt')
assert.deepEqual(code.match(/from '[^']+'/g), [
"from './termine-view.js'", "from './termine-dom.js'", "from './termine-speichern.js'", "from './termine-tab-leistungen.js'", "from './termine-tab-einstellungen.js'", "from './funktion-bestellen.js'",
"from './termine-view.js'", "from './termine-dom.js'", "from './termine-speichern.js'", "from './termine-tab-leistungen.js'", "from './termine-tab-team.js'",
"from './termine-team-ausnahmen.js'", "from './termine-tab-einstellungen.js'", "from './funktion-bestellen.js'",
])
assert.equal(code.match(/'PUT'/g).length, 1, 'PUT /fragebogen an genau einer Stelle')
assert.doesNotMatch(code, /'PATCH'/, 'PATCH nur über neuerSpeicher')
@@ -2322,3 +2959,9 @@ test('termine.css: Fortschrittsleiste (tm-schritte, tm-schritt) und die Handy-An
assert.match(handy, /\.tm-schritt-stand\s*\{[^}]*display:\s*block/, 'am Handy zeigt die Leiste den Zähler')
assert.match(handy, /\.tm-schritt-titel\s*\{[^}]*(clip|position:\s*absolute)/, 'am Handy bleiben nur die Nummern sichtbar')
})
test('termine.css: Klassen der Schritte 4 bis 8 (Person, Ausnahmen, Sammelknopf, Einbau)', () => {
const css = readFileSync(new URL('../public/termine.css', import.meta.url), 'utf8')
for (const klasse of ['tm-fb-person', 'tm-fb-ausnahmen', 'tm-fb-sammel', 'tm-fb-einbau']) assert.match(css, new RegExp(`\\.${klasse}[\\s{,.:\\[]`), `.${klasse} ist gestaltet`)
assert.match(css.match(/\.tm-fb-sammel\s*\{[^}]*\}/)?.[0] ?? '', /flex-wrap:\s*wrap/, 'die Knöpfe brechen am Handy um')
})
+26
View File
@@ -373,6 +373,32 @@ test('Fragebogen (TP2 C9a): renderFragebogen wird nur über termine.js geladen,
assert.match(lies('public/termine-fragebogen.js'), /export function renderFragebogen\(root, ctx\)/)
})
test('Fragebogen (TP2 C9b): Schritte 4 bis 8 nutzen die Bausteine der Reiter, PATCH und PUT nur an den erlaubten Stellen, Bundesland ohne Vorauswahl', () => {
const ohneKommentare = (text) => text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|\s)\/\/.*$/gm, '')
const quelle = ohneKommentare(lies('public/termine-fragebogen.js'))
const importAus = (datei) => [...quelle.matchAll(new RegExp(`import \\{([^}]*)\\} from '\\./${datei.replace('.', '\\.')}'`, 'g'))].flatMap((m) => m[1].split(',').map((x) => x.trim().split(/\s+as\s+/)[0]))
// Ausnahmen (Urlaub, Sonderöffnungen) und Wochenplan kommen aus den Reiter-Modulen, nicht aus einer zweiten Fassung im Fragebogen
assert.deepEqual(importAus('termine-team-ausnahmen.js').sort(), ['abwesenheiten', 'sonderoeffnungen'])
assert.deepEqual(importAus('termine-tab-team.js'), ['wochenplanFeld'])
for (const name of ['betrieb', 'darstellung', 'erinnerung', 'gastFelder', 'regeln', 'rechtliches', 'storno', 'zahlung']) {
assert.ok(importAus('termine-tab-einstellungen.js').includes(name), `${name} kommt aus termine-tab-einstellungen.js`)
}
assert.ok(importAus('termine-view.js').includes('buchungsartFuerAlle') && importAus('termine-view.js').includes('buchungsartLage'), 'der Sammelknopf nutzt die Helfer aus termine-view.js')
assert.ok(importAus('funktion-bestellen.js').includes('renderBestellen'))
assert.doesNotMatch(quelle, /\bfeiertage\b/, 'Bundesland und Feiertage stehen im Formular des Schritts, nicht im eigenen Speichern des Reiters')
// PATCH gibt es nur in termine-speichern.js, PUT nur in termine-fragebogen.js – und dort an genau einer Stelle
const dateien = readdirSync(new URL('../public/', import.meta.url)).filter((f) => /^(termine.*|funktion-bestellen)\.js$/.test(f))
assert.deepEqual(dateien.filter((f) => /'PATCH'/.test(ohneKommentare(lies(`public/${f}`)))), ['termine-speichern.js'])
assert.deepEqual(dateien.filter((f) => /'PUT'/.test(ohneKommentare(lies(`public/${f}`)))), ['termine-fragebogen.js'])
assert.equal(quelle.match(/'PUT'/g).length, 1)
assert.doesNotMatch(quelle, /'PATCH'|\bfetch\(/)
// Bundesland: leere erste Option, danach die Länder; gewählt ist nur, was der Kunde schon ausdrücklich gewählt hatte (nicht der Serverstandard im Entwurf)
const wahl = quelle.slice(quelle.indexOf('BUNDESLAENDER.map') - 200, quelle.indexOf('BUNDESLAENDER.map') + 200)
assert.match(wahl, /h\('option', \{ value: '' \}[^)]*\),\s*BUNDESLAENDER\.map/, 'die erste Option ist leer und kommt vor den Ländern')
assert.doesNotMatch(quelle, /selected:\s*(true|e\.bundesland|einstellung\.bundesland)/, 'keine Vorauswahl aus dem Entwurf')
assert.match(quelle, /bundeslandGewaehlt/)
})
test('Reiter-Exporte für den Fragebogen (TP2 C7b): acht Abschnitte, wochenplanFeld, baueCtx, fehlt aus der Antwort', async () => {
const ohneKommentare = (text) => text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|\s)\/\/.*$/gm, '')
// Einstellungen: die acht Abschnitte sind Exporte; der Reiter baut sie weiter selbst (betrieb ohne nur = alle neun Felder, Verhalten in termineFragebogenView.test.js)