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>
1097 lines
61 KiB
JavaScript
1097 lines
61 KiB
JavaScript
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\]/)
|
||
})
|