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:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 23:10:54 +00:00
1 parent 52456dac76
commit 87d267e51a
13 files changed
+1077 -71

No files matched your search

+564 -11
View File
@@ -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)
}))