Terminplaner TP2: Reiter-Exporte und wochenplanFeld fuer den Fragebogen

- termine-tab-einstellungen.js: die acht Abschnitte (betrieb, darstellung, regeln,
  gastFelder, erinnerung, storno, rechtliches, zahlung) sind Exporte; betrieb nimmt
  { nur, titel, hinweis } (nur die gewaehlten Felder, lese schreibt nur diese)
- termine-tab-team.js: wochenplanFeld(r, pfad) => { el, lese() => { plan } | { fehler } },
  der Editor einer Person benutzt es selbst
- termine-speichern.js: fehlt kommt aus der Antwort des Servers
- termine.js: baueCtx aus zeigeReiter herausgeloest (Verhalten gleich)
- test/support/fakeDom.js: Minimal-DOM und Fake-fetch fuer die Client-Tests

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 21:06:42 +00:00
1 parent 0090332c70
commit 747be432c5
7 files changed
+1032 -59

No files matched your search

+359
View File
@@ -8,6 +8,14 @@ import {
import { BRANCHEN, SCHRITTE, SCHRITT_TITEL, leerFragebogen } from '../server/services/termine/fragebogen.js'
import { normalisiereEinstellung } from '../server/services/termine/einstellungen.js'
import { ausVorlage } from '../server/services/termine/vorlagen.js'
import { installFakeDom } from './support/fakeDom.js'
// Namensraeume statt benannter Importe: fehlt ein Export, scheitert nur der Test, der ihn braucht (nicht die ganze Datei)
import * as einstellungenTab from '../public/termine-tab-einstellungen.js'
import * as teamTab from '../public/termine-tab-team.js'
import * as huelle from '../public/termine.js'
import { FENSTER_FEHLER } from '../public/termine-view.js'
import { h, request } from '../public/termine-dom.js'
import { neuerSpeicher } from '../public/termine-speichern.js'
// Reine Helfer des Fragebogens in public/termine-view.js (TP2, Task C7a): Schrittstand, Namen der Personen/Plaetze, sofort/Anfrage fuer alle
// und die Zusammenfassung auf der Bestellseite. Kein DOM; der Spiegel zum Server (SCHRITTE) steht in test/termineView.test.js.
@@ -478,3 +486,354 @@ test('termine-view.js bleibt rein: kein DOM, kein Netz, nur der Import der Anred
assert.doesNotMatch(quelle, /\b(document|window|navigator|fetch|XMLHttpRequest|localStorage|sessionStorage)\b/)
assert.deepEqual(quelle.match(/^import .*$/gm), ["import { ANREDE_WAHL } from './termine-gaestetexte.js'"])
})
// ------------------------------------------------------- Fake-DOM (Testhilfe, TP2 C7b)
// test/support/fakeDom.js tragen auch C8, C9a, C9b und C10b: die Tests hier halten fest, worauf sie sich verlassen.
/** Fake-DOM fuer die Dauer von fn aufbauen (und immer abbauen). */
const mitDom = (optionen, fn) => async () => {
const fx = installFakeDom(optionen)
try {
await fn(fx)
} finally {
fx.restore()
}
}
/** Namen (data-feld) aller Formularfelder im Baum, in Dokumentreihenfolge. */
const felderVon = (fx, baum) => fx.alle(baum, (n) => typeof n.dataset.feld === 'string').map((n) => n.dataset.feld)
test('Fake-DOM: globale Objekte nur zwischen installFakeDom und restore', () => {
const vorher = { document: globalThis.document, fetch: globalThis.fetch, CSS: globalThis.CSS, window: globalThis.window }
const fx = installFakeDom()
assert.equal(globalThis.document, fx.document)
assert.notEqual(globalThis.fetch, vorher.fetch)
assert.equal(globalThis.CSS.escape('leistungen.2.dauer'), 'leistungen\\.2\\.dauer')
fx.restore()
assert.deepEqual({ document: globalThis.document, fetch: globalThis.fetch, CSS: globalThis.CSS, window: globalThis.window }, vorher)
})
test('Fake-DOM: Baum, Attribute, Eigenschaften und isConnected', mitDom(undefined, (fx) => {
const knopf = h('button', { type: 'button', class: 'kb-btn tm-x', 'data-aktion': 'weiter', hidden: true }, 'Weiter ', h('b', null, 'jetzt'), 5)
assert.equal(knopf.tagName, 'BUTTON')
assert.equal(knopf.className, 'kb-btn tm-x')
assert.equal(knopf.dataset.aktion, 'weiter')
assert.equal(knopf.getAttribute('data-aktion'), 'weiter')
assert.equal(knopf.hidden, true)
assert.equal(fx.text(knopf), 'Weiter jetzt5')
assert.equal(knopf.children.length, 1, 'children: nur Elemente')
assert.equal(knopf.childNodes.length, 3)
const wurzel = h('div', null, knopf)
assert.equal(knopf.isConnected, false)
document.body.append(wurzel)
assert.equal(knopf.isConnected, true)
wurzel.replaceChildren('neu')
assert.equal(knopf.isConnected, false)
assert.equal(knopf.parentNode, null)
wurzel.textContent = ''
assert.equal(wurzel.childNodes.length, 0)
}))
test('Fake-DOM: Selektoren wie die Client-Dateien sie brauchen', mitDom(undefined, (fx) => {
const zeile = h('li', { class: 'tm-eintrag', 'data-id': 'r_abc' }, h('button', { 'data-aktion': 'bearbeiten' }, 'Bearbeiten'), h('button', { disabled: true }, 'Aus'))
const feld = h('p', { 'data-fehler': 'leistungen.2.dauer' })
const sperre = h('fieldset', { disabled: true }, h('input', { type: 'text' }))
const tafel = h('div', { role: 'tabpanel' }, h('ul', null, zeile), feld, sperre)
document.body.append(tafel)
assert.equal(tafel.querySelector('li[data-id="r_abc"] [data-aktion="bearbeiten"]').textContent, 'Bearbeiten')
assert.equal(tafel.querySelector(`[data-fehler="${CSS.escape('leistungen.2.dauer')}"]`), feld)
assert.deepEqual(zeile.querySelectorAll('button:not(:disabled)').map((n) => n.textContent), ['Bearbeiten'])
assert.equal(zeile.querySelectorAll('button:disabled').length, 1)
assert.equal(sperre.querySelector('input:not(:disabled)'), null, 'fieldset disabled sperrt die Felder darin')
assert.equal(zeile.closest('[role="tabpanel"]'), tafel)
assert.equal(zeile.closest('form'), null)
assert.deepEqual(fx.alle(tafel, 'li, p').map((n) => n.tagName), ['LI', 'P'])
assert.equal(fx.finde(tafel, (n) => n.tagName === 'FIELDSET'), sperre)
assert.equal(tafel.querySelector('ul > li'), zeile)
assert.throws(() => tafel.querySelector('a:hover'), /nicht unterstuetzt/)
}))
test('Fake-DOM: value, checked und selected wie im Browser', mitDom(undefined, async (fx) => {
const auswahl = h('select', null, h('option', { value: '' }, '– bitte wählen –'), h('option', { value: 'RP' }, 'Rheinland-Pfalz'), h('option', { value: 'SL', selected: true }, 'Saarland'))
assert.equal(auswahl.value, 'SL', 'selected-Attribut gilt')
const ohneWahl = h('select', null, h('option', { value: '' }, '–'), h('option', { value: 'RP' }, 'RP'))
assert.equal(ohneWahl.value, '', 'ohne selected zeigt der Browser die erste Option')
await fx.setze(ohneWahl, 'RP')
assert.equal(ohneWahl.value, 'RP')
const text = h('input', { type: 'text', value: 'alt' })
assert.equal(text.value, 'alt')
await fx.setze(text, 'neu')
assert.equal(text.value, 'neu')
assert.equal(text.getAttribute('value'), 'alt', 'die Eigenschaft ändert das Attribut nicht')
const haken = h('input', { type: 'checkbox', checked: true })
assert.equal(haken.checked, true)
await fx.klick(haken)
assert.equal(haken.checked, false)
const area = h('textarea', null, 'Vorgabe')
assert.equal(area.value, 'Vorgabe')
const a = h('input', { type: 'radio', name: 'x', checked: true })
const b = h('input', { type: 'radio', name: 'x' })
h('div', null, a, b)
await fx.klick(b)
assert.deepEqual([a.checked, b.checked], [false, true], 'Radios einer Gruppe schließen sich aus')
}))
test('Fake-DOM: Ereignisse steigen auf, klick auf Absenden-Knopf sendet das Formular, gesperrte Knöpfe tun nichts, async-Handler werden abgewartet', mitDom(undefined, async (fx) => {
const log = []
const knopf = h('button', { type: 'submit' }, 'Weiter')
const aus = h('button', { type: 'submit', disabled: true }, 'Aus')
const lauf = h('button', { type: 'button' }, 'Lauf')
const form = h('form', null, knopf, aus, lauf)
form.addEventListener('submit', async (ev) => {
ev.preventDefault()
log.push('submit')
await Promise.resolve()
await new Promise((r) => setImmediate(r))
log.push('submit fertig')
})
form.addEventListener('click', (ev) => log.push(`click ${ev.target.textContent} ${ev.currentTarget.tagName}`))
lauf.addEventListener('click', async () => { await new Promise((r) => setImmediate(r)); log.push('lauf fertig') })
document.body.append(form)
await fx.klick(aus)
assert.deepEqual(log, [], 'gesperrt: kein Ereignis')
await fx.klick(knopf)
assert.deepEqual(log, ['click Weiter FORM', 'submit', 'submit fertig'])
log.length = 0
await fx.klick(lauf)
assert.deepEqual(log, ['click Lauf FORM', 'lauf fertig'], 'kein submit bei type=button')
log.length = 0
form.addEventListener('keydown', (ev) => log.push(`${ev.type} ${ev.key}`))
await fx.sende(lauf, 'keydown', { key: 'ArrowRight' })
assert.deepEqual(log, ['keydown ArrowRight'])
}))
test('Fake-DOM: fetch schreibt { method, url, body } ins Protokoll und liefert die Antworten der Reihe nach', mitDom({ antworten: [{ status: 400, body: { error: 'Nein', fehler: { 'betrieb.telefon': 'fehlt' }, code: 'X' } }, { body: { ok: 1 } }] }, async (fx) => {
await assert.rejects(request('/api/termine/einstellungen', { method: 'PATCH', body: { projectId: 'p1', version: 3 } }), (err) => err.status === 400 && err.code === 'X' && err.fehler['betrieb.telefon'] === 'fehlt')
assert.deepEqual(await request('/api/termine/x?projectId=p1'), { ok: 1 })
assert.deepEqual(await request('/api/termine/y'), {}, 'ohne Eintrag: 200 mit {}')
assert.deepEqual(fx.fetchLog, [
{ method: 'PATCH', url: '/api/termine/einstellungen', body: { projectId: 'p1', version: 3 } },
{ method: 'GET', url: '/api/termine/x?projectId=p1', body: undefined },
{ method: 'GET', url: '/api/termine/y', body: undefined },
])
}))
// ------------------------------------------------------- betrieb(ctx, e, { nur, titel, hinweis })
const BETRIEB_FELDER = ['name', 'email', 'telefon', 'adresse', 'kleinunternehmer', 'impressumUrl', 'datenschutzUrl', 'agbUrl', 'websiteUrl']
const betriebCtx = (fehlt = []) => ({ daten: { fehlt }, nurLesen: false })
test('betrieb mit nur: [email] baut nur das Feld email, lese() liefert nur { email }', mitDom(undefined, async (fx) => {
const e = salon()
const { el, lese } = einstellungenTab.betrieb(betriebCtx(), e, { nur: ['email'] })
assert.deepEqual(felderVon(fx, el), ['betrieb.email'])
assert.equal(fx.alle(el, 'input').length, 1)
const eingabe = fx.finde(el, '[data-feld="betrieb.email"]')
assert.equal(eingabe.value, BETRIEB.email, 'vorbelegt aus der Einstellung')
await fx.setze(eingabe, 'neu@example.invalid')
const gelesen = lese()
assert.deepEqual(Object.keys(gelesen), ['email'], 'Schlüsselmenge')
assert.deepEqual(gelesen, { email: 'neu@example.invalid' })
// in den Entwurf geschrieben ändert sich nur email, alles andere bleibt
const entwurf = structuredClone(e)
lese(entwurf)
assert.deepEqual(entwurf.betrieb, { ...e.betrieb, email: 'neu@example.invalid' })
}))
test('betrieb mit nur: Teilmenge in der Reihenfolge der Seite, Haken und Adressen gelesen, Unbekanntes wirft', mitDom(undefined, async (fx) => {
const e = salon()
const { el, lese } = einstellungenTab.betrieb(betriebCtx(), e, { nur: ['websiteUrl', 'name', 'kleinunternehmer'] })
assert.deepEqual(felderVon(fx, el), ['betrieb.name', 'betrieb.kleinunternehmer', 'betrieb.websiteUrl'], 'Reihenfolge der Seite, nicht der Aufzählung')
await fx.klick(fx.finde(el, '[data-feld="betrieb.kleinunternehmer"]'))
await fx.setze(fx.finde(el, '[data-feld="betrieb.websiteUrl"]'), 'https://salon.example.invalid')
const gelesen = lese()
assert.deepEqual(Object.keys(gelesen).sort(), ['kleinunternehmer', 'name', 'websiteUrl'])
assert.equal(gelesen.kleinunternehmer, true)
assert.equal(gelesen.websiteUrl, 'https://salon.example.invalid')
assert.equal(gelesen.name, BETRIEB.name)
assert.throws(() => einstellungenTab.betrieb(betriebCtx(), e, { nur: ['email', 'steuernummer'] }), /steuernummer/)
}))
test('betrieb ohne nur baut weiter alle neun Felder und schreibt alle neun in den Entwurf', mitDom(undefined, async (fx) => {
const e = salon()
const { el, lese } = einstellungenTab.betrieb(betriebCtx(), e)
assert.deepEqual(felderVon(fx, el).sort(), BETRIEB_FELDER.map((n) => `betrieb.${n}`).sort())
assert.equal(fx.alle(el, 'input').length, 9)
assert.equal(fx.text(fx.finde(el, 'h3')), 'Dein Betrieb')
const entwurf = { betrieb: {} }
lese(entwurf)
assert.deepEqual(Object.keys(entwurf.betrieb).sort(), [...BETRIEB_FELDER].sort())
assert.equal(entwurf.betrieb.telefon, BETRIEB.telefon)
assert.equal(entwurf.betrieb.kleinunternehmer, false)
}))
test('betrieb: titel und hinweis ersetzen Überschrift und Erklärsatz', mitDom(undefined, (fx) => {
const e = salon()
const eigen = einstellungenTab.betrieb(betriebCtx(), e, { nur: ['email'], titel: 'Wohin sollen die Nachrichten?', hinweis: 'Eine Adresse für alles.' })
assert.equal(fx.text(fx.finde(eigen.el, 'h3')), 'Wohin sollen die Nachrichten?')
assert.equal(fx.text(fx.finde(eigen.el, '.tm-abschnitt-text')), 'Eine Adresse für alles.')
const ohneHinweis = einstellungenTab.betrieb(betriebCtx(), e, { nur: ['email'], hinweis: null })
assert.equal(fx.finde(ohneHinweis.el, '.tm-abschnitt-text'), null)
const standard = einstellungenTab.betrieb(betriebCtx(), e, { nur: ['email'] })
assert.equal(fx.text(fx.finde(standard.el, 'h3')), 'Dein Betrieb')
assert.match(fx.text(fx.finde(standard.el, '.tm-abschnitt-text')), /Diese Angaben sehen deine Gäste/)
}))
test('betrieb: der Hinweis auf fehlende Angaben nennt mit nur nur die Felder, die zu sehen sind', mitDom(undefined, (fx) => {
const e = salon()
const fehlt = ['betrieb.telefon', 'betrieb.adresse']
const alles = einstellungenTab.betrieb(betriebCtx(fehlt), e)
assert.match(fx.text(fx.finde(alles.el, '.tm-box-warn')), /deine Telefonnummer und deine Anschrift/, 'ohne nur wie bisher')
assert.equal(fx.finde(einstellungenTab.betrieb(betriebCtx(fehlt), e, { nur: ['email'] }).el, '.tm-box-warn'), null)
const nurTelefon = fx.text(fx.finde(einstellungenTab.betrieb(betriebCtx(fehlt), e, { nur: ['telefon'] }).el, '.tm-box-warn'))
assert.match(nurTelefon, /Es fehlt noch: deine Telefonnummer\.$/, 'die Anschrift steht nicht in der Liste dessen, was fehlt')
assert.equal(fx.finde(einstellungenTab.betrieb(betriebCtx([]), e).el, '.tm-box-warn'), null)
}))
// ------------------------------------------------------- wochenplanFeld(r, pfad)
test('wochenplanFeld: sieben Tagesfelder mit Namen aus dem Pfad, gefüllt aus dem Plan der Person', mitDom(undefined, (fx) => {
const r = { id: 'r_anna0001', plan: { 1: [['09:00', '12:30'], ['13:30', '18:00']], 6: [['09:00', '13:00']] } }
const { el } = teamTab.wochenplanFeld(r, 'ressourcen.2')
assert.deepEqual(felderVon(fx, el), [1, 2, 3, 4, 5, 6, 7].map((nr) => `ressourcen.2.plan.${nr}`))
assert.deepEqual(fx.alle(el, 'label').map((l) => fx.text(l)), ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'])
const eingaben = fx.alle(el, 'input')
assert.deepEqual(eingaben.map((i) => i.value), ['09:00–12:30, 13:30–18:00', '', '', '', '', '09:00–13:00', ''])
assert.ok(eingaben.every((i) => i.getAttribute('placeholder') === 'geschlossen'))
assert.equal(fx.text(fx.finde(el, 'legend')), 'Wochenplan')
assert.equal(fx.alle(el, '[data-fehler]').length, 7, 'je Tag ein Platz für den Fehler')
}))
test('wochenplanFeld: gültige Fenster ⇒ { plan } mit genau diesen Fenstern, ein leerer Tag ist geschlossen', mitDom(undefined, async (fx) => {
const { el, lese } = teamTab.wochenplanFeld({ id: 'r1', plan: {} }, 'ressourcen.0')
await fx.setze(fx.finde(el, '[data-feld="ressourcen.0.plan.1"]'), '09:00–12:30, 13:30–18:00')
await fx.setze(fx.finde(el, '[data-feld="ressourcen.0.plan.3"]'), '8:00-12:00')
const gelesen = lese()
assert.deepEqual(gelesen, { plan: { 1: [['09:00', '12:30'], ['13:30', '18:00']], 3: [['08:00', '12:00']] } })
assert.equal(gelesen.fehler, undefined)
assert.ok(!(2 in gelesen.plan) && !(7 in gelesen.plan), 'leerer Tag ⇒ geschlossen (kein Eintrag)')
// ein vorher offener Tag, der geleert wird, ist danach geschlossen
const offen = teamTab.wochenplanFeld({ id: 'r1', plan: { 1: [['09:00', '12:00']], 2: [['10:00', '11:00']] } }, 'ressourcen.0')
await fx.setze(fx.finde(offen.el, '[data-feld="ressourcen.0.plan.1"]'), '')
assert.deepEqual(offen.lese(), { plan: { 2: [['10:00', '11:00']] } })
assert.deepEqual(teamTab.wochenplanFeld(null, 'ressourcen.0').lese(), { plan: {} }, 'ohne Person: alles geschlossen')
}))
test('wochenplanFeld: ein ungültiges Fenster ⇒ { fehler } mit Text am Feld des Tages, kein Teilplan', mitDom(undefined, async (fx) => {
const { el, lese } = teamTab.wochenplanFeld({ id: 'r1', plan: {} }, 'ressourcen.1')
await fx.setze(fx.finde(el, '[data-feld="ressourcen.1.plan.1"]'), '09:00–12:00')
await fx.setze(fx.finde(el, '[data-feld="ressourcen.1.plan.2"]'), 'abc')
await fx.setze(fx.finde(el, '[data-feld="ressourcen.1.plan.5"]'), '09:00–09:00')
const gelesen = lese()
assert.deepEqual(gelesen, { fehler: { 'ressourcen.1.plan.2': FENSTER_FEHLER, 'ressourcen.1.plan.5': FENSTER_FEHLER } })
assert.equal(gelesen.plan, undefined)
assert.ok(typeof gelesen.fehler['ressourcen.1.plan.2'] === 'string' && gelesen.fehler['ressourcen.1.plan.2'].length > 0)
}))
test('Reiter Team & Plätze: der Editor einer Person speichert den Wochenplan aus wochenplanFeld und zeigt Fehler am Tag', mitDom(undefined, async (fx) => {
const einstellung = salon()
const anna = einstellung.ressourcen[0]
const ctx = {
s: {}, projectId: 'p1', daten: { einstellung, version: 4, feeds: {}, fehlt: [] }, nurLesen: false, auswahl: {}, aktuell: () => true,
api: { send: (method, pfad, body = {}) => request(`/api/termine${pfad}`, { method, body: { projectId: 'p1', ...body } }) },
meldung() {}, hinweise() {}, zumReiter() {}, neuLaden: async () => {},
}
const tafel = h('div', { role: 'tabpanel' })
document.body.append(tafel)
teamTab.render(tafel, ctx)
await fx.ruhe()
await fx.klick(fx.finde(tafel, `li[data-id="${anna.id}"] [data-aktion="bearbeiten"]`))
const form = fx.finde(tafel, (n) => n.tagName === 'FORM' && n.getAttribute('aria-label') === `Bearbeiten: ${anna.name}`)
assert.ok(form, 'Editor ist offen')
const speichern = fx.finde(form, (n) => n.tagName === 'BUTTON' && n.type === 'submit')
const montag = fx.finde(form, '[data-feld="ressourcen.0.plan.1"]')
// Fehler: nichts wird gesendet, der Text steht am Tag
await fx.setze(montag, 'abc')
await fx.klick(speichern)
assert.deepEqual(fx.fetchLog, [])
assert.equal(fx.text(fx.finde(form, '[data-fehler="ressourcen.0.plan.1"]')), FENSTER_FEHLER)
assert.equal(montag.getAttribute('aria-invalid'), 'true')
// gültig: PATCH mit dem Plan (alle anderen Tage unverändert)
await fx.setze(montag, '10:00-13:00')
fx.antworten.push({ body: { einstellung, version: 5, fehlt: [] } })
await fx.klick(speichern)
assert.equal(fx.fetchLog.length, 1)
const [aufruf] = fx.fetchLog
assert.deepEqual([aufruf.method, aufruf.url], ['PATCH', '/api/termine/einstellungen'])
assert.equal(aufruf.body.version, 4)
assert.deepEqual(aufruf.body.einstellung.ressourcen[0].plan[1], [['10:00', '13:00']])
assert.deepEqual(aufruf.body.einstellung.ressourcen[0].plan[2], anna.plan[2])
}))
// ------------------------------------------------------- termine-speichern.js: fehlt aus der Antwort
const speicherCtx = (einstellung, fehlt) => ({
aktuell: () => true,
daten: { einstellung, version: 3, fehlt },
api: { send: (method, pfad, body = {}) => request(`/api/termine${pfad}`, { method, body: { projectId: 'p1', ...body } }) },
hinweise() {}, meldung() {}, neuLaden: async () => {},
})
test('Speichern: antwortet der PATCH mit fehlt, steht es danach in den Daten des Reiters', mitDom({ antworten: [{ body: { einstellung: salon(), version: 4, fehlt: ['betrieb.telefon'] } }] }, async (fx) => {
const ctx = speicherCtx(salon(), ['betrieb.telefon', 'betrieb.adresse'])
const wurzel = h('div')
document.body.append(wurzel)
const sp = neuerSpeicher(ctx, wurzel, () => {})
assert.equal(await sp.speichere((entwurf) => { entwurf.betrieb.adresse = 'Hauptstraße 1' }, { container: wurzel }), true)
assert.deepEqual(ctx.daten.fehlt, ['betrieb.telefon'])
assert.equal(ctx.daten.version, 4)
assert.deepEqual(fx.fetchLog.map((a) => [a.method, a.url]), [['PATCH', '/api/termine/einstellungen']])
}))
test('Speichern: ohne fehlt in der Antwort ist es leer', mitDom({ antworten: [{ body: { einstellung: salon(), version: 4 } }] }, async () => {
const ctx = speicherCtx(salon(), ['betrieb.telefon'])
const wurzel = h('div')
document.body.append(wurzel)
const sp = neuerSpeicher(ctx, wurzel, () => {})
assert.equal(await sp.speichere(() => true, { container: wurzel }), true)
assert.deepEqual(ctx.daten.fehlt, [])
}))
// ------------------------------------------------------- termine.js: baueCtx
test('baueCtx: Schnittstelle, Daten und Meldungen wie der Reiter-Kontext', mitDom({ antworten: [{}, {}, {}, { body: { version: 9, einstellung: null } }, { body: { version: 11 } }] }, async (fx) => {
const s = { daten: { version: 3 }, nurLesen: false, meldungEl: h('div', { class: 'tm-meldung', tabindex: '-1' }) }
document.body.append(s.meldungEl)
let aktuell = true
let gezeichnet = 0
const ctx = huelle.baueCtx(s, { projekt: { projectId: 'p 1' }, aktuell: () => aktuell, hinweise: () => { gezeichnet += 1 } })
assert.equal(ctx.s, s)
assert.equal(ctx.projectId, 'p 1')
assert.equal(ctx.daten, s.daten)
assert.equal(ctx.nurLesen, false)
assert.equal(typeof ctx.auswahl, 'object')
assert.equal(ctx.aktuell(), true)
assert.equal(typeof ctx.zumReiter, 'function')
// Schnittstelle: GET und DELETE mit projectId in der Adresse, PATCH/POST/PUT mit projectId im Body
await ctx.api.get('/buchungen', { von: '2026-10-05' })
await ctx.api.send('DELETE', '/abwesenheiten/a1')
await ctx.api.send('PATCH', '/einstellungen', { version: 3 })
assert.deepEqual(fx.fetchLog.map((a) => [a.method, a.url, a.body]), [
['GET', '/api/termine/buchungen?projectId=p+1&von=2026-10-05', undefined],
['DELETE', '/api/termine/abwesenheiten/a1?projectId=p+1', undefined],
['PATCH', '/api/termine/einstellungen', { projectId: 'p 1', version: 3 }],
])
// Meldung nur solange die Ansicht aktuell ist; hinweise() zeichnet nur dann
ctx.meldung('Gespeichert.')
assert.equal(s.meldungEl.textContent, 'Gespeichert.')
ctx.hinweise()
assert.equal(gezeichnet, 1)
// neu laden: Daten und ctx.daten werden ersetzt und die Hinweise neu gezeichnet
const daten = await ctx.neuLaden()
assert.deepEqual(daten, { version: 9, einstellung: null })
assert.equal(s.daten, daten)
assert.equal(ctx.daten, daten)
assert.equal(gezeichnet, 2)
assert.equal(fx.fetchLog.at(-1).url, '/api/termine/einstellungen?projectId=p%201')
aktuell = false
ctx.meldung('Zu spät.')
ctx.hinweise()
await ctx.neuLaden()
assert.equal(s.meldungEl.textContent, 'Gespeichert.', 'eine veraltete Ansicht bekommt keine Meldung')
assert.equal(gezeichnet, 2)
assert.equal(ctx.daten, daten, 'und keine neuen Daten')
// ohne hinweise/zumReiter ist der Kontext trotzdem benutzbar (Fragebogen)
const schlicht = huelle.baueCtx(s, { projekt: { projectId: 'p1' }, aktuell: () => true })
schlicht.hinweise()
schlicht.zumReiter('team')
}))