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:
1 parent
9bf5ec09f8
commit
8d628ef906
4 files changed
+939
-34
No files matched your search
@@ -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')
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user