TP2 Abschlussprüfung: Fragebogen und Bestellseite (veralteter Tab, Abo-Hinweis, Zurück aus Stripe)

- PUT /fragebogen: ein Tab mit veralteter Branche bekommt 409 EINSTELLUNG_VERALTET (die Branche wechselt nur die Vorlage), bestaetigt wird mit
  dem gespeicherten Stand vereinigt; die ungeprüften Vorlagenpreise können nicht mehr als geprüft gelten
- GET /einstellungen liefert in off aboHinweis (aboMeldung, auch bei nicht lesbarem Abo); die Hinweiskarte zeigt ihn immer und hat „Neu laden“
- Bestellseite und Statuskarte laden bei pageshow (persisted) neu, wenn der Browser sie nach der Zahlungsseite aus dem bfcache holt
- renderBestellen zeichnet „Ändern“ und „Zum Schritt“ nur mit zumSchritt; nach einem 409 bekommt die Meldung den Fokus
- Zusammenfassung nennt „Kleinunternehmer: ja/nein“

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-05 06:35:42 +00:00
1 parent 2fc48df915
commit 5f99f08a39
8 files changed
+314 -39

No files matched your search

+138 -7
View File
@@ -337,6 +337,23 @@ test('zusammenfassung (Vorlage salon): Name, Branche und Anschrift', () => {
assert.ok(zeilen.includes('Anschrift: Hauptstraße 1, 67655 Kaiserslautern'))
})
test('B5: zusammenfassung nennt in „Branche und Betrieb“ ausdrücklich „Kleinunternehmer: ja“ oder „nein“; ohne Angabe keine Zeile', () => {
const zeilen = (einstellung, fragebogen) => gruppe(zusammenfassung(einstellung, fragebogen, BRANCHEN), 'branche').zeilen
const nein = stand()
assert.equal(nein.einstellung.betrieb.kleinunternehmer, false)
assert.deepEqual(zeilen(nein.einstellung, nein.fragebogen).filter((z) => z.startsWith('Kleinunternehmer')), ['Kleinunternehmer: nein'])
const ja = stand((e) => { e.betrieb.kleinunternehmer = true })
assert.deepEqual(zeilen(ja.einstellung, ja.fragebogen).filter((z) => z.startsWith('Kleinunternehmer')), ['Kleinunternehmer: ja'])
assert.deepEqual(zeilen(ja.einstellung, ja.fragebogen).slice(0, 4), ['Branche: Friseur und Salon', 'Betrieb: Salon Muster', 'Anschrift: Hauptstraße 1, 67655 Kaiserslautern', 'Kleinunternehmer: ja'], 'nach der Anschrift')
// fehlt die Angabe oder ist sie kein Wahrheitswert, wird nichts erfunden
for (const wert of [undefined, null, 'ja', 1]) {
const ohne = stand((e) => { e.betrieb.kleinunternehmer = wert })
assert.ok(!zeilen(ohne.einstellung, ohne.fragebogen).some((z) => z.startsWith('Kleinunternehmer')), String(wert))
}
const keine = zusammenfassung(null, null, BRANCHEN)
assert.ok(!JSON.stringify(keine).includes('Kleinunternehmer'))
})
test('zusammenfassung (Vorlage salon): Leistungen mit Preis', () => {
const { einstellung, fragebogen } = stand()
const zeilen = gruppe(zusammenfassung(einstellung, fragebogen, BRANCHEN), 'leistungen').zeilen
@@ -1189,16 +1206,64 @@ test('renderBestellen: Zusammenfassung zeigt die acht Gruppen aus zusammenfassun
assert.deepEqual(schritte, ['leistungen', 'website'])
}))
test('renderBestellen: eine Gruppe ohne Angaben sagt es und hat trotzdem „Ändern“; fehlendes zumSchritt ist kein Fehler', mitDom(undefined, async (fx) => {
test('renderBestellen: eine Gruppe ohne Angaben sagt es und hat trotzdem „Ändern“ (mit zumSchritt)', mitDom(undefined, async (fx) => {
const daten = await bestellDaten({ einstellung: null, fragebogen: null })
const wurzel = h('div')
document.body.append(wurzel)
const ctx = huelle.baueCtx({ daten, nurLesen: false }, { projekt: { projectId: 'p1' }, aktuell: () => true })
bestellen.renderBestellen(wurzel, { ctx, daten })
const { wurzel, schritte } = await bestellSeite(daten)
const gruppen = fx.alle(wurzel, '.tm-bestellen-gruppe')
assert.equal(gruppen.length, 8)
assert.match(fx.text(gruppen[0]), /Noch keine Angaben/)
await fx.klick(fx.finde(gruppen[0], '[data-aktion="aendern"]'))
assert.deepEqual(schritte, ['branche'])
}))
test('B4: ohne zumSchritt (keine Funktion) zeichnet die Bestellseite weder „Ändern“ noch „Zum Schritt“ - keine Knöpfe, die nichts tun', mitDom(undefined, async (fx) => {
const offen = [{ schritt: 'website', text: 'Bitte trage die Adresse deines Impressums ein.' }, { schritt: 'bestellen', text: 'Das Abo konnte gerade nicht geprüft werden.' }]
for (const zumSchritt of [undefined, null, 'website', 42, {}]) {
const daten = await bestellDaten({ offen })
const wurzel = h('div')
document.body.append(wurzel)
const ctx = huelle.baueCtx({ daten, nurLesen: false }, { projekt: { projectId: 'p1' }, aktuell: () => true })
bestellen.renderBestellen(wurzel, { ctx, daten, zumSchritt })
const name = JSON.stringify(zumSchritt)
assert.equal(fx.alle(wurzel, '.tm-bestellen-gruppe').length, 8, `${name}: die Zusammenfassung steht`)
assert.equal(fx.alle(wurzel, '[data-aktion="aendern"]').length, 0, `${name}: kein „Ändern“`)
assert.equal(fx.alle(wurzel, '[data-aktion="zum-schritt"]').length, 0, `${name}: kein „Zum Schritt“`)
assert.deepEqual(fx.alle(wurzel, '.tm-bestellen-offen li span').map((span) => span.textContent), offen.map((p) => p.text), `${name}: die offenen Punkte bleiben als Text`)
}
// mit Funktion ist alles wie bisher da
const { wurzel } = await bestellSeite(await bestellDaten({ offen }))
assert.equal(fx.alle(wurzel, '[data-aktion="aendern"]').length, 8)
assert.deepEqual(fx.alle(wurzel, '[data-aktion="zum-schritt"]').map((k) => k.dataset.schritt), ['website'])
}))
test('B4: nach einem 409 bekommt die Meldung den Fokus (tabindex -1), auf der neuen Seite, bei fehlgeschlagenem Neuladen und auf der Hinweiskarte', mitDom(undefined, async (fx) => {
const fokusAufMeldung = (wurzel, was) => {
const meldung = fx.finde(wurzel, '[data-meldung="bestellen"]')
assert.ok(meldung, was)
assert.equal(meldung.getAttribute('tabindex'), '-1', `${was}: fokussierbar, aber nicht im Tabulatorweg`)
assert.equal(fx.document.activeElement, meldung, `${was}: Fokus auf der Meldung`)
}
// a) Seite wird neu gezeichnet
fx.antworten.push({ status: 409, body: { error: 'Die Bestellung ist noch nicht vollständig.', code: 'bestellung_offen', offen: [{ schritt: 'website', text: 'x' }] } }, { body: await bestellDaten() })
const a = await bestellSeite(await bestellDaten())
await alleAnhaken(fx, a.wurzel)
await fx.klick(bestellKnopf(fx, a.wurzel))
fokusAufMeldung(a.wurzel, 'neu gezeichnet')
// b) Neuladen scheitert: dieselbe Seite, Meldung mit Fokus
fx.antworten.push({ status: 409, body: { error: 'Dein Wartungs-Abo ist gekündigt.', code: 'gekuendigt' } }, { status: 500, body: { error: 'Server kaputt.' } })
const b = await bestellSeite(await bestellDaten())
await alleAnhaken(fx, b.wurzel)
await fx.klick(bestellKnopf(fx, b.wurzel))
fokusAufMeldung(b.wurzel, 'Neuladen gescheitert')
// c) die Bestellung ist inzwischen aufgegeben: die Karte mit der Meldung bekommt den Fokus
fx.antworten.push({ status: 409, body: { error: 'Der Terminplaner ist für dieses Projekt schon bestellt.' } }, { body: await bestellDaten({ state: 'ordered', bestellung: null, offen: null }) })
const c = await bestellSeite(await bestellDaten())
await alleAnhaken(fx, c.wurzel)
await fx.klick(bestellKnopf(fx, c.wurzel))
const karteEl = fx.finde(c.wurzel, '.tm-info')
assert.ok(karteEl)
assert.equal(karteEl.getAttribute('tabindex'), '-1')
assert.equal(fx.document.activeElement, karteEl, 'Fokus auf der Hinweiskarte')
}))
test('renderBestellen: offene Punkte mit „Zum Schritt“ zu ihrem Schritt, der Abo-Punkt (bestellen) ohne Knopf; Knopf gesperrt', mitDom(undefined, async (fx) => {
@@ -1443,6 +1508,41 @@ test('statusKarte ordered: „Zahlung fortsetzen“ sendet fortsetzenKoerper und
assert.equal(fx.window.location.href, KNOPF_URL)
}))
test('B3: zurück aus dem Bfcache (pageshow persisted) lädt die Karte „Zahlung fortsetzen“ neu - nicht gesperrt und ohne Meldung stehen lassen', mitDom({ antworten: [{ body: { url: KNOPF_URL } }] }, async (fx) => {
const wurzel = karte()
const knopf = fx.finde(wurzel, '[data-aktion="fortsetzen"]')
await fx.klick(knopf)
assert.equal(fx.window.location.href, KNOPF_URL)
assert.equal(knopf.disabled, true, 'bis zum Verlassen der Seite gesperrt')
// ein normales pageshow (erste Anzeige) ändert nichts
fx.window.dispatchEvent({ type: 'pageshow', persisted: false })
assert.deepEqual(fx.reloads, [])
// Zurück aus der Zahlungsseite: der Browser stellt die Seite samt gesperrtem Knopf wieder her
fx.window.dispatchEvent({ type: 'pageshow', persisted: true })
assert.equal(fx.reloads.length, 1, 'die Seite wird neu geladen')
}))
test('B3: eine Karte, die nicht mehr im Baum steht, lädt nichts neu und ihr Handler verschwindet', mitDom(undefined, (fx) => {
const wurzel = karte()
assert.equal(fx.window.anzahlHandler('pageshow'), 1)
wurzel.remove()
fx.window.dispatchEvent({ type: 'pageshow', persisted: true })
assert.deepEqual(fx.reloads, [])
assert.equal(fx.window.anzahlHandler('pageshow'), 0)
}))
test('B3: auch die Bestellseite lädt bei der Rückkehr aus dem Bfcache neu, solange sie im Baum steht', mitDom({ antworten: [{ body: { url: KNOPF_URL } }] }, async (fx) => {
const { wurzel } = await bestellSeite(await bestellDaten())
await alleAnhaken(fx, wurzel)
await fx.klick(bestellKnopf(fx, wurzel))
assert.equal(fx.window.location.href, KNOPF_URL)
assert.equal(bestellKnopf(fx, wurzel).disabled, true)
fx.window.dispatchEvent({ type: 'pageshow', persisted: false })
assert.deepEqual(fx.reloads, [])
fx.window.dispatchEvent({ type: 'pageshow', persisted: true })
assert.equal(fx.reloads.length, 1)
}))
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) => {
@@ -3099,8 +3199,9 @@ test('Ansicht off ohne laufendes Abo: der Hinweis vom Server (kein_abo mit Link
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')
const link = fx.alle(karteEl, 'button[data-view]')
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.deepEqual(fx.alle(karteEl, 'button').map((b) => b.getAttribute('data-aktion') ?? b.getAttribute('data-view')), ['vertraege', 'neu-laden'], 'dazu „Neu laden“ (B2)')
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)
@@ -3120,11 +3221,41 @@ test('Ansicht off ohne Abo: gekündigt, nicht aktiv, nicht lesbar und fehlendes
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.equal(fx.alle(karteEl, 'button[data-view]').length, 0, `${name}: kein Link`)
assert.ok(fx.finde(wurzel, 'nav.tm-fortschritt') === null, name)
}
}))
test('B2: der Text zum Abo kommt mit den Daten (aboHinweis), auch wenn die Liste „bestellbar“ sagt und das Abo nicht lesbar war; Link bei kein_abo; „Neu laden“ holt alles neu', mitDom(undefined, async (fx) => {
// das Abo war beim Lesen der Daten nicht lesbar (abo null), die Liste kennt keinen Hinweis: früher stand nur die Überschrift da
const HINWEIS = 'Das Abo konnte gerade nicht geprüft werden.'
const wurzel = await oeffneAnsicht(fx, { tabelle: await einProjekt('off', { daten: { abo: null, aboHinweis: HINWEIS } }) })
const karteEl = fx.finde(wurzel, '[data-hinweis="abo"]')
assert.ok(karteEl)
assert.equal(fx.text(fx.finde(karteEl, 'p')), HINWEIS, 'der Text des Servers aus den Daten')
assert.deepEqual(AUFRUFE(fx), [AN_FUNKTIONEN, an('p1')], 'dafür braucht es keine weitere Abfrage der Liste')
assert.equal(fx.alle(karteEl, 'button[data-view]').length, 0, 'ohne Grund kein_abo kein Link')
// kein_abo steht in den Daten: der Link zu Verträge auch ohne Angaben der Liste
const kein = await oeffneAnsicht(fx, { tabelle: await einProjekt('off', { daten: { abo: { ok: false, grund: 'kein_abo' }, aboHinweis: SERVER_HINWEIS } }) })
const kleiner = fx.finde(kein, '[data-hinweis="abo"]')
assert.equal(fx.text(fx.finde(kleiner, 'p')), `${SERVER_HINWEIS} Zu Verträge`)
// „Neu laden“: Daten noch einmal holen und die Ansicht nach dem neuen Stand zeichnen (das Abo läuft inzwischen: der Fragebogen)
const neu = await oeffneAnsicht(fx, { tabelle: await einProjekt('off', {
daten: { abo: null, aboHinweis: HINWEIS },
tabelle: { [an('p1')]: [{ body: await ansichtDaten('off', { abo: null, aboHinweis: HINWEIS }) }, { body: await ansichtDaten('off', { fragebogen: fbBis(1) }) }] },
}) })
const laden = fx.finde(neu, '[data-hinweis="abo"] button[data-aktion="neu-laden"]')
assert.ok(laden, 'Knopf „Neu laden“')
assert.equal(fx.text(laden), 'Neu laden')
assert.equal(laden.getAttribute('type'), 'button')
await fx.klick(laden)
await fx.ruhe()
assert.ok(fx.finde(neu, '[data-hinweis="abo"]') === null, 'die Karte ist weg')
assert.ok(fx.finde(neu, 'nav.tm-fortschritt'), 'der Fragebogen steht da')
}))
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) }) })