Speichern über PUT /api/zeiten (Version, 409, Feldfehler per ID-Pfad), Zeitpläne mit Wochentagen, Hauptplan wechseln und Löschen (R10, R33), Feiertage je Bundesland, Betriebsurlaub und Sondertage mit Hinweis-Leisten-Frage. Die Hülle behält den Editor bei einem neuen Stand und zeichnet nur den Kopf neu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
962 lines
60 KiB
JavaScript
962 lines
60 KiB
JavaScript
import { test } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import { readFileSync, readdirSync } from 'node:fs'
|
||
import { installFakeDom } from './support/fakeDom.js'
|
||
import { config } from '../server/config.js'
|
||
import { oeffentlicheAnsicht } from '../server/services/featureRegistry.js'
|
||
import { h } from '../public/termine-dom.js'
|
||
import { heuteBerlin, tagPlus } from '../public/termine-view.js'
|
||
import { ZEITEN_LIMITS, hinweisVorschlag } from '../public/zeiten-view.js'
|
||
import { initZeiten } from '../public/zeiten.js'
|
||
// Namensraeume statt benannter Importe: fehlt ein Export, scheitert nur der Test, der ihn braucht
|
||
import * as editor from '../public/zeiten-editor.js'
|
||
import * as speichern from '../public/zeiten-speichern.js'
|
||
|
||
// Editor der Öffnungszeiten live, Teil I (Task 29): Speichern, Zeitpläne, Feiertage, Ausnahmen (public/zeiten-editor.js, zeiten-speichern.js,
|
||
// zeiten-plaene.js, zeiten-ausnahmen.js; Spec 3.4 Bereiche 1 bis 3 und der Absatz „Speichern“). Fake-DOM und Fake-fetch aus
|
||
// test/support/fakeDom.js: jeder Aufruf steht in fx.fetchLog, die Antworten kommen der Reihe nach aus fx.antworten.
|
||
|
||
const mitDom = (optionen, fn) => async () => {
|
||
const fx = installFakeDom(optionen)
|
||
try {
|
||
await fn(fx)
|
||
} finally {
|
||
fx.restore()
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------- Fixtures
|
||
const HEUTE = heuteBerlin()
|
||
const P1 = 'p_haupt001'
|
||
const P2 = 'p_kueche02'
|
||
const WOCHE = ['1', '2', '3', '4', '5', '6', '7']
|
||
const wochenplan = (extra = {}) => Object.fromEntries(WOCHE.map((nr) => [nr, extra[nr] || { modus: 'zu', fenster: [] }]))
|
||
const werktags = () => wochenplan({
|
||
1: { modus: 'zeiten', fenster: [['09:00', '12:30'], ['13:30', '18:00']] },
|
||
2: { modus: 'zeiten', fenster: [['09:00', '18:00']] },
|
||
3: { modus: 'zeiten', fenster: [['09:00', '18:00']] },
|
||
4: { modus: 'zeiten', fenster: [['09:00', '18:00']] },
|
||
5: { modus: 'zeiten', fenster: [['09:00', '16:00']] },
|
||
6: { modus: 'rund', fenster: [] },
|
||
})
|
||
const plan = (id, extra = {}) => ({ id, kurz: id === P1 ? 'oeffnungszeiten' : 'kueche', name: id === P1 ? 'Öffnungszeiten' : 'Küche', haupt: id === P1, tage: werktags(), ...extra })
|
||
const KONTAKT = { an: true, telefon: '496301123456', adresse: 'Hauptstraße 1, 67655 Kaiserslautern', whatsapp: { nummer: '4917112345678', starttext: 'Hallo!', bestaetigtAt: '2026-10-01T08:00:00.000Z' } }
|
||
const daten = (extra = {}) => ({ land: 'RP', plaene: [plan(P1)], feiertage: {}, ausnahmen: [], hinweise: [], kontakt: KONTAKT, ...extra })
|
||
const FEIERTAGE = [
|
||
{ tag: '2026-12-24', name: 'Heiligabend', art: 'pruefen' },
|
||
{ tag: '2026-12-25', name: 'Erster Weihnachtstag', art: 'feiertag' },
|
||
{ tag: '2026-12-31', name: 'Silvester', art: 'pruefen' },
|
||
{ tag: '2027-01-01', name: 'Neujahr', art: 'feiertag' },
|
||
]
|
||
const stand = (d, extra = {}) => ({ state: 'active', daten: d, version: d ? 3 : 0, kundenansicht: false, vorschau: null, feiertage: FEIERTAGE, googleText: '', ...extra })
|
||
const antwort = (d, version = 4, extra = {}) => ({ status: 200, body: stand(d, { version, ...extra }) })
|
||
const fehler = (status, error, extra = {}) => ({ status, body: { error, ...extra } })
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------- Hilfen
|
||
/** Den Editor in einen Platz mit data-state hängen und laden; die Antwort des GET steht als erste in der Warteschlange. */
|
||
async function oeffne(fx, { start = stand(daten()), state = 'active', kundenansicht, ctx = {} } = {}) {
|
||
const root = h('div', { class: 'zt-editor', 'data-state': state })
|
||
document.body.append(root)
|
||
fx.antworten.unshift({ status: 200, body: start })
|
||
const neuGezeichnet = []
|
||
await editor.renderEditor(root, { projectId: 'p1', kundenansicht, neuZeichnen: async (optionen) => { neuGezeichnet.push(optionen) }, ...ctx })
|
||
return { root, neuGezeichnet }
|
||
}
|
||
const puts = (fx) => fx.fetchLog.filter((a) => a.method === 'PUT')
|
||
const gets = (fx) => fx.fetchLog.filter((a) => a.method === 'GET')
|
||
const planKarte = (fx, root, id) => fx.finde(root, `details[data-plan="${id}"]`)
|
||
const tagZeile = (fx, root, id, nr) => fx.finde(planKarte(fx, root, id), `[data-tag="${nr}"]`)
|
||
const zeitFelder = (fx, zeile) => fx.alle(zeile, 'input[type="time"]')
|
||
const haken = (fx, zeile) => fx.finde(zeile, 'input[type="checkbox"]')
|
||
const art = (fx, zeile) => fx.finde(zeile, 'select')
|
||
const speichernKnopf = (fx, root, id) => fx.finde(planKarte(fx, root, id), '[data-aktion="speichern"]')
|
||
const abschnitt = (fx, root, key) => fx.finde(root, `[data-abschnitt="${key}"]`)
|
||
const meldung = (fx, root) => fx.text(fx.finde(root, '[data-meldung="editor"]'))
|
||
const fehlerAn = (fx, root, pfad) => fx.finde(root, `[data-fehler="${pfad}"]`)
|
||
const lesen = (pfad) => readFileSync(new URL(`../${pfad}`, import.meta.url), 'utf8')
|
||
const optionen = (fx, select) => fx.alle(select, 'option').map((o) => o.value)
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------- Leerer Start
|
||
test('leerer Start: „Trag deine Öffnungszeiten ein“ und ein leerer Hauptplan „Öffnungszeiten“ (Kurzname oeffnungszeiten)', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx, { start: stand(null) })
|
||
assert.deepEqual(gets(fx).map((a) => a.url), ['/api/zeiten/p1'])
|
||
assert.match(fx.text(fx.finde(root, '[data-hinweis="leer"]')), /Trag deine Öffnungszeiten ein/)
|
||
const karten = fx.alle(root, 'details[data-plan]')
|
||
assert.equal(karten.length, 1, 'genau ein Plan')
|
||
const kopf = fx.finde(karten[0], 'summary')
|
||
assert.match(fx.text(kopf), /Öffnungszeiten/)
|
||
assert.match(fx.text(kopf), /Hauptplan/)
|
||
assert.equal(karten[0].hasAttribute('open'), true, 'der Hauptplan steht offen')
|
||
assert.equal(fx.text(fx.finde(karten[0], '[data-kurz]')), 'oeffnungszeiten')
|
||
const id = karten[0].getAttribute('data-plan')
|
||
assert.match(id, /^p_[a-z0-9]{8}$/)
|
||
assert.equal(fx.finde(karten[0], `[data-feld="plaene.${id}.name"]`).value, 'Öffnungszeiten')
|
||
for (const nr of WOCHE) {
|
||
const zeile = tagZeile(fx, root, id, nr)
|
||
assert.equal(haken(fx, zeile).checked, false, `Tag ${nr} ist geschlossen`)
|
||
}
|
||
assert.equal(fx.finde(karten[0], '[data-aktion="entfernen"]'), null, 'der einzige Plan lässt sich nie löschen')
|
||
assert.equal(fx.finde(root, '[data-werkzeug="plan-neu"]'), null, 'weitere Pläne erst nach dem ersten Speichern')
|
||
assert.equal(abschnitt(fx, root, 'feiertage'), null, 'Feiertage und Ausnahmen erst nach dem ersten Speichern')
|
||
assert.equal(abschnitt(fx, root, 'ausnahmen'), null)
|
||
assert.match(fx.text(root), /sobald deine Öffnungszeiten gespeichert sind/)
|
||
}))
|
||
|
||
test('erstes Speichern: PUT mit version 0 und dem Plan, danach erscheinen Feiertage und Ausnahmen und die Hülle wird neu gezeichnet', mitDom({}, async (fx) => {
|
||
const { root, neuGezeichnet } = await oeffne(fx, { start: stand(null) })
|
||
const id = fx.finde(root, 'details[data-plan]').getAttribute('data-plan')
|
||
await fx.klick(haken(fx, tagZeile(fx, root, id, '1')))
|
||
const [von1, bis1, von2, bis2] = zeitFelder(fx, tagZeile(fx, root, id, '1'))
|
||
await fx.setze(von1, '09:00')
|
||
await fx.setze(bis1, '12:30')
|
||
await fx.setze(von2, '13:30')
|
||
await fx.setze(bis2, '18:00')
|
||
const gespeichert = daten({ plaene: [plan(id, { tage: wochenplan({ 1: { modus: 'zeiten', fenster: [['09:00', '12:30'], ['13:30', '18:00']] } }) })], kontakt: { an: false, telefon: '', adresse: '', whatsapp: null } })
|
||
fx.antworten.push(antwort(gespeichert, 1))
|
||
await fx.klick(speichernKnopf(fx, root, id))
|
||
assert.equal(puts(fx).length, 1)
|
||
assert.equal(puts(fx)[0].url, '/api/zeiten/p1')
|
||
const body = puts(fx)[0].body
|
||
assert.equal(body.version, 0)
|
||
assert.deepEqual(Object.keys(body).sort(), ['daten', 'version'])
|
||
assert.equal(body.daten.land, 'RP')
|
||
assert.equal(body.daten.plaene.length, 1)
|
||
assert.deepEqual(body.daten.plaene[0], {
|
||
id, kurz: 'oeffnungszeiten', name: 'Öffnungszeiten', haupt: true,
|
||
tage: wochenplan({ 1: { modus: 'zeiten', fenster: [['09:00', '12:30'], ['13:30', '18:00']] } }),
|
||
})
|
||
assert.deepEqual([body.daten.feiertage, body.daten.ausnahmen, body.daten.hinweise], [{}, [], []])
|
||
assert.match(meldung(fx, root), /gespeichert/)
|
||
assert.equal(fx.finde(root, '[data-hinweis="leer"]'), null, 'nach dem Speichern kein „Trag ein“ mehr')
|
||
assert.ok(abschnitt(fx, root, 'feiertage'))
|
||
assert.ok(abschnitt(fx, root, 'ausnahmen'))
|
||
assert.ok(fx.finde(root, '[data-werkzeug="plan-neu"]'))
|
||
assert.equal(neuGezeichnet.length, 1, 'die Hülle holt die Liste neu (zeitenLeer wechselt)')
|
||
// der zweite Speichervorgang beruht auf der neuen Version
|
||
fx.antworten.push(antwort(gespeichert, 2))
|
||
await fx.klick(speichernKnopf(fx, root, id))
|
||
assert.equal(puts(fx)[1].body.version, 1)
|
||
assert.equal(neuGezeichnet.length, 1, 'nur das erste Speichern wechselt den Stand der Hülle')
|
||
}))
|
||
|
||
test('leerer Start: der Kurzname folgt dem Namen bis zum ersten Speichern (so bildet ihn der Server) und geht dann mit dem Plan hinaus', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx, { start: stand(null) })
|
||
const id = fx.finde(root, 'details[data-plan]').getAttribute('data-plan')
|
||
assert.match(fx.text(planKarte(fx, root, id)), /beim ersten Speichern aus dem Namen gebildet/)
|
||
await fx.setze(fx.finde(planKarte(fx, root, id), `[data-feld="plaene.${id}.name"]`), 'Praxis Dr. Müller')
|
||
assert.equal(fx.text(fx.finde(planKarte(fx, root, id), '[data-kurz]')), 'praxis-dr-mueller')
|
||
fx.antworten.push(antwort(daten({ plaene: [plan(id, { name: 'Praxis Dr. Müller', kurz: 'praxis-dr-mueller' })] }), 1))
|
||
await fx.klick(speichernKnopf(fx, root, id))
|
||
assert.deepEqual([puts(fx)[0].body.daten.plaene[0].name, puts(fx)[0].body.daten.plaene[0].kurz], ['Praxis Dr. Müller', 'praxis-dr-mueller'])
|
||
assert.match(fx.text(planKarte(fx, root, id)), /Der Kurzname bleibt gleich, auch wenn du den Namen änderst/, 'danach ist er unveränderlich')
|
||
assert.equal(fx.text(fx.finde(planKarte(fx, root, id), '[data-kurz]')), 'praxis-dr-mueller')
|
||
}))
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------- Wochentage
|
||
test('Wochentag: Haken geöffnet, zwei Zeitpaare (type time, step 300), Auswahl rund um die Uhr / nach Vereinbarung', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
const montag = tagZeile(fx, root, P1, '1')
|
||
assert.match(fx.text(montag), /Montag/)
|
||
assert.match(fx.text(montag), /geöffnet/)
|
||
assert.equal(haken(fx, montag).checked, true)
|
||
const felder = zeitFelder(fx, montag)
|
||
assert.equal(felder.length, 4, 'zwei Fenster, je von und bis')
|
||
for (const feld of felder) {
|
||
assert.equal(feld.getAttribute('type'), 'time')
|
||
assert.equal(feld.getAttribute('step'), '300')
|
||
}
|
||
assert.deepEqual(felder.map((f) => f.value), ['09:00', '12:30', '13:30', '18:00'])
|
||
assert.deepEqual(optionen(fx, art(fx, montag)), ['zeiten', 'rund', 'vereinbarung'])
|
||
assert.match(fx.text(art(fx, montag)), /rund um die Uhr/)
|
||
assert.match(fx.text(art(fx, montag)), /nach Vereinbarung/)
|
||
|
||
// Dienstag hat nur ein Fenster, das zweite Paar ist leer; Samstag „rund“; Sonntag zu
|
||
assert.deepEqual(zeitFelder(fx, tagZeile(fx, root, P1, '2')).map((f) => f.value), ['09:00', '18:00', '', ''])
|
||
const samstag = tagZeile(fx, root, P1, '6')
|
||
assert.equal(art(fx, samstag).value, 'rund')
|
||
assert.equal(fx.finde(samstag, '.zt-zeiten').hidden, true, 'rund: keine Zeitfelder')
|
||
const sonntag = tagZeile(fx, root, P1, '7')
|
||
assert.equal(haken(fx, sonntag).checked, false)
|
||
assert.equal(fx.finde(sonntag, '.zt-tag-felder').hidden, true, 'geschlossen: keine Auswahl')
|
||
await fx.klick(haken(fx, sonntag))
|
||
assert.equal(fx.finde(sonntag, '.zt-tag-felder').hidden, false)
|
||
assert.equal(fx.finde(sonntag, '.zt-zeiten').hidden, false, 'geöffnet zu festen Zeiten zeigt die Zeitfelder')
|
||
await fx.setze(art(fx, sonntag), 'vereinbarung')
|
||
assert.equal(fx.finde(sonntag, '.zt-zeiten').hidden, true)
|
||
}))
|
||
|
||
test('Wochentag speichern: geschlossen, rund, nach Vereinbarung und feste Zeiten gehen als modus/fenster hinaus', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
await fx.klick(haken(fx, tagZeile(fx, root, P1, '2'))) // Dienstag zu
|
||
await fx.setze(art(fx, tagZeile(fx, root, P1, '3')), 'vereinbarung')
|
||
await fx.klick(haken(fx, tagZeile(fx, root, P1, '7')))
|
||
await fx.setze(art(fx, tagZeile(fx, root, P1, '7')), 'rund')
|
||
const [von1] = zeitFelder(fx, tagZeile(fx, root, P1, '5'))
|
||
await fx.setze(von1, '08:30')
|
||
fx.antworten.push(antwort(daten(), 4))
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
const tage = puts(fx)[0].body.daten.plaene[0].tage
|
||
assert.deepEqual(tage['2'], { modus: 'zu', fenster: [] })
|
||
assert.deepEqual(tage['3'], { modus: 'vereinbarung', fenster: [] })
|
||
assert.deepEqual(tage['7'], { modus: 'rund', fenster: [] })
|
||
assert.deepEqual(tage['5'], { modus: 'zeiten', fenster: [['08:30', '16:00']] })
|
||
assert.deepEqual(tage['1'], { modus: 'zeiten', fenster: [['09:00', '12:30'], ['13:30', '18:00']] }, 'Unverändertes bleibt')
|
||
assert.deepEqual(Object.keys(tage).sort(), WOCHE)
|
||
}))
|
||
|
||
test('Wochentag: Fehler stehen vor dem Senden am Tag (Überschneidung, halbes Fenster, kein Fenster), nichts geht hinaus', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
const fensterMitLuecke = tagZeile(fx, root, P1, '1')
|
||
await fx.setze(zeitFelder(fx, fensterMitLuecke)[2], '12:00') // 2. Fenster beginnt vor dem Ende des ersten
|
||
const halb = tagZeile(fx, root, P1, '2')
|
||
await fx.setze(zeitFelder(fx, halb)[2], '19:00') // zweites Paar nur halb
|
||
const leer = tagZeile(fx, root, P1, '3')
|
||
await fx.setze(zeitFelder(fx, leer)[0], '')
|
||
await fx.setze(zeitFelder(fx, leer)[1], '')
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
assert.equal(puts(fx).length, 0)
|
||
assert.match(fx.text(fehlerAn(fx, root, `plaene.${P1}.tage.1`)), /überschneiden/)
|
||
assert.match(fx.text(fehlerAn(fx, root, `plaene.${P1}.tage.2`)), /Beginn und Ende/)
|
||
assert.match(fx.text(fehlerAn(fx, root, `plaene.${P1}.tage.3`)), /mindestens ein Zeitfenster/)
|
||
assert.equal(fx.finde(root, `[data-feld="plaene.${P1}.tage.1"]`).getAttribute('aria-invalid'), 'true')
|
||
// ein leerer Name ist ein Fehler am Namen
|
||
await fx.setze(fx.finde(planKarte(fx, root, P1), `[data-feld="plaene.${P1}.name"]`), ' ')
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
assert.equal(puts(fx).length, 0)
|
||
assert.match(fx.text(fehlerAn(fx, root, `plaene.${P1}.name`)), /Namen/)
|
||
}))
|
||
|
||
test('Zeitfelder zeigen ein gespeichertes „24:00“ als 00:00 (type time kennt kein 24:00)', mitDom({}, async (fx) => {
|
||
const d = daten({ plaene: [plan(P1, { tage: wochenplan({ 1: { modus: 'zeiten', fenster: [['18:00', '24:00']] } }) })] })
|
||
const { root } = await oeffne(fx, { start: stand(d) })
|
||
assert.deepEqual(zeitFelder(fx, tagZeile(fx, root, P1, '1')).map((f) => f.value), ['18:00', '00:00', '', ''])
|
||
}))
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------- Pläne
|
||
test('Plan hinzufügen: Name, Kurzname-Vorschau, PUT mit leerem Plan; nach dem Speichern gilt der Kurzname der Antwort (R33)', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
const neu = fx.finde(root, '[data-werkzeug="plan-neu"]')
|
||
assert.match(fx.text(neu), /Plan hinzufügen/)
|
||
await fx.klick(neu)
|
||
const form = fx.finde(root, 'form[aria-label="Plan hinzufügen"]')
|
||
const name = fx.finde(form, 'input[type="text"]')
|
||
await fx.setze(name, 'Küche & Lieferdienst')
|
||
assert.match(fx.text(fx.finde(form, '[data-kurz-vorschau]')), /kueche-lieferdienst/)
|
||
await fx.setze(name, 'Küche')
|
||
assert.equal(fx.text(fx.finde(form, '[data-kurz-vorschau]')), 'kueche')
|
||
// der Server vergibt den Kurzname endgültig (reserviert die Namen gelöschter Pläne): hier „kueche-2“
|
||
// die Antwort trägt die ID des gesendeten Plans (der Server behält gültige IDs) mit dem Kurznamen des Servers
|
||
fx.antworten.push((aufruf) => antwort(daten({ plaene: [plan(P1), plan(aufruf.body.daten.plaene[1].id, { name: 'Küche', kurz: 'kueche-2', haupt: false, tage: wochenplan() })] }), 4))
|
||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||
const neuerPlan = puts(fx)[0].body.daten.plaene[1]
|
||
assert.match(neuerPlan.id, /^p_[a-z0-9]{8}$/)
|
||
assert.deepEqual({ ...neuerPlan, id: 'x' }, { id: 'x', kurz: 'kueche', name: 'Küche', haupt: false, tage: wochenplan() })
|
||
assert.equal(puts(fx)[0].body.daten.plaene[0].haupt, true)
|
||
assert.match(meldung(fx, root), /Küche/)
|
||
assert.match(meldung(fx, root), /kueche-2/, 'die Meldung nennt den Kurznamen der Antwort')
|
||
const karte = planKarte(fx, root, neuerPlan.id)
|
||
assert.equal(fx.text(fx.finde(karte, '[data-kurz]')), 'kueche-2', 'angezeigt wird der Kurzname des Servers')
|
||
assert.equal(karte.hasAttribute('open'), true, 'der neue Plan steht zum Ausfüllen offen')
|
||
assert.equal(fx.finde(root, 'form[aria-label="Plan hinzufügen"]'), null, 'das Formular ist zu')
|
||
}))
|
||
|
||
test('Plan hinzufügen: ohne Namen kein Senden; bei fünf Plänen gesperrt mit Hinweis', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
await fx.klick(fx.finde(root, '[data-werkzeug="plan-neu"]'))
|
||
const form = fx.finde(root, 'form[aria-label="Plan hinzufügen"]')
|
||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||
assert.equal(puts(fx).length, 0)
|
||
assert.match(fx.text(fehlerAn(fx, form, 'name')), /Namen/)
|
||
|
||
const fuenf = daten({ plaene: ['p_aaaaaaa1', 'p_aaaaaaa2', 'p_aaaaaaa3', 'p_aaaaaaa4', 'p_aaaaaaa5'].map((id, i) => plan(id, { haupt: i === 0, kurz: `plan-${i}`, name: `Plan ${i}` })) })
|
||
const voll = await oeffne(fx, { start: stand(fuenf) })
|
||
const knopf = fx.finde(voll.root, '[data-werkzeug="plan-neu"]')
|
||
assert.equal(knopf.disabled, true)
|
||
assert.match(fx.text(abschnitt(fx, voll.root, 'plaene')), new RegExp(`Mehr als ${ZEITEN_LIMITS.plaene} Zeitpläne sind nicht möglich`))
|
||
}))
|
||
|
||
test('Hauptplan wechseln: Knopf nur an den anderen Plänen, PUT mit getauschtem Haupt', mitDom({}, async (fx) => {
|
||
const zwei = daten({ plaene: [plan(P1), plan(P2)] })
|
||
const { root } = await oeffne(fx, { start: stand(zwei) })
|
||
assert.equal(fx.finde(planKarte(fx, root, P1), '[data-aktion="haupt"]'), null, 'der Hauptplan hat den Knopf nicht')
|
||
assert.match(fx.text(fx.finde(planKarte(fx, root, P1), 'summary')), /Hauptplan/)
|
||
assert.doesNotMatch(fx.text(fx.finde(planKarte(fx, root, P2), 'summary')), /Hauptplan/)
|
||
const knopf = fx.finde(planKarte(fx, root, P2), '[data-aktion="haupt"]')
|
||
assert.match(fx.text(knopf), /Hauptplan/)
|
||
fx.antworten.push(antwort(daten({ plaene: [plan(P1, { haupt: false }), plan(P2, { haupt: true })] }), 4))
|
||
await fx.klick(knopf)
|
||
assert.deepEqual(puts(fx)[0].body.daten.plaene.map((p) => [p.id, p.haupt]), [[P1, false], [P2, true]])
|
||
assert.match(meldung(fx, root), /Küche.*Hauptplan/)
|
||
assert.match(fx.text(fx.finde(planKarte(fx, root, P2), 'summary')), /Hauptplan/)
|
||
assert.equal(fx.finde(planKarte(fx, root, P2), '[data-aktion="haupt"]'), null)
|
||
assert.ok(fx.finde(planKarte(fx, root, P1), '[data-aktion="haupt"]'))
|
||
}))
|
||
|
||
test('Plan löschen warnt („Steht dieser Plan auf deiner Website, zeigt sie dort wieder die feste Tabelle.“) und entfernt auch die Ausnahmen des Plans (R10)', mitDom({}, async (fx) => {
|
||
const von = tagPlus(HEUTE, 20)
|
||
const ausnahmen = [
|
||
{ id: 'x_alle0001', art: 'urlaub', von, bis: von, text: 'Alle', modus: 'zu', fenster: [], plan: null },
|
||
{ id: 'x_kueche01', art: 'urlaub', von, bis: von, text: 'Nur Küche', modus: 'zu', fenster: [], plan: P2 },
|
||
{ id: 'x_kueche02', art: 'sondertag', von, bis: von, text: 'Küche offen', modus: 'rund', fenster: [], plan: P2 },
|
||
]
|
||
const d = daten({ plaene: [plan(P1), plan(P2)], ausnahmen })
|
||
const { root } = await oeffne(fx, { start: stand(d) })
|
||
assert.equal(fx.finde(planKarte(fx, root, P1), '[data-aktion="entfernen"]'), null, 'der Hauptplan lässt sich nicht löschen')
|
||
assert.match(fx.text(planKarte(fx, root, P1)), /Erst einen anderen Plan zum Hauptplan machen/)
|
||
const knopf = fx.finde(planKarte(fx, root, P2), '[data-aktion="entfernen"]')
|
||
assert.match(fx.text(knopf), /löschen/)
|
||
await fx.klick(knopf)
|
||
const frage = fx.finde(planKarte(fx, root, P2), '.tm-frage-box')
|
||
assert.ok(frage, 'die Rückfrage steht in der Seite')
|
||
assert.ok(fx.text(frage).includes('Steht dieser Plan auf deiner Website, zeigt sie dort wieder die feste Tabelle.'))
|
||
assert.match(fx.text(frage), /Die 2 Ausnahmen, die nur für diesen Plan gelten, werden mit gelöscht/)
|
||
assert.equal(puts(fx).length, 0, 'ohne Bestätigung passiert nichts')
|
||
fx.antworten.push(antwort(daten({ plaene: [plan(P1)], ausnahmen: [ausnahmen[0]] }), 4))
|
||
await fx.klick(fx.finde(frage, 'button[type="submit"]'))
|
||
const gesendet = puts(fx)[0].body.daten
|
||
assert.deepEqual(gesendet.plaene.map((p) => p.id), [P1])
|
||
assert.deepEqual(gesendet.ausnahmen.map((a) => a.id), ['x_alle0001'], 'die Ausnahmen des Plans gehen mit, die übrigen bleiben')
|
||
assert.equal(planKarte(fx, root, P2), null)
|
||
assert.match(meldung(fx, root), /gelöscht/)
|
||
}))
|
||
|
||
test('Plan löschen: Abbrechen schließt die Rückfrage ohne zu senden; ein einzelner Plan zeigt den Grund „letzter Zeitplan“', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx, { start: stand(daten({ plaene: [plan(P1), plan(P2)] })) })
|
||
await fx.klick(fx.finde(planKarte(fx, root, P2), '[data-aktion="entfernen"]'))
|
||
const frage = fx.finde(planKarte(fx, root, P2), '.tm-frage-box')
|
||
assert.doesNotMatch(fx.text(frage), /Ausnahmen, die nur für diesen Plan/, 'ohne Ausnahmen des Plans kein Zusatz')
|
||
await fx.klick(fx.finde(frage, 'button[type="button"]'))
|
||
assert.equal(fx.finde(planKarte(fx, root, P2), '.tm-frage-box'), null)
|
||
assert.equal(puts(fx).length, 0)
|
||
|
||
const allein = await oeffne(fx)
|
||
assert.match(fx.text(planKarte(fx, allein.root, P1)), /Der letzte Zeitplan kann nicht gelöscht werden/)
|
||
}))
|
||
|
||
test('Plan speichern: Name ändern lässt den Kurznamen stehen; die Auswahl „offen“ bleibt nach dem Neuzeichnen', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx, { start: stand(daten({ plaene: [plan(P1), plan(P2)] })) })
|
||
const karte = planKarte(fx, root, P2)
|
||
assert.equal(karte.hasAttribute('open'), false, 'weitere Pläne sind zugeklappt')
|
||
karte.setAttribute('open', '')
|
||
await fx.sende(karte, 'toggle', { bubbles: false })
|
||
await fx.setze(fx.finde(karte, `[data-feld="plaene.${P2}.name"]`), 'Küche neu')
|
||
fx.antworten.push(antwort(daten({ plaene: [plan(P1), plan(P2, { name: 'Küche neu' })] }), 4))
|
||
await fx.klick(speichernKnopf(fx, root, P2))
|
||
const gesendet = puts(fx)[0].body.daten.plaene[1]
|
||
assert.equal(gesendet.name, 'Küche neu')
|
||
assert.equal(gesendet.kurz, 'kueche', 'der Kurzname ändert sich nicht')
|
||
assert.equal(planKarte(fx, root, P2).hasAttribute('open'), true, 'der Plan bleibt aufgeklappt')
|
||
}))
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------- Feiertage
|
||
test('Feiertage: Bundesland, Liste mit Namen, „bitte prüfen“ markiert, Hinweis auf Gemeinde-Feiertage', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
const bereich = abschnitt(fx, root, 'feiertage')
|
||
assert.ok(bereich)
|
||
const land = fx.finde(bereich, 'select[data-feld="land"]')
|
||
assert.equal(land.value, 'RP')
|
||
assert.equal(fx.alle(land, 'option').length, 16)
|
||
assert.match(fx.text(land), /Rheinland-Pfalz/)
|
||
assert.match(fx.text(bereich), /Gemeinde-Feiertage sind nicht enthalten/)
|
||
const zeilen = fx.alle(bereich, '[data-feiertag]')
|
||
assert.deepEqual(zeilen.map((z) => z.getAttribute('data-feiertag')), FEIERTAGE.map((f) => f.tag))
|
||
assert.match(fx.text(zeilen[1]), /Erster Weihnachtstag/)
|
||
assert.match(fx.text(zeilen[0]), /Heiligabend/)
|
||
assert.match(fx.text(zeilen[0]), /bitte prüfen/i)
|
||
assert.doesNotMatch(fx.text(zeilen[1]), /bitte prüfen/i)
|
||
// Vorgabe: gesetzlicher Feiertag geschlossen, Prüf-Tag nach dem Wochenplan
|
||
assert.equal(fx.finde(zeilen[1], 'select').value, '')
|
||
assert.match(fx.text(fx.finde(zeilen[1], 'select')), /geschlossen/)
|
||
assert.match(fx.text(fx.finde(zeilen[0], 'select')), /Wochenplan/)
|
||
assert.deepEqual(optionen(fx, fx.finde(zeilen[0], 'select')), ['', 'zu', 'zeiten', 'rund', 'vereinbarung'])
|
||
assert.deepEqual(optionen(fx, fx.finde(zeilen[1], 'select')), ['', 'zeiten', 'rund', 'vereinbarung'])
|
||
}))
|
||
|
||
test('Feiertage speichern: nur Abweichungen von der Vorgabe gehen in daten.feiertage, alles andere bleibt unverändert', mitDom({}, async (fx) => {
|
||
const vorhanden = { '2027-01-01': { modus: 'vereinbarung', fenster: [] }, '2030-05-01': { modus: 'rund', fenster: [] } } // 2030: nicht in der Liste, bleibt
|
||
const { root } = await oeffne(fx, { start: stand(daten({ feiertage: vorhanden })) })
|
||
const zeile = (tag) => fx.finde(root, `[data-feiertag="${tag}"]`)
|
||
assert.equal(fx.finde(zeile('2027-01-01'), 'select').value, 'vereinbarung', 'gespeicherte Abweichung ist vorbelegt')
|
||
// 25.12. mit eigenen Zeiten
|
||
await fx.setze(fx.finde(zeile('2026-12-25'), 'select'), 'zeiten')
|
||
const [von, bis] = zeitFelder(fx, zeile('2026-12-25'))
|
||
await fx.setze(von, '10:00')
|
||
await fx.setze(bis, '12:00')
|
||
// Heiligabend geschlossen (Abweichung vom Wochenplan)
|
||
await fx.setze(fx.finde(zeile('2026-12-24'), 'select'), 'zu')
|
||
// Neujahr zurück auf die Vorgabe
|
||
await fx.setze(fx.finde(zeile('2027-01-01'), 'select'), '')
|
||
fx.antworten.push(antwort(daten(), 4))
|
||
await fx.klick(fx.finde(abschnitt(fx, root, 'feiertage'), 'form[aria-label="Feiertage"] button[type="submit"]'))
|
||
assert.deepEqual(puts(fx)[0].body.daten.feiertage, {
|
||
'2026-12-24': { modus: 'zu', fenster: [] },
|
||
'2026-12-25': { modus: 'zeiten', fenster: [['10:00', '12:00']] },
|
||
'2030-05-01': { modus: 'rund', fenster: [] },
|
||
})
|
||
assert.match(meldung(fx, root), /Feiertage gespeichert/)
|
||
}))
|
||
|
||
test('Feiertage: „geschlossen“ an einem gesetzlichen Feiertag ist die Vorgabe und wird nicht gespeichert; eigene Zeiten ohne Fenster sind ein Fehler am Tag', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx, { start: stand(daten({ feiertage: { '2026-12-25': { modus: 'zu', fenster: [] } } })) })
|
||
assert.equal(fx.finde(fx.finde(root, '[data-feiertag="2026-12-25"]'), 'select').value, '', 'gespeichertes „zu“ ist die Vorgabe')
|
||
await fx.setze(fx.finde(fx.finde(root, '[data-feiertag="2027-01-01"]'), 'select'), 'zeiten')
|
||
await fx.klick(fx.finde(root, 'form[aria-label="Feiertage"] button[type="submit"]'))
|
||
assert.equal(puts(fx).length, 0)
|
||
assert.match(fx.text(fehlerAn(fx, root, 'feiertage.2027-01-01')), /mindestens ein Zeitfenster/)
|
||
await fx.setze(fx.finde(fx.finde(root, '[data-feiertag="2027-01-01"]'), 'select'), '')
|
||
fx.antworten.push(antwort(daten(), 4))
|
||
await fx.klick(fx.finde(root, 'form[aria-label="Feiertage"] button[type="submit"]'))
|
||
assert.deepEqual(puts(fx)[0].body.daten.feiertage, {}, 'das gespeicherte „zu“ fällt weg')
|
||
}))
|
||
|
||
test('Bundesland speichern: PUT mit land, danach zeigt die Liste die Feiertage der Antwort', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
await fx.setze(fx.finde(root, 'select[data-feld="land"]'), 'BY')
|
||
const bayern = [{ tag: '2026-12-25', name: 'Erster Weihnachtstag', art: 'feiertag' }, { tag: '2027-01-06', name: 'Heilige Drei Könige', art: 'feiertag' }]
|
||
fx.antworten.push(antwort(daten({ land: 'BY' }), 4, { feiertage: bayern }))
|
||
await fx.klick(fx.finde(root, 'form[aria-label="Bundesland"] button[type="submit"]'))
|
||
assert.equal(puts(fx)[0].body.daten.land, 'BY')
|
||
assert.deepEqual(fx.alle(root, '[data-feiertag]').map((z) => z.getAttribute('data-feiertag')), ['2026-12-25', '2027-01-06'])
|
||
assert.equal(fx.finde(root, 'select[data-feld="land"]').value, 'BY')
|
||
}))
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------- Ausnahmen
|
||
const urlaubsformular = (fx, root) => fx.finde(root, 'form[aria-label="Betriebsurlaub eintragen"]')
|
||
const feldIn = (fx, form, name) => fx.finde(form, (el) => el.nodeType === 1 && (el.getAttribute('data-feld') || '').endsWith(`.${name}`))
|
||
const modusAuswahl = (fx, form) => fx.finde(form, (el) => el.tagName === 'SELECT' && (el.getAttribute('data-feld') || '').endsWith('.modus'))
|
||
|
||
test('Urlaub anlegen: von–bis und Text, Geltung „alle Pläne“ (Vorgabe) oder ein Plan; PUT mit neuer Ausnahme', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx, { start: stand(daten({ plaene: [plan(P1), plan(P2)] })) })
|
||
await fx.klick(fx.finde(root, '[data-werkzeug="urlaub-neu"]'))
|
||
const form = urlaubsformular(fx, root)
|
||
const von = tagPlus(HEUTE, 20)
|
||
const bis = tagPlus(HEUTE, 25)
|
||
await fx.setze(feldIn(fx, form, 'von'), von)
|
||
await fx.setze(feldIn(fx, form, 'bis'), bis)
|
||
await fx.setze(feldIn(fx, form, 'text'), 'Sommerpause')
|
||
const geltung = feldIn(fx, form, 'plan')
|
||
assert.deepEqual(optionen(fx, geltung), ['', P1, P2])
|
||
assert.match(fx.text(geltung), /Alle Pläne/)
|
||
assert.equal(geltung.value, '', 'Vorgabe: alle Pläne')
|
||
fx.antworten.push(antwort(daten(), 4))
|
||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||
const a = puts(fx)[0].body.daten.ausnahmen
|
||
assert.equal(a.length, 1)
|
||
assert.match(a[0].id, /^x_[a-z0-9]{8}$/)
|
||
assert.deepEqual({ ...a[0], id: 'x' }, { id: 'x', art: 'urlaub', von, bis, text: 'Sommerpause', modus: 'zu', fenster: [], plan: null })
|
||
}))
|
||
|
||
test('Urlaub ab 2 Tagen: „Hinweis-Leiste dazu anlegen?“ – Ja legt den vorbefüllten Hinweis an und speichert, Nein lässt es', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
await fx.klick(fx.finde(root, '[data-werkzeug="urlaub-neu"]'))
|
||
const form = urlaubsformular(fx, root)
|
||
const von = tagPlus(HEUTE, 20)
|
||
const bis = tagPlus(HEUTE, 25)
|
||
await fx.setze(feldIn(fx, form, 'von'), von)
|
||
await fx.setze(feldIn(fx, form, 'bis'), bis)
|
||
const urlaub = { id: 'x_urlaub01', art: 'urlaub', von, bis, text: '', modus: 'zu', fenster: [], plan: null }
|
||
fx.antworten.push(antwort(daten({ ausnahmen: [urlaub] }), 4))
|
||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||
const frage = fx.finde(root, '[data-frage="hinweis"]')
|
||
assert.ok(frage, 'die Frage steht in der Seite')
|
||
assert.match(fx.text(frage), /Hinweis-Leiste dazu anlegen\?/)
|
||
const ymd = (tag) => `${tag.slice(8)}.${tag.slice(5, 7)}.`
|
||
assert.ok(fx.text(frage).includes(`Betriebsurlaub vom ${ymd(von)} bis ${ymd(bis)}`), 'der Text des Vorschlags steht in der Frage')
|
||
assert.equal(puts(fx).length, 1, 'bis zur Antwort nur der Urlaub')
|
||
|
||
const vorschlag = { id: 'h_xxxxxxxx', art: 'urlaub', text: `Betriebsurlaub vom ${ymd(von)} bis ${ymd(bis)}`, knopf: null, von: tagPlus(von, -7), bis }
|
||
fx.antworten.push(antwort(daten({ ausnahmen: [urlaub], hinweise: [vorschlag] }), 5))
|
||
await fx.klick(fx.finde(frage, '[data-aktion="hinweis-ja"]'))
|
||
assert.equal(puts(fx).length, 2)
|
||
const gesendet = puts(fx)[1].body
|
||
assert.equal(gesendet.version, 4)
|
||
assert.deepEqual(gesendet.daten.ausnahmen.map((a) => a.id), ['x_urlaub01'], 'der Urlaub bleibt')
|
||
assert.equal(gesendet.daten.hinweise.length, 1)
|
||
assert.match(gesendet.daten.hinweise[0].id, /^h_[a-z0-9]{8}$/)
|
||
assert.deepEqual({ ...gesendet.daten.hinweise[0], id: 'h' }, { id: 'h', art: 'urlaub', text: vorschlag.text, knopf: null, von: tagPlus(von, -7), bis })
|
||
assert.equal(fx.finde(root, '[data-frage="hinweis"]'), null, 'die Frage ist beantwortet')
|
||
assert.match(meldung(fx, root), /Hinweis-Leiste/)
|
||
|
||
// Nein: kein weiterer Speichervorgang
|
||
await fx.klick(fx.finde(root, '[data-werkzeug="urlaub-neu"]'))
|
||
const zweites = urlaubsformular(fx, root)
|
||
await fx.setze(feldIn(fx, zweites, 'von'), tagPlus(HEUTE, 40))
|
||
await fx.setze(feldIn(fx, zweites, 'bis'), tagPlus(HEUTE, 42))
|
||
fx.antworten.push(antwort(daten({ ausnahmen: [urlaub] }), 6))
|
||
await fx.klick(fx.finde(zweites, 'button[type="submit"]'))
|
||
const davor = puts(fx).length
|
||
await fx.klick(fx.finde(fx.finde(root, '[data-frage="hinweis"]'), '[data-aktion="hinweis-nein"]'))
|
||
assert.equal(puts(fx).length, davor)
|
||
assert.equal(fx.finde(root, '[data-frage="hinweis"]'), null)
|
||
}))
|
||
|
||
test('Urlaub: keine Frage, wenn es denselben Hinweis schon gibt oder sich beim Bearbeiten der Zeitraum nicht ändert', mitDom({}, async (fx) => {
|
||
const von = tagPlus(HEUTE, 20)
|
||
const bis = tagPlus(HEUTE, 25)
|
||
const vorschlag = hinweisVorschlag({ von, bis })
|
||
const hinweis = { id: 'h_schonda1', art: 'urlaub', text: vorschlag.text, knopf: null, von: vorschlag.von, bis: vorschlag.bis }
|
||
const urlaub = { id: 'x_urlaub01', art: 'urlaub', von, bis, text: '', modus: 'zu', fenster: [], plan: null }
|
||
// 1. ein gleicher Hinweis existiert schon
|
||
const { root } = await oeffne(fx, { start: stand(daten({ hinweise: [hinweis] })) })
|
||
await fx.klick(fx.finde(root, '[data-werkzeug="urlaub-neu"]'))
|
||
const form = urlaubsformular(fx, root)
|
||
await fx.setze(feldIn(fx, form, 'von'), von)
|
||
await fx.setze(feldIn(fx, form, 'bis'), bis)
|
||
fx.antworten.push(antwort(daten({ hinweise: [hinweis], ausnahmen: [urlaub] }), 4))
|
||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||
assert.equal(puts(fx).length, 1)
|
||
assert.equal(fx.finde(root, '[data-frage="hinweis"]'), null)
|
||
// 2. der Urlaub wird nur im Text geändert: keine Frage; ändert sich der Zeitraum (ohne Hinweis dazu): Frage
|
||
const zwei = await oeffne(fx, { start: stand(daten({ ausnahmen: [urlaub] })) })
|
||
const zeile = fx.finde(zwei.root, 'li[data-id="x_urlaub01"]')
|
||
await fx.klick(fx.finde(zeile, '[data-aktion="bearbeiten"]'))
|
||
await fx.setze(feldIn(fx, fx.finde(zeile, 'form'), 'text'), 'Sommer')
|
||
fx.antworten.push(antwort(daten({ ausnahmen: [{ ...urlaub, text: 'Sommer' }] }), 4))
|
||
await fx.klick(fx.finde(fx.finde(zeile, 'form'), 'button[type="submit"]'))
|
||
assert.equal(fx.finde(zwei.root, '[data-frage="hinweis"]'), null, 'nur der Text hat sich geändert')
|
||
const zeile2 = fx.finde(zwei.root, 'li[data-id="x_urlaub01"]')
|
||
await fx.klick(fx.finde(zeile2, '[data-aktion="bearbeiten"]'))
|
||
await fx.setze(feldIn(fx, fx.finde(zeile2, 'form'), 'bis'), tagPlus(bis, 3))
|
||
fx.antworten.push(antwort(daten({ ausnahmen: [{ ...urlaub, text: 'Sommer', bis: tagPlus(bis, 3) }] }), 5))
|
||
await fx.klick(fx.finde(fx.finde(zeile2, 'form'), 'button[type="submit"]'))
|
||
assert.ok(fx.finde(zwei.root, '[data-frage="hinweis"]'), 'der Zeitraum hat sich geändert')
|
||
}))
|
||
|
||
test('Urlaub von einem Tag: keine Frage nach der Hinweis-Leiste', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
await fx.klick(fx.finde(root, '[data-werkzeug="urlaub-neu"]'))
|
||
const form = urlaubsformular(fx, root)
|
||
const tag = tagPlus(HEUTE, 10)
|
||
await fx.setze(feldIn(fx, form, 'von'), tag)
|
||
await fx.setze(feldIn(fx, form, 'bis'), tag)
|
||
fx.antworten.push(antwort(daten(), 4))
|
||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||
assert.equal(puts(fx).length, 1)
|
||
assert.equal(fx.finde(root, '[data-frage="hinweis"]'), null)
|
||
assert.match(meldung(fx, root), /eingetragen/)
|
||
}))
|
||
|
||
test('Hinweis-Leiste anlegen scheitert (400, zu viele Hinweise): Fehler an der Frage, die Frage bleibt', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
await fx.klick(fx.finde(root, '[data-werkzeug="urlaub-neu"]'))
|
||
const form = urlaubsformular(fx, root)
|
||
await fx.setze(feldIn(fx, form, 'von'), tagPlus(HEUTE, 20))
|
||
await fx.setze(feldIn(fx, form, 'bis'), tagPlus(HEUTE, 25))
|
||
fx.antworten.push(antwort(daten(), 4))
|
||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||
fx.antworten.push(fehler(400, 'Bitte prüfe deine Eingaben.', { fehler: { hinweise: 'Es sind höchstens 20 Hinweise möglich.' } }))
|
||
await fx.klick(fx.finde(fx.finde(root, '[data-frage="hinweis"]'), '[data-aktion="hinweis-ja"]'))
|
||
const frage = fx.finde(root, '[data-frage="hinweis"]')
|
||
assert.ok(frage, 'die Frage bleibt')
|
||
assert.match(fx.text(frage), /höchstens 20 Hinweise/)
|
||
}))
|
||
|
||
test('Sondertag: Datum, geschlossen oder Zeiten, Text, Geltung; PUT mit bis = von', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
await fx.klick(fx.finde(root, '[data-werkzeug="sondertag-neu"]'))
|
||
const form = fx.finde(root, 'form[aria-label="Sondertag eintragen"]')
|
||
const tag = tagPlus(HEUTE, 14)
|
||
await fx.setze(feldIn(fx, form, 'von'), tag)
|
||
assert.equal(feldIn(fx, form, 'plan'), null, 'mit einem Plan gibt es keine Wahl der Geltung')
|
||
assert.equal(feldIn(fx, form, 'bis'), null, 'ein Sondertag hat nur ein Datum')
|
||
const modus = modusAuswahl(fx, form)
|
||
assert.deepEqual(optionen(fx, modus), ['zu', 'zeiten', 'rund', 'vereinbarung'])
|
||
assert.equal(modus.value, 'zu', 'Vorgabe: geschlossen')
|
||
assert.equal(fx.finde(form, '.zt-zeiten').hidden, true)
|
||
await fx.setze(modus, 'zeiten')
|
||
assert.equal(fx.finde(form, '.zt-zeiten').hidden, false)
|
||
const [von1, bis1] = zeitFelder(fx, form)
|
||
await fx.setze(von1, '10:00')
|
||
await fx.setze(bis1, '14:00')
|
||
await fx.setze(feldIn(fx, form, 'text'), 'Tag der offenen Tür')
|
||
fx.antworten.push(antwort(daten(), 4))
|
||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||
const a = puts(fx)[0].body.daten.ausnahmen[0]
|
||
assert.deepEqual({ ...a, id: 'x' }, { id: 'x', art: 'sondertag', von: tag, bis: tag, text: 'Tag der offenen Tür', modus: 'zeiten', fenster: [['10:00', '14:00']], plan: null })
|
||
assert.equal(fx.finde(root, '[data-frage="hinweis"]'), null, 'für einen Sondertag fragt der Editor nicht nach einer Leiste')
|
||
}))
|
||
|
||
test('Ausnahmen: Liste kommend und vergangen, Bearbeiten und Entfernen (je ein Speichern)', mitDom({}, async (fx) => {
|
||
const vor = tagPlus(HEUTE, -3)
|
||
const spaeter = tagPlus(HEUTE, 30)
|
||
const bald = tagPlus(HEUTE, 5)
|
||
const ausnahmen = [
|
||
{ id: 'x_spaeter1', art: 'sondertag', von: spaeter, bis: spaeter, text: 'Später', modus: 'zeiten', fenster: [['10:00', '14:00']], plan: null },
|
||
{ id: 'x_vorbei01', art: 'urlaub', von: vor, bis: vor, text: 'Vorbei', modus: 'zu', fenster: [], plan: null },
|
||
{ id: 'x_bald0001', art: 'urlaub', von: bald, bis: tagPlus(bald, 2), text: 'Bald', modus: 'zu', fenster: [], plan: P2 },
|
||
]
|
||
const { root } = await oeffne(fx, { start: stand(daten({ plaene: [plan(P1), plan(P2)], ausnahmen })) })
|
||
const bereich = abschnitt(fx, root, 'ausnahmen')
|
||
const kommend = fx.alle(fx.finde(bereich, 'ul[aria-label="Kommende Ausnahmen"]'), 'li[data-id]')
|
||
assert.deepEqual(kommend.map((li) => li.getAttribute('data-id')), ['x_bald0001', 'x_spaeter1'], 'nach Beginn sortiert')
|
||
assert.match(fx.text(kommend[0]), /Bald/)
|
||
assert.match(fx.text(kommend[0]), /Küche/, 'die Geltung steht in der Zeile')
|
||
assert.match(fx.text(kommend[1]), /10:00–14:00/)
|
||
const vergangen = fx.finde(bereich, 'details.tm-details')
|
||
assert.match(fx.text(fx.finde(vergangen, 'summary')), /Vergangene Ausnahmen \(1\)/)
|
||
assert.match(fx.text(vergangen), /Vorbei/)
|
||
|
||
// Bearbeiten: Text ändern
|
||
await fx.klick(fx.finde(kommend[1], '[data-aktion="bearbeiten"]'))
|
||
const form = fx.finde(kommend[1], 'form')
|
||
await fx.setze(feldIn(fx, form, 'text'), 'Später neu')
|
||
const nachBearbeiten = ausnahmen.map((a) => (a.id === 'x_spaeter1' ? { ...a, text: 'Später neu' } : a))
|
||
fx.antworten.push(antwort(daten({ plaene: [plan(P1), plan(P2)], ausnahmen: nachBearbeiten }), 4))
|
||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||
const gesendet = puts(fx)[0].body.daten.ausnahmen
|
||
assert.deepEqual(gesendet.map((a) => a.id), ['x_spaeter1', 'x_vorbei01', 'x_bald0001'], 'Reihenfolge und Stelle bleiben, gesucht wird per ID')
|
||
assert.equal(gesendet[0].text, 'Später neu')
|
||
assert.equal(gesendet[2].plan, P2)
|
||
|
||
// Entfernen
|
||
const zeile = fx.finde(root, 'li[data-id="x_bald0001"]')
|
||
await fx.klick(fx.finde(zeile, '[data-aktion="bearbeiten"]'))
|
||
await fx.klick(fx.finde(zeile, '[data-aktion="entfernen"]'))
|
||
fx.antworten.push(antwort(daten({ plaene: [plan(P1), plan(P2)], ausnahmen: nachBearbeiten.filter((a) => a.id !== 'x_bald0001') }), 5))
|
||
await fx.klick(fx.finde(fx.finde(zeile, '.tm-frage-box'), 'button[type="submit"]'))
|
||
assert.deepEqual(puts(fx)[1].body.daten.ausnahmen.map((a) => a.id), ['x_spaeter1', 'x_vorbei01'])
|
||
assert.equal(fx.finde(root, 'li[data-id="x_bald0001"]'), null)
|
||
}))
|
||
|
||
test('Ausnahmen: bei 100 Einträgen sind beide Knöpfe gesperrt', mitDom({}, async (fx) => {
|
||
const viele = Array.from({ length: ZEITEN_LIMITS.ausnahmen }, (_, i) => ({ id: `x_aaaa${String(i).padStart(4, '0')}`, art: 'sondertag', von: tagPlus(HEUTE, 1 + (i % 300)), bis: tagPlus(HEUTE, 1 + (i % 300)), text: '', modus: 'zu', fenster: [], plan: null }))
|
||
const { root } = await oeffne(fx, { start: stand(daten({ ausnahmen: viele })) })
|
||
assert.equal(fx.finde(root, '[data-werkzeug="urlaub-neu"]').disabled, true)
|
||
assert.equal(fx.finde(root, '[data-werkzeug="sondertag-neu"]').disabled, true)
|
||
assert.match(fx.text(abschnitt(fx, root, 'ausnahmen')), /Mehr als 100 Ausnahmen sind nicht möglich/)
|
||
}))
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------- Fehler des Servers
|
||
test('409 beim Speichern: Meldung „Inzwischen geändert – bitte neu laden.“ und Neu-laden-Knopf; der Knopf lädt und zeichnet neu', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
fx.antworten.push(fehler(409, 'Inzwischen geändert – bitte neu laden.', { code: 'ZEITEN_VERALTET' }))
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
const box = fx.finde(root, '.tm-box-warn')
|
||
assert.equal(box.hidden, false)
|
||
assert.match(fx.text(box), /Inzwischen geändert – bitte neu laden\./)
|
||
const neuLaden = fx.finde(box, 'button')
|
||
assert.equal(fx.text(neuLaden), 'Neu laden')
|
||
assert.equal(puts(fx).length, 1)
|
||
// die neue Fassung hat einen anderen Namen
|
||
const neu = daten({ plaene: [plan(P1, { name: 'Praxis' })] })
|
||
fx.antworten.push({ status: 200, body: stand(neu, { version: 7 }) })
|
||
await fx.klick(neuLaden)
|
||
assert.equal(gets(fx).length, 2, 'die Daten werden neu geholt')
|
||
assert.equal(fx.finde(root, `[data-feld="plaene.${P1}.name"]`).value, 'Praxis')
|
||
assert.equal(fx.finde(root, '.tm-box-warn').hidden, true)
|
||
fx.antworten.push(antwort(neu, 8))
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
assert.equal(puts(fx)[1].body.version, 7, 'weiter mit der neuen Version')
|
||
}))
|
||
|
||
test('400: Fehler am richtigen Feld (plaene.<id>.tage.3), Fokus dorthin, Fremdes allgemein mit dem Namen des Plans', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
fx.antworten.push(fehler(400, 'Bitte prüfe deine Eingaben.', { fehler: {
|
||
[`plaene.${P1}.tage.3`]: 'Die Zeitfenster überschneiden sich.',
|
||
[`plaene.${P1}.name`]: 'Höchstens 60 Zeichen.',
|
||
'ausnahmen.x_fremd001.von': 'Bitte gib ein gültiges Datum an.',
|
||
daten: 'Zu viele Einträge – bitte lösche alte Ausnahmen oder Hinweise.',
|
||
} }))
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
assert.equal(fx.text(fehlerAn(fx, root, `plaene.${P1}.tage.3`)), 'Die Zeitfenster überschneiden sich.')
|
||
assert.equal(fx.text(fehlerAn(fx, root, `plaene.${P1}.name`)), 'Höchstens 60 Zeichen.')
|
||
const feld = fx.finde(root, `[data-feld="plaene.${P1}.tage.3"]`)
|
||
assert.equal(feld.getAttribute('aria-invalid'), 'true')
|
||
assert.equal(fx.finde(root, `[data-feld="plaene.${P1}.name"]`).getAttribute('aria-invalid'), 'true')
|
||
assert.ok(planKarte(fx, root, P1).contains(fx.finde(root, '[data-fehler="_"]')), 'Platz für allgemeine Fehler')
|
||
assert.match(fx.text(fx.finde(planKarte(fx, root, P1), '[data-fehler="_"]')), /Zu viele Einträge/)
|
||
assert.match(fx.text(fx.finde(planKarte(fx, root, P1), '[data-fehler="_"]')), /gültiges Datum/)
|
||
assert.equal(document.activeElement, fx.finde(root, '[aria-invalid="true"]'), 'der Fokus geht zum ersten Feld mit Fehler')
|
||
// ein Speichern mit Erfolg räumt die Fehler ab
|
||
fx.antworten.push(antwort(daten(), 4))
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
assert.equal(fx.finde(root, '[aria-invalid="true"]'), null)
|
||
}))
|
||
|
||
test('400 in Ausnahmen und Feiertagen: Fehler an Datum, Fenster und Tag (Pfade mit ID)', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
await fx.klick(fx.finde(root, '[data-werkzeug="sondertag-neu"]'))
|
||
const form = fx.finde(root, 'form[aria-label="Sondertag eintragen"]')
|
||
await fx.setze(feldIn(fx, form, 'von'), tagPlus(HEUTE, 3))
|
||
await fx.setze(modusAuswahl(fx, form), 'zeiten')
|
||
await fx.setze(zeitFelder(fx, form)[0], '09:00')
|
||
await fx.setze(zeitFelder(fx, form)[1], '12:00')
|
||
fx.antworten.push(fehler(400, 'Bitte prüfe deine Eingaben.', { fehler: {} }))
|
||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||
const id = puts(fx)[0].body.daten.ausnahmen[0].id
|
||
fx.antworten.push(fehler(400, 'Bitte prüfe deine Eingaben.', { fehler: { [`ausnahmen.${id}.von`]: 'Bitte gib ein gültiges Datum an.', [`ausnahmen.${id}.fenster`]: 'Die Zeitfenster überschneiden sich.' } }))
|
||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||
assert.equal(fx.text(fehlerAn(fx, form, `ausnahmen.${id}.von`)), 'Bitte gib ein gültiges Datum an.')
|
||
assert.equal(fx.text(fehlerAn(fx, form, `ausnahmen.${id}.fenster`)), 'Die Zeitfenster überschneiden sich.')
|
||
// Feiertag: Pfad feiertage.<tag>
|
||
await fx.setze(fx.finde(fx.finde(root, '[data-feiertag="2026-12-25"]'), 'select'), 'rund')
|
||
fx.antworten.push(fehler(400, 'Bitte prüfe deine Eingaben.', { fehler: { 'feiertage.2026-12-25': 'Bitte wähle „geschlossen“, „geöffnet“, „rund um die Uhr“ oder „nach Vereinbarung“.' } }))
|
||
await fx.klick(fx.finde(root, 'form[aria-label="Feiertage"] button[type="submit"]'))
|
||
assert.match(fx.text(fehlerAn(fx, root, 'feiertage.2026-12-25')), /Bitte wähle/)
|
||
}))
|
||
|
||
test('Sonstige Fehler (429, 500, offline) stehen als Meldung über dem Editor, nie als HTML', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
const bose = '<img src=x onerror=alert(1)>'
|
||
fx.antworten.push(fehler(429, bose))
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
assert.equal(meldung(fx, root), bose)
|
||
assert.equal(fx.alle(root, 'img').length, 0)
|
||
assert.equal(fx.finde(root, '[data-meldung="editor"]').getAttribute('role'), 'alert')
|
||
fx.antworten.push(() => { throw new TypeError('x') })
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
assert.match(meldung(fx, root), /Fehler|Verbindung|Interner/)
|
||
// ein Erfolg ersetzt die Fehlermeldung durch eine ruhige Meldung
|
||
fx.antworten.push(antwort(daten(), 4))
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
assert.equal(fx.finde(root, '[data-meldung="editor"]').getAttribute('role'), 'status')
|
||
}))
|
||
|
||
test('Laden schlägt fehl: Fehlerkarte mit „Noch einmal versuchen“; eine unbrauchbare Antwort ebenso', mitDom({}, async (fx) => {
|
||
const root = h('div', { class: 'zt-editor', 'data-state': 'active' })
|
||
document.body.append(root)
|
||
fx.antworten.push(fehler(500, 'Interner Serverfehler'))
|
||
await editor.renderEditor(root, { projectId: 'p1' })
|
||
assert.match(fx.text(root), /Interner Serverfehler/)
|
||
const nochmal = fx.finde(root, 'button')
|
||
assert.match(fx.text(nochmal), /Noch einmal versuchen/)
|
||
fx.antworten.push({ status: 200, body: { irgendwas: true } })
|
||
await fx.klick(nochmal)
|
||
assert.match(fx.text(root), /konnten gerade nicht geladen werden/)
|
||
fx.antworten.push({ status: 200, body: stand(daten()) })
|
||
await fx.klick(fx.finde(root, 'button'))
|
||
assert.ok(planKarte(fx, root, P1), 'der dritte Versuch zeigt den Editor')
|
||
}))
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------- Kundenansicht, nur lesen, R12
|
||
test('Kundenansicht: Hinweis-Box, Speichern ist erlaubt', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx, { start: stand(daten(), { kundenansicht: true }) })
|
||
const box = fx.finde(root, '[data-hinweis="kundenansicht"]')
|
||
assert.ok(box)
|
||
assert.match(fx.text(box), /Kundenansicht/)
|
||
assert.match(fx.text(box), /Protokoll/)
|
||
assert.equal(speichernKnopf(fx, root, P1).disabled, false)
|
||
fx.antworten.push(antwort(daten(), 4))
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
assert.equal(puts(fx).length, 1)
|
||
// ohne Kundenansicht keine Box
|
||
const kunde = await oeffne(fx)
|
||
assert.equal(fx.finde(kunde.root, '[data-hinweis="kundenansicht"]'), null)
|
||
// auch die Angabe der Hülle genügt, falls die Antwort sie nicht trägt
|
||
const huelle = await oeffne(fx, { kundenansicht: true })
|
||
assert.ok(fx.finde(huelle.root, '[data-hinweis="kundenansicht"]'))
|
||
}))
|
||
|
||
test('abgeschaltet (disabled mit Daten): alles gesperrt, keine Knöpfe zum Ändern, Hinweis „nur ansehen“', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx, { state: 'disabled', start: stand(daten({ plaene: [plan(P1), plan(P2)], ausnahmen: [{ id: 'x_aaaaaaa1', art: 'urlaub', von: tagPlus(HEUTE, 3), bis: tagPlus(HEUTE, 4), text: 'Urlaub', modus: 'zu', fenster: [], plan: null }] }), { state: 'disabled' }) })
|
||
assert.ok(fx.finde(root, '[data-hinweis="nurlesen"]'))
|
||
assert.equal(fx.finde(root, '[data-aktion="speichern"]'), null)
|
||
assert.equal(fx.finde(root, '[data-aktion="haupt"]'), null)
|
||
assert.equal(fx.finde(root, '[data-aktion="entfernen"]'), null)
|
||
assert.equal(fx.finde(root, '[data-werkzeug]'), null)
|
||
assert.equal(fx.finde(root, 'button[type="submit"]'), null)
|
||
for (const feld of fx.alle(root, 'input, select')) assert.equal(feld.matches(':disabled'), true, `${feld.tagName} ist gesperrt`)
|
||
assert.equal(puts(fx).length, 0)
|
||
}))
|
||
|
||
test('R12: die Kontaktdaten gehen unverändert zurück (kein neues bestaetigtAt), auch wenn nur ein Plan gespeichert wird', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
fx.antworten.push(antwort(daten(), 4))
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
assert.deepEqual(puts(fx)[0].body.daten.kontakt, KONTAKT)
|
||
// keine Kontaktdaten: der Editor erfindet keine
|
||
const ohne = daten()
|
||
delete ohne.kontakt
|
||
const zwei = await oeffne(fx, { start: stand(ohne) })
|
||
fx.antworten.push(antwort(daten(), 4))
|
||
await fx.klick(speichernKnopf(fx, zwei.root, P1))
|
||
assert.equal(Object.hasOwn(puts(fx)[1].body.daten, 'kontakt'), false)
|
||
}))
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------- Robustheit
|
||
test('Fremdtext (Plan-, Feiertags-, Ausnahme- und Hinweisnamen) kommt nur als Text in die Seite', mitDom({}, async (fx) => {
|
||
const bose = '<img src=x onerror=alert(1)>'
|
||
const tag = tagPlus(HEUTE, 9)
|
||
const d = daten({
|
||
plaene: [plan(P1, { name: bose })],
|
||
ausnahmen: [{ id: 'x_bose0001', art: 'urlaub', von: tag, bis: tag, text: bose, modus: 'zu', fenster: [], plan: null }],
|
||
})
|
||
const { root } = await oeffne(fx, { start: stand(d, { feiertage: [{ tag: '2026-12-25', name: bose, art: 'feiertag' }] }) })
|
||
assert.equal(fx.alle(root, 'img').length, 0)
|
||
assert.ok(fx.text(root).includes(bose))
|
||
assert.equal(fx.finde(root, `[data-feld="plaene.${P1}.name"]`).value, bose)
|
||
fx.antworten.push(fehler(400, 'x', { fehler: { [`ausnahmen.x_bose0001.text`]: bose } }))
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
assert.equal(fx.alle(root, 'img').length, 0)
|
||
}))
|
||
|
||
test('IDs wie „__proto__“ und „constructor“ sind gewöhnliche Texte: Speichern, Fehler und Löschen finden den richtigen Plan', mitDom({}, async (fx) => {
|
||
const alt = daten({ plaene: [plan('__proto__', { name: 'Eins' }), plan('constructor', { haupt: false, kurz: 'zwei', name: 'Zwei' })] })
|
||
const { root } = await oeffne(fx, { start: stand(alt) })
|
||
const karte = fx.alle(root, 'details[data-plan]').find((k) => k.getAttribute('data-plan') === 'constructor')
|
||
assert.ok(karte)
|
||
karte.setAttribute('open', '')
|
||
await fx.setze(fx.finde(karte, '[data-feld="plaene.constructor.name"]'), 'Zwei neu')
|
||
fx.antworten.push(fehler(400, 'x', { fehler: { 'plaene.constructor.tage.3': 'Die Zeitfenster überschneiden sich.' } }))
|
||
await fx.klick(fx.finde(karte, '[data-aktion="speichern"]'))
|
||
assert.deepEqual(puts(fx)[0].body.daten.plaene.map((p) => [p.id, p.name]), [['__proto__', 'Eins'], ['constructor', 'Zwei neu']])
|
||
assert.equal(fx.text(fehlerAn(fx, karte, 'plaene.constructor.tage.3')), 'Die Zeitfenster überschneiden sich.')
|
||
// Löschen
|
||
await fx.klick(fx.finde(karte, '[data-aktion="entfernen"]'))
|
||
fx.antworten.push(antwort(daten({ plaene: [plan('__proto__', { name: 'Eins' })] }), 4))
|
||
await fx.klick(fx.finde(fx.finde(karte, '.tm-frage-box'), 'button[type="submit"]'))
|
||
assert.deepEqual(puts(fx)[1].body.daten.plaene.map((p) => p.id), ['__proto__'])
|
||
}))
|
||
|
||
test('Ein Speichern zur Zeit: während es läuft sind die Knöpfe gesperrt und ein zweiter Aufruf geht nicht hinaus', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
const echtes = globalThis.fetch
|
||
let freigeben
|
||
const wartet = new Promise((resolve) => { freigeben = resolve })
|
||
globalThis.fetch = async (url, optionen) => {
|
||
if (optionen?.method === 'PUT') await wartet
|
||
return echtes(url, optionen)
|
||
}
|
||
try {
|
||
fx.antworten.push(antwort(daten(), 4))
|
||
const erster = fx.klick(speichernKnopf(fx, root, P1))
|
||
await fx.ruhe()
|
||
assert.equal(planKarte(fx, root, P1).closest('[aria-busy="true"]') !== null || root.getAttribute('aria-busy') === 'true', true, 'der Editor ist beschäftigt')
|
||
assert.equal(speichernKnopf(fx, root, P1).disabled, true)
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
freigeben()
|
||
await erster
|
||
assert.equal(puts(fx).length, 1, 'nur ein Speichervorgang')
|
||
assert.equal(root.hasAttribute('aria-busy'), false)
|
||
} finally {
|
||
globalThis.fetch = echtes
|
||
}
|
||
}))
|
||
|
||
test('neuerZeitenSpeicher: speichere ohne laufende Hülle (ctx mit Stand) – Erfolg liefert true, der Stand wird übernommen', mitDom({}, async (fx) => {
|
||
const root = h('div')
|
||
document.body.append(root)
|
||
const form = h('form', null, h('p', { 'data-fehler': '_' }))
|
||
root.append(form)
|
||
const ctx = {
|
||
projectId: 'p1',
|
||
stand: { daten: daten(), version: 3 },
|
||
aktuell: () => true,
|
||
neuLaden: async () => {},
|
||
meldung: () => {},
|
||
uebernimm(a) { this.stand = { daten: a.daten, version: a.version } },
|
||
}
|
||
const sp = speichern.neuerZeitenSpeicher(ctx, root, () => {})
|
||
assert.equal(typeof sp.speichere, 'function')
|
||
assert.ok(sp.konflikt)
|
||
assert.equal(typeof sp.nochHier, 'function')
|
||
fx.antworten.push(antwort(daten({ land: 'BY' }), 4))
|
||
assert.equal(await sp.speichere((entwurf) => { entwurf.land = 'BY' }, { container: form }), true)
|
||
assert.deepEqual(puts(fx)[0], { method: 'PUT', url: '/api/zeiten/p1', body: { version: 3, daten: daten({ land: 'BY' }) } })
|
||
assert.equal(ctx.stand.version, 4)
|
||
assert.equal(await sp.speichere(() => false, { container: form }), false, 'aendere kann abbrechen (Fehler am Feld schon gezeigt)')
|
||
assert.equal(puts(fx).length, 1)
|
||
}))
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------- Hülle: Editor bleibt stehen
|
||
const zeitenFeature = (state = 'off', extra = {}) => ({
|
||
state, siteKey: null, ticketWoid: null, orderedAt: null, activatedAt: null, invoiceUrl: null, disabledReason: null, disabledAt: null, loeschungAm: null,
|
||
unlockedBy: state === 'off' ? null : 'abo', aboEndeAt: null, einbau: null, ...extra,
|
||
})
|
||
const projekt = (id, state, extra = {}) => ({ projectId: id, projectName: `Website ${id}`, liveUrl: '', features: { news: { state: 'off' }, zeiten: zeitenFeature(state) }, zeitenStatus: null, zeitenLeer: true, ...extra })
|
||
const angebot = { verfuegbar: true, inklusive: true, grund: null, preis: '30 €', einschalten: { consent: {}, leistungsbeschreibung: {} }, belehrung: '' }
|
||
const liste = (projects) => ({ status: 200, body: { projects, features: { zeiten: oeffentlicheAnsicht('zeiten') }, ustHinweis: config.ustHinweis, zeiten: angebot } })
|
||
|
||
test('Hülle: ein Wechsel des Standes (zeitenLeer, setup → active) zeichnet nur den Kopf neu; der Editor und seine ungespeicherten Eingaben bleiben', mitDom({}, async (fx) => {
|
||
const root = h('div', { id: 'zeiten-body' })
|
||
document.body.append(root)
|
||
fx.antworten.push(liste([projekt('p1', 'setup', { zeitenLeer: true })]), { status: 200, body: stand(null, { state: 'setup' }) })
|
||
await initZeiten(root, { projectId: 'p1' })
|
||
await fx.ruhe()
|
||
const platz = fx.finde(root, '.zt-editor')
|
||
assert.ok(platz)
|
||
assert.equal(platz.getAttribute('data-state'), 'setup')
|
||
const id = fx.finde(platz, 'details[data-plan]').getAttribute('data-plan')
|
||
const name = fx.finde(platz, `[data-feld="plaene.${id}.name"]`)
|
||
await fx.setze(name, 'Praxis Dr. Muster')
|
||
const kopf = fx.finde(root, '.zt-kopf')
|
||
|
||
// Stand ändert sich (nach dem ersten Speichern ist zeitenLeer false, der Zustand wechselt auf aktiv): neuer Kopf, derselbe Editor
|
||
fx.antworten.push(liste([projekt('p1', 'active', { zeitenLeer: false })]))
|
||
await initZeiten(root, { projectId: 'p1' })
|
||
assert.equal(fx.finde(root, '.zt-editor'), platz, 'derselbe Editorplatz')
|
||
assert.equal(platz.getAttribute('data-state'), 'active', 'der Zustand wird nachgeführt')
|
||
assert.equal(fx.finde(platz, `[data-feld="plaene.${id}.name"]`), name, 'dasselbe Eingabefeld')
|
||
assert.equal(name.value, 'Praxis Dr. Muster', 'die ungespeicherte Eingabe steht noch da')
|
||
assert.notEqual(fx.finde(root, '.zt-kopf'), kopf, 'der Kopf (Statuskarte) ist neu')
|
||
assert.equal(fx.finde(root, '.zt-status').getAttribute('data-state'), 'active')
|
||
assert.equal(gets(fx).filter((a) => a.url === '/api/zeiten/p1').length, 1, 'der Editor lädt nicht noch einmal')
|
||
assert.equal(platz.hidden, false)
|
||
}))
|
||
|
||
test('Hülle: erstes Speichern im Editor holt die Liste neu (zeitenLeer) und zeichnet nur den Kopf neu, der Editor bleibt', mitDom({}, async (fx) => {
|
||
const root = h('div', { id: 'zeiten-body' })
|
||
document.body.append(root)
|
||
fx.antworten.push(liste([projekt('p1', 'active', { zeitenLeer: true })]), { status: 200, body: stand(null) })
|
||
await initZeiten(root, { projectId: 'p1' })
|
||
await fx.ruhe()
|
||
const platz = fx.finde(root, '.zt-editor')
|
||
const id = fx.finde(platz, 'details[data-plan]').getAttribute('data-plan')
|
||
const gespeichert = daten({ plaene: [plan(id)], kontakt: { an: false, telefon: '', adresse: '', whatsapp: null } })
|
||
fx.antworten.push(antwort(gespeichert, 1), liste([projekt('p1', 'active', { zeitenLeer: false })]))
|
||
await fx.klick(fx.finde(platz, '[data-aktion="speichern"]'))
|
||
await fx.ruhe()
|
||
assert.deepEqual(fx.fetchLog.map((a) => `${a.method} ${a.url}`), ['GET /api/funktionen', 'GET /api/zeiten/p1', 'PUT /api/zeiten/p1', 'GET /api/funktionen'])
|
||
assert.equal(fx.finde(root, '.zt-editor'), platz, 'derselbe Editorplatz nach dem Neuzeichnen der Hülle')
|
||
assert.ok(abschnitt(fx, platz, 'feiertage'), 'der Editor zeigt jetzt Feiertage und Ausnahmen')
|
||
assert.doesNotMatch(fx.text(fx.finde(root, '.zt-kopf')), /Trag jetzt deine Zeiten ein/)
|
||
}))
|
||
|
||
test('Hülle: eine andere Website oder ein anderer Schreibstatus (abgeschaltet) bekommt einen frischen Editor', mitDom({}, async (fx) => {
|
||
const root = h('div', { id: 'zeiten-body' })
|
||
document.body.append(root)
|
||
fx.antworten.push(liste([projekt('p1', 'active', { zeitenLeer: false }), projekt('p2', 'active', { zeitenLeer: false })]), { status: 200, body: stand(daten()) })
|
||
await initZeiten(root, { projectId: 'p1' })
|
||
await fx.ruhe()
|
||
const erster = fx.finde(root, '.zt-editor')
|
||
// Website wechseln
|
||
fx.antworten.push({ status: 200, body: stand(daten({ plaene: [plan(P1, { name: 'Zweite Seite' })] })) })
|
||
await fx.setze(fx.finde(root, '#zeiten-project'), 'p2')
|
||
await fx.ruhe()
|
||
const zweiter = fx.finde(root, '.zt-editor')
|
||
assert.notEqual(zweiter, erster)
|
||
assert.equal(fx.finde(zweiter, `[data-feld="plaene.${P1}.name"]`).value, 'Zweite Seite')
|
||
assert.equal(erster.isConnected, false)
|
||
assert.deepEqual(gets(fx).filter((a) => a.url.startsWith('/api/zeiten/')).map((a) => a.url), ['/api/zeiten/p1', '/api/zeiten/p2'])
|
||
}))
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------- Quelltext
|
||
const ohneKommentare = (quelle) => quelle.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|\s)\/\/.*$/gm, '')
|
||
const DATEIEN = ['zeiten-speichern.js', 'zeiten-editor.js', 'zeiten-plaene.js', 'zeiten-ausnahmen.js']
|
||
|
||
test('Quelltext: vier neue Dateien, nur relative Importe, kein HTML aus Daten, keine Browser-Rückfragen, kein Speicher, Du-Form', () => {
|
||
const vorhanden = readdirSync(new URL('../public/', import.meta.url))
|
||
for (const datei of DATEIEN) {
|
||
assert.ok(vorhanden.includes(datei), `${datei} fehlt`)
|
||
const quelle = ohneKommentare(lesen(`public/${datei}`))
|
||
for (const m of quelle.matchAll(/\bfrom\s+'([^']+)'/g)) assert.match(m[1], /^\.\/(termine-dom|termine-view|zeiten[a-z-]*)\.js$/, `${datei}: Import ${m[1]}`)
|
||
assert.doesNotMatch(quelle, /innerHTML|insertAdjacentHTML|outerHTML|document\.write/, `${datei}: Daten nie als HTML`)
|
||
assert.doesNotMatch(quelle, /\bconfirm\(|\balert\(|\bprompt\(/, `${datei}: Rückfragen stehen in der Seite`)
|
||
assert.doesNotMatch(quelle, /\bSie\b|\bIhr(e[nmrs]?)?\b/, `${datei}: Portaltexte in der Du-Form`)
|
||
assert.doesNotMatch(quelle, /localStorage|sessionStorage|document\.cookie/, `${datei}: kein Browserspeicher`)
|
||
assert.doesNotMatch(quelle, /24 Uhr/, `${datei}: Wortwahl`)
|
||
assert.doesNotMatch(quelle, /import\s*\(/, `${datei}: kein dynamischer Import`)
|
||
assert.doesNotMatch(quelle, /\btermine-(speichern|team|tab|aktionen|formulare)\b/, `${datei}: keine termine-Dateien angefasst`)
|
||
}
|
||
// die Hülle bindet den Editor ein
|
||
assert.match(ohneKommentare(lesen('public/zeiten.js')), /from '\.\/zeiten-editor\.js'/)
|
||
})
|
||
|
||
test('zeiten.css: jede zt-Klasse der Editor-Module ist gestaltet', () => {
|
||
const css = lesen('public/zeiten.css')
|
||
const quelle = DATEIEN.map((d) => lesen(`public/${d}`)).join('\n')
|
||
const gesetzt = new Set([...quelle.matchAll(/class: '([^']+)'/g)].flatMap((m) => m[1].split(/\s+/)).filter((k) => /^zt-/.test(k)))
|
||
assert.ok(gesetzt.size >= 5)
|
||
for (const klasse of gesetzt) assert.ok(css.includes(`.${klasse}`), `.${klasse} hat keine Gestaltung`)
|
||
})
|