Buchungskalender: Website-Widget mit Kalender und Anfrageformular

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-10-07 14:06:53 +00:00
1 parent d7b5b98187
commit 9b509fefbc
5 files changed
+688

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync, statSync } from 'node:fs'
import vm from 'node:vm'
// Statische Pruefung des Einbau-Skripts public/embed/unterkunft.js (Task 16; Muster test/shopEmbedStatic.test.js).
const QUELLE = readFileSync(new URL('../public/embed/unterkunft.js', import.meta.url), 'utf8')
const OHNE_KOMMENTARE = QUELLE.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|\s)\/\/.*$/gm, '$1')
test('gueltiges JavaScript, unter 40 KB, IIFE mit use strict', () => {
assert.doesNotThrow(() => new vm.Script(QUELLE, { filename: 'unterkunft.js' }))
assert.ok(statSync(new URL('../public/embed/unterkunft.js', import.meta.url)).size < 40 * 1024)
assert.match(OHNE_KOMMENTARE, /^\s*\(function \(\) \{\s*'use strict'/)
})
test('kein innerHTML, outerHTML, insertAdjacentHTML, document.write, eval, new Function', () => {
assert.doesNotMatch(OHNE_KOMMENTARE, /innerHTML|outerHTML|insertAdjacentHTML|document\.write|\beval\s*\(|new Function/)
})
test('API-Pfad, Praefix .wku-, @layer wku, Style-Id, kein Storage, keine Cookies, keine Fremdadressen', () => {
assert.ok(OHNE_KOMMENTARE.includes('/api/public/unterkunft/v1/'))
assert.ok(OHNE_KOMMENTARE.includes('@layer wku'))
assert.ok(OHNE_KOMMENTARE.includes('wku-style'))
assert.match(OHNE_KOMMENTARE, /\.wku-root/)
assert.doesNotMatch(OHNE_KOMMENTARE, /localStorage|sessionStorage|document\.cookie|https?:\/\//)
})
test('Pflichttexte und Honeypot', () => {
for (const t of ['Anfragen gerne per Telefon oder E-Mail.', 'Bitte wählen Sie Anreise und Abreise im Kalender.', 'Anfrage senden', 'kontakt_url', 'formToken']) assert.ok(QUELLE.includes(t), t)
assert.match(QUELLE, /Danke! Ihre Anfrage/)
})
+88
View File
@@ -0,0 +1,88 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import vm from 'node:vm'
// Reine Kalenderlogik des Widgets public/embed/unterkunft.js (Task 16): Datei per node:vm laden, data-test="1" macht WKU_LOGIK als __wkuLogik sichtbar.
const QUELLE = readFileSync(new URL('../public/embed/unterkunft.js', import.meta.url), 'utf8')
function lade() {
const doc = { readyState: 'complete', currentScript: { dataset: { test: '1' } }, querySelectorAll: () => [], addEventListener() {} }
const ctx = vm.createContext({ document: doc, console })
vm.runInContext(QUELLE, ctx, { filename: 'unterkunft.js' })
return ctx.__wkuLogik
}
const L = lade()
test('ohne data-test bleibt die Logik verborgen', () => {
const doc = { readyState: 'complete', currentScript: { dataset: {} }, querySelectorAll: () => [], addEventListener() {} }
const ctx = vm.createContext({ document: doc })
vm.runInContext(QUELLE, ctx)
assert.equal(ctx.__wkuLogik, undefined)
})
test('monatsRaster: Montag zuerst, 1.11.2026 (Sonntag) hat 6 Leerfelder, 30 Tage', () => {
const r = L.monatsRaster(2026, 11)
assert.equal(r.length % 7, 0)
assert.ok(r.slice(0, 6).every((z) => z.tag === null))
assert.equal(r[6].tag, '2026-11-01')
assert.equal(r.filter((z) => z.tag).length, 30)
assert.equal(L.monatsRaster(2026, 2).filter((z) => z.tag).length, 28)
assert.equal(L.monatsRaster(2028, 2).filter((z) => z.tag).length, 29)
assert.equal(L.monatsRaster(2026, 12)[1].tag, '2026-12-01') // 1.12.2026 ist Dienstag
})
test('nachtBelegt: Nacht von von (inklusive) bis bis (exklusive)', () => {
const b = [{ von: '2026-11-10', bis: '2026-11-12' }]
assert.equal(L.nachtBelegt('2026-11-09', b), false)
assert.equal(L.nachtBelegt('2026-11-10', b), true)
assert.equal(L.nachtBelegt('2026-11-11', b), true)
assert.equal(L.nachtBelegt('2026-11-12', b), false)
})
test('waehle: Anreise, Abreise, Neustart', () => {
const b = [{ von: '2026-11-20', bis: '2026-11-25' }]
let z = L.waehle({}, '2026-11-12', b, 1)
assert.deepEqual({ von: z.von, bis: z.bis }, { von: '2026-11-12', bis: null })
z = L.waehle(z, '2026-11-15', b, 1)
assert.deepEqual({ von: z.von, bis: z.bis }, { von: '2026-11-12', bis: '2026-11-15' })
z = L.waehle(z, '2026-11-14', b, 1) // dritter Klick = neue Anreise
assert.deepEqual({ von: z.von, bis: z.bis }, { von: '2026-11-14', bis: null })
z = L.waehle(z, '2026-11-10', b, 1) // Klick vor Anreise
assert.deepEqual({ von: z.von, bis: z.bis }, { von: '2026-11-10', bis: null })
})
test('waehle: belegte Nacht dazwischen -> neue Anreise; Abreise am Anreisetag eines Fremdbuchers erlaubt', () => {
const b = [{ von: '2026-11-20', bis: '2026-11-25' }]
let z = L.waehle({ von: '2026-11-18', bis: null }, '2026-11-27', b, 1)
assert.deepEqual({ von: z.von, bis: z.bis }, { von: '2026-11-27', bis: null })
z = L.waehle({ von: '2026-11-18', bis: null }, '2026-11-20', b, 1) // Nacht 18., 19. frei, Abreise am 20.
assert.deepEqual({ von: z.von, bis: z.bis }, { von: '2026-11-18', bis: '2026-11-20' })
z = L.waehle({ von: '2026-11-18', bis: null }, '2026-11-21', b, 1)
assert.deepEqual({ von: z.von, bis: z.bis }, { von: '2026-11-18', bis: null }) // Nacht 20. belegt, 21. selbst belegt: weder Abreise noch Anreise, Zustand bleibt
})
test('waehle: Anreise auf belegter Nacht nicht moeglich; Anreise am Abreisetag eines Fremdbuchers moeglich', () => {
const b = [{ von: '2026-11-20', bis: '2026-11-25' }]
const z1 = L.waehle({}, '2026-11-22', b, 1)
assert.equal(z1.von, null)
const z2 = L.waehle({}, '2026-11-25', b, 1)
assert.equal(z2.von, '2026-11-25')
})
test('waehle: unter den Mindestnaechten -> Hinweis-Zustand, Anreise bleibt', () => {
const z = L.waehle({ von: '2026-11-12', bis: null }, '2026-11-13', [], 3)
assert.equal(z.fehler, 'mindestnaechte')
assert.equal(z.von, '2026-11-12')
assert.equal(z.bis, null)
const ok = L.waehle({ von: '2026-11-12', bis: null }, '2026-11-15', [], 3)
assert.equal(ok.bis, '2026-11-15')
assert.equal(ok.fehler, undefined)
})
test('zusammenfassung', () => {
assert.equal(L.zusammenfassung('2026-11-12', '2026-11-30'), '12.–30. Nov. · 18 Nächte')
assert.equal(L.zusammenfassung('2026-11-28', '2026-12-03'), '28. Nov. – 3. Dez. · 5 Nächte')
assert.equal(L.zusammenfassung('2026-11-12', '2026-11-13'), '12.–13. Nov. · 1 Nacht')
assert.equal(L.zusammenfassung('2026-12-30', '2027-01-02'), '30. Dez. 2026 – 2. Jan. 2027 · 3 Nächte')
assert.equal(L.zusammenfassung('2026-03-01', '2026-03-05').slice(0, 18), '1.–5. März · 4 Näc')
})
+72
View File
@@ -0,0 +1,72 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import vm from 'node:vm'
import { installFakeDom } from './support/fakeDom.js'
// Ablauf des Widgets public/embed/unterkunft.js gegen den Vertrag docs/funktionen/unterkunft-api.md (Fake-fetch, Minimal-DOM).
const QUELLE = readFileSync(new URL('../public/embed/unterkunft.js', import.meta.url), 'utf8')
const CFG = { aktiv: true, anbieter: { name: 'Haus Test', ort: 'X', datenschutzUrl: 'https://kunde.example/datenschutz' }, akzentfarbe: '#0f766e', formToken: 'tok1', unterkuenfte: [{ id: 'u1', name: 'Ferienwohnung', maxPersonen: 3, mindestNaechte: 2, checkIn: '15:00', checkOut: '11:00' }] }
const BEL = { belegt: [{ von: '2026-11-20', bis: '2026-11-25' }], heute: '2026-11-01', bisMax: '2028-04-30' }
function start(antworten) {
const fx = installFakeDom({ antworten })
fx.document.readyState = 'complete'
fx.document.currentScript = { dataset: {}, src: 'https://project.webklar.com/embed/unterkunft.js' }
const el = fx.document.createElement('div')
el.setAttribute('data-webklar-unterkunft', 'abcdefghijklmnop')
fx.document.body.appendChild(el)
vm.runInThisContext(QUELLE, { filename: 'unterkunft.js' })
return { fx, el }
}
const tag = (fx, el, t) => fx.finde(el, `[data-tag="${t}"]`)
test('Ladefehler und aktiv:false zeigen nur den Ausweichsatz', async () => {
for (const antwort of [{ status: 200, body: { aktiv: false } }, { status: 500, body: {} }]) {
const { fx, el } = start([antwort])
try {
await fx.ruhe()
assert.equal(fx.text(el), 'Anfragen gerne per Telefon oder E-Mail.')
} finally { fx.restore() }
}
})
test('Kalender, Auswahl, Mindestnaechte, Absenden mit Vertragsfeldern, Danke-Text', async () => {
const { fx, el } = start([{ status: 200, body: CFG }, { status: 200, body: BEL }, { status: 201, body: { nummer: 'A-1234' } }, { status: 200, body: BEL }])
try {
await fx.ruhe(); await fx.ruhe()
assert.match(fx.fetchLog[0].url, /^https:\/\/project\.webklar\.com\/api\/public\/unterkunft\/v1\/abcdefghijklmnop$/)
assert.match(fx.fetchLog[1].url, /\/belegung\?unterkunft=u1&von=\d{4}-\d{2}-\d{2}&bis=\d{4}-\d{2}-\d{2}$/)
assert.equal(tag(fx, el, '2026-11-10').getAttribute('aria-label'), 'Dienstag, 10. November 2026, frei')
assert.equal(tag(fx, el, '2026-11-21').getAttribute('aria-label').includes('belegt'), true)
assert.ok(fx.finde(el, '[name="kontakt_url"]'), 'Honeypot')
// ohne Zeitraum
await fx.setze(fx.finde(el, '[name="name"]'), 'Max'); await fx.setze(fx.finde(el, '[name="email"]'), 'max@example.com')
await fx.klick(fx.finde(el, 'button[type="submit"]'))
assert.match(fx.text(el), /Bitte wählen Sie Anreise und Abreise im Kalender\./)
assert.equal(fx.fetchLog.length, 2)
// Mindestnaechte (2): 1 Nacht -> Hinweis
await fx.klick(tag(fx, el, '2026-11-10')); await fx.klick(tag(fx, el, '2026-11-11'))
assert.match(fx.text(el), /Mindestaufenthalt: 2 Nächte/)
await fx.klick(tag(fx, el, '2026-11-14'))
assert.match(fx.text(el), /10\.–14\. Nov\. · 4 Nächte/)
await fx.klick(fx.finde(el, 'button[type="submit"]'))
const post = fx.fetchLog[2]
assert.equal(post.method, 'POST')
assert.match(post.url, /\/anfrage$/)
assert.deepEqual(post.body, { unterkunft: 'u1', von: '2026-11-10', bis: '2026-11-14', personen: 1, name: 'Max', firma: '', email: 'max@example.com', telefon: '', nachricht: '', formToken: 'tok1', kontakt_url: '' })
assert.match(fx.text(el), /Danke! Ihre Anfrage A-1234 ist eingegangen\. Sie erhalten gleich eine Bestätigung per E-Mail\./)
} finally { fx.restore() }
})
test('Pfeiltasten bewegen den Fokus', async () => {
const { fx, el } = start([{ status: 200, body: CFG }, { status: 200, body: BEL }])
try {
await fx.ruhe(); await fx.ruhe()
await fx.klick(tag(fx, el, '2026-11-10'))
await fx.sende(tag(fx, el, '2026-11-10'), 'keydown', { key: 'ArrowRight' })
assert.equal(fx.document.activeElement.getAttribute('data-tag'), '2026-11-11')
await fx.sende(fx.document.activeElement, 'keydown', { key: 'ArrowDown' })
assert.equal(fx.document.activeElement.getAttribute('data-tag'), '2026-11-18')
} finally { fx.restore() }
})