Files
Webklar-Kundenbereich/test/buchungenUi.test.js
T
knsoandClaude Opus 5.5 d33a483fd8 Buchungskalender: Freigeben zeigt bei 409 die richtige Meldung
Bisher stand bei jedem 409 „Der Zeitraum ist inzwischen belegt.“, auch wenn der Rechnungsauftrag fehlte (zustimmung_fehlt) oder sich der Stand geändert hatte (status_ungueltig). Jetzt nur bei zeitraum_belegt; sonst der Text aus buchungen-fehler.js. Aufgefallen beim ersten Test mit Donau Home.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:35:40 +00:00

1097 lines
61 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { installFakeDom } from './support/fakeDom.js'
import { h, request } from '../public/shop-dom.js'
import { REITER } from '../public/buchungen.js'
import { render, zeitraumText, naechteText } from '../public/buchungen-tab-anfragen.js'
import { summeLive, baueEditor } from '../public/buchungen-posten.js'
// Task 12: Gerüst und Reiter „Anfragen“ gegen eine Anfrage-Attrappe nach docs/funktionen/unterkunft-api.md.
const POSTEN = [
{ text: 'Übernachtung', menge: 7, einzelCent: 5500, ustProzent: 7 },
{ text: 'Reinigung', menge: 1, einzelCent: 6000, ustProzent: 7 },
{ text: 'Direktbuchungsrabatt 10 %', menge: 1, einzelCent: -3850, ustProzent: 7 },
]
const zeile = (nr, extra = {}) => ({ id: `a${nr}`, nummer: `A-${nr}`, von: '2026-11-12', bis: '2026-11-30', gast: { name: `Gast ${nr}` }, status: 'neu', summen: { bruttoCent: 40650 }, ...extra })
const anfrage = (extra = {}) => ({
id: 'a1', nummer: 'A-1', von: '2026-11-12', bis: '2026-11-19', personen: 2, status: 'neu', steuerart: 'regelbesteuert', unterkunftName: 'Haus am See',
gast: { name: 'Anna <b>Muster</b>', email: 'anna@example.com', telefon: '0151', nachricht: 'Hund dabei' }, ...extra,
})
const vorschlag = { naechte: 7, posten: POSTEN, summen: { bruttoCent: 40650 } }
const mit = (route, fn) => async () => {
const fx = installFakeDom({ antworten: Array.from({ length: 40 }, () => route) })
try { await fn(fx) } finally { fx.restore() }
}
function baueCtx() {
const ctx = { projectId: 'p1', status: { state: 'active' }, nurLesen: false, meldungen: [], aktuell: () => true, request, zeigeMeldung: (t, a) => ctx.meldungen.push({ text: t, art: a }), neuLaden: async () => ctx.status }
return ctx
}
async function zeige(fx) {
const ctx = baueCtx()
const panel = h('div')
fx.document.body.append(panel)
await render(panel, ctx)
await fx.ruhe()
return { panel, ctx }
}
const server = (o = {}) => (a) => {
const key = `${a.method} ${a.url.split('?')[0]}`
if (o[key]) return o[key](a)
if (a.method === 'GET' && a.url.startsWith('/api/unterkunft/anfragen?')) return { body: { anfragen: [zeile(1), zeile(2, { status: 'gebucht' })] } }
if (a.method === 'GET' && a.url.startsWith('/api/unterkunft/anfragen/')) return { body: { anfrage: anfrage(), vorschlag } }
return { body: {} }
}
const posts = (fx) => fx.fetchLog.filter((a) => a.method !== 'GET')
const aktion = (fx, el, n) => fx.finde(el, `[data-aktion="${n}"]`)
const oeffne = async (fx, panel, id = 'a1') => { await fx.klick(fx.finde(panel, `[data-anfrage="${id}"]`)); await fx.ruhe() }
test('REITER: fünf Reiter mit den Labels', () => {
assert.deepEqual(REITER.map((r) => r.key), ['anfragen', 'kalender', 'unterkuenfte', 'rechnungen', 'einstellungen'])
assert.deepEqual(REITER.map((r) => r.label), ['Anfragen', 'Kalender', 'Unterkünfte & Preise', 'Rechnungen', 'Einstellungen'])
})
test('summeLive spiegelt summiere: 40650 / 37991 / 2659', () => {
const s = summeLive(POSTEN, 'regelbesteuert')
assert.equal(s.bruttoCent, 40650)
assert.equal(s.nettoCent, 37991)
assert.equal(s.ust[0].steuerCent, 2659)
const k = summeLive(POSTEN, 'kleinunternehmer')
assert.deepEqual(k, { bruttoCent: 40650, nettoCent: 40650, ust: [] })
})
test('Zeitraum und Nächte', () => {
assert.equal(zeitraumText('2026-11-12', '2026-11-30'), '12.–30.11.2026')
assert.equal(zeitraumText('2026-10-28', '2026-11-03'), '28.10.–03.11.2026')
assert.equal(zeitraumText('2026-12-28', '2027-01-03'), '28.12.2026–03.01.2027')
assert.equal(naechteText('2026-11-12', '2026-11-30'), '18 Nächte')
assert.equal(naechteText('2026-11-12', '2026-11-13'), '1 Nacht')
})
test('Liste: Nummer, Zeitraum, Gast, Status-Pille, Betrag; Filter offen als Standard', mit(server(), async (fx) => {
const { panel } = await zeige(fx)
assert.equal(fx.fetchLog[0].url, '/api/unterkunft/anfragen?projectId=p1&filter=offen')
const z = fx.alle(panel, '[data-anfrage]')
assert.equal(z.length, 2)
const t = fx.text(z[0])
for (const re of [/A-1/, /12\.–30\.11\.2026 · 18 Nächte/, /Gast 1/, /Neu/, /406,50\s€/]) assert.match(t, re)
assert.match(fx.text(z[1]), /Gebucht/)
await fx.setze(fx.finde(panel, '#bk-a-filter'), 'alle')
await fx.ruhe()
assert.equal(fx.fetchLog.at(-1).url, '/api/unterkunft/anfragen?projectId=p1&filter=alle')
}))
test('Detail: Vorschlag editierbar, Summe aktualisiert sich, Freigeben sendet posten/fristTage/nachricht und zeigt den Link', mit(server({
'POST /api/unterkunft/anfragen/a1/freigeben': () => ({ body: { buchungsLink: 'https://x.test/buchen/a1?t=abc' } }),
}), async (fx) => {
const { panel, ctx } = await zeige(fx)
await oeffne(fx, panel)
assert.match(fx.text(panel), /Anna <b>Muster<\/b>/)
assert.equal(fx.finde(panel, 'b'), null, 'Fremdtext bleibt Text')
assert.match(fx.text(fx.finde(panel, '[data-summen]')), /Gesamt 406,50\s€/)
const zeilen = fx.alle(panel, '[data-zeile]')
assert.equal(zeilen.length, 3)
const einzel = fx.finde(zeilen[1], '[data-posten="einzel"]')
await fx.setze(einzel, '70,00')
assert.match(fx.text(fx.finde(panel, '[data-summen]')), /Gesamt 416,50\s€/)
await fx.setze(einzel, '60,00')
await fx.setze(fx.finde(panel, '#bk-a-frist'), '5')
await fx.setze(fx.finde(panel, '#bk-a-nachricht'), 'Gern!')
await fx.klick(aktion(fx, panel, 'freigeben'))
await fx.ruhe()
const [p] = posts(fx)
assert.equal(p.url, '/api/unterkunft/anfragen/a1/freigeben?projectId=p1')
assert.deepEqual(p.body, { posten: POSTEN, fristTage: 5, nachricht: 'Gern!' })
assert.equal(fx.finde(panel, '[data-link]').getAttribute('value'), 'https://x.test/buchen/a1?t=abc')
assert.ok(aktion(fx, panel, 'link-kopieren'))
assert.equal(ctx.meldungen.at(-1).art, 'ok')
}))
test('Freigeben: 409 zeigt „Der Zeitraum ist inzwischen belegt.“', mit(server({
'POST /api/unterkunft/anfragen/a1/freigeben': () => ({ status: 409, body: { fehler: 'zeitraum_belegt' } }),
}), async (fx) => {
const { panel } = await zeige(fx)
await oeffne(fx, panel)
await fx.klick(aktion(fx, panel, 'freigeben'))
await fx.ruhe()
assert.match(fx.text(panel), /Der Zeitraum ist inzwischen belegt\./)
assert.equal(aktion(fx, panel, 'freigeben').disabled, false)
}))
test('Freigeben: 409 ohne Rechnungsauftrag zeigt die Zustimmungs-Meldung, nicht „belegt“', mit(server({
'POST /api/unterkunft/anfragen/a1/freigeben': () => ({ status: 409, body: { fehler: 'zustimmung_fehlt' } }),
}), async (fx) => {
const { panel } = await zeige(fx)
await oeffne(fx, panel)
await fx.klick(aktion(fx, panel, 'freigeben'))
await fx.ruhe()
assert.match(fx.text(panel), /Bitte erteile zuerst in den Einstellungen die Zustimmung zur Rechnungsstellung in deinem Namen\./)
assert.doesNotMatch(fx.text(panel), /belegt/)
assert.equal(aktion(fx, panel, 'freigeben').disabled, false)
}))
test('Freigeben: 409 status_ungueltig zeigt den Stand-Hinweis, nicht „belegt“', mit(server({
'POST /api/unterkunft/anfragen/a1/freigeben': () => ({ status: 409, body: { fehler: 'status_ungueltig' } }),
}), async (fx) => {
const { panel } = await zeige(fx)
await oeffne(fx, panel)
await fx.klick(aktion(fx, panel, 'freigeben'))
await fx.ruhe()
assert.match(fx.text(panel), /Lade die Seite neu und prüfe den Status\./)
assert.doesNotMatch(fx.text(panel), /belegt/)
}))
test('Ablehnen sendet den Text', mit(server({ 'POST /api/unterkunft/anfragen/a1/ablehnen': () => ({ body: {} }) }), async (fx) => {
const { panel } = await zeige(fx)
await oeffne(fx, panel)
await fx.klick(aktion(fx, panel, 'ablehnen'))
await fx.setze(fx.finde(panel, '#bk-a-ablehn'), 'Leider belegt')
await fx.klick(aktion(fx, panel, 'ablehnen-senden'))
await fx.ruhe()
assert.deepEqual(posts(fx)[0].body, { text: 'Leider belegt' })
}))
const gebucht = (extra = {}) => ({ anfrage: anfrage({ status: 'gebucht', posten: POSTEN, summen: { bruttoCent: 40650 }, rechnungId: 'r1', rechnungNummer: 'R-2026-001', ...extra }) })
test('Gebucht: Rechnung-Link, „Als bezahlt (vor Ort)“, Storno ohne Erstattungsfeld bei Vor-Ort-Zahlung', mit(server({
'GET /api/unterkunft/anfragen/a1': () => ({ body: gebucht({ zahlart: 'vorort' }) }),
'POST /api/unterkunft/anfragen/a1/bezahlt': () => ({ body: {} }),
'POST /api/unterkunft/anfragen/a1/stornieren': () => ({ body: {} }),
}), async (fx) => {
const { panel } = await zeige(fx)
await oeffne(fx, panel)
assert.equal(aktion(fx, panel, 'rechnung').getAttribute('href'), '/api/unterkunft/rechnungen/r1.pdf?projectId=p1')
await fx.klick(aktion(fx, panel, 'bezahlt'))
await fx.ruhe()
assert.equal(posts(fx)[0].url, '/api/unterkunft/anfragen/a1/bezahlt?projectId=p1')
await fx.klick(aktion(fx, panel, 'stornieren'))
assert.equal(fx.finde(panel, '#bk-a-erstattung'), null)
await fx.setze(fx.finde(panel, '#bk-a-grund'), 'Gast krank')
await fx.klick(aktion(fx, panel, 'storno-senden'))
await fx.ruhe()
assert.deepEqual(posts(fx).at(-1).body, { grund: 'Gast krank', erstattungCent: 0 })
}))
test('Storno bei Online-Zahlung: Erstattungsbetrag wird gesendet', mit(server({
'GET /api/unterkunft/anfragen/a1': () => ({ body: gebucht({ status: 'bezahlt', zahlart: 'online', bezahltVia: 'stripe' }) }),
'POST /api/unterkunft/anfragen/a1/stornieren': () => ({ body: {} }),
}), async (fx) => {
const { panel } = await zeige(fx)
await oeffne(fx, panel)
assert.equal(aktion(fx, panel, 'bezahlt'), null)
await fx.klick(aktion(fx, panel, 'stornieren'))
assert.equal(fx.finde(panel, '#bk-a-erstattung').value, '406,50')
await fx.setze(fx.finde(panel, '#bk-a-erstattung'), '200,00')
await fx.klick(aktion(fx, panel, 'storno-senden'))
await fx.ruhe()
assert.equal(posts(fx)[0].body.erstattungCent, 20000)
}))
// Task 13: Reiter „Kalender“ und „Unterkünfte & Preise“ (Antwortformen: ui-erwartungen.md)
const UK = { id: 'u1', name: 'Haus <i>am</i> See', beschreibung: 'Schön', maxPersonen: 4, mindestNaechte: 2, checkIn: '15:00', checkOut: '10:00', sort: 0, aktiv: true, icalExportUrl: 'https://x.test/unterkunft/ical/tok.ics',
icalImporte: [{ id: 'i1', name: 'Booking.com', url: 'https://b.test/x.ics', letzterErfolgAt: '2026-10-07T10:00:00.000Z', fehler: '' }, { id: 'i2', name: 'Airbnb', url: 'https://a.test/x.ics', fehler: 'Zeitüberschreitung' }] }
const PREISE = { staffeln: [{ abNaechten: 1, preisProNachtCent: 5500 }, { abNaechten: 7, preisProNachtCent: 5000 }], reinigungCent: 6000, personenInklusive: 2, aufpreisPersonNachtCent: 1000, direktrabattProzent: 10, steuerart: 'regel', ustUebernachtung: 7, ustReinigung: 7, fristTage: 3, stornobedingungen: 'Kostenlos bis 14 Tage vorher.' }
const BEREICHE = [
{ von: '2026-11-03', bis: '2026-11-06', quelle: 'gebucht', anfrageNummer: 'A-1' }, { von: '2026-11-10', bis: '2026-11-12', quelle: 'reserviert' },
{ von: '2026-11-15', bis: '2026-11-17', quelle: 'ical:i1' }, { von: '2026-11-20', bis: '2026-11-22', quelle: 'manuell', sperreId: 's1', notiz: 'Eigenbedarf' },
]
const ukServer = (o = {}) => (a) => {
const key = `${a.method} ${a.url.split('?')[0]}`
if (o[key]) return o[key](a)
if (key === 'GET /api/unterkunft/unterkuenfte') return { body: { unterkuenfte: [UK] } }
if (key === 'GET /api/unterkunft/unterkuenfte/u1/kalender') return { body: { bereiche: BEREICHE } }
if (key === 'GET /api/unterkunft/unterkuenfte/u1/preise') return { body: { preise: PREISE } }
if (key === 'POST /api/unterkunft/preisvorschau') return { body: { naechte: 7, posten: [], summen: { bruttoCent: 40650 } } }
return { body: {} }
}
const zeigeTab = async (fx, modul) => {
const ctx = baueCtx()
const panel = h('div')
fx.document.body.append(panel)
await modul.render(panel, ctx)
await fx.ruhe()
return { panel, ctx }
}
const kal = await import('../public/buchungen-tab-kalender.js')
const ukTab = await import('../public/buchungen-tab-unterkuenfte.js')
import { setzeUhrFuerTests } from '../public/buchungen-uk-hilfe.js'
setzeUhrFuerTests(() => new Date('2026-10-07T10:00:00Z')) // feste Uhr: Kalender und Sperren-Tests nutzen Daten im Herbst 2026
const tag = (fx, panel, t) => fx.finde(panel, `[data-tag="${t}"]`)
const gehZuNovember = async (fx, panel) => {
for (let i = 0; i < 24 && !/November 2026/.test(fx.text(fx.finde(panel, '[data-monat]'))); i += 1) { await fx.klick(aktion(fx, panel, 'weiter')); await fx.ruhe() }
assert.match(fx.text(fx.finde(panel, '[data-monat]')), /November 2026/, 'November 2026 nicht erreicht')
}
test('rasterTage: November 2026 beginnt am Montag 26.10. und endet am Sonntag 29.11.... ', () => {
const t = kal.rasterTage(2026, 10)
assert.equal(t[0], '2026-10-26')
assert.equal(t.length % 7, 0)
assert.equal(t.at(-1), '2026-12-06')
assert.equal(kal.rasterTage(2026, 5)[0], '2026-06-01') // Juni 2026 beginnt am Montag
})
test('Kalender: Raster Montag zuerst, Farben nach Quelle, Legende', mit(ukServer(), async (fx) => {
const { panel } = await zeigeTab(fx, kal)
await gehZuNovember(fx, panel)
assert.equal(fx.text(fx.alle(panel, '[role="columnheader"]')[0]), 'Mo')
assert.equal(fx.alle(panel, '[data-tag]')[0].dataset.tag, '2026-10-26')
assert.ok(tag(fx, panel, '2026-11-03').classList.contains('ist-gebucht'))
assert.ok(!tag(fx, panel, '2026-11-06').classList.contains('ist-gebucht'), 'Abreisetag ist frei')
assert.ok(tag(fx, panel, '2026-11-10').classList.contains('ist-reserviert'))
assert.ok(tag(fx, panel, '2026-11-16').classList.contains('ist-ical'))
assert.ok(tag(fx, panel, '2026-11-20').classList.contains('ist-manuell'))
assert.ok(tag(fx, panel, '2026-11-08').classList.contains('ist-frei'))
for (const t of ['Direkt gebucht', 'Reserviert', 'Booking.com / Airbnb', 'Von dir gesperrt']) assert.match(fx.text(panel), new RegExp(t))
assert.ok(fx.fetchLog.some((a) => /kalender\?projectId=p1&von=2026-10-26&bis=2026-12-07$/.test(a.url)))
}))
test('Kalender: zwei Klicks auf freie Tage → Notiz → POST sperren', mit(ukServer({ 'POST /api/unterkunft/unterkuenfte/u1/sperren': () => ({ status: 201, body: {} }) }), async (fx) => {
const { panel } = await zeigeTab(fx, kal)
await gehZuNovember(fx, panel)
await fx.klick(tag(fx, panel, '2026-11-25'))
assert.equal(posts(fx).length, 0)
await fx.klick(tag(fx, panel, '2026-11-27'))
assert.match(fx.text(panel), /Zeitraum sperren/)
await fx.setze(fx.finde(panel, '#bk-k-notiz'), 'Familie')
await fx.klick(aktion(fx, panel, 'sperren-senden'))
await fx.ruhe()
const [p] = posts(fx)
assert.equal(p.url, '/api/unterkunft/unterkuenfte/u1/sperren?projectId=p1')
assert.deepEqual(p.body, { von: '2026-11-25', bis: '2026-11-28', notiz: 'Familie' })
}))
test('Kalender: Zeitraum über belegte Tage wird nicht angeboten; 409 vom Server wird gezeigt', mit(ukServer({ 'POST /api/unterkunft/unterkuenfte/u1/sperren': () => ({ status: 409, body: { fehler: 'zeitraum_belegt' } }) }), async (fx) => {
const { panel } = await zeigeTab(fx, kal)
await gehZuNovember(fx, panel)
await fx.klick(tag(fx, panel, '2026-11-08'))
await fx.klick(tag(fx, panel, '2026-11-13'))
assert.equal(aktion(fx, panel, 'sperren-senden'), null)
assert.match(fx.text(fx.finde(panel, '[data-hinweis]')), /belegte Tage/)
await fx.klick(tag(fx, panel, '2026-11-24'))
await fx.klick(tag(fx, panel, '2026-11-24'))
await fx.klick(aktion(fx, panel, 'sperren-senden'))
await fx.ruhe()
assert.match(fx.text(panel), /Der Zeitraum ist bereits belegt\./)
}))
test('Kalender: Klick auf manuelle Sperre bietet „Sperre aufheben“ (DELETE); andere Quellen nicht', mit(ukServer({ 'DELETE /api/unterkunft/sperren/s1': () => ({ body: {} }) }), async (fx) => {
const { panel } = await zeigeTab(fx, kal)
await gehZuNovember(fx, panel)
await fx.klick(tag(fx, panel, '2026-11-04'))
assert.equal(aktion(fx, panel, 'aufheben'), null)
await fx.klick(tag(fx, panel, '2026-11-21'))
assert.match(fx.text(panel), /Eigenbedarf/)
await fx.klick(aktion(fx, panel, 'aufheben'))
await fx.ruhe()
const [p] = posts(fx)
assert.equal(p.method, 'DELETE')
assert.equal(p.url, '/api/unterkunft/sperren/s1?projectId=p1')
}))
test('Unterkünfte: Liste mit Fremdtext als Text; Formular hat alle Felder; Speichern sendet PUT', mit(ukServer({ 'PUT /api/unterkunft/unterkuenfte/u1': () => ({ body: { unterkunft: UK } }) }), async (fx) => {
const { panel } = await zeigeTab(fx, ukTab)
assert.match(fx.text(panel), /Haus <i>am<\/i> See/)
assert.equal(fx.finde(panel, 'i'), null)
await fx.klick(aktion(fx, panel, 'bearbeiten'))
await fx.ruhe()
for (const f of ['name', 'beschreibung', 'maxPersonen', 'mindestNaechte', 'checkIn', 'checkOut', 'aktiv']) assert.ok(fx.finde(panel, `[data-feld="${f}"]`), f)
await fx.setze(fx.finde(panel, '#bk-u-max'), '6')
await fx.klick(fx.finde(panel, '.bk-unterkuenfte form [data-aktion="speichern"]'))
await fx.ruhe()
const put = posts(fx).find((a) => a.method === 'PUT')
assert.equal(put.url, '/api/unterkunft/unterkuenfte/u1?projectId=p1')
assert.deepEqual(put.body, { name: UK.name, beschreibung: 'Schön', maxPersonen: 6, mindestNaechte: 2, checkIn: '15:00', checkOut: '10:00', sort: 0, aktiv: true })
}))
test('Unterkünfte: Feldfehler (400 mit felder) kommen am Feld an', mit(ukServer({ 'PUT /api/unterkunft/unterkuenfte/u1': () => ({ status: 400, body: { fehler: 'ungueltig', felder: { name: 'Name ist schon vergeben.' } } }) }), async (fx) => {
const { panel } = await zeigeTab(fx, ukTab)
await fx.klick(aktion(fx, panel, 'bearbeiten'))
await fx.ruhe()
await fx.klick(fx.finde(panel, '.bk-unterkuenfte form [data-aktion="speichern"]'))
await fx.ruhe()
assert.equal(fx.text(fx.finde(panel, '[data-fehler="name"]')), 'Name ist schon vergeben.')
}))
test('Unterkünfte: Neue Unterkunft prüft Felder lokal und sendet POST', mit(ukServer({ 'POST /api/unterkunft/unterkuenfte': () => ({ status: 201, body: { unterkunft: { ...UK, id: 'u2', name: 'Neu' } } }) }), async (fx) => {
const { panel } = await zeigeTab(fx, ukTab)
await fx.klick(aktion(fx, panel, 'neu'))
await fx.klick(aktion(fx, panel, 'speichern'))
assert.match(fx.text(fx.finde(panel, '[data-fehler="name"]')), /Namen/)
assert.equal(posts(fx).length, 0)
await fx.setze(fx.finde(panel, '#bk-u-name'), 'Neu')
await fx.klick(aktion(fx, panel, 'speichern'))
await fx.ruhe()
const [p] = posts(fx)
assert.equal(p.method, 'POST')
assert.equal(p.body.name, 'Neu')
assert.equal(p.body.sort, 1)
}))
test('Preis-Editor: Staffelzeilen +/−, Beispielrechnung über /preisvorschau, Speichern sendet Cent', mit(ukServer({ 'PUT /api/unterkunft/unterkuenfte/u1/preise': () => ({ body: {} }) }), async (fx) => {
const { panel } = await zeigeTab(fx, ukTab)
await fx.klick(aktion(fx, panel, 'bearbeiten'))
await fx.ruhe()
assert.match(fx.text(fx.finde(panel, '[data-vorschau]')), /7 Nächte, 2 Personen = 406,50\s€/)
const vor = fx.fetchLog.find((a) => a.url.startsWith('/api/unterkunft/preisvorschau'))
assert.equal(vor.method, 'POST')
assert.equal(vor.body.unterkunftId, 'u1')
assert.equal(vor.body.personen, 2)
assert.equal(fx.alle(panel, '[data-staffel-zeile]').length, 2)
await fx.klick(aktion(fx, panel, 'staffel-dazu'))
assert.equal(fx.alle(panel, '[data-staffel-zeile]').length, 3)
const neu = fx.alle(panel, '[data-staffel-zeile]')[2]
await fx.setze(fx.finde(neu, '[data-staffel="ab"]'), '14')
await fx.setze(fx.finde(neu, '[data-staffel="preis"]'), '45')
await fx.klick(fx.finde(fx.alle(panel, '[data-staffel-zeile]')[1], '[data-aktion="staffel-weg"]'))
assert.equal(fx.alle(panel, '[data-staffel-zeile]').length, 2)
await fx.klick(aktion(fx, panel, 'preise-speichern'))
await fx.ruhe()
const put = posts(fx).find((a) => a.url.includes('/preise'))
assert.equal(put.method, 'PUT')
assert.deepEqual(put.body.staffeln, [{ abNaechten: 1, preisProNachtCent: 5500 }, { abNaechten: 14, preisProNachtCent: 4500 }])
assert.equal(put.body.reinigungCent, 6000)
assert.equal(put.body.aufpreisPersonNachtCent, 1000)
assert.equal(put.body.steuerart, 'regel')
assert.equal(put.body.direktrabattProzent, 10)
assert.equal(put.body.stornobedingungen, 'Kostenlos bis 14 Tage vorher.')
}))
test('iCal: Status je Import, Fehlertext, Export-Link, Anleitungen', mit(ukServer(), async (fx) => {
const { panel } = await zeigeTab(fx, ukTab)
await fx.klick(aktion(fx, panel, 'bearbeiten'))
await fx.ruhe()
assert.match(fx.text(fx.finde(panel, '[data-import="i1"]')), /Zuletzt abgeglichen 07\.10\.2026/)
assert.match(fx.text(fx.finde(panel, '[data-import="i2"]')), /nicht geklappt: Zeitüberschreitung/)
assert.equal(fx.finde(panel, '[data-export]').getAttribute('value'), UK.icalExportUrl)
assert.ok(aktion(fx, panel, 'export-kopieren'))
assert.match(fx.text(fx.finde(panel, '[data-anleitung="booking"]')), /Unterkunft → Tarife & Verfügbarkeit → Kalender synchronisieren → Kalender importieren → Link einfügen/)
assert.ok(fx.finde(panel, '[data-anleitung="airbnb"]'))
}))
test('iCal: Import hinzufügen (POST), entfernen mit Rückfrage, Link erneuern mit Bestätigung', mit(ukServer({
'POST /api/unterkunft/unterkuenfte/u1/ical-importe': () => ({ status: 201, body: {} }),
'DELETE /api/unterkunft/unterkuenfte/u1/ical-importe/i1': () => ({ body: {} }),
'POST /api/unterkunft/unterkuenfte/u1/ical-export/erneuern': () => ({ body: {} }),
}), async (fx) => {
const { panel } = await zeigeTab(fx, ukTab)
await fx.klick(aktion(fx, panel, 'bearbeiten'))
await fx.ruhe()
await fx.setze(fx.finde(panel, '#bk-i-name'), 'Vrbo')
await fx.setze(fx.finde(panel, '#bk-i-url'), 'http://unsicher.test/x.ics')
const vorher = posts(fx).length
await fx.klick(aktion(fx, panel, 'import-dazu'))
assert.equal(posts(fx).length, vorher, 'nur https')
await fx.setze(fx.finde(panel, '#bk-i-url'), 'https://vrbo.test/x.ics')
await fx.klick(aktion(fx, panel, 'import-dazu'))
await fx.ruhe()
assert.deepEqual(posts(fx).find((a) => a.url.includes('ical-importe')).body, { name: 'Vrbo', url: 'https://vrbo.test/x.ics' })
await fx.klick(fx.finde(fx.finde(panel, '[data-import="i1"]'), '[data-aktion="import-weg"]'))
assert.equal(posts(fx).filter((a) => a.method === 'DELETE').length, 0)
await fx.klick(aktion(fx, panel, 'import-weg-ok'))
await fx.ruhe()
assert.equal(posts(fx).find((a) => a.method === 'DELETE').url, '/api/unterkunft/unterkuenfte/u1/ical-importe/i1?projectId=p1')
await fx.klick(aktion(fx, panel, 'export-erneuern'))
assert.equal(posts(fx).filter((a) => a.url.includes('erneuern')).length, 0)
await fx.klick(aktion(fx, panel, 'erneuern-ok'))
await fx.ruhe()
assert.equal(posts(fx).filter((a) => a.url.includes('erneuern')).length, 1)
}))
test('Freigeben: Serverfehler 400 mit felder zeigt genau den Feldtext', mit(server({
'POST /api/unterkunft/anfragen/a1/freigeben': () => ({ status: 400, body: { fehler: 'ungueltig', felder: { posten: 'Die Summe muss größer als 0 sein.' } } }),
}), async (fx) => {
const { panel } = await zeige(fx)
await oeffne(fx, panel)
await fx.klick(aktion(fx, panel, 'freigeben'))
await fx.ruhe()
assert.equal(fx.text(fx.finde(panel, '[data-fehler="posten"]')), 'Die Summe muss größer als 0 sein.')
assert.equal(fx.text(fx.finde(panel, '[data-fehler="_"]')), '')
}))
// Fix round 1: Preis-Editor prüft Grenzen wie der Server
const preisEditor = async (fx, preise = PREISE) => {
const { panel } = await zeigeTab(fx, ukTab)
await fx.klick(aktion(fx, panel, 'bearbeiten'))
await fx.ruhe()
return panel
}
const versuche = async (fx, panel) => {
const vorher = posts(fx).filter((a) => a.url.includes('/preise')).length
await fx.klick(aktion(fx, panel, 'preise-speichern'))
await fx.ruhe()
return posts(fx).filter((a) => a.url.includes('/preise')).length - vorher
}
const fehlerText = (fx, panel, n) => fx.text(fx.finde(panel, `[data-fehler="${n}"]`))
test('Preis-Editor: Staffeln (Anzahl, erste ab 1, aufsteigend, Preisgrenzen)', mit(ukServer({ 'PUT /api/unterkunft/unterkuenfte/u1/preise': () => ({ body: {} }) }), async (fx) => {
const panel = await preisEditor(fx)
const zeilen = () => fx.alle(panel, '[data-staffel-zeile]')
const ab = (i) => fx.finde(zeilen()[i], '[data-staffel="ab"]')
const preis = (i) => fx.finde(zeilen()[i], '[data-staffel="preis"]')
await fx.setze(ab(1), '1')
assert.equal(await versuche(fx, panel), 0)
assert.match(fehlerText(fx, panel, 'staffeln'), /größer werden/)
await fx.setze(ab(1), '7')
await fx.setze(preis(1), '0,50')
assert.equal(await versuche(fx, panel), 0)
assert.match(fehlerText(fx, panel, 'staffeln'), /1,00 € und 1\.000,00 €/)
await fx.setze(preis(1), '1.000,01')
assert.equal(await versuche(fx, panel), 0)
await fx.setze(preis(1), '50')
for (let i = 0; i < 5; i += 1) { await fx.klick(aktion(fx, panel, 'staffel-dazu')); await fx.setze(ab(zeilen().length - 1), String(10 + i)); await fx.setze(preis(zeilen().length - 1), '40') }
assert.equal(zeilen().length, 7)
assert.equal(await versuche(fx, panel), 0)
assert.match(fehlerText(fx, panel, 'staffeln'), /1 bis 6/)
await fx.klick(fx.finde(zeilen()[6], '[data-aktion="staffel-weg"]'))
assert.equal(await versuche(fx, panel), 1)
}))
test('Preis-Editor: Rabatt, Frist, Personen, Stornobedingungen, USt', mit(ukServer({ 'PUT /api/unterkunft/unterkuenfte/u1/preise': () => ({ body: {} }) }), async (fx) => {
const panel = await preisEditor(fx)
const fall = async (sel, wert, ok, feldName, re) => {
const alt = fx.finde(panel, sel).value
await fx.setze(fx.finde(panel, sel), wert)
assert.equal(await versuche(fx, panel), 0, `${sel}=${wert}`)
assert.match(fehlerText(fx, panel, feldName), re)
await fx.setze(fx.finde(panel, sel), alt)
}
await fall('#bk-p-rabatt', '51', 0, 'direktrabattProzent', /0 bis 50/)
await fall('#bk-p-frist', '0', 0, 'fristTage', /1 bis 14/)
await fall('#bk-p-frist', '15', 0, 'fristTage', /1 bis 14/)
await fall('#bk-p-inklusive', '0', 0, 'personenInklusive', /ab 1/)
await fall('#bk-p-storno', 'zu kurz', 0, 'stornobedingungen', /20 bis 5000/)
await fall('#bk-p-ust-ue', '8', 0, 'ustUebernachtung', /0, 7 oder 19/)
await fall('#bk-p-ust-re', '20', 0, 'ustReinigung', /0, 7 oder 19/)
assert.equal(await versuche(fx, panel), 1)
}))
test('Preis-Editor: gespeicherte USt 19 bleibt bei Kleinunternehmer unverändert', mit(ukServer({
'GET /api/unterkunft/unterkuenfte/u1/preise': () => ({ body: { preise: { ...PREISE, steuerart: 'kleinunternehmer', ustUebernachtung: 19, ustReinigung: 0 } } }),
'PUT /api/unterkunft/unterkuenfte/u1/preise': () => ({ body: {} }),
}), async (fx) => {
const panel = await preisEditor(fx)
assert.equal(await versuche(fx, panel), 1)
const put = posts(fx).find((a) => a.url.includes('/preise'))
assert.equal(put.body.steuerart, 'kleinunternehmer')
assert.equal(put.body.ustUebernachtung, 19)
assert.equal(put.body.ustReinigung, 0)
}))
test('fehlerFuerFormular: deutsche Texte fuer die Fachcodes des Buchungskalenders', async () => {
const { fehlerFuerFormular } = await import('../public/buchungen-fehler.js')
const text = (fehler) => fehlerFuerFormular({ fehler, message: 'Fehler 409' }).message
assert.equal(text('zustimmung_fehlt'), 'Bitte erteile zuerst in den Einstellungen die Zustimmung zur Rechnungsstellung in deinem Namen.')
assert.equal(text('stripe_nicht_aktiv'), 'Online-Zahlung geht erst, wenn dein Stripe-Konto freigeschaltet ist.')
assert.match(text('erstattung_fehlgeschlagen'), /Erstattung ist fehlgeschlagen/)
assert.match(text('frist_abgelaufen'), /Frist .* abgelaufen/)
assert.equal(text('zeitraum_belegt'), 'Der Zeitraum ist bereits belegt.')
})
test('fehlerFuerFormular: unbekannter Code → deutscher Standardtext', async () => {
const { fehlerFuerFormular } = await import('../public/buchungen-fehler.js')
assert.equal(fehlerFuerFormular({ fehler: 'komischer_code', message: 'Fehler 400' }).fehler._, 'Das hat nicht geklappt. Bitte versuch es noch einmal.')
assert.equal(fehlerFuerFormular({ fehler: 'zeitraum_belegt', message: 'x' }).fehler._, 'Der Zeitraum ist bereits belegt.')
})
test('Kalender: vergangene Tage lassen sich nicht sperren', mit(ukServer(), async (fx) => {
const { panel } = await zeigeTab(fx, kal)
await fx.klick(tag(fx, panel, '2026-10-01'))
assert.match(fx.text(fx.finde(panel, '[data-hinweis]')), /Vergangene Tage/)
assert.equal(fx.finde(panel, '[data-aktion="sperren-senden"]'), null)
}))
/* ===================================================== Task 14: Rechnungen, Einstellungen, Anfrage ===================================================== */
const { render: renderRechnungen } = await import('../public/buchungen-tab-rechnungen.js')
const { STORNO_MUSTER, STORNO_HINWEIS } = await import('../public/buchungen-preise.js')
const { render: renderEinstellungen, AUFTRAG_TEXT, AUFTRAG_WIDERRUF } = await import('../public/buchungen-tab-einstellungen.js')
const { renderAnfrageFormular, anfrageKarte } = await import('../public/buchungen-anfrage.js')
const { initBuchungen, vergissAuswahl } = await import('../public/buchungen.js')
const zeigeMit = async (fx, modul, ctxExtra = {}) => {
const ctx = { ...baueCtx(), ...ctxExtra }
const panel = h('div')
fx.document.body.append(panel)
await modul(panel, ctx)
await fx.ruhe()
return { panel, ctx }
}
const ES = { firma: 'Haus am See', inhaber: 'Mimi M.', strasse: 'Seeweg 1', plz: '78532', ort: 'Tuttlingen', email: 'mimi@example.com', telefon: '', steuernummer: '12/345/67890', ustId: '', iban: '', bic: '', bank: '', rechnungsPraefix: 'RE', akzentfarbe: '#336699', onlineZahlung: false, rechnungAuftrag: true }
const esServer = (o = {}, antwort = {}) => server({ 'GET /api/unterkunft/einstellungen': () => ({ body: { einstellungen: ES, stripe: { status: 'keins', vorhandenesShopKonto: false }, rechnungenVorhanden: false, ...antwort } }), 'PUT /api/unterkunft/einstellungen': () => ({ body: {} }), ...o })
test('Rechnungen: Liste mit PDF-Link, Art und Betrag; Fremdtext nur als Text', mit(server({
'GET /api/unterkunft/rechnungen': () => ({ body: { rechnungen: [
{ id: 'r1', nummer: 'RE-2026-0001', datum: '2026-10-07', art: 'rechnung', gastName: 'Anna <b>M</b>', bruttoCent: 40650 },
{ id: 'r2', nummer: 'RE-2026-0002', datum: '2026-10-08', art: 'storno', gastName: 'Anna', bruttoCent: -40650 },
] } }),
}), async (fx) => {
const { panel } = await zeigeMit(fx, renderRechnungen)
assert.equal(fx.fetchLog[0].url, '/api/unterkunft/rechnungen?projectId=p1')
const z = fx.alle(panel, '[data-rechnung]')
assert.equal(z.length, 2)
assert.match(fx.text(z[0]), /RE-2026-0001/)
assert.match(fx.text(z[0]), /07\.10\.2026/)
assert.match(fx.text(z[0]), /Anna <b>M<\/b>/)
assert.match(fx.text(z[0]), /406,50\s€/)
assert.match(fx.text(z[1]), /Storno-Rechnung/)
assert.match(fx.text(z[1]), /−406,50\s€/)
assert.equal(fx.finde(z[0], '[data-aktion="pdf"]').getAttribute('href'), '/api/unterkunft/rechnungen/r1.pdf?projectId=p1')
assert.equal(fx.finde(z[0], 'b'), null)
}))
test('Rechnungen: leer → Hinweis', mit(server({ 'GET /api/unterkunft/rechnungen': () => ({ body: { rechnungen: [] } }) }), async (fx) => {
const { panel } = await zeigeMit(fx, renderRechnungen)
assert.match(fx.text(panel), /Noch keine Rechnungen/)
assert.equal(fx.alle(panel, '[data-rechnung]').length, 0)
}))
test('Einstellungen: Pflichtfelder markiert, leeres Formular sendet nicht und zeigt Feldfehler', mit(esServer({}, { einstellungen: { ...ES, firma: '', steuernummer: '', ustId: '' } }), async (fx) => {
const { panel } = await zeigeMit(fx, renderEinstellungen)
for (const n of ['firma', 'inhaber', 'strasse', 'plz', 'ort', 'email']) assert.match(fx.text(fx.finde(panel, `[data-feld="${n}"]`).parentNode.querySelector('label')), /\*/, n)
await fx.klick(fx.finde(panel, '[data-aktion="einstellungen-speichern"]'))
await fx.ruhe()
assert.equal(posts(fx).length, 0)
assert.match(fx.text(fx.finde(panel, '[data-fehler="firma"]')), /Firma/)
assert.match(fx.text(fx.finde(panel, '[data-fehler="steuernummer"]')), /Steuernummer oder USt-IdNr/)
assert.match(fx.text(fx.finde(panel, '[data-fehler="ustId"]')), /Steuernummer oder USt-IdNr/)
}))
test('Einstellungen: speichern sendet die Felder ohne Wrapper; Serverfehler am Feld', mit(esServer({ 'PUT /api/unterkunft/einstellungen': () => ({ status: 400, body: { fehler: 'ungueltig', felder: { iban: 'Bitte gib eine gültige IBAN an.' } } }) }), async (fx) => {
const { panel } = await zeigeMit(fx, renderEinstellungen)
await fx.setze(fx.finde(panel, '[data-feld="iban"]'), 'xx')
await fx.klick(fx.finde(panel, '[data-aktion="einstellungen-speichern"]'))
await fx.ruhe()
const put = posts(fx)[0]
assert.equal(put.url, '/api/unterkunft/einstellungen?projectId=p1')
assert.equal(put.body.firma, 'Haus am See')
assert.equal(put.body.rechnungsPraefix, 'RE')
assert.equal(put.body.rechnungAuftrag, true)
assert.equal(put.body.iban, 'xx')
assert.equal(put.body.onlineZahlung, false)
assert.match(fx.text(fx.finde(panel, '[data-fehler="iban"]')), /gültige IBAN/)
}))
test('Einstellungen: Präfix prüfen; gesperrt mit Hinweis, sobald Rechnungen existieren', async () => {
await mit(esServer(), async (fx) => {
const { panel } = await zeigeMit(fx, renderEinstellungen)
await fx.setze(fx.finde(panel, '[data-feld="rechnungsPraefix"]'), 'a')
await fx.klick(fx.finde(panel, '[data-aktion="einstellungen-speichern"]'))
await fx.ruhe()
assert.equal(posts(fx).length, 0)
assert.match(fx.text(fx.finde(panel, '[data-fehler="rechnungsPraefix"]')), /2 bis 8/)
await fx.setze(fx.finde(panel, '[data-feld="rechnungsPraefix"]'), 'ab1')
await fx.klick(fx.finde(panel, '[data-aktion="einstellungen-speichern"]'))
await fx.ruhe()
assert.equal(posts(fx)[0].body.rechnungsPraefix, 'AB1')
})()
await mit(esServer({}, { rechnungenVorhanden: true }), async (fx) => {
const { panel } = await zeigeMit(fx, renderEinstellungen)
const p = fx.finde(panel, '[data-feld="rechnungsPraefix"]')
assert.ok(p.disabled)
assert.match(fx.text(p.parentNode), /gesperrt/)
await fx.klick(fx.finde(panel, '[data-aktion="einstellungen-speichern"]'))
await fx.ruhe()
assert.equal(posts(fx)[0].body.rechnungsPraefix, 'RE')
})()
})
test('Einstellungen: keine Stornobedingungen mehr; IBAN maxlength 34', mit(esServer(), async (fx) => {
const { panel } = await zeigeMit(fx, renderEinstellungen)
assert.equal(fx.finde(panel, '[data-feld="stornobedingungen"]'), null)
assert.equal(fx.finde(panel, '[data-feld="iban"]').getAttribute('maxlength'), '34')
await fx.klick(fx.finde(panel, '[data-aktion="einstellungen-speichern"]'))
await fx.ruhe()
assert.equal('stornobedingungen' in posts(fx)[0].body, false)
}))
test('Preis-Editor: Mustertext übernehmen füllt nur das Feld (kein Speichern), Hinweis „keine Rechtsberatung“', mit(ukServer(), async (fx) => {
const panel = await preisEditor(fx)
const t = fx.finde(panel, '#bk-p-storno')
assert.match(fx.text(fx.finde(panel, '[data-hinweis="storno-muster"]')), /Vorlage – bitte an deine Bedingungen anpassen, keine Rechtsberatung\./)
assert.equal(STORNO_HINWEIS, 'Vorlage – bitte an deine Bedingungen anpassen, keine Rechtsberatung.')
const vorher = posts(fx).length
await fx.klick(fx.finde(panel, '[data-aktion="storno-muster"]'))
assert.equal(t.value, STORNO_MUSTER)
assert.match(STORNO_MUSTER, /^1\. Stornierung durch den Gast\./)
assert.match(STORNO_MUSTER, /Reiserücktrittsversicherung\.$/)
assert.ok(STORNO_MUSTER.length <= 5000, 'passt in das Feld (max. 5000 Zeichen)')
// wortgleich zur Doku (maßgeblich), ohne Markdown-Fettdruck
const doku = readFileSync(new URL('../docs/funktionen/unterkunft-storno-muster.md', import.meta.url), 'utf8').split('## Stornobedingungen (Muster)')[1].trim().replaceAll('**', '')
assert.equal(STORNO_MUSTER, doku)
assert.equal(posts(fx).length, vorher)
}))
test('Einstellungen: Zustimmung zurückziehen fragt nach; Abbrechen behält, Ja sendet rechnungAuftrag:false', mit(esServer(), async (fx) => {
const { panel } = await zeigeMit(fx, renderEinstellungen)
const box = fx.finde(panel, '[data-feld="rechnungAuftrag"]')
await fx.klick(box)
assert.ok(box.checked)
assert.match(fx.text(panel), new RegExp(AUFTRAG_WIDERRUF.slice(0, 50)))
await fx.klick(fx.finde(panel, '[data-aktion="widerruf-nein"]'))
assert.ok(box.checked)
assert.equal(fx.finde(panel, '[data-aktion="widerruf-ja"]'), null)
await fx.klick(box)
await fx.klick(fx.finde(panel, '[data-aktion="widerruf-ja"]'))
assert.equal(box.checked, false)
await fx.klick(fx.finde(panel, '[data-aktion="einstellungen-speichern"]'))
await fx.ruhe()
assert.equal(posts(fx)[0].body.rechnungAuftrag, false)
}))
test('Einstellungen: Auftrag zur Rechnungsstellung – ohne Haken Hinweis, mit Haken weg und rechnungAuftrag im PUT', mit(esServer({}, { einstellungen: { ...ES, rechnungAuftrag: false } }), async (fx) => {
const { panel } = await zeigeMit(fx, renderEinstellungen)
const box = fx.finde(panel, '[data-feld="rechnungAuftrag"]')
assert.match(fx.text(box.parentNode), new RegExp(AUFTRAG_TEXT.slice(0, 40)))
const hinweis = fx.finde(panel, '[data-hinweis="auftrag"]')
assert.equal(hinweis.hidden, false)
assert.match(fx.text(hinweis), /nicht bestätigen/)
await fx.klick(box)
assert.equal(hinweis.hidden, true)
await fx.klick(fx.finde(panel, '[data-aktion="einstellungen-speichern"]'))
await fx.ruhe()
assert.equal(posts(fx)[0].body.rechnungAuftrag, true)
}))
test('Einstellungen: Stripe nicht verbunden – Verbinden leitet nur auf https; „Vorhandenes Webshop-Konto“ nur wenn gemeldet', async () => {
await mit(esServer({ 'POST /api/unterkunft/stripe/verbinden': () => ({ body: { url: 'https://connect.stripe.com/x' } }) }), async (fx) => {
const { panel } = await zeigeMit(fx, renderEinstellungen)
assert.equal(fx.finde(panel, '[data-stripe-status]').getAttribute('data-stripe-status'), 'keins')
assert.equal(fx.finde(panel, '[data-aktion="stripe-vorhanden"]'), null)
assert.equal(fx.finde(panel, '[data-feld="onlineZahlung"]'), null)
await fx.klick(fx.finde(panel, '[data-aktion="stripe-verbinden"]'))
await fx.ruhe()
assert.equal(posts(fx)[0].url, '/api/unterkunft/stripe/verbinden?projectId=p1')
assert.equal(fx.window.location.href, 'https://connect.stripe.com/x')
})()
await mit(esServer({ 'POST /api/unterkunft/stripe/verbinden': () => ({ body: { url: 'javascript:alert(1)' } }) }), async (fx) => {
const { panel, ctx } = await zeigeMit(fx, renderEinstellungen)
await fx.klick(fx.finde(panel, '[data-aktion="stripe-verbinden"]'))
await fx.ruhe()
assert.notEqual(fx.window.location.href, 'javascript:alert(1)')
assert.equal(ctx.meldungen.at(-1).art, 'fehler')
})()
await mit(esServer({ 'POST /api/unterkunft/stripe/vorhandenes-konto': () => ({ body: { stripe: { status: 'aktiv' } } }) }, { stripe: { status: 'keins', vorhandenesShopKonto: true } }), async (fx) => {
const { panel } = await zeigeMit(fx, renderEinstellungen)
await fx.klick(fx.finde(panel, '[data-aktion="stripe-vorhanden"]'))
await fx.ruhe()
assert.equal(posts(fx)[0].url, '/api/unterkunft/stripe/vorhandenes-konto?projectId=p1')
assert.ok(fx.fetchLog.filter((a) => a.method === 'GET' && a.url.startsWith('/api/unterkunft/einstellungen')).length >= 2) // neu geladen
})()
})
test('Einstellungen: Stripe Onboarding offen und aktiv; Schalter Online-Zahlung nur bei aktiv', async () => {
await mit(esServer({ 'POST /api/unterkunft/stripe/aktualisieren': () => ({ body: { stripe: { status: 'aktiv' } } }) }, { stripe: { status: 'onboarding', vorhandenesShopKonto: true } }), async (fx) => {
const { panel } = await zeigeMit(fx, renderEinstellungen)
assert.equal(fx.finde(panel, '[data-stripe-status]').getAttribute('data-stripe-status'), 'onboarding')
assert.match(fx.text(panel), /Angaben bei Stripe/)
assert.ok(fx.finde(panel, '[data-aktion="stripe-aktualisieren"]'))
assert.equal(fx.finde(panel, '[data-aktion="stripe-vorhanden"]'), null)
assert.equal(fx.finde(panel, '[data-feld="onlineZahlung"]'), null)
})()
await mit(esServer({}, { einstellungen: { ...ES, onlineZahlung: true }, stripe: { status: 'aktiv', vorhandenesShopKonto: false } }), async (fx) => {
const { panel } = await zeigeMit(fx, renderEinstellungen)
assert.equal(fx.finde(panel, '[data-stripe-status]').getAttribute('data-stripe-status'), 'aktiv')
const schalter = fx.finde(panel, '[data-feld="onlineZahlung"]')
assert.ok(schalter.checked)
await fx.klick(schalter)
await fx.klick(fx.finde(panel, '[data-aktion="einstellungen-speichern"]'))
await fx.ruhe()
assert.equal(posts(fx)[0].body.onlineZahlung, false)
})()
})
test('Einstellungen: nur lesen sperrt Formular und Stripe-Knöpfe', mit(esServer(), async (fx) => {
const { panel } = await zeigeMit(fx, renderEinstellungen, { nurLesen: true })
assert.equal(fx.finde(panel, '[data-aktion="stripe-verbinden"]'), null)
assert.ok(fx.finde(panel, '[data-feld="firma"]').disabled)
assert.equal(fx.finde(panel, '[data-aktion="einstellungen-speichern"]'), null)
}))
test('Anfrageformular sendet { art, einheiten, portale, nachricht }; Feldfehler am Feld', async () => {
let antwort = { body: { anfrage: { status: 'offen' } } }
await mit((a) => (a.method === 'POST' ? antwort : { body: {} }), async (fx) => {
const wurzel = h('div')
fx.document.body.append(wurzel)
let danach = 0
renderAnfrageFormular(wurzel, { projectId: 'p1', nachSenden: async () => { danach += 1 } })
antwort = { status: 400, body: { fehler: 'ungueltig', felder: { art: 'Bitte wähle die Art deiner Unterkunft.' } } }
await fx.klick(fx.finde(wurzel, '[data-aktion="anfrage-senden"]'))
await fx.ruhe()
assert.match(fx.text(fx.finde(wurzel, '[data-fehler="art"]')), /Art deiner Unterkunft/)
assert.equal(danach, 0)
antwort = { body: { anfrage: { status: 'offen' } } }
await fx.setze(fx.finde(wurzel, '[data-feld="art"]'), 'ferienwohnung')
await fx.setze(fx.finde(wurzel, '[data-feld="einheiten"]'), '3')
await fx.klick(fx.finde(wurzel, '[data-feld="portale.airbnb"]'))
await fx.setze(fx.finde(wurzel, '[data-feld="nachricht"]'), ' Hallo ')
await fx.klick(fx.finde(wurzel, '[data-aktion="anfrage-senden"]'))
await fx.ruhe()
const p = posts(fx).at(-1)
assert.equal(p.url, '/api/unterkunft/anfrage-funktion?projectId=p1')
assert.deepEqual(p.body, { art: 'ferienwohnung', einheiten: 3, portale: ['airbnb'], nachricht: 'Hallo' })
assert.equal(danach, 1)
assert.match(fx.text(wurzel), /199 €/)
})()
})
test('Anfragekarte: offen mit Zurückziehen, abgelehnt mit Begründung als Text', async () => {
await mit(server(), async (fx) => {
let neuGeladen = 0
const k = anfrageKarte({ status: 'offen', at: '2026-10-07T10:00:00Z', anfrage: { art: 'monteur', einheiten: 2, portale: ['booking'], nachricht: 'x' } }, { projectId: 'p1', neuLaden: async () => { neuGeladen += 1 } })
fx.document.body.append(k)
assert.match(fx.text(k), /In Prüfung/)
assert.match(fx.text(k), /Monteurwohnung/)
assert.match(fx.text(k), /Booking\.com/)
await fx.klick(fx.finde(k, '[data-aktion="zurueckziehen"]'))
await fx.ruhe()
assert.equal(posts(fx)[0].url, '/api/unterkunft/anfrage-funktion/zurueckziehen?projectId=p1')
assert.equal(neuGeladen, 1)
const b = anfrageKarte({ status: 'abgelehnt', begruendung: 'Passt <i>nicht</i>' }, { projectId: 'p1', neu: () => {} })
assert.match(fx.text(b), /Passt <i>nicht<\/i>/)
assert.equal(fx.finde(b, 'i'), null)
assert.ok(fx.finde(b, '[data-aktion="neue-anfrage"]'))
})()
})
test('Anfrageformular sendet monteurzimmer; ein vorhandenes monteur wird als Monteurwohnung gewaehlt', async () => {
await mit(() => ({ body: { anfrage: { status: 'offen' } } }), async (fx) => {
const wurzel = h('div')
fx.document.body.append(wurzel)
renderAnfrageFormular(wurzel, { projectId: 'p1', vorgabe: { art: 'monteur', einheiten: 2, portale: [], nachricht: '' }, nachSenden: async () => {} })
assert.equal(fx.finde(wurzel, '[data-feld="art"]').value, 'monteurzimmer')
await fx.setze(fx.finde(wurzel, '[data-feld="einheiten"]'), '2')
await fx.klick(fx.finde(wurzel, '[data-aktion="anfrage-senden"]'))
await fx.ruhe()
assert.equal(posts(fx).at(-1).body.art, 'monteurzimmer')
})()
})
test('Zurückziehen 409: Stand wird mit der Meldung neu geladen', async () => {
await mit((a) => (a.url.includes('zurueckziehen') ? { status: 409, body: { fehler: 'status_ungueltig', error: 'Die Anfrage wurde schon bearbeitet.' } } : { body: {} }), async (fx) => {
const gesehen = []
const k = anfrageKarte({ status: 'offen', at: '2026-10-07T10:00:00Z', anfrage: { art: 'pension' } }, { projectId: 'p1', neuLaden: async (o) => { gesehen.push(o) } })
fx.document.body.append(k)
await fx.klick(fx.finde(k, '[data-aktion="zurueckziehen"]'))
await fx.ruhe()
assert.equal(gesehen.length, 1)
assert.equal(typeof gesehen[0]?.meldung, 'string')
assert.ok(gesehen[0].meldung.length > 0)
})()
})
test('Ansicht #buchungen: Zustand off zeigt das Anfrageformular, Projekt vom Wunsch-Attribut', async () => {
vergissAuswahl()
await mit((a) => {
if (a.url === '/api/funktionen') return { body: { unterkunft: { verfuegbar: true }, projects: [{ projectId: 'p1', projectName: 'A', features: { unterkunft: { state: 'off' } } }, { projectId: 'p2', projectName: 'B', features: { unterkunft: { state: 'off' } } }] } }
if (a.url.startsWith('/api/unterkunft/status')) return { body: { state: 'off' } }
return { body: {} }
}, async (fx) => {
const root = h('div')
root.dataset.wunschProjekt = 'p2'
fx.document.body.append(root)
await initBuchungen(root)
await fx.ruhe()
assert.equal(fx.fetchLog[1].url, '/api/unterkunft/status?projectId=p2')
assert.ok(fx.finde(root, '[data-aktion="anfrage-senden"]'))
assert.match(fx.text(root), /Buchungskalender anfragen/)
})()
vergissAuswahl()
})
test('Ansicht #buchungen: Zustand ordered zeigt „Zahlung offen“ mit Zahlung fortsetzen (Checkout unterkunft)', async () => {
vergissAuswahl()
await mit((a) => {
if (a.url === '/api/funktionen') return { body: { unterkunft: { verfuegbar: true }, projects: [{ projectId: 'p1', projectName: 'A', features: { unterkunft: { state: 'ordered' } } }] } }
if (a.url.startsWith('/api/unterkunft/status')) return { body: { state: 'ordered' } }
if (a.url === '/api/funktionen/unterkunft/checkout') return { status: 409, body: { error: 'Bitte stimme zu.', code: 'andere' } }
return { body: {} }
}, async (fx) => {
const root = h('div')
fx.document.body.append(root)
await initBuchungen(root)
await fx.ruhe()
assert.match(fx.text(root), /Zahlung offen/)
await fx.klick(fx.finde(root, '[data-aktion="fortsetzen"]'))
await fx.ruhe()
assert.ok(posts(fx).some((a) => a.url === '/api/funktionen/unterkunft/checkout'))
})()
vergissAuswahl()
})
test('Anfrageformular: 409 lädt die Ansicht neu, die Meldung bleibt sichtbar; Projektwechsel setzt „neue Anfrage“ zurück', async () => {
vergissAuswahl()
let status = { state: 'off', anfrage: { status: 'abgelehnt', begruendung: 'Nein' } }
await mit((a) => {
if (a.url === '/api/funktionen') return { body: { unterkunft: { verfuegbar: true }, projects: [{ projectId: 'p1', projectName: 'A', features: { unterkunft: { state: 'off' } } }, { projectId: 'p2', projectName: 'B', features: { unterkunft: { state: 'off' } } }] } }
if (a.url.startsWith('/api/unterkunft/status')) return { body: status }
if (a.method === 'POST') { status = { state: 'off', anfrage: { status: 'offen', anfrage: {} } }; return { status: 409, body: { fehler: 'anfrage_laeuft' } } }
return { body: {} }
}, async (fx) => {
const root = h('div')
fx.document.body.append(root)
await initBuchungen(root)
await fx.ruhe()
await fx.klick(fx.finde(root, '[data-aktion="neue-anfrage"]'))
await fx.klick(fx.finde(root, '[data-aktion="anfrage-senden"]'))
await fx.ruhe()
assert.ok(fx.finde(root, '[data-anfrage="offen"]'))
assert.match(fx.text(fx.finde(root, '.sh-meldung')), /nicht geklappt/)
status = { state: 'off', anfrage: { status: 'abgelehnt', begruendung: 'Nein' } }
await fx.setze(fx.finde(root, '#buchungen-project'), 'p2')
await fx.ruhe()
assert.ok(fx.finde(root, '[data-anfrage="abgelehnt"]'))
})()
vergissAuswahl()
})
test('CSS: ein ausgeblendetes .sh-box (hidden) bleibt unsichtbar (Hinweis zur Zustimmung in den Einstellungen)', () => {
const css = readFileSync(new URL('../public/shop.css', import.meta.url), 'utf8')
assert.match(css, /\.sh-box\[hidden\]\s*\{\s*display:\s*none/)
})
test('Preis-Editor: Stornobedingungen mit [Platzhalter] werden nicht gespeichert (Mustertext unveraendert nicht speicherbar)', mit(ukServer({ 'PUT /api/unterkunft/unterkuenfte/u1/preise': () => ({ body: {} }) }), async (fx) => {
const panel = await preisEditor(fx)
await fx.klick(aktion(fx, panel, 'storno-muster'))
assert.equal(await versuche(fx, panel), 0)
assert.equal(fehlerText(fx, panel, 'stornobedingungen'), 'Bitte ersetze die Platzhalter in eckigen Klammern durch deine Werte.')
await fx.setze(fx.finde(panel, '#bk-p-storno'), 'Bis 30 Tage vor Anreise kostenfrei, danach 50 Prozent.')
assert.equal(await versuche(fx, panel), 1)
}))
test('Posten-Editor: die Summenanzeige enthaelt bei Regelbesteuerung kein Text-"null"', () => {
const fx = installFakeDom({ antworten: [] })
try {
const e = baueEditor([{ text: 'Übernachtung', menge: 1, einzelCent: 10000, ustProzent: 7 }], 'regel')
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'), '')
}))
// ---- Preis-Editor: Preise je Personenzahl (staffeln[].personenCent), Haken #bk-p-personenmodus
const PERSONEN_PREISE = {
...PREISE,
staffeln: [
{ abNaechten: 1, preisProNachtCent: 5500, personenCent: [5500, 7000, 8200] },
{ abNaechten: 7, preisProNachtCent: 5000, personenCent: [5000, 6500, 7700] },
],
}
const PUT_PREISE = { 'PUT /api/unterkunft/unterkuenfte/u1/preise': () => ({ body: {} }) }
/** Server mit anderen Preisen und/oder anderer Gaestezahl der Unterkunft. */
const personenServer = (preise, maxPersonen = 3, o = {}) => ukServer({
'GET /api/unterkunft/unterkuenfte': () => ({ body: { unterkuenfte: [{ ...UK, maxPersonen }] } }),
'GET /api/unterkunft/unterkuenfte/u1/preise': () => ({ body: { preise } }),
...PUT_PREISE,
...o,
})
const zeilen = (fx, panel) => fx.alle(panel, '[data-staffel-zeile]')
const spalten = (fx, zeile) => fx.alle(zeile, '[data-staffel="person"]')
const putPreise = (fx) => posts(fx).filter((a) => a.url.includes('/preise')).at(-1)
const modusBox = (fx, panel) => fx.finde(panel, '#bk-p-personenmodus')
const sichtbar = (el) => { for (let e = el; e && e.hidden !== undefined; e = e.parentElement) if (e.hidden) return false; return true }
test('Preis-Editor Personen-Modus: gespeicherte personenCent laden (Haken an, k Felder je Staffel), Speichern sendet sie unverändert', mit(personenServer(PERSONEN_PREISE), async (fx) => {
const panel = await preisEditor(fx)
assert.equal(modusBox(fx, panel).checked, true)
assert.equal(zeilen(fx, panel).length, 2)
for (const [i, erwartet] of [['5500', '7000', '8200'], ['5000', '6500', '7700']].entries()) {
const felder = spalten(fx, zeilen(fx, panel)[i])
assert.equal(felder.length, 3)
assert.deepEqual(felder.map((f) => f.value), erwartet.map((c) => `${c.slice(0, -2)},${c.slice(-2)}`))
assert.deepEqual(felder.map((f) => f.dataset.personen), ['1', '2', '3'])
assert.ok(felder.every(sichtbar))
assert.equal(sichtbar(fx.finde(zeilen(fx, panel)[i], '[data-staffel="preis"]')), false, 'der eine Preis ist ausgeblendet')
}
const beschriftung = fx.text(zeilen(fx, panel)[0])
assert.match(beschriftung, /1 Person(?!en)/)
assert.match(beschriftung, /2 Personen/)
assert.match(beschriftung, /3 Personen/)
assert.equal(sichtbar(fx.finde(panel, '[data-hinweis="personenpreise"]')), true)
assert.equal(fx.text(fx.finde(panel, '[data-hinweis="personenpreise"]')), 'Preis pro Nacht für die ganze Unterkunft, je nach Anzahl der Gäste.')
assert.equal(sichtbar(fx.finde(panel, '#bk-p-inklusive')), false)
assert.equal(sichtbar(fx.finde(panel, '#bk-p-aufpreis')), false)
assert.equal(await versuche(fx, panel), 1)
const put = putPreise(fx)
assert.deepEqual(put.body.staffeln, PERSONEN_PREISE.staffeln)
assert.equal(put.body.personenInklusive, 2, 'ausgeblendete Felder gehen unverändert mit')
assert.equal(put.body.aufpreisPersonNachtCent, 1000)
assert.equal(put.body.reinigungCent, 6000)
}))
test('Preis-Editor Personen-Modus: preisProNachtCent im Body ist personenCent[0]; Felder lassen sich ändern', mit(personenServer(PERSONEN_PREISE), async (fx) => {
const panel = await preisEditor(fx)
await fx.setze(spalten(fx, zeilen(fx, panel)[1])[0], '48,50')
await fx.setze(spalten(fx, zeilen(fx, panel)[1])[2], '75')
assert.equal(await versuche(fx, panel), 1)
assert.deepEqual(putPreise(fx).body.staffeln[1], { abNaechten: 7, preisProNachtCent: 4850, personenCent: [4850, 6500, 7500] })
}))
test('Preis-Editor Personen-Modus: Haken schaltet vom alten Modell um (k Felder aus maxPersonen, Hilfsfelder weg) und zurück', mit(personenServer(PREISE, 4), async (fx) => {
const panel = await preisEditor(fx)
const box = modusBox(fx, panel)
assert.equal(box.checked, false)
assert.equal(sichtbar(box), true)
assert.equal(sichtbar(fx.finde(panel, '#bk-p-inklusive')), true)
assert.equal(sichtbar(fx.finde(panel, '#bk-p-aufpreis')), true)
assert.equal(sichtbar(fx.finde(panel, '[data-hinweis="personenpreise"]')), false)
assert.ok(zeilen(fx, panel).every((z) => sichtbar(fx.finde(z, '[data-staffel="preis"]')) && spalten(fx, z).every((f) => !sichtbar(f))))
await fx.klick(box)
assert.equal(box.checked, true)
for (const z of zeilen(fx, panel)) {
assert.equal(spalten(fx, z).length, 4, 'k = maxPersonen')
assert.ok(spalten(fx, z).every((f) => sichtbar(f) && f.value === ''), 'fehlende Werte bleiben leer')
assert.equal(sichtbar(fx.finde(z, '[data-staffel="preis"]')), false)
}
assert.equal(sichtbar(fx.finde(panel, '#bk-p-inklusive')), false)
assert.equal(sichtbar(fx.finde(panel, '#bk-p-aufpreis')), false)
assert.equal(sichtbar(fx.finde(panel, '[data-hinweis="personenpreise"]')), true)
// neue Staffel hat die Felder ebenfalls
await fx.klick(aktion(fx, panel, 'staffel-dazu'))
assert.equal(spalten(fx, zeilen(fx, panel)[2]).length, 4)
assert.ok(spalten(fx, zeilen(fx, panel)[2]).every(sichtbar))
await fx.klick(fx.finde(zeilen(fx, panel)[2], '[data-aktion="staffel-weg"]'))
// zurück: alter Modus wie bisher, der eine Preis blieb erhalten
await fx.klick(box)
assert.equal(box.checked, false)
assert.equal(fx.finde(zeilen(fx, panel)[0], '[data-staffel="preis"]').value, '55,00')
assert.equal(sichtbar(fx.finde(panel, '#bk-p-inklusive')), true)
assert.equal(await versuche(fx, panel), 1)
assert.deepEqual(putPreise(fx).body.staffeln, PREISE.staffeln)
assert.ok(putPreise(fx).body.staffeln.every((s) => !('personenCent' in s)))
}))
test('Preis-Editor Personen-Modus: Umschalten, alle Felder füllen, Speichern', mit(personenServer(PREISE, 3), async (fx) => {
const panel = await preisEditor(fx)
await fx.klick(modusBox(fx, panel))
assert.equal(await versuche(fx, panel), 0, 'leere Felder werden nicht gesendet')
assert.match(fehlerText(fx, panel, 'staffeln'), /für jede Personenzahl einen Preis/)
const werte = [['55', '70', '82'], ['50', '65', '77']]
for (const [i, z] of zeilen(fx, panel).entries()) for (const [j, f] of spalten(fx, z).entries()) await fx.setze(f, werte[i][j])
assert.equal(await versuche(fx, panel), 1)
assert.deepEqual(putPreise(fx).body.staffeln, [
{ abNaechten: 1, preisProNachtCent: 5500, personenCent: [5500, 7000, 8200] },
{ abNaechten: 7, preisProNachtCent: 5000, personenCent: [5000, 6500, 7700] },
])
assert.equal(putPreise(fx).body.personenInklusive, 2)
assert.equal(putPreise(fx).body.aufpreisPersonNachtCent, 1000)
}))
test('Preis-Editor Personen-Modus: Client prüft Preisgrenzen und Staffeln wie der Server', mit(personenServer(PERSONEN_PREISE), async (fx) => {
const panel = await preisEditor(fx)
const zeile = (i) => zeilen(fx, panel)[i]
await fx.setze(spalten(fx, zeile(0))[1], '0,50')
assert.equal(await versuche(fx, panel), 0)
assert.match(fehlerText(fx, panel, 'staffeln'), /1,00 € und 1\.000,00 €/)
await fx.setze(spalten(fx, zeile(0))[1], '1.000,01')
assert.equal(await versuche(fx, panel), 0)
await fx.setze(spalten(fx, zeile(0))[1], 'abc')
assert.equal(await versuche(fx, panel), 0)
assert.match(fehlerText(fx, panel, 'staffeln'), /für jede Personenzahl einen Preis/)
await fx.setze(spalten(fx, zeile(0))[1], '70')
await fx.setze(fx.finde(zeile(1), '[data-staffel="ab"]'), '1')
assert.equal(await versuche(fx, panel), 0)
assert.match(fehlerText(fx, panel, 'staffeln'), /größer werden/)
await fx.setze(fx.finde(zeile(1), '[data-staffel="ab"]'), '7')
assert.equal(await versuche(fx, panel), 1)
}))
test('Preis-Editor Personen-Modus: weniger gespeicherte Werte als Felder bleiben leer, überzählige werden verworfen', async () => {
const gespeichert = { ...PERSONEN_PREISE, staffeln: [{ abNaechten: 1, preisProNachtCent: 5500, personenCent: [5500, 7000, 8200] }] }
// Unterkunft jetzt für 4 Gäste: vierte Spalte leer
await mit(personenServer(gespeichert, 4), async (fx) => {
const panel = await preisEditor(fx)
assert.deepEqual(spalten(fx, zeilen(fx, panel)[0]).map((f) => f.value), ['55,00', '70,00', '82,00', ''])
assert.equal(await versuche(fx, panel), 0)
assert.match(fehlerText(fx, panel, 'staffeln'), /für jede Personenzahl einen Preis/)
})()
// Unterkunft jetzt für 2 Gäste: dritter Wert fällt weg
await mit(personenServer(gespeichert, 2), async (fx) => {
const panel = await preisEditor(fx)
assert.deepEqual(spalten(fx, zeilen(fx, panel)[0]).map((f) => f.value), ['55,00', '70,00'])
assert.equal(await versuche(fx, panel), 1)
assert.deepEqual(putPreise(fx).body.staffeln, [{ abNaechten: 1, preisProNachtCent: 5500, personenCent: [5500, 7000] }])
})()
// mehr als 10 Gäste: höchstens 10 Felder
await mit(personenServer(gespeichert, 15), async (fx) => {
const panel = await preisEditor(fx)
assert.equal(spalten(fx, zeilen(fx, panel)[0]).length, 10)
assert.match(fx.text(zeilen(fx, panel)[0]), /10 Personen/)
})()
})
test('Preis-Editor Personen-Modus: bei einer Person Höchstzahl kein Haken, altes Modell, personenCent fällt weg', mit(personenServer(PERSONEN_PREISE, 1), async (fx) => {
const panel = await preisEditor(fx)
const box = modusBox(fx, panel)
assert.equal(box.checked, false)
assert.equal(sichtbar(box), false)
assert.ok(zeilen(fx, panel).every((z) => spalten(fx, z).length === 0 && sichtbar(fx.finde(z, '[data-staffel="preis"]'))))
assert.equal(sichtbar(fx.finde(panel, '#bk-p-inklusive')), true)
assert.equal(await versuche(fx, panel), 1)
assert.deepEqual(putPreise(fx).body.staffeln, [{ abNaechten: 1, preisProNachtCent: 5500 }, { abNaechten: 7, preisProNachtCent: 5000 }])
}))
test('Preis-Editor Personen-Modus: nur lesen sperrt Haken und alle Felder', async () => {
await mit(personenServer(PERSONEN_PREISE), async (fx) => {
const { panel } = await zeigeMit(fx, ukTab.render, { nurLesen: true })
await fx.klick(aktion(fx, panel, 'bearbeiten'))
await fx.ruhe()
assert.ok(modusBox(fx, panel).disabled)
assert.equal(spalten(fx, zeilen(fx, panel)[0]).length, 3)
assert.ok(fx.alle(panel, '.bk-preisform input, .bk-preisform select, .bk-preisform textarea, .bk-preisform button').every((el) => el.disabled), 'alles gesperrt')
assert.equal(aktion(fx, panel, 'preise-speichern'), null)
// Klick auf den gesperrten Haken ändert nichts
await fx.klick(modusBox(fx, panel))
assert.equal(modusBox(fx, panel).checked, true)
})()
// auch im alten Modell sperrt nur lesen den Haken
await mit(personenServer(PREISE, 3), async (fx) => {
const { panel } = await zeigeMit(fx, ukTab.render, { nurLesen: true })
await fx.klick(aktion(fx, panel, 'bearbeiten'))
await fx.ruhe()
assert.ok(modusBox(fx, panel).disabled)
await fx.klick(modusBox(fx, panel))
assert.equal(modusBox(fx, panel).checked, false)
assert.ok(zeilen(fx, panel).every((z) => spalten(fx, z).every((f) => f.disabled)))
})()
})
test('Preis-Editor Personen-Modus: unlesbare Reste in den ausgeblendeten Feldern blockieren das Speichern nicht', mit(personenServer(PERSONEN_PREISE), async (fx) => {
const panel = await preisEditor(fx)
await fx.setze(fx.finde(panel, '#bk-p-inklusive'), 'xyz')
await fx.setze(fx.finde(panel, '#bk-p-aufpreis'), 'abc')
assert.equal(await versuche(fx, panel), 1)
assert.equal(putPreise(fx).body.personenInklusive, 2)
assert.equal(putPreise(fx).body.aufpreisPersonNachtCent, 1000)
}))
test('CSS: Felder je Personenzahl brechen um, ausgeblendete Teile bleiben unsichtbar', () => {
const css = readFileSync(new URL('../public/buchungen.css', import.meta.url), 'utf8')
assert.match(css, /\.bk-personenpreise\s*\{[^}]*flex-wrap:\s*wrap/)
assert.match(css, /\.bk-personenpreise\[hidden\][^{]*\{\s*display:\s*none/)
assert.match(css, /\.bk-preis-einzeln\[hidden\]/)
})