Ungespeicherte Eingaben in anderen Formularen bleiben stehen: nach dem Speichern wird nur die Plankarte, die Planliste (mit den Eingaben der übrigen Karten), Feiertage oder Ausnahmen neu gezeichnet, „Nein, danke“ entfernt nur die Frage. Während des Speicherns sind auch die Eingabefelder gesperrt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1176 lines
75 KiB
JavaScript
1176 lines
75 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/)
|
||
}))
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------- Nur neu zeichnen, was sich geändert hat
|
||
const kurzerUrlaub = (id, text = '') => ({ id, art: 'urlaub', von: tagPlus(HEUTE, 10), bis: tagPlus(HEUTE, 10), text, modus: 'zu', fenster: [], plan: null })
|
||
/** Einen eintägigen Betriebsurlaub über das Formular eintragen (ohne Frage nach der Hinweis-Leiste); die Antwort kommt als nächste aus der Warteschlange. */
|
||
async function urlaubEintragen(fx, root, antwortDaten, version) {
|
||
await fx.klick(fx.finde(root, '[data-werkzeug="urlaub-neu"]'))
|
||
const form = urlaubsformular(fx, root)
|
||
await fx.setze(feldIn(fx, form, 'von'), tagPlus(HEUTE, 10))
|
||
await fx.setze(feldIn(fx, form, 'bis'), tagPlus(HEUTE, 10))
|
||
fx.antworten.push(antwort(antwortDaten, version))
|
||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||
}
|
||
|
||
test('Plan speichern zeichnet nur diese Karte: ungespeicherte Eingaben in einem anderen Plan und in anderen Abschnitten bleiben stehen', mitDom({}, async (fx) => {
|
||
const zwei = daten({ plaene: [plan(P1), plan(P2)] })
|
||
const { root } = await oeffne(fx, { start: stand(zwei) })
|
||
const karteA = planKarte(fx, root, P1)
|
||
const karteB = planKarte(fx, root, P2)
|
||
const nameB = fx.finde(karteB, `[data-feld="plaene.${P2}.name"]`)
|
||
await fx.setze(nameB, 'Küche Entwurf')
|
||
await fx.klick(haken(fx, tagZeile(fx, root, P2, '7'))) // Sonntag im zweiten Plan geöffnet, nicht gespeichert
|
||
await fx.setze(fx.finde(root, 'select[data-feld="land"]'), 'BY') // Bundesland gewählt, nicht gespeichert
|
||
await fx.setze(fx.finde(fx.finde(root, '[data-feiertag="2026-12-25"]'), 'select'), 'rund')
|
||
await fx.setze(zeitFelder(fx, tagZeile(fx, root, P1, '2'))[0], '08:15') // Dienstag in A: wird gleich gespeichert
|
||
fx.antworten.push(antwort(daten({ plaene: [plan(P1, { tage: { ...werktags(), 2: { modus: 'zeiten', fenster: [['08:15', '18:00']] } } }), plan(P2)] }), 4))
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
const gesendet = puts(fx)[0].body.daten
|
||
assert.deepEqual(gesendet.plaene[0].tage['2'].fenster, [['08:15', '18:00']])
|
||
assert.equal(gesendet.plaene[1].name, 'Küche', 'der Entwurf des anderen Plans wird nicht mitgespeichert')
|
||
assert.equal(gesendet.plaene[1].tage['7'].modus, 'zu')
|
||
assert.notEqual(planKarte(fx, root, P1), karteA, 'die gespeicherte Karte ist neu gezeichnet')
|
||
assert.equal(planKarte(fx, root, P2), karteB, 'die andere Karte ist dieselbe geblieben')
|
||
assert.equal(fx.finde(planKarte(fx, root, P2), `[data-feld="plaene.${P2}.name"]`), nameB)
|
||
assert.equal(nameB.value, 'Küche Entwurf')
|
||
assert.equal(haken(fx, tagZeile(fx, root, P2, '7')).checked, true)
|
||
assert.equal(fx.finde(root, 'select[data-feld="land"]').value, 'BY', 'das ungespeicherte Bundesland bleibt')
|
||
assert.equal(fx.finde(fx.finde(root, '[data-feiertag="2026-12-25"]'), 'select').value, 'rund')
|
||
assert.deepEqual(zeitFelder(fx, tagZeile(fx, root, P1, '2')).slice(0, 2).map((f) => f.value), ['08:15', '18:00'], 'die gespeicherte Karte zeigt den gespeicherten Stand')
|
||
}))
|
||
|
||
test('Betriebsurlaub speichern zeichnet nur „Ausnahmen“: eine ungespeicherte Änderung am Wochenplan bleibt', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
const montag = tagZeile(fx, root, P1, '1')
|
||
const feld = zeitFelder(fx, montag)[0]
|
||
await fx.setze(feld, '07:45')
|
||
const karte = planKarte(fx, root, P1)
|
||
const feiertage = abschnitt(fx, root, 'feiertage')
|
||
const ausnahmenVorher = abschnitt(fx, root, 'ausnahmen')
|
||
await urlaubEintragen(fx, root, daten({ ausnahmen: [kurzerUrlaub('x_urlaub01')] }), 4)
|
||
assert.equal(planKarte(fx, root, P1), karte, 'dieselbe Karte')
|
||
assert.equal(zeitFelder(fx, tagZeile(fx, root, P1, '1'))[0], feld)
|
||
assert.equal(feld.value, '07:45', 'die ungespeicherte Eingabe steht noch da')
|
||
assert.equal(abschnitt(fx, root, 'feiertage'), feiertage, 'Feiertage unberührt')
|
||
assert.notEqual(abschnitt(fx, root, 'ausnahmen'), ausnahmenVorher, 'Ausnahmen neu gezeichnet')
|
||
assert.ok(fx.finde(root, 'li[data-id="x_urlaub01"]'))
|
||
assert.match(meldung(fx, root), /eingetragen/)
|
||
assert.equal(puts(fx)[0].body.daten.plaene[0].tage['1'].fenster[0][0], '09:00', 'der Entwurf ging nicht mit hinaus')
|
||
// dasselbe beim Entfernen
|
||
const zeile = fx.finde(root, 'li[data-id="x_urlaub01"]')
|
||
await fx.klick(fx.finde(zeile, '[data-aktion="bearbeiten"]'))
|
||
await fx.klick(fx.finde(zeile, '[data-aktion="entfernen"]'))
|
||
fx.antworten.push(antwort(daten(), 5))
|
||
await fx.klick(fx.finde(fx.finde(zeile, '.tm-frage-box'), 'button[type="submit"]'))
|
||
assert.equal(feld.value, '07:45')
|
||
assert.equal(fx.finde(root, 'li[data-id="x_urlaub01"]'), null)
|
||
}))
|
||
|
||
test('„Nein, danke“ entfernt nur die Frage: andere ungespeicherte Eingaben und offene Formulare bleiben', 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)
|
||
fx.antworten.push(antwort(daten({ ausnahmen: [{ ...kurzerUrlaub('x_urlaub01'), von, bis }] }), 4))
|
||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||
assert.ok(fx.finde(root, '[data-frage="hinweis"]'))
|
||
// jetzt nebenher etwas eingeben: ein Wochentag, ein offenes Sondertag-Formular mit Text
|
||
const feld = zeitFelder(fx, tagZeile(fx, root, P1, '3'))[1]
|
||
await fx.setze(feld, '17:20')
|
||
await fx.klick(fx.finde(root, '[data-werkzeug="sondertag-neu"]'))
|
||
const sonder = fx.finde(root, 'form[aria-label="Sondertag eintragen"]')
|
||
await fx.setze(feldIn(fx, sonder, 'text'), 'Tag der offenen Tür')
|
||
await fx.klick(fx.finde(fx.finde(root, '[data-frage="hinweis"]'), '[data-aktion="hinweis-nein"]'))
|
||
assert.equal(fx.finde(root, '[data-frage="hinweis"]'), null, 'die Frage ist weg')
|
||
assert.equal(feld.value, '17:20')
|
||
assert.equal(fx.finde(root, 'form[aria-label="Sondertag eintragen"]'), sonder, 'das offene Formular ist dasselbe')
|
||
assert.equal(feldIn(fx, sonder, 'text').value, 'Tag der offenen Tür')
|
||
assert.equal(puts(fx).length, 1)
|
||
}))
|
||
|
||
test('Feiertage speichern lässt ein noch ungespeichertes Bundesland stehen; Bundesland speichern nimmt Eingaben für Tage mit ins neue Land', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
const zeile = (tag) => fx.finde(root, `[data-feiertag="${tag}"]`)
|
||
await fx.setze(fx.finde(root, 'select[data-feld="land"]'), 'BY')
|
||
await fx.setze(fx.finde(zeile('2026-12-25'), 'select'), 'vereinbarung')
|
||
fx.antworten.push(antwort(daten({ feiertage: { '2026-12-25': { modus: 'vereinbarung', fenster: [] } } }), 4))
|
||
await fx.klick(fx.finde(root, 'form[aria-label="Feiertage"] button[type="submit"]'))
|
||
assert.equal(puts(fx)[0].body.daten.land, 'RP', 'das Bundesland ging nicht mit')
|
||
assert.equal(fx.finde(root, 'select[data-feld="land"]').value, 'BY', 'und steht noch da')
|
||
assert.match(meldung(fx, root), /Feiertage gespeichert/)
|
||
|
||
// jetzt das Bundesland speichern: ein Wochenplan-Entwurf bleibt, die Eingabe zu Neujahr (gibt es auch in Bayern) wandert mit
|
||
await fx.setze(fx.finde(zeile('2027-01-01'), 'select'), 'rund')
|
||
const feld = zeitFelder(fx, tagZeile(fx, root, P1, '4'))[0]
|
||
await fx.setze(feld, '10:10')
|
||
const bayern = [{ tag: '2026-12-25', name: 'Erster Weihnachtstag', art: 'feiertag' }, { tag: '2027-01-01', name: 'Neujahr', art: 'feiertag' }, { tag: '2027-01-06', name: 'Heilige Drei Könige', art: 'feiertag' }]
|
||
fx.antworten.push(antwort(daten({ land: 'BY', feiertage: { '2026-12-25': { modus: 'vereinbarung', fenster: [] } } }), 5, { feiertage: bayern }))
|
||
await fx.klick(fx.finde(root, 'form[aria-label="Bundesland"] button[type="submit"]'))
|
||
assert.deepEqual(fx.alle(root, '[data-feiertag]').map((z) => z.getAttribute('data-feiertag')), ['2026-12-25', '2027-01-01', '2027-01-06'])
|
||
assert.equal(fx.finde(zeile('2027-01-01'), 'select').value, 'rund', 'die Eingabe zu Neujahr ist erhalten')
|
||
assert.equal(fx.finde(zeile('2026-12-25'), 'select').value, 'vereinbarung', 'der gespeicherte Stand')
|
||
assert.equal(fx.finde(zeile('2027-01-06'), 'select').value, '')
|
||
assert.equal(feld.value, '10:10', 'der Wochenplan-Entwurf bleibt')
|
||
}))
|
||
|
||
test('Plan hinzufügen, Hauptplan wechseln und Plan löschen zeichnen die Planliste neu, behalten aber die ungespeicherten Eingaben der übrigen Karten und das offene „Plan hinzufügen“', mitDom({}, async (fx) => {
|
||
const P3 = 'p_dritter3'
|
||
const d3 = daten({
|
||
plaene: [plan(P1), plan(P2), plan(P3, { kurz: 'bar', name: 'Bar', haupt: false })],
|
||
ausnahmen: [{ ...kurzerUrlaub('x_kueche01', 'Nur Küche'), plan: P2 }, kurzerUrlaub('x_alle0001', 'Alle')],
|
||
})
|
||
const { root } = await oeffne(fx, { start: stand(d3) })
|
||
const name3 = fx.finde(planKarte(fx, root, P3), `[data-feld="plaene.${P3}.name"]`)
|
||
await fx.setze(name3, 'Bar Entwurf')
|
||
const montag = zeitFelder(fx, tagZeile(fx, root, P1, '1'))[0]
|
||
await fx.setze(montag, '06:30')
|
||
await fx.klick(fx.finde(root, '[data-werkzeug="plan-neu"]'))
|
||
const neuForm = fx.finde(root, 'form[aria-label="Plan hinzufügen"]')
|
||
await fx.setze(fx.finde(neuForm, 'input[type="text"]'), 'Halbfertig')
|
||
const ausnahmenVorher = abschnitt(fx, root, 'ausnahmen')
|
||
|
||
// Hauptplan wechseln (P2): Pillen und Knöpfe aller Karten ändern sich, die Eingaben bleiben
|
||
fx.antworten.push(antwort(daten({ ...d3, plaene: [plan(P1, { haupt: false }), plan(P2, { haupt: true }), plan(P3, { kurz: 'bar', name: 'Bar', haupt: false })] }), 4))
|
||
await fx.klick(fx.finde(planKarte(fx, root, P2), '[data-aktion="haupt"]'))
|
||
assert.match(fx.text(fx.finde(planKarte(fx, root, P2), 'summary')), /Hauptplan/)
|
||
assert.doesNotMatch(fx.text(fx.finde(planKarte(fx, root, P1), 'summary')), /Hauptplan/)
|
||
assert.ok(fx.finde(planKarte(fx, root, P1), '[data-aktion="haupt"]'), 'der frühere Hauptplan hat jetzt den Knopf')
|
||
assert.equal(fx.finde(planKarte(fx, root, P3), `[data-feld="plaene.${P3}.name"]`).value, 'Bar Entwurf')
|
||
assert.equal(zeitFelder(fx, tagZeile(fx, root, P1, '1'))[0].value, '06:30')
|
||
assert.equal(fx.finde(root, 'form[aria-label="Plan hinzufügen"]'), neuForm, 'das offene Formular bleibt')
|
||
assert.equal(fx.finde(neuForm, 'input[type="text"]').value, 'Halbfertig')
|
||
assert.equal(abschnitt(fx, root, 'ausnahmen'), ausnahmenVorher, 'die Ausnahmen kennen die Pläne nur beim Namen: nichts zu tun')
|
||
|
||
// Plan löschen (P3, jetzt weder Haupt- noch letzter Plan): P1 und P2 behalten ihre Eingaben, Ausnahmen werden neu gezeichnet
|
||
const montag2 = zeitFelder(fx, tagZeile(fx, root, P1, '1'))[0]
|
||
await fx.klick(fx.finde(planKarte(fx, root, P3), '[data-aktion="entfernen"]'))
|
||
fx.antworten.push(antwort(daten({ ...d3, plaene: [plan(P1, { haupt: false }), plan(P2, { haupt: true })] }), 5))
|
||
await fx.klick(fx.finde(fx.finde(planKarte(fx, root, P3), '.tm-frage-box'), 'button[type="submit"]'))
|
||
assert.equal(planKarte(fx, root, P3), null)
|
||
assert.equal(zeitFelder(fx, tagZeile(fx, root, P1, '1'))[0].value, '06:30', 'Eingabe in P1 bleibt')
|
||
assert.notEqual(zeitFelder(fx, tagZeile(fx, root, P1, '1'))[0], montag2, '(die Karte wurde mit ihren Werten neu gebaut)')
|
||
assert.equal(fx.finde(root, 'form[aria-label="Plan hinzufügen"]'), neuForm)
|
||
assert.notEqual(abschnitt(fx, root, 'ausnahmen'), ausnahmenVorher, 'Ausnahmen neu gezeichnet')
|
||
|
||
// Plan hinzufügen: neue Karte, Formular zu, P1 behält die Eingabe, „Gilt für“ in neuen Formularen kennt den neuen Plan
|
||
await fx.setze(fx.finde(neuForm, 'input[type="text"]'), 'Lieferdienst')
|
||
fx.antworten.push((aufruf) => antwort(daten({ ...d3, plaene: [plan(P1, { haupt: false }), plan(P2, { haupt: true }), plan(aufruf.body.daten.plaene[2].id, { kurz: 'lieferdienst', name: 'Lieferdienst', haupt: false, tage: wochenplan() })] }), 6))
|
||
await fx.klick(fx.finde(neuForm, 'button[type="submit"]'))
|
||
const neueKarte = fx.alle(root, 'details[data-plan]').find((k) => /Lieferdienst/.test(fx.text(fx.finde(k, 'summary'))))
|
||
assert.ok(neueKarte, 'die neue Karte steht in der Liste')
|
||
assert.equal(fx.alle(root, 'details[data-plan]').length, 3)
|
||
assert.equal(fx.finde(root, 'form[aria-label="Plan hinzufügen"]'), null, 'das Formular ist zu')
|
||
assert.equal(zeitFelder(fx, tagZeile(fx, root, P1, '1'))[0].value, '06:30', 'auch das Hinzufügen lässt die Eingaben der anderen Karten stehen')
|
||
await fx.klick(fx.finde(root, '[data-werkzeug="urlaub-neu"]'))
|
||
assert.match(fx.text(feldIn(fx, urlaubsformular(fx, root), 'plan')), /Lieferdienst/)
|
||
}))
|
||
|
||
test('Plan umbenennen: die Liste der Ausnahmen nennt den neuen Namen, sonst bleibt sie stehen', mitDom({}, async (fx) => {
|
||
const ausnahme = { ...kurzerUrlaub('x_kueche01', 'Nur Küche'), plan: P2 }
|
||
const { root } = await oeffne(fx, { start: stand(daten({ plaene: [plan(P1), plan(P2)], ausnahmen: [ausnahme] })) })
|
||
const vorher = abschnitt(fx, root, 'ausnahmen')
|
||
assert.match(fx.text(fx.finde(root, 'li[data-id="x_kueche01"]')), /nur Küche/)
|
||
// erst gespeichert ohne neuen Namen: Ausnahmen bleiben
|
||
fx.antworten.push(antwort(daten({ plaene: [plan(P1), plan(P2)], ausnahmen: [ausnahme] }), 4))
|
||
await fx.klick(speichernKnopf(fx, root, P2))
|
||
assert.equal(abschnitt(fx, root, 'ausnahmen'), vorher)
|
||
await fx.setze(fx.finde(planKarte(fx, root, P2), `[data-feld="plaene.${P2}.name"]`), 'Restaurant')
|
||
fx.antworten.push(antwort(daten({ plaene: [plan(P1), plan(P2, { name: 'Restaurant' })], ausnahmen: [ausnahme] }), 5))
|
||
await fx.klick(speichernKnopf(fx, root, P2))
|
||
assert.notEqual(abschnitt(fx, root, 'ausnahmen'), vorher)
|
||
assert.match(fx.text(fx.finde(root, 'li[data-id="x_kueche01"]')), /nur Restaurant/)
|
||
}))
|
||
|
||
test('Während ein Speichern läuft, sind Knöpfe und Eingabefelder gesperrt (nichts geht beim Tippen verloren); danach sind sie wieder frei', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx, { start: stand(daten({ plaene: [plan(P1), plan(P2)] })) })
|
||
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 {
|
||
const ausserhalb = zeitFelder(fx, tagZeile(fx, root, P2, '1'))[0]
|
||
const selbst = zeitFelder(fx, tagZeile(fx, root, P1, '1'))[0]
|
||
fx.antworten.push(antwort(daten({ plaene: [plan(P1), plan(P2)] }), 4))
|
||
const lauf = fx.klick(speichernKnopf(fx, root, P1))
|
||
await fx.ruhe()
|
||
assert.equal(root.getAttribute('aria-busy'), 'true')
|
||
for (const steuerung of fx.alle(root, 'input, select, textarea, button')) assert.equal(steuerung.disabled, true, `${steuerung.tagName} ist gesperrt`)
|
||
await fx.setze(ausserhalb, '05:55')
|
||
assert.notEqual(ausserhalb.value, '05:55', 'in ein gesperrtes Feld lässt sich nichts tippen')
|
||
freigeben()
|
||
await lauf
|
||
assert.equal(root.hasAttribute('aria-busy'), false)
|
||
assert.equal(ausserhalb.disabled, false, 'die Karte, die stehen blieb, ist wieder frei')
|
||
assert.equal(fx.alle(root, 'input:disabled, select:disabled, button:disabled').length, 0)
|
||
void selbst
|
||
} finally {
|
||
globalThis.fetch = echtes
|
||
}
|
||
}))
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------- 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`)
|
||
})
|