Buchungskalender: Abschluss-Fixwelle B zusammenführen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-10-07 15:34:30 +00:00
commit 1c6b6bf8d6
10 files changed
+257 -71

No files matched your search

+25
View File
@@ -843,3 +843,28 @@ test('Posten-Editor: die Summenanzeige enthaelt bei Regelbesteuerung kein Text-"
assert.doesNotMatch(fx.text(e.el), /null/)
} finally { fx.restore() }
})
// Abschluss F3: jeder Servercode hat einen deutschen Du-Satz mit Handlungshinweis
test('Fehlertexte: alle Codes des Portals sind übersetzt (Du-Form, kein Standardtext)', async () => {
const { fehlerFuerFormular } = await import('../public/buchungen-fehler.js')
const standard = fehlerFuerFormular({ fehler: 'gibt_es_nicht' }).message
for (const code of ['preise_unvollstaendig', 'einstellungen_unvollstaendig', 'status_ungueltig', 'limit_erreicht', 'kein_konto', 'nicht_aktiv', 'praefix_gesperrt', 'sperre_aus_ical', 'online_aus', 'zahlung_fehlgeschlagen']) {
const m = fehlerFuerFormular({ fehler: code }).message
assert.notEqual(m, standard, code)
assert.ok(m.length > 30 && m.endsWith('.'), code)
assert.doesNotMatch(m, /\bSie\b|\bIhre?\b/, code)
}
})
test('Preis-Editor: Platzhalter-Feldfehler verschwindet beim Tippen', mit(ukServer(), async (fx) => {
const { panel } = await zeigeTab(fx, ukTab)
await fx.klick(aktion(fx, panel, 'bearbeiten'))
await fx.ruhe()
const storno = fx.finde(panel, '#bk-p-storno')
await fx.setze(storno, 'Bis zu [X] Tage vor Anreise kostenfrei stornierbar, danach 100 %.')
await fx.klick(aktion(fx, panel, 'preise-speichern'))
await fx.ruhe()
assert.match(fehlerText(fx, panel, 'stornobedingungen'), /Platzhalter/)
await fx.setze(storno, 'Bis zu 14 Tage vor Anreise kostenfrei stornierbar, danach 100 %.')
assert.equal(fehlerText(fx, panel, 'stornobedingungen'), '')
}))
+16 -2
View File
@@ -17,9 +17,8 @@ test('kein innerHTML, outerHTML, insertAdjacentHTML, document.write, eval, new F
assert.doesNotMatch(OHNE_KOMMENTARE, /innerHTML|outerHTML|insertAdjacentHTML|document\.write|\beval\s*\(|new Function/)
})
test('API-Pfad, Praefix .wku-, @layer wku, Style-Id, kein Storage, keine Cookies, keine Fremdadressen', () => {
test('API-Pfad, Praefix .wku-, Style-Id, kein Storage, keine Cookies, keine Fremdadressen', () => {
assert.ok(OHNE_KOMMENTARE.includes('/api/public/unterkunft/v1/'))
assert.ok(OHNE_KOMMENTARE.includes('@layer wku'))
assert.ok(OHNE_KOMMENTARE.includes('wku-style'))
assert.match(OHNE_KOMMENTARE, /\.wku-root/)
assert.doesNotMatch(OHNE_KOMMENTARE, /localStorage|sessionStorage|document\.cookie|https?:\/\//)
@@ -29,3 +28,18 @@ test('Pflichttexte und Honeypot', () => {
for (const t of ['Anfragen gerne per Telefon oder E-Mail.', 'Bitte wählen Sie Anreise und Abreise im Kalender.', 'Anfrage senden', 'kontakt_url', 'formToken']) assert.ok(QUELLE.includes(t), t)
assert.match(QUELLE, /Danke! Ihre Anfrage/)
})
// F1 (Abschluss): kein @layer (Seitenregeln wuerden gewinnen), jede Regel unter .wku-root, Reset der Formularelemente.
test('CSS-Isolation: kein @layer, jeder Selektor beginnt mit .wku-root, all:revert fuer Elemente, Fenster +600 Tage', () => {
assert.doesNotMatch(OHNE_KOMMENTARE, /@layer/)
const m = QUELLE.match(/var CSS = \[([\s\S]*?)\]\.join\(''\)/)
assert.ok(m)
const css = [...m[1].matchAll(/'((?:[^'\\]|\\.)*)'/g)].map((x) => x[1]).join('')
const flach = css.replace(/@(container|media|supports)[^{]*\{/g, '')
const regeln = [...flach.matchAll(/([^{}]+)\{[^{}]*\}/g)].map((x) => x[1])
assert.ok(regeln.length > 40)
for (const sel of regeln) for (const teil of sel.split(/,(?![^(]*\))/)) assert.match(teil.trim(), /^\.wku-root(?![\w-])/, teil)
assert.match(css, /\.wku-root :is\(button,input,select,textarea[^)]*\)\{all:revert/)
assert.match(css, /\.wku-root\{[^}]*width:100%/)
assert.ok(OHNE_KOMMENTARE.includes('plusTage(von, 600)'))
})
+59 -1
View File
@@ -106,7 +106,7 @@ test('freigegeben: Pflichtangaben in Reihenfolge direkt ueber dem Knopf, Abruf m
const block = fx.finde(el, '[data-tag="pflichtangaben"]')
assert.equal(kinder[kinder.indexOf(block) + 1], knopf, 'Block steht direkt ueber dem Knopf')
const t = fx.text(block)
const marken = ['Anbieter: Haus Test GmbH', 'Weg 1', 'Unterkunft: Ferienwohnung, 10.11.2026 bis 14.11.2026', 'Check-in ab 15:00 Uhr', 'Check-out bis 11:00 Uhr', 'Personen: 2', 'Übernachtung, 4 Nächte', 'Endreinigung', 'Gesamtpreis: 450,00', '7 % USt', 'Zahlarten:', 'Stornobedingungen: Bis 14 Tage vor Anreise kostenfrei.', 'Ein Widerrufsrecht besteht nicht (§ 312g Abs. 2 Nr. 9 BGB).', 'Datenschutz:', 'Reserviert bis 10.10.2026 14:30 Uhr']
const marken = ['Anbieter: Haus Test GmbH', 'Weg 1', 'Unterkunft: Ferienwohnung, 10.11.2026 bis 14.11.2026', 'Check-in ab 15:00 Uhr', 'Check-out bis 11:00 Uhr', 'Personen: 2', 'Übernachtung, 4 Nächte', 'Endreinigung', 'Gesamtpreis: 450,00', '7 % USt', 'Zahlarten:', 'Stornobedingungen: Bis 14 Tage vor Anreise kostenfrei.', 'Ein Widerrufsrecht besteht nicht (§ 312g Abs. 2 Satz 1 Nr. 9 BGB).', 'Datenschutz:', 'Reserviert bis 10.10.2026 14:30 Uhr']
let pos = -1
for (const m of marken) {
const i = t.indexOf(m, pos + 1)
@@ -289,3 +289,61 @@ test('Fehler der Rechnungsadresse verschwinden, sobald der Gast die Felder ausfu
assert.ok(meldung.hidden)
})
})
/* ----------------------------------------------------- Abschluss: 409-Codes, a11y --- */
test('409-Codes beim Buchen: eigene Banner-Texte statt stillem Neuladen', async () => {
const faelle = [
['online_aus', /Online-Zahlung ist gerade nicht möglich – bitte wählen Sie „Vor Ort bezahlen“\./, false],
['einstellungen_unvollstaendig', /Die Buchung ist gerade nicht möglich\. Bitte wenden Sie sich direkt an den Anbieter\./, false],
['zustimmung_fehlt', /Die Buchung ist gerade nicht möglich\. Bitte wenden Sie sich direkt an den Anbieter\./, false],
['status_ungueltig', /Der Stand Ihrer Buchung hat sich geändert\./, true],
]
for (const [code, muster, neuLaden] of faelle) {
const antworten = [{ status: 200, body: DATEN }, { status: 409, body: { fehler: code } }]
if (neuLaden) antworten.push({ status: 200, body: { ...DATEN, status: 'gebucht', zahlart: 'vorort' } })
await mitFx(antworten, undefined, async (fx, el) => {
await fuelle(fx, el)
await fx.klick(fx.finde(el, 'button[type="submit"]'))
assert.match(fx.text(el), muster, code)
assert.equal(fx.fetchLog.length, neuLaden ? 3 : 2, code)
})
}
})
test('409 bei /zahlung: status_ungueltig laedt neu mit Hinweis; online_aus zeigt Text', async () => {
const d = { ...DATEN, status: 'gebucht', zahlart: 'online', bezahlt: false, rechnungNummer: 'R-1' }
await mitFx([{ status: 200, body: d }, { status: 409, body: { fehler: 'status_ungueltig' } }, { status: 200, body: { ...d, bezahlt: true } }], undefined, async (fx, el) => {
await fx.klick(fx.finde(el, '[data-tag="online-zahlen"]'))
assert.match(fx.text(el), /Der Stand Ihrer Buchung hat sich geändert\./)
assert.match(fx.text(el), /Bezahlt – vielen Dank\./)
})
await mitFx([{ status: 200, body: d }, { status: 409, body: { fehler: 'online_aus' } }], undefined, async (fx, el) => {
await fx.klick(fx.finde(el, '[data-tag="online-zahlen"]'))
assert.match(fx.text(el), /Online-Zahlung ist gerade nicht möglich/)
})
})
test('zahlungUrl nur bei https; Widerrufs-Zitat vollstaendig', async () => {
await mitFx([{ status: 200, body: DATEN }, { status: 200, body: { status: 'gebucht', rechnungNummer: 'R-1', zahlungUrl: 'javascript:alert(1)' } }], undefined, async (fx, el) => {
assert.match(fx.text(el), /§ 312g Abs\. 2 Satz 1 Nr\. 9 BGB/)
await fuelle(fx, el)
await fx.klick(fx.finde(el, 'input[value="online"]'))
await fx.klick(fx.finde(el, 'button[type="submit"]'))
assert.deepEqual(fx.assigned, [])
})
})
test('Feldfehler: aria-invalid, aria-describedby, Fokus auf erstes Fehlerfeld; Zahlart als fieldset/legend', async () => {
await mitFx([{ status: 200, body: DATEN }], undefined, async (fx, el) => {
const gruppe = fx.finde(el, 'fieldset[data-tag="zahlart"]')
assert.ok(gruppe)
assert.equal(fx.text(fx.finde(gruppe, 'legend')), 'Zahlart')
await fx.klick(fx.finde(el, 'button[type="submit"]'))
const strasse = fx.finde(el, '[name="strasse"]')
assert.equal(strasse.getAttribute('aria-invalid'), 'true')
assert.equal(strasse.getAttribute('aria-describedby'), 'uk-fehler-strasse')
assert.equal(fx.document.activeElement, strasse)
await fx.setze(strasse, 'Weg 1')
assert.equal(strasse.getAttribute('aria-invalid'), null)
})
})
+18
View File
@@ -90,3 +90,21 @@ test('Monatsnavigation: erster Monat hat aktiven Weiter-Knopf, Fokus bleibt, Tab
assert.doesNotMatch(tag(fx, el, '2026-11-22').getAttribute('aria-label'), /Abreisetag möglich/)
} finally { fx.restore() }
})
test('Serverfehler: felder.zeitraum, neu_laden und mindestnaechte erscheinen mit eigenem Text', async () => {
const faelle = [
[{ status: 400, body: { fehler: 'ungueltig', felder: { zeitraum: 'Ungültiger Zeitraum.' } } }, /Ungültiger Zeitraum\./],
[{ status: 400, body: { fehler: 'neu_laden' } }, /zu lange geöffnet/],
[{ status: 400, body: { fehler: 'mindestnaechte', felder: { mindestNaechte: 3 } } }, /Mindestaufenthalt: 3 Nächte\./],
]
for (const [antwort, muster] of faelle) {
const { fx, el } = start([{ status: 200, body: CFG }, { status: 200, body: BEL }, antwort, { status: 200, body: CFG }])
try {
await fx.ruhe(); await fx.ruhe()
await fx.setze(fx.finde(el, '[name="name"]'), 'Max'); await fx.setze(fx.finde(el, '[name="email"]'), 'max@example.com')
await fx.klick(tag(fx, el, '2026-11-10')); await fx.klick(tag(fx, el, '2026-11-14'))
await fx.klick(fx.finde(el, 'button[type="submit"]'))
assert.match(fx.text(el), muster)
} finally { fx.restore() }
}
})