Files
Webklar-Kundenbereich/test/buchungenUi.test.js
T

465 lines
26 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 { 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 } 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('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')
const tag = (fx, panel, t) => fx.finde(panel, `[data-tag="${t}"]`)
const gehZuNovember = async (fx, panel) => { while (!/November 2026/.test(fx.text(fx.finde(panel, '[data-monat]')))) { await fx.klick(aktion(fx, panel, 'weiter')); await fx.ruhe() } }
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: 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)
}))