Terminplaner TP2: Ansicht nach Zustand und Rueckkehr von Stripe
Die Ansicht #termine folgt dem Zustand aus GET /api/termine/einstellungen: off mit laufendem Abo zeigt den Fragebogen samt Bestellseite, ohne Abo den Hinweis des Servers (termine.hinweis, R111) und bei kein_abo den Link zu Vertraege aus einer gemeinsamen Quelle (vertraege-link.js); ordered und setup zeigen die Statuskarte und die Reiter Leistungen, Team & Plaetze und Einstellungen nur zum Lesen (ohne Reiter Termine); active und disabled bleiben wie bisher. Rueckkehr von Stripe: #termine?bestellt=1&projekt=<id> und #termine?projekt=<id> waehlen das Projekt, "Danke!" nur mit bestellt=1, die Adresse wird einmal gelesen und bereinigt. R110: GET /einstellungen liefert bestellung und offen auch in ordered (ohne Abo-Punkt, abo bleibt null); scheitert "Zahlung fortsetzen", zeichnet die Huelle Liste und Daten neu (bei 409 zustimmung_fehlt mit dem Bestellformular, sonst nach dem neuen Zustand, die Meldung bleibt am Knopf, solange die Bestellung offen ist). Dazu: "null" als Text im lesbaren Reiter Leistungen entfernt, Export und Status der Einstellungen erst ab der Freischaltung. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
52456dac76
commit
87d267e51a
13 files changed
+1077
-71
No files matched your search
@@ -1443,35 +1443,72 @@ test('statusKarte ordered: „Zahlung fortsetzen“ sendet fortsetzenKoerper und
|
||||
assert.equal(fx.window.location.href, KNOPF_URL)
|
||||
}))
|
||||
|
||||
test('statusKarte ordered: 409 zustimmung_fehlt ⇒ weiter() lädt neu und zeigt das Bestellformular, keine Navigation', mitDom({
|
||||
test('statusKarte ordered: 409 zustimmung_fehlt ⇒ weiter({ bestellen: true, meldung }) lädt neu und zeigt das Bestellformular, keine Navigation', mitDom({
|
||||
antworten: [{ status: 409, body: { error: 'Bitte stimme der Erklärung zu, um zu bestellen.', code: 'zustimmung_fehlt' } }],
|
||||
}, async (fx) => {
|
||||
let weiter = 0
|
||||
const wurzel = karte({}, {}, { weiter: async () => { weiter += 1 } })
|
||||
const aufrufe = []
|
||||
const wurzel = karte({}, {}, { weiter: async (fall) => { aufrufe.push(fall) } })
|
||||
await fx.klick(fx.finde(wurzel, '[data-aktion="fortsetzen"]'))
|
||||
assert.equal(weiter, 1)
|
||||
assert.deepEqual(aufrufe, [{ bestellen: true, meldung: 'Bitte stimme der Erklärung zu, um zu bestellen.' }])
|
||||
assert.equal(fx.window.location.href, 'http://localhost/')
|
||||
}))
|
||||
|
||||
test('statusKarte ordered: ohne weiter() oder bei anderem Fehler steht die Meldung am Knopf, der Knopf ist wieder frei', mitDom({
|
||||
antworten: [{ status: 409, body: { error: 'Bitte stimme der Erklärung zu, um zu bestellen.', code: 'zustimmung_fehlt' } }, { status: 409, body: { error: 'Der Terminplaner ist für dieses Projekt schon bestellt.' } }, { body: { url: 'http://unsicher.example/x' } }],
|
||||
test('statusKarte ordered: jeder andere Fehler ruft weiter({ bestellen: false, meldung }) (R110), die Karte bleibt nie veraltet stehen', mitDom({
|
||||
antworten: [
|
||||
{ status: 400, body: { error: 'Bitte stimme der Erklärung zu, um zu bestellen.', code: 'zustimmung_fehlt' } },
|
||||
{ status: 409, body: { error: 'Der Terminplaner ist für dieses Projekt schon bestellt.' } },
|
||||
{ status: 500, body: {} },
|
||||
],
|
||||
}, async (fx) => {
|
||||
let weiter = 0
|
||||
const aufrufe = []
|
||||
// wie im Browser zeichnet sich die Ansicht nach weiter() neu: der Knopf der alten Karte bleibt gesperrt, jeder Fall hat seine Karte
|
||||
for (let i = 0; i < 3; i += 1) await fx.klick(fx.finde(karte({}, {}, { weiter: (fall) => { aufrufe.push(fall) } }), '[data-aktion="fortsetzen"]'))
|
||||
assert.deepEqual(aufrufe, [
|
||||
{ bestellen: false, meldung: 'Bitte stimme der Erklärung zu, um zu bestellen.' },
|
||||
{ bestellen: false, meldung: 'Der Terminplaner ist für dieses Projekt schon bestellt.' },
|
||||
{ bestellen: false, meldung: 'Fehler 500' },
|
||||
], 'auch bei 400 (Zustand inzwischen setup oder active) und 409 ohne zustimmung_fehlt')
|
||||
}))
|
||||
|
||||
test('statusKarte ordered: scheitert weiter() (Neuladen nicht möglich), bleibt die Karte mit dem Fehler am Knopf; ohne weiter() steht die Meldung des Servers dort', mitDom({
|
||||
antworten: [
|
||||
{ status: 409, body: { error: 'Bitte stimme der Erklärung zu, um zu bestellen.', code: 'zustimmung_fehlt' } },
|
||||
{ status: 409, body: { error: 'Der Terminplaner ist für dieses Projekt schon bestellt.' } },
|
||||
{ status: 409, body: { error: 'Bitte stimme der Erklärung zu, um zu bestellen.', code: 'zustimmung_fehlt' } },
|
||||
{ status: 409, body: { error: 'Der Terminplaner ist für dieses Projekt schon bestellt.' } },
|
||||
{ body: { url: 'http://unsicher.example/x' } },
|
||||
],
|
||||
}, async (fx) => {
|
||||
const kaputt = () => { throw new Error('Keine Verbindung. Bitte versuch es gleich noch einmal.') }
|
||||
const mit = karte({}, {}, { weiter: kaputt })
|
||||
const meldungMit = fx.finde(mit, '[data-meldung="fortsetzen"]')
|
||||
const knopfMit = fx.finde(mit, '[data-aktion="fortsetzen"]')
|
||||
await fx.klick(knopfMit)
|
||||
assert.equal(fx.text(meldungMit), 'Keine Verbindung. Bitte versuch es gleich noch einmal.', 'bei fehlender Zustimmung der Grund des Neuladens')
|
||||
assert.equal(knopfMit.disabled, false)
|
||||
await fx.klick(knopfMit)
|
||||
assert.equal(fx.text(meldungMit), 'Der Terminplaner ist für dieses Projekt schon bestellt.', 'sonst der Fehler des Versuchs')
|
||||
const ohne = karte()
|
||||
const knopf = fx.finde(ohne, '[data-aktion="fortsetzen"]')
|
||||
await fx.klick(knopf)
|
||||
assert.equal(fx.text(fx.finde(ohne, '[data-meldung="fortsetzen"]')), 'Bitte stimme der Erklärung zu, um zu bestellen.')
|
||||
assert.equal(knopf.disabled, false)
|
||||
const mit = karte({}, {}, { weiter: () => { weiter += 1 } })
|
||||
await fx.klick(fx.finde(mit, '[data-aktion="fortsetzen"]'))
|
||||
assert.equal(weiter, 0, 'weiter() nur bei zustimmung_fehlt')
|
||||
assert.equal(fx.text(fx.finde(mit, '[data-meldung="fortsetzen"]')), 'Der Terminplaner ist für dieses Projekt schon bestellt.')
|
||||
const ohneZwei = karte()
|
||||
await fx.klick(fx.finde(ohneZwei, '[data-aktion="fortsetzen"]'))
|
||||
assert.equal(fx.text(fx.finde(ohneZwei, '[data-meldung="fortsetzen"]')), 'Der Terminplaner ist für dieses Projekt schon bestellt.')
|
||||
const unsicher = karte()
|
||||
await fx.klick(fx.finde(unsicher, '[data-aktion="fortsetzen"]'))
|
||||
assert.equal(fx.window.location.href, 'http://localhost/', 'keine http-Navigation')
|
||||
assert.match(fx.text(fx.finde(unsicher, '[data-meldung="fortsetzen"]')), /Zahlungsseite/)
|
||||
}))
|
||||
|
||||
test('statusKarte: meldung steht beim Zeichnen am Knopf „Zahlung fortsetzen“ (als Text)', mitDom(undefined, (fx) => {
|
||||
const wurzel = karte({}, { meldung: 'Deine Zahlung läuft schon. <b>x</b>' })
|
||||
assert.equal(fx.text(fx.finde(wurzel, '[data-meldung="fortsetzen"]')), 'Deine Zahlung läuft schon. <b>x</b>')
|
||||
assert.equal(fx.alle(wurzel, 'b').length, 0)
|
||||
assert.equal(fx.text(fx.finde(karte(), '[data-meldung="fortsetzen"]')), '')
|
||||
}))
|
||||
|
||||
test('statusKarte ordered: in der Kundenansicht ist „Zahlung fortsetzen“ gesperrt, mit Hinweis und ohne Anfrage', mitDom(undefined, async (fx) => {
|
||||
const wurzel = karte({}, { kundenansicht: true })
|
||||
const knopf = fx.finde(wurzel, '[data-aktion="fortsetzen"]')
|
||||
@@ -2965,3 +3002,519 @@ test('termine.css: Klassen der Schritte 4 bis 8 (Person, Ausnahmen, Sammelknopf,
|
||||
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')
|
||||
})
|
||||
|
||||
// ------------------------------------------------------- Ansicht #termine nach Zustand und Rückkehr von Stripe (TP2, Task C10b)
|
||||
// initTermine in public/termine.js. Der Fake-fetch antwortet hier nach Adresse statt der Reihe nach: Antworten von GET /api/funktionen (dort
|
||||
// steht der Abo-Hinweis, R111) und GET /api/termine/einstellungen liefert der Test. Ein Array in der Tabelle sind aufeinanderfolgende Antworten
|
||||
// (die letzte gilt weiter). Der Zustand der Huelle (Auswahl des Projekts und des Reiters) bleibt zwischen den Tests im Modul: jeder Test nennt
|
||||
// sein Projekt (projectId), und kein Test verlangt einen bestimmten Reiter, ohne ihn zu waehlen.
|
||||
const AN_FUNKTIONEN = 'GET /api/funktionen'
|
||||
const AN_CHECKOUT = 'POST /api/funktionen/termine/checkout'
|
||||
const an = (projectId) => `GET /api/termine/einstellungen?projectId=${projectId}`
|
||||
const SERVER_HINWEIS = 'Dieser Satz steht nur beim Server, so wie der Server ihn schreibt.'
|
||||
const AUFRUFE = (fx) => fx.fetchLog.map((a) => `${a.method} ${a.url}`)
|
||||
|
||||
function routen(fx, tabelle) {
|
||||
globalThis.fetch = async (url, optionen = {}) => {
|
||||
const aufruf = { method: optionen.method || 'GET', url: String(url), body: optionen.body ? JSON.parse(optionen.body) : undefined }
|
||||
fx.fetchLog.push(aufruf)
|
||||
let eintrag = tabelle[`${aufruf.method} ${aufruf.url}`]
|
||||
if (eintrag === undefined && aufruf.method === 'GET' && aufruf.url.startsWith('/api/termine/buchungen')) eintrag = { body: { buchungen: [] } }
|
||||
if (Array.isArray(eintrag)) eintrag = eintrag.length > 1 ? eintrag.shift() : eintrag[0]
|
||||
if (typeof eintrag === 'function') eintrag = eintrag(aufruf)
|
||||
const { status = 200, body = {} } = eintrag ?? { status: 404, body: { error: `nicht vorgesehen: ${aufruf.method} ${aufruf.url}` } }
|
||||
return { ok: status >= 200 && status < 300, status, json: async () => structuredClone(body) }
|
||||
}
|
||||
}
|
||||
|
||||
/** Ansicht öffnen (Adresse mit hash, Tabelle der Antworten) und warten, bis auch die Reiter gezeichnet sind. => Wurzel der Ansicht. */
|
||||
async function oeffneAnsicht(fx, { projectId = 'p1', hash = '', tabelle }) {
|
||||
routen(fx, tabelle)
|
||||
fx.window.location.hash = hash
|
||||
const wurzel = h('div')
|
||||
document.body.append(wurzel)
|
||||
await huelle.initTermine(wurzel, { projectId })
|
||||
await fx.ruhe()
|
||||
return wurzel
|
||||
}
|
||||
|
||||
const projektEintrag = (id, state, extra = {}) => ({ projectId: id, projectName: `Website ${id}`, liveUrl: '', features: { news: { state: 'off' }, termine: { state, ...extra } } })
|
||||
const GEORDERT = { orderedAt: '2026-10-05T10:00:00.000Z', ticketWoid: null, invoiceUrl: null }
|
||||
const funktionenAntwort = (projekte, termine = {}) => ({
|
||||
body: { projects: projekte, termine: { verfuegbar: true, bestellbar: true, grund: null, preis: '149 €', einrichtung: EINRICHTUNG, hinweis: null, ...termine } },
|
||||
})
|
||||
const ohneAbo = (grund, hinweis = SERVER_HINWEIS) => ({ bestellbar: false, grund, hinweis })
|
||||
const ABGESCHALTET = { grund: 'abo_ende', seit: '2026-10-04T10:00:00.000Z', loeschungAm: '2026-11-03' }
|
||||
|
||||
/** Antwort von GET /api/termine/einstellungen im jeweiligen Zustand (off: Entwurf mit Abo, ordered: dazu die Sicht der Bestellung ohne Abo). */
|
||||
async function ansichtDaten(state, extra = {}) {
|
||||
const basis = { ...fbDaten({ state }), abo: null, offen: null, bestellung: null, feeds: {}, anfragenOffen: 0, aktivSeit: null, abgeschaltet: null }
|
||||
if (state === 'off') return { ...basis, ...fbDaten(), ...extra }
|
||||
if (state === 'ordered') return { ...basis, offen: [], bestellung: await bestellungTest(), ...extra }
|
||||
if (state === 'active') return { ...basis, aktivSeit: '2026-10-01T09:00:00.000Z', einbau: { snippet: '<script></script>' }, ...extra }
|
||||
if (state === 'disabled') return { ...basis, aktivSeit: '2026-10-01T09:00:00.000Z', abgeschaltet: ABGESCHALTET, ...extra }
|
||||
return { ...basis, ...extra }
|
||||
}
|
||||
const reiterTexte = (fx, wurzel) => fx.alle(wurzel, '[role="tab"]').map((tab) => fx.text(tab))
|
||||
const meldungDerHuelle = (fx, wurzel) => fx.text(fx.finde(wurzel, '.tm-meldung'))
|
||||
/** Antwort-Tabelle für ein Projekt p1 im Zustand state mit den Daten daten. */
|
||||
async function einProjekt(state, { eintrag = {}, daten = {}, termine = {}, tabelle = {} } = {}) {
|
||||
return {
|
||||
[AN_FUNKTIONEN]: funktionenAntwort([projektEintrag('p1', state, eintrag)], termine),
|
||||
[an('p1')]: { body: await ansichtDaten(state, daten) },
|
||||
...tabelle,
|
||||
}
|
||||
}
|
||||
|
||||
test('Ansicht off mit laufendem Abo: der Fragebogen (Fortschrittsleiste im Baum), weder Hinweis noch Reiter', mitDom(undefined, async (fx) => {
|
||||
const wurzel = await oeffneAnsicht(fx, { tabelle: await einProjekt('off', { daten: { fragebogen: fbBis(1) } }) })
|
||||
assert.ok(fx.finde(wurzel, 'nav.tm-fortschritt ol.tm-schritte'), 'renderFragebogen hat gezeichnet')
|
||||
assert.equal(fbSchritt(fx, wurzel), 'wer', 'Start beim ersten offenen Schritt des Entwurfs')
|
||||
assert.ok(fx.finde(wurzel, '[data-hinweis="abo"]') === null)
|
||||
assert.ok(fx.finde(wurzel, '.tm-reiterleiste') === null)
|
||||
assert.ok(fx.finde(wurzel, '.tm-statuskarte') === null)
|
||||
assert.deepEqual(AUFRUFE(fx), [AN_FUNKTIONEN, an('p1')])
|
||||
}))
|
||||
|
||||
test('Ansicht off: ohne Entwurf die Branchenwahl; die Meldung der Hülle (ctx.meldung) steht über dem Fragebogen', mitDom(undefined, async (fx) => {
|
||||
const wurzel = await oeffneAnsicht(fx, { tabelle: await einProjekt('off', {
|
||||
daten: { einstellung: null, version: 0, fragebogen: leerFragebogen(), betriebVorschlag: BETRIEB_KONTO },
|
||||
tabelle: {
|
||||
'POST /api/termine/einstellungen/vorlage': { status: 201, body: { einstellung: salon(), version: 1, fehlt: [], fragebogen: fbBis(0) } },
|
||||
[an('p1')]: [{ body: fbStart() }, { body: fbDaten({ version: 1, fragebogen: fbBis(0) }) }],
|
||||
'PUT /api/termine/fragebogen': fbAntwort(1),
|
||||
},
|
||||
}) })
|
||||
assert.equal(fbSchritt(fx, wurzel), 'branche')
|
||||
assert.equal(meldungDerHuelle(fx, wurzel), '')
|
||||
await fx.klick(fbRadio(fx, wurzel, 'salon'))
|
||||
await fx.klick(fx.finde(wurzel, '[data-aktion="branche-starten"]'))
|
||||
assert.equal(fbSchritt(fx, wurzel), 'wer')
|
||||
assert.match(meldungDerHuelle(fx, wurzel), /^Die Branche „Friseur und Salon“ ist angelegt\./, 'ohne meldungEl bliebe die Meldung stumm')
|
||||
assert.equal(fx.finde(wurzel, '.tm-meldung').getAttribute('role'), 'status')
|
||||
}))
|
||||
|
||||
test('Ansicht off ohne laufendes Abo: der Hinweis vom Server (kein_abo mit Link zu Verträge), kein Fragebogen, keine weitere Abfrage', mitDom(undefined, async (fx) => {
|
||||
const wurzel = await oeffneAnsicht(fx, { tabelle: await einProjekt('off', { daten: { abo: { ok: false, grund: 'kein_abo' } }, termine: ohneAbo('kein_abo') }) })
|
||||
const karteEl = fx.finde(wurzel, '[data-hinweis="abo"]')
|
||||
assert.ok(karteEl)
|
||||
assert.ok(fx.text(karteEl).includes(SERVER_HINWEIS), 'der Text des Servers, unverändert')
|
||||
const link = fx.alle(karteEl, 'button')
|
||||
assert.deepEqual(link.map((b) => [fx.text(b), b.getAttribute('data-view'), b.getAttribute('class'), b.getAttribute('type')]), [['Zu Verträge', 'vertraege', 'kb-link', 'button']])
|
||||
assert.equal(fx.text(fx.finde(karteEl, 'p')), `${SERVER_HINWEIS} Zu Verträge`, 'Text, Leerzeichen, Link wie in der Kachel')
|
||||
assert.ok(fx.finde(wurzel, 'nav.tm-fortschritt') === null, 'kein Fragebogen')
|
||||
assert.equal(fx.alle(wurzel, 'input').length, 0)
|
||||
assert.ok(fx.finde(wurzel, '.tm-reiterleiste') === null)
|
||||
assert.deepEqual(AUFRUFE(fx), [AN_FUNKTIONEN, an('p1')])
|
||||
}))
|
||||
|
||||
test('Ansicht off ohne Abo: gekündigt, nicht aktiv, nicht lesbar und fehlendes abo zeigen nur den Text des Servers, ohne Link', mitDom(undefined, async (fx) => {
|
||||
const faelle = [
|
||||
['gekuendigt', { ok: false, grund: 'gekuendigt' }, ohneAbo('gekuendigt', 'Server: Dein Wartungs-Abo ist gekündigt.')],
|
||||
['nicht_aktiv', { ok: false, grund: 'nicht_aktiv' }, ohneAbo('nicht_aktiv', 'Server: Dein Wartungs-Abo ist gerade nicht aktiv.')],
|
||||
['Lesefehler (abo null)', null, { bestellbar: null, grund: null, hinweis: 'Server: Das Abo konnte gerade nicht geprüft werden.' }],
|
||||
['abo fehlt in der Antwort', undefined, ohneAbo('gekuendigt', 'Server: ohne abo.')],
|
||||
]
|
||||
for (const [name, abo, termine] of faelle) {
|
||||
const wurzel = await oeffneAnsicht(fx, { tabelle: await einProjekt('off', { daten: { abo }, termine }) })
|
||||
const karteEl = fx.finde(wurzel, '[data-hinweis="abo"]')
|
||||
assert.ok(karteEl, name)
|
||||
assert.equal(fx.text(fx.finde(karteEl, 'p')), termine.hinweis, name)
|
||||
assert.equal(fx.alle(karteEl, 'button').length, 0, `${name}: kein Link`)
|
||||
assert.ok(fx.finde(wurzel, 'nav.tm-fortschritt') === null, name)
|
||||
}
|
||||
}))
|
||||
|
||||
test('Ansicht off ohne Abo: der Hinweis kommt maskiert in die Seite und der Browser kennt keinen eigenen Text', mitDom(undefined, async (fx) => {
|
||||
const bose = '<img src=x onerror=alert(1)> & "Anführungszeichen"'
|
||||
const wurzel = await oeffneAnsicht(fx, { tabelle: await einProjekt('off', { daten: { abo: { ok: false, grund: 'gekuendigt' } }, termine: ohneAbo('gekuendigt', bose) }) })
|
||||
assert.equal(fx.alle(wurzel, 'img').length, 0)
|
||||
assert.equal(fx.text(fx.finde(wurzel, '[data-hinweis="abo"] p')), bose)
|
||||
}))
|
||||
|
||||
test('Ansicht off ohne Abo: fehlt der Text in der Liste (das Abo hat sich seit dem Laden geändert), wird die Liste noch einmal geholt', mitDom(undefined, async (fx) => {
|
||||
const wurzel = await oeffneAnsicht(fx, { tabelle: await einProjekt('off', {
|
||||
daten: { abo: { ok: false, grund: 'kein_abo' } },
|
||||
tabelle: { [AN_FUNKTIONEN]: [funktionenAntwort([projektEintrag('p1', 'off')]), funktionenAntwort([projektEintrag('p1', 'off')], ohneAbo('kein_abo'))] },
|
||||
}) })
|
||||
assert.deepEqual(AUFRUFE(fx), [AN_FUNKTIONEN, an('p1'), AN_FUNKTIONEN])
|
||||
assert.equal(fx.text(fx.finde(wurzel, '[data-hinweis="abo"] p')), `${SERVER_HINWEIS} Zu Verträge`)
|
||||
// bleibt der Text auch dann aus, steht nur die Überschrift (kein erfundener Ersatztext)
|
||||
const leer = await oeffneAnsicht(fx, { tabelle: await einProjekt('off', { daten: { abo: { ok: false, grund: 'gekuendigt' } } }) })
|
||||
assert.ok(fx.finde(leer, '[data-hinweis="abo"] p') === null)
|
||||
assert.equal(fx.text(fx.finde(leer, '[data-hinweis="abo"] h3')), 'Terminplaner einrichten')
|
||||
}))
|
||||
|
||||
test('Ansicht off: in der Kundenansicht gilt dasselbe (Fragebogen mit Hinweis der Kundenansicht, ohne Abo der Hinweis)', mitDom(undefined, async (fx) => {
|
||||
const mit = await oeffneAnsicht(fx, { tabelle: await einProjekt('off', { daten: { kundenansicht: true } }) })
|
||||
assert.match(fx.text(fx.finde(mit, '.tm-box[role="note"]')), /Kundenansicht/)
|
||||
const ohne = await oeffneAnsicht(fx, { tabelle: await einProjekt('off', { daten: { kundenansicht: true, abo: { ok: false, grund: 'gekuendigt' } }, termine: ohneAbo('gekuendigt') }) })
|
||||
assert.ok(fx.finde(ohne, '[data-hinweis="abo"]'))
|
||||
assert.ok(fx.finde(ohne, 'nav.tm-fortschritt') === null)
|
||||
}))
|
||||
|
||||
test('Ansicht active ohne Einstellung: weiter die Vorlagenauswahl von TP1 (kein Fragebogen), mit Einstellung die vier Reiter', mitDom(undefined, async (fx) => {
|
||||
const ohne = await oeffneAnsicht(fx, { tabelle: await einProjekt('active', { daten: { einstellung: null, version: 0, fragebogen: null } }) })
|
||||
assert.equal(fx.alle(ohne, 'input[name="termine-vorlage"]').length, 1)
|
||||
assert.ok(fx.text(ohne).includes('Womit möchtest du starten?'))
|
||||
assert.ok(fx.finde(ohne, 'nav.tm-fortschritt') === null)
|
||||
assert.ok(fx.finde(ohne, '.tm-reiterleiste') === null)
|
||||
const mit = await oeffneAnsicht(fx, { tabelle: await einProjekt('active') })
|
||||
assert.deepEqual(reiterTexte(fx, mit), ['Termine', 'Leistungen', 'Team & Plätze', 'Einstellungen'])
|
||||
assert.ok(fx.finde(mit, '.tm-statuskarte') === null)
|
||||
assert.ok(fx.finde(mit, '.tm-hinweise .tm-box-warn') === null, 'weder Abschaltung noch fehlende Angaben')
|
||||
}))
|
||||
|
||||
test('Ansicht active: fehlende Pflichtangaben zeigen den Hinweis „Noch nicht buchbar“ weiter wie bisher', mitDom(undefined, async (fx) => {
|
||||
const wurzel = await oeffneAnsicht(fx, { tabelle: await einProjekt('active', { daten: { fehlt: ['betrieb.telefon'] } }) })
|
||||
assert.match(fx.text(fx.finde(wurzel, '.tm-hinweise .tm-box-warn')), /Noch nicht buchbar/)
|
||||
}))
|
||||
|
||||
for (const [state, pille, ticket] of [['ordered', 'Zahlung offen', null], ['setup', 'In Einrichtung', 100342]]) {
|
||||
test(`Ansicht ${state}: Statuskarte „${pille}“ und die Reiter Leistungen, Team & Plätze, Einstellungen nur zum Lesen, ohne Reiter „Termine“`, mitDom(undefined, async (fx) => {
|
||||
const wurzel = await oeffneAnsicht(fx, { tabelle: await einProjekt(state, {
|
||||
eintrag: { ...GEORDERT, ticketWoid: ticket, invoiceUrl: state === 'setup' ? 'https://invoice.stripe.test/i/in_1' : null },
|
||||
daten: { fehlt: ['betrieb.telefon'], aktivSeit: null },
|
||||
}) })
|
||||
const karteEl = fx.finde(wurzel, '.tm-statuskarte')
|
||||
assert.ok(karteEl)
|
||||
assert.equal(karteEl.getAttribute('data-state'), state)
|
||||
assert.equal(fx.text(fx.finde(karteEl, '.tm-pille')), pille)
|
||||
assert.match(fx.text(karteEl), /Bestellt am\s*05\.10\.2026/, 'Bestelldatum aus dem Eintrag der Liste')
|
||||
if (state === 'setup') {
|
||||
assert.match(fx.text(karteEl), /Ticketnummer\s*#100342/)
|
||||
assert.match(fx.text(karteEl), new RegExp(`Zeiteinschätzung\\s*${EINRICHTUNG}`), 'Einrichtungsdauer aus GET /api/funktionen')
|
||||
}
|
||||
assert.ok(fx.finde(wurzel, 'nav.tm-fortschritt') === null, 'kein Fragebogen')
|
||||
assert.deepEqual(reiterTexte(fx, wurzel), ['Leistungen', 'Team & Plätze', 'Einstellungen'])
|
||||
assert.ok(fx.finde(wurzel, '#tm-tab-termine') === null)
|
||||
assert.ok(fx.finde(wurzel, '.tm-hinweise .tm-box-warn') === null, 'weder Banner der Abschaltung noch „Noch nicht buchbar“')
|
||||
// jeder Reiter ist schreibgeschützt: Felder gesperrt, keine Knöpfe zum Ändern
|
||||
for (const [key, ohneKnopf] of [['leistungen', '[data-werkzeug="leistung-neu"]'], ['team', '[data-werkzeug="ressource-neu"]'], ['einstellungen', 'button[type="submit"]']]) {
|
||||
await fx.klick(fx.finde(wurzel, `#tm-tab-${key}`))
|
||||
await fx.ruhe()
|
||||
const panel = fx.finde(wurzel, '#tm-panel')
|
||||
assert.ok(fx.finde(panel, ohneKnopf) === null, `${key}: nichts zum Ändern`)
|
||||
assert.ok(fx.finde(panel, 'fieldset[disabled]') || key !== 'einstellungen', `${key}: gesperrt`)
|
||||
assert.ok(!fx.text(panel).includes('null'), `${key}: kein „null“ als Text (replaceChildren mit null)`)
|
||||
}
|
||||
// „Export und Status“ des Reiters Einstellungen sagt vor der Freischaltung nicht „Aktiv“
|
||||
assert.ok(fx.finde(fx.finde(wurzel, '#tm-panel'), '.tm-status') === null)
|
||||
assert.equal(fx.alle(fx.finde(wurzel, '#tm-panel'), 'a').length, 0, 'noch nichts zum Exportieren')
|
||||
assert.deepEqual(AUFRUFE(fx), [AN_FUNKTIONEN, an('p1')], 'der Reiter „Termine“ wird nicht gelesen (der Server antwortete mit 409)')
|
||||
}))
|
||||
}
|
||||
|
||||
test('Ansicht ordered/setup: die Pfeiltasten wandern nur durch die drei Reiter, ein gemerkter Reiter „Termine“ fällt auf den ersten zurück und bleibt für andere Websites gemerkt', mitDom(undefined, async (fx) => {
|
||||
const tabelle = {
|
||||
[AN_FUNKTIONEN]: funktionenAntwort([projektEintrag('p1', 'active'), projektEintrag('p2', 'ordered', GEORDERT)]),
|
||||
[an('p1')]: { body: await ansichtDaten('active') },
|
||||
[an('p2')]: { body: await ansichtDaten('ordered') },
|
||||
}
|
||||
const wurzel = await oeffneAnsicht(fx, { projectId: 'p1', tabelle })
|
||||
await fx.klick(fx.finde(wurzel, '#tm-tab-termine'))
|
||||
await fx.setze(fx.finde(wurzel, '#termine-project'), 'p2')
|
||||
await fx.ruhe()
|
||||
assert.ok(fx.finde(wurzel, '.tm-statuskarte'))
|
||||
assert.deepEqual(reiterTexte(fx, wurzel), ['Leistungen', 'Team & Plätze', 'Einstellungen'])
|
||||
assert.equal(fx.finde(wurzel, '[role="tab"][aria-selected="true"]').dataset.reiter, 'leistungen', 'der erste Reiter, den es gibt')
|
||||
await fx.sende(fx.finde(wurzel, '.tm-reiterleiste'), 'keydown', { key: 'ArrowLeft' })
|
||||
assert.equal(fx.finde(wurzel, '[role="tab"][aria-selected="true"]').dataset.reiter, 'einstellungen', 'Pfeil links vom ersten Reiter springt zum letzten')
|
||||
await fx.sende(fx.finde(wurzel, '.tm-reiterleiste'), 'keydown', { key: 'ArrowRight' })
|
||||
assert.equal(fx.finde(wurzel, '[role="tab"][aria-selected="true"]').dataset.reiter, 'leistungen')
|
||||
// zurück zur aktiven Website: der zuvor gewählte Reiter „Termine“ ist wieder da
|
||||
await fx.setze(fx.finde(wurzel, '#termine-project'), 'p1')
|
||||
await fx.ruhe()
|
||||
assert.equal(fx.finde(wurzel, '[role="tab"][aria-selected="true"]').dataset.reiter, 'leistungen', 'die Wahl in p2 gilt ab jetzt')
|
||||
}))
|
||||
|
||||
test('Ansicht ordered in der Kundenansicht: „Zahlung fortsetzen“ ist gesperrt', mitDom(undefined, async (fx) => {
|
||||
const wurzel = await oeffneAnsicht(fx, { tabelle: await einProjekt('ordered', { eintrag: GEORDERT, daten: { kundenansicht: true } }) })
|
||||
assert.equal(fx.finde(wurzel, '[data-aktion="fortsetzen"]').disabled, true)
|
||||
}))
|
||||
|
||||
test('Ansicht ordered/setup ohne Einstellung: nur die Statuskarte, keine Reiter', mitDom(undefined, async (fx) => {
|
||||
const wurzel = await oeffneAnsicht(fx, { tabelle: await einProjekt('setup', { eintrag: GEORDERT, daten: { einstellung: null, version: 0 } }) })
|
||||
assert.ok(fx.finde(wurzel, '.tm-statuskarte'))
|
||||
assert.ok(fx.finde(wurzel, '.tm-reiterleiste') === null)
|
||||
assert.ok(!fx.text(wurzel).includes('null'))
|
||||
}))
|
||||
|
||||
test('Ansicht disabled: Banner der Abschaltung (hinweisAbgeschaltet) mit Löschdatum aus abgeschaltet.loeschungAm und Export-Link, alle vier Reiter, unverändert (E11)', mitDom(undefined, async (fx) => {
|
||||
const wurzel = await oeffneAnsicht(fx, { tabelle: await einProjekt('disabled') })
|
||||
const text = ansicht.abschaltText(ABGESCHALTET)
|
||||
const banner = fx.finde(wurzel, '.tm-hinweise .tm-box-warn')
|
||||
assert.ok(banner)
|
||||
assert.equal(fx.text(banner), `${text.davor}exportieren${text.danach}`)
|
||||
assert.match(fx.text(banner), /03\.11\.2026|2026-11-03/, 'das Löschdatum aus loeschungAm')
|
||||
assert.deepEqual(fx.alle(banner, 'a').map((a) => [fx.text(a), a.getAttribute('href')]), [['exportieren', '/api/termine/export.csv?projectId=p1']])
|
||||
assert.deepEqual(reiterTexte(fx, wurzel), ['Termine', 'Leistungen', 'Team & Plätze', 'Einstellungen'])
|
||||
assert.ok(fx.finde(wurzel, '.tm-statuskarte') === null)
|
||||
assert.ok(fx.finde(wurzel, 'nav.tm-fortschritt') === null)
|
||||
await fx.klick(fx.finde(wurzel, '#tm-tab-einstellungen'))
|
||||
await fx.ruhe()
|
||||
assert.match(fx.text(fx.finde(wurzel, '.tm-status')), /Abgeschaltet/, 'Export und Status bleiben in disabled')
|
||||
}))
|
||||
|
||||
test('Ansicht disabled ohne Einstellung: Banner und „Keine Termine eingerichtet“ wie bisher', mitDom(undefined, async (fx) => {
|
||||
const wurzel = await oeffneAnsicht(fx, { tabelle: await einProjekt('disabled', { daten: { einstellung: null, version: 0 } }) })
|
||||
assert.ok(fx.finde(wurzel, '.tm-box-warn[role="note"]'))
|
||||
assert.ok(fx.text(wurzel).includes('Keine Termine eingerichtet'))
|
||||
assert.ok(fx.finde(wurzel, '.tm-reiterleiste') === null)
|
||||
}))
|
||||
|
||||
test('Ansicht: der Zustand der Daten gilt, auch wenn die Liste einen anderen kennt (Liste: off, Daten: active)', mitDom(undefined, async (fx) => {
|
||||
const wurzel = await oeffneAnsicht(fx, { tabelle: { [AN_FUNKTIONEN]: funktionenAntwort([projektEintrag('p1', 'off')]), [an('p1')]: { body: await ansichtDaten('active') } } })
|
||||
assert.deepEqual(reiterTexte(fx, wurzel), ['Termine', 'Leistungen', 'Team & Plätze', 'Einstellungen'])
|
||||
}))
|
||||
|
||||
test('Ansicht ordered: kennt die Liste einen älteren Zustand als die Daten (nach der Zahlung), wird sie neu geholt und die Karte zeigt die Ticketnummer', mitDom(undefined, async (fx) => {
|
||||
const wurzel = await oeffneAnsicht(fx, { tabelle: {
|
||||
[AN_FUNKTIONEN]: [funktionenAntwort([projektEintrag('p1', 'ordered', GEORDERT)]), funktionenAntwort([projektEintrag('p1', 'setup', { ...GEORDERT, ticketWoid: 100342 })])],
|
||||
[an('p1')]: { body: await ansichtDaten('setup') },
|
||||
} })
|
||||
assert.deepEqual(AUFRUFE(fx), [AN_FUNKTIONEN, an('p1'), AN_FUNKTIONEN])
|
||||
assert.equal(fx.text(fx.finde(wurzel, '.tm-statuskarte .tm-pille')), 'In Einrichtung')
|
||||
assert.match(fx.text(fx.finde(wurzel, '.tm-statuskarte')), /Ticketnummer\s*#100342/)
|
||||
// lässt sich die Liste nicht holen, gilt der Zustand der Daten, ohne Ticket
|
||||
const ohne = await oeffneAnsicht(fx, { tabelle: {
|
||||
[AN_FUNKTIONEN]: [funktionenAntwort([projektEintrag('p1', 'ordered', GEORDERT)]), { status: 500, body: {} }],
|
||||
[an('p1')]: { body: await ansichtDaten('setup') },
|
||||
} })
|
||||
assert.equal(fx.text(fx.finde(ohne, '.tm-statuskarte .tm-pille')), 'In Einrichtung')
|
||||
assert.match(fx.text(fx.finde(ohne, '.tm-statuskarte')), /Wir legen dein Ticket gerade an\./)
|
||||
assert.ok(fx.finde(ohne, '[data-aktion="fortsetzen"]') === null, 'kein „Zahlung fortsetzen“ für eine bezahlte Bestellung')
|
||||
}))
|
||||
|
||||
test('Ansicht: ein Fehler beim Laden zeigt die Fehlerkarte mit „Noch einmal versuchen“, das lädt neu', mitDom(undefined, async (fx) => {
|
||||
const tabelle = await einProjekt('off', { tabelle: { [an('p1')]: [{ status: 500, body: { error: 'Der Server hat gerade Pause.' } }, { body: await ansichtDaten('off') }] } })
|
||||
const wurzel = await oeffneAnsicht(fx, { tabelle })
|
||||
assert.equal(fx.text(fx.finde(wurzel, '[role="alert"] p')), 'Der Server hat gerade Pause.')
|
||||
await fx.klick(fx.finde(wurzel, '[role="alert"] button'))
|
||||
await fx.ruhe()
|
||||
assert.ok(fx.finde(wurzel, 'nav.tm-fortschritt'))
|
||||
}))
|
||||
|
||||
// ------------------------------------------------------- Rückkehr von Stripe
|
||||
const zweiProjekte = async (zustandP2 = 'ordered', extraP2 = GEORDERT) => ({
|
||||
[AN_FUNKTIONEN]: funktionenAntwort([projektEintrag('p1', 'off'), projektEintrag('p2', zustandP2, extraP2)]),
|
||||
[an('p1')]: { body: await ansichtDaten('off') },
|
||||
[an('p2')]: { body: await ansichtDaten(zustandP2) },
|
||||
})
|
||||
/** Aufrufe von history.replaceState mitschreiben. */
|
||||
function merkeAdressen(fx) {
|
||||
const adressen = []
|
||||
const echt = fx.window.history.replaceState
|
||||
fx.window.history.replaceState = (zustand, titel, url) => { adressen.push(url); echt(zustand, titel, url) }
|
||||
return adressen
|
||||
}
|
||||
|
||||
test('Rückkehr nach der Zahlung: #termine?bestellt=1&projekt=p2 wählt p2, zeigt „Danke!“, bereinigt die Adresse (einmal gelesen)', mitDom(undefined, async (fx) => {
|
||||
const adressen = merkeAdressen(fx)
|
||||
const wurzel = await oeffneAnsicht(fx, { projectId: 'p1', hash: '#termine?bestellt=1&projekt=p2', tabelle: await zweiProjekte() })
|
||||
assert.equal(fx.finde(wurzel, '#termine-project').value, 'p2', 'das Projekt der Rückkehr (vor dem gemerkten p1)')
|
||||
assert.equal(fx.text(fx.finde(wurzel, '[data-danke]')), 'Danke! Sobald die Zahlung da ist, legen wir los.')
|
||||
assert.equal(fx.text(fx.finde(wurzel, '.tm-statuskarte .tm-pille')), 'Zahlung offen')
|
||||
assert.deepEqual(adressen, ['/dashboard.html#termine'], 'Pfad und Query bleiben, der Hash bis auf #termine weg')
|
||||
assert.equal(fx.window.location.hash, '#termine')
|
||||
assert.deepEqual(AUFRUFE(fx), [AN_FUNKTIONEN, an('p2')], 'nur das Projekt der Rückkehr wird geladen')
|
||||
// ein zweiter Aufruf (Hash ist bereinigt) kennt weder Rückkehr noch Danke, behält aber die Auswahl
|
||||
const wieder = await oeffneAnsicht(fx, { projectId: '', hash: fx.window.location.hash, tabelle: await zweiProjekte() })
|
||||
assert.ok(fx.finde(wieder, '[data-danke]') === null)
|
||||
assert.equal(fx.finde(wieder, '#termine-project').value, 'p2')
|
||||
}))
|
||||
|
||||
test('Rückkehr nach der Zahlung, wenn der Webhook schon durch ist (setup): „Danke!“ auch dann', mitDom(undefined, async (fx) => {
|
||||
const wurzel = await oeffneAnsicht(fx, { projectId: 'p1', hash: '#termine?bestellt=1&projekt=p2', tabelle: await zweiProjekte('setup', { ...GEORDERT, ticketWoid: 100342 }) })
|
||||
assert.ok(fx.finde(wurzel, '[data-danke]'))
|
||||
assert.equal(fx.text(fx.finde(wurzel, '.tm-statuskarte .tm-pille')), 'In Einrichtung')
|
||||
}))
|
||||
|
||||
test('Rückkehr nach dem Abbruch: #termine?projekt=p2 wählt p2 ohne „Danke!“, die Adresse ist bereinigt; „Zahlung fortsetzen“ ist da', mitDom(undefined, async (fx) => {
|
||||
const adressen = merkeAdressen(fx)
|
||||
const wurzel = await oeffneAnsicht(fx, { projectId: 'p1', hash: '#termine?projekt=p2', tabelle: await zweiProjekte() })
|
||||
assert.equal(fx.finde(wurzel, '#termine-project').value, 'p2')
|
||||
assert.ok(fx.finde(wurzel, '[data-danke]') === null)
|
||||
assert.ok(!fx.text(wurzel).includes('Danke!'))
|
||||
assert.ok(fx.finde(wurzel, '[data-aktion="fortsetzen"]'))
|
||||
assert.deepEqual(adressen, ['/dashboard.html#termine'])
|
||||
assert.equal(fx.window.location.hash, '#termine')
|
||||
}))
|
||||
|
||||
test('Rückkehr: bestellt=1 ohne Projekt gilt dem gewählten Projekt; ein unbekanntes Projekt wird ignoriert (kein „Danke!“ für ein anderes)', mitDom(undefined, async (fx) => {
|
||||
const ohneProjekt = await oeffneAnsicht(fx, { projectId: 'p2', hash: '#termine?bestellt=1', tabelle: await zweiProjekte() })
|
||||
assert.ok(fx.finde(ohneProjekt, '[data-danke]'))
|
||||
assert.equal(fx.window.location.hash, '#termine')
|
||||
const unbekannt = await oeffneAnsicht(fx, { projectId: 'p2', hash: '#termine?bestellt=1&projekt=gibtsnicht', tabelle: await zweiProjekte() })
|
||||
assert.ok(fx.finde(unbekannt, '[data-danke]') === null)
|
||||
assert.equal(fx.finde(unbekannt, '#termine-project').value, 'p2', 'es bleibt bei der gewählten Website')
|
||||
assert.equal(fx.window.location.hash, '#termine')
|
||||
}))
|
||||
|
||||
test('Rückkehr: „Danke!“ nur für das Projekt der Zahlung und nur beim ersten Zeichnen, nicht nach einem Wechsel der Website', mitDom(undefined, async (fx) => {
|
||||
const wurzel = await oeffneAnsicht(fx, { projectId: 'p1', hash: '#termine?bestellt=1&projekt=p2', tabelle: await zweiProjekte() })
|
||||
assert.ok(fx.finde(wurzel, '[data-danke]'))
|
||||
await fx.setze(fx.finde(wurzel, '#termine-project'), 'p1')
|
||||
await fx.ruhe()
|
||||
assert.ok(fx.finde(wurzel, 'nav.tm-fortschritt'), 'p1 ist die Website im Zustand off')
|
||||
await fx.setze(fx.finde(wurzel, '#termine-project'), 'p2')
|
||||
await fx.ruhe()
|
||||
assert.ok(fx.finde(wurzel, '.tm-statuskarte'))
|
||||
assert.ok(fx.finde(wurzel, '[data-danke]') === null, 'der Dank war einmal')
|
||||
}))
|
||||
|
||||
test('Rückkehr: bestellt=1 für ein Projekt, das nicht bestellt ist (active, off), zeigt kein „Danke!“; fremde Hashes werden nicht angefasst', mitDom(undefined, async (fx) => {
|
||||
const adressen = merkeAdressen(fx)
|
||||
const aktiv = await oeffneAnsicht(fx, { projectId: 'p2', hash: '#termine?bestellt=1&projekt=p2', tabelle: await zweiProjekte('active', {}) })
|
||||
assert.ok(fx.finde(aktiv, '[data-danke]') === null)
|
||||
assert.ok(!fx.text(aktiv).includes('Danke!'))
|
||||
assert.equal(adressen.length, 1)
|
||||
const fremd = await oeffneAnsicht(fx, { projectId: 'p2', hash: '#news?bestellt=1&projekt=p2', tabelle: await zweiProjekte() })
|
||||
assert.ok(fx.finde(fremd, '[data-danke]') === null, 'die Rückkehr der News gehört nicht hierher')
|
||||
assert.equal(adressen.length, 1, 'und ihr Hash bleibt stehen')
|
||||
assert.equal(fx.window.location.hash, '#news?bestellt=1&projekt=p2')
|
||||
}))
|
||||
|
||||
test('Rückkehr: Projekt-ID aus der Adresse landet nur als Wert in der Anfrage (codiert), nie im Baum', mitDom(undefined, async (fx) => {
|
||||
const wurzel = await oeffneAnsicht(fx, { projectId: 'p1', hash: '#termine?bestellt=1&projekt=%3Cimg%20src%3Dx%3E', tabelle: await zweiProjekte() })
|
||||
assert.equal(fx.alle(wurzel, 'img').length, 0)
|
||||
assert.ok(!AUFRUFE(fx).some((a) => a.includes('<img')))
|
||||
}))
|
||||
|
||||
// ------------------------------------------------------- „Zahlung fortsetzen“ scheitert: Ansicht neu aufbauen (Ruling R110)
|
||||
const ORDERED_P1 = () => projektEintrag('p1', 'ordered', GEORDERT)
|
||||
const fortsetzenKnopf = (fx, wurzel) => fx.finde(wurzel, '[data-aktion="fortsetzen"]')
|
||||
|
||||
test('R110: 409 zustimmung_fehlt ⇒ Liste und Daten werden neu geholt und das Bestellformular steht an der Stelle der Karte', mitDom(undefined, async (fx) => {
|
||||
const wurzel = await oeffneAnsicht(fx, { tabelle: await einProjekt('ordered', {
|
||||
eintrag: GEORDERT,
|
||||
tabelle: { [AN_CHECKOUT]: { status: 409, body: { error: 'Bitte stimme der Erklärung zu, um zu bestellen.', code: 'zustimmung_fehlt' } } },
|
||||
}) })
|
||||
assert.ok(fx.finde(wurzel, '.tm-statuskarte'))
|
||||
assert.ok(fx.finde(wurzel, '.tm-bestellen') === null)
|
||||
await fx.klick(fortsetzenKnopf(fx, wurzel))
|
||||
await fx.ruhe()
|
||||
assert.deepEqual(AUFRUFE(fx), [AN_FUNKTIONEN, an('p1'), AN_CHECKOUT, AN_FUNKTIONEN, an('p1')])
|
||||
assert.deepEqual(fx.fetchLog[2].body, { projectId: 'p1', fortsetzen: true })
|
||||
assert.ok(fx.finde(wurzel, '.tm-statuskarte') === null, 'die Karte ist weg')
|
||||
const seite = fx.finde(wurzel, '.tm-bestellen')
|
||||
assert.ok(seite, 'das Bestellformular')
|
||||
assert.equal(fx.text(bestellKnopf(fx, wurzel)), (await bestellungTest()).consent.knopf, 'Knopftext aus der Antwort des Servers')
|
||||
assert.equal(bestellKnopf(fx, wurzel).disabled, true, 'gesperrt, bis alle Haken stehen')
|
||||
assert.equal(hakenFelder(fx, seite).length, registerHaken().length)
|
||||
assert.equal(meldungDerHuelle(fx, wurzel), 'Bitte stimme der Erklärung zu, um zu bestellen.')
|
||||
assert.deepEqual(reiterTexte(fx, wurzel), ['Leistungen', 'Team & Plätze', 'Einstellungen'], 'die Reiter bleiben, nur lesbar')
|
||||
// „Ändern“ führt nicht in den (gesperrten) Fragebogen, sondern erklärt, warum nicht
|
||||
await fx.klick(fx.finde(wurzel, '[data-aktion="aendern"]'))
|
||||
assert.match(meldungDerHuelle(fx, wurzel), /seit der Bestellung gesperrt/)
|
||||
// nach dem Setzen aller Haken geht die neue Zustimmung an den Checkout und es folgt die Zahlungsseite
|
||||
routen(fx, { [AN_CHECKOUT]: { body: { url: KNOPF_URL } } })
|
||||
await alleAnhaken(fx, seite)
|
||||
assert.equal(bestellKnopf(fx, wurzel).disabled, false)
|
||||
await fx.klick(bestellKnopf(fx, wurzel))
|
||||
const bestellt = fx.fetchLog.at(-1)
|
||||
assert.equal(bestellt.url, '/api/funktionen/termine/checkout')
|
||||
assert.equal(bestellt.body.zustimmung.version, (await bestellungTest()).consent.version)
|
||||
assert.equal(fx.window.location.href, KNOPF_URL)
|
||||
}))
|
||||
|
||||
test('R110: 400 (die Bestellung ist inzwischen bezahlt, der Server kennt sie nicht mehr als offen) ⇒ die Ansicht folgt dem neuen Zustand, die Karte steht nicht veraltet da', mitDom(undefined, async (fx) => {
|
||||
const wurzel = await oeffneAnsicht(fx, { tabelle: await einProjekt('ordered', {
|
||||
eintrag: GEORDERT,
|
||||
tabelle: {
|
||||
[AN_CHECKOUT]: { status: 400, body: { error: 'Bitte stimme der Erklärung zu, um zu bestellen.', code: 'zustimmung_fehlt' } },
|
||||
[AN_FUNKTIONEN]: [funktionenAntwort([ORDERED_P1()]), funktionenAntwort([projektEintrag('p1', 'setup', { ...GEORDERT, ticketWoid: 100342 })])],
|
||||
[an('p1')]: [{ body: await ansichtDaten('ordered') }, { body: await ansichtDaten('setup') }],
|
||||
},
|
||||
}) })
|
||||
const alteKarte = fx.finde(wurzel, '.tm-statuskarte')
|
||||
await fx.klick(fortsetzenKnopf(fx, wurzel))
|
||||
await fx.ruhe()
|
||||
const karteEl = fx.finde(wurzel, '.tm-statuskarte')
|
||||
assert.ok(karteEl !== alteKarte, 'neu gezeichnet')
|
||||
assert.equal(fx.text(fx.finde(karteEl, '.tm-pille')), 'In Einrichtung')
|
||||
assert.match(fx.text(karteEl), /Ticketnummer\s*#100342/)
|
||||
assert.ok(fortsetzenKnopf(fx, wurzel) === null)
|
||||
assert.ok(fx.finde(wurzel, '.tm-bestellen') === null, 'kein Bestellformular für eine bezahlte Bestellung')
|
||||
assert.equal(meldungDerHuelle(fx, wurzel), '', 'der Fehler des alten Zustands steht nicht in der neuen Karte')
|
||||
assert.ok(!fx.text(wurzel).includes('stimme der Erklärung'))
|
||||
}))
|
||||
|
||||
test('R110: ist die Funktion nach dem Webhook aktiv, zeigt die Ansicht die vier Reiter statt der Karte', mitDom(undefined, async (fx) => {
|
||||
const wurzel = await oeffneAnsicht(fx, { tabelle: await einProjekt('ordered', {
|
||||
eintrag: GEORDERT,
|
||||
tabelle: {
|
||||
[AN_CHECKOUT]: { status: 400, body: { error: 'Bitte stimme der Erklärung zu, um zu bestellen.', code: 'zustimmung_fehlt' } },
|
||||
[AN_FUNKTIONEN]: [funktionenAntwort([ORDERED_P1()]), funktionenAntwort([projektEintrag('p1', 'active')])],
|
||||
[an('p1')]: [{ body: await ansichtDaten('ordered') }, { body: await ansichtDaten('active') }],
|
||||
},
|
||||
}) })
|
||||
await fx.klick(fortsetzenKnopf(fx, wurzel))
|
||||
await fx.ruhe()
|
||||
assert.ok(fx.finde(wurzel, '.tm-statuskarte') === null)
|
||||
assert.deepEqual(reiterTexte(fx, wurzel), ['Termine', 'Leistungen', 'Team & Plätze', 'Einstellungen'])
|
||||
assert.equal(meldungDerHuelle(fx, wurzel), '')
|
||||
}))
|
||||
|
||||
test('R110: ein anderer Fehler bei noch offener Bestellung (409, die Sitzung läuft schon) ⇒ die Karte wird neu gezeichnet und der Fehler steht am Knopf', mitDom(undefined, async (fx) => {
|
||||
const FEHLER = 'Deine Zahlung läuft gerade oder ist schon da. Wir richten den Terminplaner ein, sobald sie bei uns ist.'
|
||||
const wurzel = await oeffneAnsicht(fx, { tabelle: await einProjekt('ordered', {
|
||||
eintrag: GEORDERT,
|
||||
tabelle: { [AN_CHECKOUT]: { status: 409, body: { error: FEHLER } } },
|
||||
}) })
|
||||
const alteKarte = fx.finde(wurzel, '.tm-statuskarte')
|
||||
await fx.klick(fortsetzenKnopf(fx, wurzel))
|
||||
await fx.ruhe()
|
||||
assert.deepEqual(AUFRUFE(fx), [AN_FUNKTIONEN, an('p1'), AN_CHECKOUT, AN_FUNKTIONEN, an('p1')])
|
||||
const karteEl = fx.finde(wurzel, '.tm-statuskarte')
|
||||
assert.ok(karteEl !== alteKarte)
|
||||
assert.equal(fx.text(fx.finde(karteEl, '[data-meldung="fortsetzen"]')), FEHLER)
|
||||
assert.equal(fortsetzenKnopf(fx, wurzel).disabled, false, 'der Knopf ist frei')
|
||||
assert.ok(fx.finde(wurzel, '.tm-bestellen') === null)
|
||||
}))
|
||||
|
||||
test('R110: lässt sich die Ansicht nicht neu laden, bleibt die Karte stehen und der Fehler steht am Knopf (bei fehlender Zustimmung der Grund des Neuladens)', mitDom(undefined, async (fx) => {
|
||||
const wurzel = await oeffneAnsicht(fx, { tabelle: await einProjekt('ordered', {
|
||||
eintrag: GEORDERT,
|
||||
tabelle: {
|
||||
[AN_CHECKOUT]: [{ status: 409, body: { error: 'Deine Zahlung läuft gerade.' } }, { status: 409, body: { error: 'Bitte stimme der Erklärung zu, um zu bestellen.', code: 'zustimmung_fehlt' } }],
|
||||
[AN_FUNKTIONEN]: [funktionenAntwort([ORDERED_P1()]), { status: 500, body: { error: 'Der Server hat gerade Pause.' } }],
|
||||
},
|
||||
}) })
|
||||
const karteEl = fx.finde(wurzel, '.tm-statuskarte')
|
||||
await fx.klick(fortsetzenKnopf(fx, wurzel))
|
||||
await fx.ruhe()
|
||||
assert.ok(fx.finde(wurzel, '.tm-statuskarte') === karteEl, 'dieselbe Karte')
|
||||
assert.equal(fx.text(fx.finde(karteEl, '[data-meldung="fortsetzen"]')), 'Deine Zahlung läuft gerade.')
|
||||
assert.equal(fortsetzenKnopf(fx, wurzel).disabled, false)
|
||||
await fx.klick(fortsetzenKnopf(fx, wurzel))
|
||||
await fx.ruhe()
|
||||
assert.ok(fx.finde(wurzel, '.tm-statuskarte') === karteEl)
|
||||
assert.equal(fx.text(fx.finde(karteEl, '[data-meldung="fortsetzen"]')), 'Der Server hat gerade Pause.')
|
||||
assert.ok(fx.finde(wurzel, '.tm-bestellen') === null)
|
||||
}))
|
||||
|
||||
test('R110: wechselt der Nutzer die Website, während neu geladen wird, zeichnet die späte Antwort nichts mehr', mitDom(undefined, async (fx) => {
|
||||
let freigeben
|
||||
const spaet = new Promise((fertig) => { freigeben = fertig })
|
||||
const tabelle = { ...(await zweiProjekte()), [AN_CHECKOUT]: { status: 409, body: { error: 'Deine Zahlung läuft gerade.' } } }
|
||||
const wurzel = await oeffneAnsicht(fx, { projectId: 'p2', tabelle })
|
||||
// ab jetzt hängt das Laden der Daten von p2, bis der Test es freigibt
|
||||
const antwort = globalThis.fetch
|
||||
globalThis.fetch = async (url, optionen) => {
|
||||
if (String(url) !== '/api/termine/einstellungen?projectId=p2') return antwort(url, optionen)
|
||||
fx.fetchLog.push({ method: 'GET', url: String(url), body: undefined })
|
||||
await spaet
|
||||
return { ok: true, status: 200, json: async () => structuredClone(await ansichtDaten('setup')) }
|
||||
}
|
||||
const klick = fx.klick(fortsetzenKnopf(fx, wurzel))
|
||||
await fx.ruhe()
|
||||
await fx.setze(fx.finde(wurzel, '#termine-project'), 'p1')
|
||||
await fx.ruhe()
|
||||
assert.ok(fx.finde(wurzel, 'nav.tm-fortschritt'), 'p1 steht da')
|
||||
freigeben()
|
||||
await klick
|
||||
await fx.ruhe()
|
||||
assert.ok(fx.finde(wurzel, 'nav.tm-fortschritt'), 'die späte Antwort für p2 hat p1 nicht überschrieben')
|
||||
assert.ok(fx.finde(wurzel, '.tm-statuskarte') === null)
|
||||
}))
|
||||
Reference in new issue
Block a user