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() } }) test('Monatsnavigation: erster Monat hat aktiven Weiter-Knopf, Fokus bleibt, Tab-Stop im gezeigten Monat, Abreisetag-Label', async () => { const { fx, el } = start([{ status: 200, body: CFG }, { status: 200, body: BEL }]) try { await fx.ruhe(); await fx.ruhe() const erster = fx.finde(el, '.wku-monat') const weiter = fx.finde(erster, '[data-nav="vor1"]') assert.ok(weiter && !weiter.hasAttribute('disabled')) await fx.klick(weiter) assert.match(fx.text(el), /Dezember 2026/) assert.equal(fx.document.activeElement.getAttribute('data-nav'), 'vor1') assert.equal(fx.finde(el, '[data-tag][tabindex="0"]').getAttribute('data-tag'), '2026-12-01') await fx.klick(fx.finde(el, '[data-nav="zur"]')) assert.equal(fx.document.activeElement.getAttribute('data-nav'), 'vor1') // Zurueck ist im ersten Monat gesperrt, Fokus wechselt zum Weiter-Knopf assert.equal(fx.finde(el, '[data-tag][tabindex="0"]').getAttribute('data-tag'), '2026-11-01') await fx.klick(tag(fx, el, '2026-11-18')) assert.match(tag(fx, el, '2026-11-20').getAttribute('aria-label'), /belegt – als Abreisetag möglich/) assert.doesNotMatch(tag(fx, el, '2026-11-22').getAttribute('aria-label'), /Abreisetag möglich/) } finally { fx.restore() } }) test('Serverfehler: felder.zeitraum, neu_laden und mindestnaechte erscheinen mit eigenem Text', async () => { const faelle = [ [{ status: 400, body: { fehler: 'ungueltig', felder: { zeitraum: 'Ungültiger Zeitraum.' } } }, /Ungültiger Zeitraum\./], [{ status: 400, body: { fehler: 'neu_laden' } }, /zu lange geöffnet/], [{ status: 400, body: { fehler: 'mindestnaechte', felder: { mindestNaechte: 3 } } }, /Mindestaufenthalt: 3 Nächte\./], ] for (const [antwort, muster] of faelle) { const { fx, el } = start([{ status: 200, body: CFG }, { status: 200, body: BEL }, antwort, { status: 200, body: CFG }]) try { await fx.ruhe(); await fx.ruhe() await fx.setze(fx.finde(el, '[name="name"]'), 'Max'); await fx.setze(fx.finde(el, '[name="email"]'), 'max@example.com') await fx.klick(tag(fx, el, '2026-11-10')); await fx.klick(tag(fx, el, '2026-11-14')) await fx.klick(fx.finde(el, 'button[type="submit"]')) assert.match(fx.text(el), muster) } finally { fx.restore() } } })