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“). // Teil II (Task 30): Hinweis-Leiste, Kontakt-Leiste, Google und Vorschau (zeiten-leisten.js, zeiten-google.js, zeiten-editor.js). // 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 löschen: die Rückfrage zählt die Ausnahmen des Plans beim Öffnen, auch wenn sie erst nach dem Zeichnen der Karte dazukamen oder wegfielen', mitDom({}, async (fx) => { const { root } = await oeffne(fx, { start: stand(daten({ plaene: [plan(P1), plan(P2)] })) }) const loeschenOeffnen = async () => { await fx.klick(fx.finde(planKarte(fx, root, P2), '[data-aktion="entfernen"]')) return fx.finde(planKarte(fx, root, P2), '.tm-frage-box') } const abbrechen = (frage) => fx.klick(fx.finde(frage, 'button[type="button"]')) assert.doesNotMatch(fx.text(await loeschenOeffnen()), /nur für diesen Plan/, 'noch keine Ausnahme des Plans') await abbrechen(fx.finde(planKarte(fx, root, P2), '.tm-frage-box')) // ein Sondertag „nur Küche“ kommt dazu: die Karte des Plans wird dabei nicht neu gezeichnet const karte = planKarte(fx, root, P2) 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) await fx.setze(feldIn(fx, form, 'plan'), P2) const sonder = { id: 'x_sonder001', art: 'sondertag', von: tag, bis: tag, text: '', modus: 'zu', fenster: [], plan: P2 } fx.antworten.push(antwort(daten({ plaene: [plan(P1), plan(P2)], ausnahmen: [sonder] }), 4)) await fx.klick(fx.finde(form, 'button[type="submit"]')) assert.equal(puts(fx)[0].body.daten.ausnahmen[0].plan, P2) assert.equal(planKarte(fx, root, P2), karte, 'die Karte blieb stehen') const frage = await loeschenOeffnen() assert.match(fx.text(frage), /Die eine Ausnahme, die nur für diesen Plan gilt, wird mit gelöscht/) assert.ok(fx.text(frage).includes('Steht dieser Plan auf deiner Website, zeigt sie dort wieder die feste Tabelle.')) await abbrechen(frage) // der Sondertag wird wieder entfernt: der Hinweis verschwindet const zeile = fx.finde(root, 'li[data-id="x_sonder001"]') 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)] }), 5)) await fx.klick(fx.finde(fx.finde(zeile, '.tm-frage-box'), 'button[type="submit"]')) assert.equal(planKarte(fx, root, P2), karte) assert.doesNotMatch(fx.text(await loeschenOeffnen()), /nur für diesen Plan/, 'ohne Ausnahme kein Hinweis mehr') await abbrechen(fx.finde(planKarte(fx, root, P2), '.tm-frage-box')) // und der Plan wird tatsächlich mit seinen Ausnahmen gelöscht, wenn eine dazukam const noch = { ...sonder, id: 'x_sonder002' } await fx.klick(fx.finde(root, '[data-werkzeug="sondertag-neu"]')) const form2 = fx.finde(root, 'form[aria-label="Sondertag eintragen"]') await fx.setze(feldIn(fx, form2, 'von'), tag) await fx.setze(feldIn(fx, form2, 'plan'), P2) fx.antworten.push(antwort(daten({ plaene: [plan(P1), plan(P2)], ausnahmen: [noch] }), 6)) await fx.klick(fx.finde(form2, 'button[type="submit"]')) const frage2 = await loeschenOeffnen() assert.match(fx.text(frage2), /Die eine Ausnahme/) fx.antworten.push(antwort(daten({ plaene: [plan(P1)] }), 7)) await fx.klick(fx.finde(frage2, 'button[type="submit"]')) assert.deepEqual(puts(fx).at(-1).body.daten.ausnahmen, [], 'die Ausnahme des Plans ging mit') })) 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..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. 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 = '' 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 = '' 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']) })) // ---------------------------------------------------------------------------------------------------------------- Editor II (Task 30) // Hinweis-Leiste, Kontakt-Leiste, Google und Vorschau (public/zeiten-leisten.js, zeiten-google.js, Vorschau oben in zeiten-editor.js; Spec 3.4 Bereiche 4 bis 6) const ymdKurz = (tag) => `${tag.slice(8)}.${tag.slice(5, 7)}.${tag.slice(0, 4)}` const wochentagNr = (tag) => ((new Date(`${tag}T00:00:00Z`).getUTCDay() + 6) % 7) + 1 const naechster = (nr) => { for (let i = 0; i < 7; i += 1) if (wochentagNr(tagPlus(HEUTE, i)) === nr) return tagPlus(HEUTE, i); throw new Error('unmöglich') } const hinweisAktion = { id: 'h_aktion001', art: 'aktion', text: '20 % auf alle Torten', knopf: { art: 'link', ziel: 'https://beispiel.de/torten', text: 'Mehr erfahren' }, von: tagPlus(HEUTE, -1), bis: tagPlus(HEUTE, 10) } const hinweisUrlaub = { id: 'h_urlaub001', art: 'urlaub', text: 'Betriebsurlaub im Sommer', knopf: null, von: tagPlus(HEUTE, 30), bis: tagPlus(HEUTE, 40) } const hinweisAlt = { id: 'h_vorbei001', art: 'info', text: 'Alter Hinweis', knopf: null, von: tagPlus(HEUTE, -20), bis: tagPlus(HEUTE, -10) } const hinweisFeld = (fx, form, name) => fx.finde(form, (el) => el.nodeType === 1 && new RegExp(`^hinweise\\.[^.]+\\.${name.replace('.', '\\.')}$`).test(el.getAttribute('data-feld') || '')) const hinweisFehlerPfad = (fx, form, name) => fx.finde(form, (el) => el.nodeType === 1 && new RegExp(`^hinweise\\.[^.]+\\.${name.replace('.', '\\.')}$`).test(el.getAttribute('data-fehler') || '')) const neuerHinweis = (fx, root) => fx.finde(root, 'form[aria-label="Neuer Hinweis"]') const kontaktForm = (fx, root) => fx.finde(root, 'form[aria-label="Kontakt-Leiste"]') const kfeld = (fx, root, name) => fx.finde(kontaktForm(fx, root), `[data-feld="kontakt.${name}"]`) const sichtbar = (el) => el !== null && el.hidden !== true /** Zwischenablage des Browsers für die Dauer von fn ersetzen (Node hat seit 21 ein eigenes navigator-Objekt: Eigenschaft sichern und danach zurückgeben). */ async function mitZwischenablage(clipboard, fn) { const vorher = Object.getOwnPropertyDescriptor(globalThis, 'navigator') Object.defineProperty(globalThis, 'navigator', { value: clipboard === undefined ? {} : { clipboard }, configurable: true, writable: true, enumerable: true }) try { await fn() } finally { if (vorher) Object.defineProperty(globalThis, 'navigator', vorher) else delete globalThis.navigator } } test('Editor II: die Abschnitte Hinweis-Leiste, Kontakt-Leiste und Google stehen nach den Ausnahmen, erst nach dem ersten Speichern', mitDom({}, async (fx) => { const { root } = await oeffne(fx) assert.deepEqual(fx.alle(root, '[data-abschnitt]').map((el) => el.getAttribute('data-abschnitt')), ['plaene', 'feiertage', 'ausnahmen', 'hinweise', 'kontakt', 'google']) const leer = await oeffne(fx, { start: stand(null) }) for (const key of ['hinweise', 'kontakt', 'google']) assert.equal(abschnitt(fx, leer.root, key), null, `${key}: erst nach dem ersten Speichern`) for (const [bauen, datei] of [['hinweise', 'zeiten-leisten'], ['kontakt', 'zeiten-leisten'], ['google', 'zeiten-google']]) { const modul = await import(`../public/${datei}.js`) assert.equal(typeof modul[bauen], 'function', `${datei}.js exportiert ${bauen}(t)`) } })) // ---------------------------------------------------------------------------------------------------------------- Hinweis-Leiste test('Hinweis-Leiste: der Knopf heißt genau „Hinweis-Leiste anlegen“, das Formular hat Text, Art und Zeitraum, PUT mit neuem Hinweis', mitDom({}, async (fx) => { const { root } = await oeffne(fx) const knopf = fx.finde(root, '[data-werkzeug="hinweis-neu"]') assert.equal(fx.text(knopf), 'Hinweis-Leiste anlegen') assert.equal(knopf.tagName, 'BUTTON') assert.match(fx.text(abschnitt(fx, root, 'hinweise')), /Keine Hinweise eingetragen/) await fx.klick(knopf) const form = neuerHinweis(fx, root) assert.ok(form) assert.equal(hinweisFeld(fx, form, 'text').getAttribute('maxlength'), '140') assert.deepEqual(optionen(fx, hinweisFeld(fx, form, 'art')), ['info', 'urlaub', 'aktion', 'wichtig']) assert.deepEqual(fx.alle(hinweisFeld(fx, form, 'art'), 'option').map((o) => fx.text(o)), ['Info', 'Urlaub', 'Aktion', 'Wichtig']) assert.equal(hinweisFeld(fx, form, 'von').getAttribute('type'), 'date') assert.equal(hinweisFeld(fx, form, 'bis').getAttribute('type'), 'date') await fx.setze(hinweisFeld(fx, form, 'text'), ' Heute Kuchen im Angebot ') await fx.setze(hinweisFeld(fx, form, 'art'), 'wichtig') const von = tagPlus(HEUTE, 1) const bis = tagPlus(HEUTE, 3) await fx.setze(hinweisFeld(fx, form, 'von'), von) await fx.setze(hinweisFeld(fx, form, 'bis'), bis) const gespeichert = { id: 'h_neu00001', art: 'wichtig', text: 'Heute Kuchen im Angebot', knopf: null, von, bis } fx.antworten.push(antwort(daten({ hinweise: [gespeichert] }), 4)) await fx.klick(fx.finde(form, 'button[type="submit"]')) assert.equal(puts(fx).length, 1) const h = puts(fx)[0].body.daten.hinweise assert.equal(h.length, 1) assert.match(h[0].id, /^h_[a-z0-9]{8}$/) assert.deepEqual({ ...h[0], id: 'h' }, { id: 'h', art: 'wichtig', text: 'Heute Kuchen im Angebot', knopf: null, von, bis }) assert.match(meldung(fx, root), /Hinweis.*gespeichert/) const zeile = fx.finde(abschnitt(fx, root, 'hinweise'), 'li[data-id="h_neu00001"]') assert.ok(zeile, 'der Hinweis steht in der Liste') assert.ok(fx.text(zeile).includes('Heute Kuchen im Angebot')) assert.ok(fx.text(zeile).includes('Wichtig')) assert.ok(fx.text(zeile).includes(ymdKurz(von))) assert.equal(neuerHinweis(fx, root), null, 'das Formular ist nach dem Speichern zu') })) test('Hinweis: Zeitraum ist optional (ohne Daten gilt der Hinweis immer), Datum leer ⇒ null', mitDom({}, async (fx) => { const { root } = await oeffne(fx) await fx.klick(fx.finde(root, '[data-werkzeug="hinweis-neu"]')) const form = neuerHinweis(fx, root) await fx.setze(hinweisFeld(fx, form, 'text'), 'Heute Ruhetag') fx.antworten.push(antwort(daten(), 4)) await fx.klick(fx.finde(form, 'button[type="submit"]')) const h = puts(fx)[0].body.daten.hinweise[0] assert.equal(h.art, 'info') assert.equal(h.von, null) assert.equal(h.bis, null) })) test('Hinweis: Aktion ohne Enddatum ⇒ Feldfehler am Enddatum (nichts geht hinaus); % im Text ⇒ Preis-Hinweis', mitDom({}, async (fx) => { const { root } = await oeffne(fx) await fx.klick(fx.finde(root, '[data-werkzeug="hinweis-neu"]')) const form = neuerHinweis(fx, root) const hinweis = fx.finde(form, '[data-hinweis="preis"]') assert.ok(hinweis, 'der Preis-Hinweis hat seinen Platz im Formular') assert.equal(sichtbar(hinweis), false, 'zuerst nicht sichtbar') await fx.setze(hinweisFeld(fx, form, 'text'), 'Alles 20 % günstiger') assert.equal(sichtbar(hinweis), true, '% im Text') assert.match(fx.text(hinweis), /Endpreise/) assert.match(fx.text(hinweis), /niedrigste/) assert.match(fx.text(hinweis), /30 Tage/) await fx.setze(hinweisFeld(fx, form, 'text'), 'Jetzt nur 5 € statt 8 €') assert.equal(sichtbar(hinweis), true, '„statt“ im Text') await fx.setze(hinweisFeld(fx, form, 'text'), 'Heute Schlussverkauf') assert.equal(sichtbar(hinweis), false, 'ohne % und „statt“ weg') await fx.setze(hinweisFeld(fx, form, 'art'), 'aktion') await fx.klick(fx.finde(form, 'button[type="submit"]')) assert.equal(puts(fx).length, 0, 'ohne Enddatum geht nichts hinaus') assert.equal(fx.text(hinweisFehlerPfad(fx, form, 'bis')), 'Eine Aktion braucht ein Enddatum.') assert.equal(hinweisFeld(fx, form, 'bis').getAttribute('aria-invalid'), 'true') // mit Enddatum geht es hinaus await fx.setze(hinweisFeld(fx, form, 'bis'), tagPlus(HEUTE, 5)) fx.antworten.push(antwort(daten(), 4)) await fx.klick(fx.finde(form, 'button[type="submit"]')) assert.equal(puts(fx).length, 1) assert.equal(puts(fx)[0].body.daten.hinweise[0].art, 'aktion') })) test('Hinweis: Ende vor Beginn ⇒ Feldfehler; ein Fehler des Servers steht am Feld', mitDom({}, async (fx) => { const { root } = await oeffne(fx) await fx.klick(fx.finde(root, '[data-werkzeug="hinweis-neu"]')) const form = neuerHinweis(fx, root) await fx.setze(hinweisFeld(fx, form, 'text'), 'Zeitraum falsch') await fx.setze(hinweisFeld(fx, form, 'von'), tagPlus(HEUTE, 5)) await fx.setze(hinweisFeld(fx, form, 'bis'), tagPlus(HEUTE, 2)) await fx.klick(fx.finde(form, 'button[type="submit"]')) assert.equal(puts(fx).length, 0) assert.equal(fx.text(hinweisFehlerPfad(fx, form, 'bis')), 'Das Ende darf nicht vor dem Beginn liegen.') // der Server hat das letzte Wort: Fehler an einem Textfeld await fx.setze(hinweisFeld(fx, form, 'bis'), tagPlus(HEUTE, 6)) const pfad = hinweisFeld(fx, form, 'text').getAttribute('data-feld') fx.antworten.push(fehler(400, 'x', { fehler: { [pfad]: 'Bitte gib einen Text ein.' } })) await fx.klick(fx.finde(form, 'button[type="submit"]')) assert.equal(puts(fx).length, 1) assert.equal(fx.text(fehlerAn(fx, form, pfad)), 'Bitte gib einen Text ein.') assert.equal(hinweisFeld(fx, form, 'text').getAttribute('aria-invalid'), 'true') })) test('Hinweis-Knopf: nur Link (https) oder Anrufen, Link-Prüfung als Hinweis, der Server entscheidet', mitDom({}, async (fx) => { const { root } = await oeffne(fx) await fx.klick(fx.finde(root, '[data-werkzeug="hinweis-neu"]')) const form = neuerHinweis(fx, root) const art = hinweisFeld(fx, form, 'knopf.art') assert.deepEqual(optionen(fx, art), ['', 'link', 'anruf'], 'kein Knopf, Link, Anrufen – sonst nichts') assert.deepEqual(fx.alle(art, 'option').map((o) => fx.text(o)), ['Kein Knopf', 'Link', 'Anrufen']) assert.equal(art.value, '', 'Vorgabe: kein Knopf') const felder = fx.finde(form, '[data-knopf-felder]') assert.equal(felder.hidden, true, 'ohne Knopf keine Knopf-Felder') assert.equal(hinweisFeld(fx, form, 'knopf.text').getAttribute('maxlength'), '30') await fx.setze(hinweisFeld(fx, form, 'text'), 'Neue Torten') await fx.setze(art, 'link') assert.equal(felder.hidden, false) assert.equal(hinweisFeld(fx, form, 'knopf.ziel').getAttribute('maxlength'), '300') const linkHinweis = fx.finde(form, '[data-hinweis="knopf-ziel"]') await fx.setze(hinweisFeld(fx, form, 'knopf.text'), 'Mehr erfahren') await fx.setze(hinweisFeld(fx, form, 'knopf.ziel'), 'http://beispiel.de/torten') assert.equal(sichtbar(linkHinweis), true, 'http ist kein https') assert.match(fx.text(linkHinweis), /https:\/\//) await fx.setze(hinweisFeld(fx, form, 'knopf.ziel'), 'javascript:alert(1)') assert.equal(sichtbar(linkHinweis), true) await fx.setze(hinweisFeld(fx, form, 'knopf.ziel'), 'https://beispiel.de/torten') assert.equal(sichtbar(linkHinweis), false, 'ein https-Link ist in Ordnung') // die Prüfung ist nur ein Hinweis: ein schlechter Link geht hinaus, der Server lehnt ihn ab, der Fehler steht am Feld await fx.setze(hinweisFeld(fx, form, 'knopf.ziel'), 'http://beispiel.de/torten') const pfad = hinweisFeld(fx, form, 'knopf.ziel').getAttribute('data-feld') fx.antworten.push(fehler(400, 'x', { fehler: { [pfad]: 'Bitte gib eine vollständige Adresse mit https:// an (höchstens 300 Zeichen, ohne Zugangsdaten).' } })) await fx.klick(fx.finde(form, 'button[type="submit"]')) assert.equal(puts(fx).length, 1, 'der Server entscheidet') assert.deepEqual(puts(fx)[0].body.daten.hinweise[0].knopf, { art: 'link', ziel: 'http://beispiel.de/torten', text: 'Mehr erfahren' }) assert.match(fx.text(fehlerAn(fx, form, pfad)), /https:\/\//) // Anrufen: Ziffern, Leerzeichen, + / - await fx.setze(art, 'anruf') const anrufHinweis = fx.finde(form, '[data-hinweis="knopf-ziel"]') await fx.setze(hinweisFeld(fx, form, 'knopf.ziel'), '06301 abc') assert.equal(sichtbar(anrufHinweis), true) assert.match(fx.text(anrufHinweis), /Ziffern/) await fx.setze(hinweisFeld(fx, form, 'knopf.ziel'), '+49 6301 123-456') assert.equal(sichtbar(anrufHinweis), false) await fx.setze(hinweisFeld(fx, form, 'knopf.text'), 'Anrufen') fx.antworten.push(antwort(daten(), 4)) await fx.klick(fx.finde(form, 'button[type="submit"]')) assert.deepEqual(puts(fx)[1].body.daten.hinweise[0].knopf, { art: 'anruf', ziel: '+49 6301 123-456', text: 'Anrufen' }) // Knopf wieder „Kein Knopf“ ⇒ knopf null const zwei = await oeffne(fx) await fx.klick(fx.finde(zwei.root, '[data-werkzeug="hinweis-neu"]')) const form2 = neuerHinweis(fx, zwei.root) await fx.setze(hinweisFeld(fx, form2, 'text'), 'Ohne Knopf') await fx.setze(hinweisFeld(fx, form2, 'knopf.art'), 'link') await fx.setze(hinweisFeld(fx, form2, 'knopf.art'), '') fx.antworten.push(antwort(daten(), 4)) await fx.klick(fx.finde(form2, 'button[type="submit"]')) assert.equal(puts(fx)[2].body.daten.hinweise[0].knopf, null) })) test('Hinweis bearbeiten und entfernen: der Eintrag wird per ID gefunden, die Reihenfolge der übrigen bleibt, Rückfrage in der Seite', mitDom({}, async (fx) => { const { root } = await oeffne(fx, { start: stand(daten({ hinweise: [hinweisAktion, hinweisUrlaub] })) }) const liste = abschnitt(fx, root, 'hinweise') const zeile = fx.finde(liste, 'li[data-id="h_aktion001"]') assert.ok(fx.text(zeile).includes('20 % auf alle Torten')) assert.ok(fx.text(zeile).includes('Aktion')) assert.match(fx.text(zeile), /Mehr erfahren/, 'der Knopf steht in der Zeile') await fx.klick(fx.finde(zeile, '[data-aktion="bearbeiten"]')) const form = fx.finde(zeile, 'form[aria-label="Hinweis bearbeiten"]') assert.ok(form) assert.equal(hinweisFeld(fx, form, 'text').value, '20 % auf alle Torten') assert.equal(hinweisFeld(fx, form, 'art').value, 'aktion') assert.equal(hinweisFeld(fx, form, 'knopf.art').value, 'link') assert.equal(hinweisFeld(fx, form, 'knopf.ziel').value, 'https://beispiel.de/torten') assert.equal(hinweisFeld(fx, form, 'knopf.text').value, 'Mehr erfahren') assert.equal(hinweisFeld(fx, form, 'bis').value, hinweisAktion.bis) assert.equal(sichtbar(fx.finde(form, '[data-hinweis="preis"]')), true, '% im gespeicherten Text zeigt den Preis-Hinweis gleich') await fx.setze(hinweisFeld(fx, form, 'text'), '30 % auf alle Torten') fx.antworten.push(antwort(daten(), 4)) await fx.klick(fx.finde(form, 'button[type="submit"]')) const h = puts(fx)[0].body.daten.hinweise assert.deepEqual(h.map((x) => x.id), ['h_aktion001', 'h_urlaub001'], 'dieselbe Reihenfolge') assert.equal(h[0].text, '30 % auf alle Torten') assert.deepEqual(h[1], hinweisUrlaub) assert.deepEqual(h[0].knopf, hinweisAktion.knopf) // Entfernen mit Rückfrage const zwei = await oeffne(fx, { start: stand(daten({ hinweise: [hinweisAktion, hinweisUrlaub] })) }) const zeile2 = fx.finde(zwei.root, 'li[data-id="h_urlaub001"]') await fx.klick(fx.finde(zeile2, '[data-aktion="bearbeiten"]')) await fx.klick(fx.finde(zeile2, '[data-aktion="entfernen"]')) const rueckfrage = fx.finde(zeile2, '.tm-frage-box') assert.match(fx.text(rueckfrage), /Hinweis entfernen\?/) assert.equal(puts(fx).length, 1, 'erst die Rückfrage') fx.antworten.push(antwort(daten({ hinweise: [hinweisAktion] }), 5)) await fx.klick(fx.finde(rueckfrage, 'button[type="submit"]')) assert.deepEqual(puts(fx)[1].body.daten.hinweise.map((x) => x.id), ['h_aktion001']) assert.equal(fx.finde(zwei.root, 'li[data-id="h_urlaub001"]'), null, 'der Hinweis ist aus der Liste') })) test('Hinweis-Liste: laufend und geplant mit Pille, Vergangenes eingeklappt, nach Beginn sortiert; Text als Text', mitDom({}, async (fx) => { const bose = '' const { root } = await oeffne(fx, { start: stand(daten({ hinweise: [hinweisUrlaub, { ...hinweisAktion, text: bose }, hinweisAlt] })) }) const liste = abschnitt(fx, root, 'hinweise') assert.equal(fx.alle(root, 'img').length, 0) const kommend = fx.alle(fx.finde(liste, 'ul[aria-label="Laufende und geplante Hinweise"]'), 'li[data-id]').map((li) => li.getAttribute('data-id')) assert.deepEqual(kommend, ['h_aktion001', 'h_urlaub001'], 'nach Beginn sortiert') assert.match(fx.text(fx.finde(liste, 'li[data-id="h_aktion001"]')), /läuft/) assert.match(fx.text(fx.finde(liste, 'li[data-id="h_urlaub001"]')), /geplant/) const vorbei = fx.finde(liste, 'details') assert.match(fx.text(fx.finde(vorbei, 'summary')), /Vergangene Hinweise \(1\)/) assert.ok(fx.finde(vorbei, 'li[data-id="h_vorbei001"]')) })) test('Hinweise: bei 20 Hinweisen ist „Hinweis-Leiste anlegen“ gesperrt und sagt warum', mitDom({}, async (fx) => { const viele = Array.from({ length: ZEITEN_LIMITS.hinweise }, (_, i) => ({ id: `h_viele${String(i).padStart(3, '0')}`, art: 'info', text: `Hinweis ${i}`, knopf: null, von: null, bis: null })) const { root } = await oeffne(fx, { start: stand(daten({ hinweise: viele })) }) assert.equal(fx.finde(root, '[data-werkzeug="hinweis-neu"]').disabled, true) assert.match(fx.text(abschnitt(fx, root, 'hinweise')), /Mehr als 20 Hinweise sind nicht möglich/) })) test('Hinweis speichern zeichnet nur die Hinweise neu: Wochenplan-Entwurf und Kontakt-Eingaben bleiben', mitDom({}, async (fx) => { const { root } = await oeffne(fx) const [von1] = zeitFelder(fx, tagZeile(fx, root, P1, '5')) await fx.setze(von1, '08:15') await fx.setze(kfeld(fx, root, 'adresse'), 'Neue Straße 9') const kontaktAlt = abschnitt(fx, root, 'kontakt') await fx.klick(fx.finde(root, '[data-werkzeug="hinweis-neu"]')) const form = neuerHinweis(fx, root) await fx.setze(hinweisFeld(fx, form, 'text'), 'Neu') fx.antworten.push(antwort(daten({ hinweise: [{ id: 'h_neu00002', art: 'info', text: 'Neu', knopf: null, von: null, bis: null }] }), 4)) await fx.klick(fx.finde(form, 'button[type="submit"]')) assert.equal(zeitFelder(fx, tagZeile(fx, root, P1, '5'))[0].value, '08:15', 'der Wochenplan-Entwurf steht noch da') assert.equal(abschnitt(fx, root, 'kontakt'), kontaktAlt, 'der Kontakt-Abschnitt ist dasselbe Element') assert.equal(kfeld(fx, root, 'adresse').value, 'Neue Straße 9') assert.ok(fx.finde(root, 'li[data-id="h_neu00002"]')) assert.equal(puts(fx)[0].body.daten.plaene[0].tage['5'].fenster[0][0], '09:00', 'der Entwurf ging nicht mit hinaus') })) test('Urlaub-Frage „Ja, anlegen“: der neue Hinweis steht danach auch in der Hinweis-Liste', 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_urlaub02', 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 vorschlag = { id: 'h_vorschlag', art: 'urlaub', text: 'Betriebsurlaub', knopf: null, von: tagPlus(von, -7), bis } fx.antworten.push(antwort(daten({ ausnahmen: [urlaub], hinweise: [vorschlag] }), 5)) await fx.klick(fx.finde(fx.finde(root, '[data-frage="hinweis"]'), '[data-aktion="hinweis-ja"]')) assert.ok(fx.finde(abschnitt(fx, root, 'hinweise'), 'li[data-id="h_vorschlag"]'), 'die Hinweis-Liste zeigt ihn') })) test('Urlaub-Frage „Ja, anlegen“ lässt offene, ungespeicherte Hinweis-Formulare stehen (neuer Hinweis und Bearbeiten) und zeigt den neuen Hinweis trotzdem in der Liste', mitDom({}, async (fx) => { const { root } = await oeffne(fx, { start: stand(daten({ hinweise: [hinweisAktion] })) }) // ein neuer Hinweis wird getippt, aber nicht gespeichert; an der Aktion wird etwas geändert, aber nicht gespeichert await fx.klick(fx.finde(root, '[data-werkzeug="hinweis-neu"]')) const neuForm = neuerHinweis(fx, root) await fx.setze(hinweisFeld(fx, neuForm, 'text'), 'Noch nicht gespeichert') const zeile = fx.finde(root, 'li[data-id="h_aktion001"]') await fx.klick(fx.finde(zeile, '[data-aktion="bearbeiten"]')) const bearbeiten = fx.finde(zeile, 'form[aria-label="Hinweis bearbeiten"]') await fx.setze(hinweisFeld(fx, bearbeiten, 'text'), '30 % auf alle Torten') // Betriebsurlaub speichern und die Frage mit „Ja, anlegen“ beantworten 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_urlaub03', art: 'urlaub', von, bis, text: '', modus: 'zu', fenster: [], plan: null } fx.antworten.push(antwort(daten({ ausnahmen: [urlaub], hinweise: [hinweisAktion] }), 4)) await fx.klick(fx.finde(form, 'button[type="submit"]')) const vorschlag = { id: 'h_vorschlag', art: 'urlaub', text: 'Betriebsurlaub', knopf: null, von: tagPlus(von, -7), bis } fx.antworten.push(antwort(daten({ ausnahmen: [urlaub], hinweise: [hinweisAktion, vorschlag] }), 5)) await fx.klick(fx.finde(fx.finde(root, '[data-frage="hinweis"]'), '[data-aktion="hinweis-ja"]')) assert.deepEqual(puts(fx)[1].body.daten.hinweise.map((x) => x.id).slice(0, 1), ['h_aktion001']) assert.equal(puts(fx)[1].body.daten.hinweise.length, 2, 'nur der Vorschlag kommt dazu, getippte Entwürfe gehen nicht mit hinaus') // der neue Hinweis steht in der Liste, in der Reihenfolge nach Beginn const liste = abschnitt(fx, root, 'hinweise') assert.deepEqual(fx.alle(fx.finde(liste, 'ul[aria-label="Laufende und geplante Hinweise"]'), 'li[data-id]').map((li) => li.getAttribute('data-id')), ['h_aktion001', 'h_vorschlag']) // die offenen Formulare sind dieselben Elemente mit ihren Eingaben assert.ok(neuerHinweis(fx, root) === neuForm, 'das Formular „Neuer Hinweis“ steht noch') // ok statt equal: ein Fehlschlag soll keine DOM-Knoten ausgeben assert.equal(hinweisFeld(fx, neuForm, 'text')?.value, 'Noch nicht gespeichert') assert.ok(fx.finde(root, 'li[data-id="h_aktion001"]') === zeile, 'dieselbe Zeile') assert.ok(fx.finde(zeile, 'form[aria-label="Hinweis bearbeiten"]') === bearbeiten, 'dasselbe Bearbeiten-Formular') assert.equal(hinweisFeld(fx, bearbeiten, 'text')?.value, '30 % auf alle Torten') // und das offene Formular lässt sich danach speichern: es beruht auf dem neuen Stand (Vorschlag ist dabei) fx.antworten.push(antwort(daten({ hinweise: [hinweisAktion, vorschlag, { id: 'h_getippt1', art: 'info', text: 'Noch nicht gespeichert', knopf: null, von: null, bis: null }] }), 6)) await fx.klick(fx.finde(neuForm, 'button[type="submit"]')) const h = puts(fx)[2].body.daten.hinweise assert.deepEqual(h.map((x) => x.text), [hinweisAktion.text, 'Betriebsurlaub', 'Noch nicht gespeichert']) })) test('Urlaub-Frage „Ja, anlegen“ bringt die Hinweis-Liste auf 20: „Hinweis-Leiste anlegen“ ist danach gesperrt und sagt warum (ohne den Abschnitt neu zu zeichnen)', mitDom({}, async (fx) => { const viele = Array.from({ length: ZEITEN_LIMITS.hinweise - 1 }, (_, i) => ({ id: `h_viele${String(i).padStart(3, '0')}`, art: 'info', text: `Hinweis ${i}`, knopf: null, von: null, bis: null })) const { root } = await oeffne(fx, { start: stand(daten({ hinweise: viele })) }) const liste = abschnitt(fx, root, 'hinweise') assert.equal(fx.finde(liste, '[data-werkzeug="hinweis-neu"]').disabled, false) 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_urlaub04', art: 'urlaub', von, bis, text: '', modus: 'zu', fenster: [], plan: null } fx.antworten.push(antwort(daten({ ausnahmen: [urlaub], hinweise: viele }), 4)) await fx.klick(fx.finde(form, 'button[type="submit"]')) const vorschlag = { id: 'h_vorschlag', art: 'urlaub', text: 'Betriebsurlaub', knopf: null, von: tagPlus(von, -7), bis } fx.antworten.push(antwort(daten({ ausnahmen: [urlaub], hinweise: [...viele, vorschlag] }), 5)) await fx.klick(fx.finde(fx.finde(root, '[data-frage="hinweis"]'), '[data-aktion="hinweis-ja"]')) assert.ok(abschnitt(fx, root, 'hinweise') === liste, 'der Abschnitt ist dasselbe Element') assert.equal(fx.finde(liste, '[data-werkzeug="hinweis-neu"]').disabled, true) assert.match(fx.text(liste), /Mehr als 20 Hinweise sind nicht möglich/) assert.ok(fx.finde(liste, 'li[data-id="h_vorschlag"]')) })) test('Hinweise abgeschaltet (nur ansehen): Liste ja, aber kein Knopf zum Anlegen und alle Felder gesperrt', mitDom({}, async (fx) => { const { root } = await oeffne(fx, { state: 'disabled', start: stand(daten({ hinweise: [hinweisAktion] }), { state: 'disabled' }) }) const liste = abschnitt(fx, root, 'hinweise') assert.ok(fx.finde(liste, 'li[data-id="h_aktion001"]')) assert.equal(fx.finde(liste, '[data-werkzeug="hinweis-neu"]'), null) await fx.klick(fx.finde(liste, '[data-aktion="bearbeiten"]')) assert.equal(fx.text(fx.finde(liste, '[data-aktion="bearbeiten"]')), 'Ansehen') const form = fx.finde(liste, 'form[aria-label="Hinweis bearbeiten"]') assert.equal(fx.finde(form, 'button[type="submit"]'), null) for (const feld of fx.alle(form, 'input, select')) assert.equal(feld.matches(':disabled'), true, `${feld.tagName} ist gesperrt`) })) // ---------------------------------------------------------------------------------------------------------------- Kontakt-Leiste test('Kontakt-Leiste: Ein/Aus, Telefon, Adresse und WhatsApp aus den Daten; unverändert gespeichert bleibt bestaetigtAt (R12)', mitDom({}, async (fx) => { const { root } = await oeffne(fx) const form = kontaktForm(fx, root) assert.ok(form) assert.equal(kfeld(fx, root, 'an').checked, true) assert.equal(kfeld(fx, root, 'telefon').value, '+49 6301123456', 'die gespeicherte Nummer (Ziffern mit Vorwahl) steht mit + da') assert.equal(kfeld(fx, root, 'telefon').getAttribute('maxlength'), '30') assert.equal(kfeld(fx, root, 'adresse').value, KONTAKT.adresse) assert.equal(kfeld(fx, root, 'adresse').getAttribute('maxlength'), '200') assert.equal(kfeld(fx, root, 'whatsapp').checked, true) assert.equal(kfeld(fx, root, 'whatsapp.nummer').value, '+49 17112345678') assert.equal(kfeld(fx, root, 'whatsapp.starttext').value, 'Hallo!') assert.equal(kfeld(fx, root, 'whatsapp.starttext').getAttribute('maxlength'), '200') assert.equal(kfeld(fx, root, 'whatsapp.bestaetigtAt').checked, true, 'schon bestätigt') assert.match(fx.text(form), /Ich habe den WhatsApp-Hinweis in meine Datenschutzerklärung aufgenommen/) assert.equal(fx.finde(form, '[data-whatsapp]').hidden, false) fx.antworten.push(antwort(daten(), 4)) await fx.klick(fx.finde(form, '[data-aktion="kontakt-speichern"]')) assert.deepEqual(puts(fx)[0].body.daten.kontakt, KONTAKT, 'dieselben Werte, bestaetigtAt unverändert') })) test('Kontakt: das Telefon geht so hinaus, wie der Kunde es schreibt; die Nummer aus der Antwort des Servers steht danach im Feld und in der Meldung', mitDom({}, async (fx) => { const { root } = await oeffne(fx) await fx.setze(kfeld(fx, root, 'telefon'), '0171 1234567') await fx.setze(kfeld(fx, root, 'adresse'), 'Marktplatz 1') const gespeichert = daten({ kontakt: { ...KONTAKT, telefon: '491711234567', adresse: 'Marktplatz 1' } }) fx.antworten.push(antwort(gespeichert, 4)) await fx.klick(fx.finde(kontaktForm(fx, root), '[data-aktion="kontakt-speichern"]')) const k = puts(fx)[0].body.daten.kontakt assert.equal(k.telefon, '0171 1234567') assert.equal(k.adresse, 'Marktplatz 1') assert.equal(kfeld(fx, root, 'telefon').value, '+49 1711234567', 'das Feld zeigt, was der Server gespeichert hat') assert.ok(meldung(fx, root).includes('+49 1711234567'), 'die Meldung nennt die gespeicherte Nummer') assert.match(meldung(fx, root), /Kontakt-Leiste gespeichert/) })) test('Kontakt: ohne gespeicherte Kontaktdaten sind die Felder leer und die Leiste aus', mitDom({}, async (fx) => { const ohne = daten() delete ohne.kontakt const { root } = await oeffne(fx, { start: stand(ohne) }) assert.equal(kfeld(fx, root, 'an').checked, false) assert.equal(kfeld(fx, root, 'telefon').value, '') assert.equal(kfeld(fx, root, 'whatsapp').checked, false) assert.equal(fx.finde(kontaktForm(fx, root), '[data-whatsapp]').hidden, true) await fx.klick(kfeld(fx, root, 'an')) await fx.setze(kfeld(fx, root, 'telefon'), '06301 123456') fx.antworten.push(antwort(daten(), 4)) await fx.klick(fx.finde(kontaktForm(fx, root), '[data-aktion="kontakt-speichern"]')) assert.deepEqual(puts(fx)[0].body.daten.kontakt, { an: true, telefon: '06301 123456', adresse: '', whatsapp: null }) })) test('Kontakt: WhatsApp verlangt den Datenschutz-Haken – ohne Haken geht nichts hinaus, mit neuem Haken ein frischer ISO-Zeitpunkt, ein gespeicherter bleibt', mitDom({}, async (fx) => { const ohne = daten({ kontakt: { an: true, telefon: '496301123456', adresse: '', whatsapp: null } }) const { root } = await oeffne(fx, { start: stand(ohne) }) const form = kontaktForm(fx, root) assert.equal(fx.finde(form, '[data-whatsapp]').hidden, true) await fx.klick(kfeld(fx, root, 'whatsapp')) assert.equal(fx.finde(form, '[data-whatsapp]').hidden, false, 'WhatsApp an zeigt die Felder') assert.equal(kfeld(fx, root, 'whatsapp.bestaetigtAt').checked, false) await fx.setze(kfeld(fx, root, 'whatsapp.nummer'), '0171 7654321') await fx.setze(kfeld(fx, root, 'whatsapp.starttext'), 'Hallo, ich habe eine Frage.') await fx.klick(fx.finde(form, '[data-aktion="kontakt-speichern"]')) assert.equal(puts(fx).length, 0, 'ohne Haken geht nichts hinaus') assert.match(fx.text(fehlerAn(fx, form, 'kontakt.whatsapp.bestaetigtAt')), /Bitte bestätige, dass du den WhatsApp-Hinweis in deine Datenschutzerklärung aufgenommen hast/) assert.equal(kfeld(fx, root, 'whatsapp.bestaetigtAt').getAttribute('aria-invalid'), 'true') // mit Haken: frischer Zeitpunkt (der Server ersetzt ihn durch seine Zeit) await fx.klick(kfeld(fx, root, 'whatsapp.bestaetigtAt')) const vorher = Date.now() const serverZeit = '2026-10-05T08:00:00.000Z' fx.antworten.push(antwort(daten({ kontakt: { an: true, telefon: '496301123456', adresse: '', whatsapp: { nummer: '491717654321', starttext: 'Hallo, ich habe eine Frage.', bestaetigtAt: serverZeit } } }), 4)) await fx.klick(fx.finde(form, '[data-aktion="kontakt-speichern"]')) const w = puts(fx)[0].body.daten.kontakt.whatsapp assert.equal(w.nummer, '0171 7654321') assert.equal(w.starttext, 'Hallo, ich habe eine Frage.') assert.match(w.bestaetigtAt, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/) assert.ok(Math.abs(Date.parse(w.bestaetigtAt) - vorher) < 5000, 'ein frischer Zeitpunkt') assert.equal(kfeld(fx, root, 'whatsapp.nummer').value, '+49 1717654321', 'die Nummer des Servers') assert.ok(meldung(fx, root).includes('+49 1717654321')) // der Haken steht jetzt (gespeichert): das nächste Speichern schickt den gespeicherten Wert unverändert zurück assert.equal(kfeld(fx, root, 'whatsapp.bestaetigtAt').checked, true) fx.antworten.push(antwort(daten(), 5)) await fx.klick(fx.finde(kontaktForm(fx, root), '[data-aktion="kontakt-speichern"]')) assert.equal(puts(fx)[1].body.daten.kontakt.whatsapp.bestaetigtAt, serverZeit) })) test('Kontakt: WhatsApp abwählen ⇒ whatsapp null (und der Haken ist weg); die Leiste ausschalten ⇒ an false', mitDom({}, async (fx) => { const { root } = await oeffne(fx) await fx.klick(kfeld(fx, root, 'whatsapp')) assert.equal(fx.finde(kontaktForm(fx, root), '[data-whatsapp]').hidden, true) await fx.klick(kfeld(fx, root, 'an')) fx.antworten.push(antwort(daten(), 4)) await fx.klick(fx.finde(kontaktForm(fx, root), '[data-aktion="kontakt-speichern"]')) const k = puts(fx)[0].body.daten.kontakt assert.equal(k.whatsapp, null) assert.equal(k.an, false) assert.equal(k.telefon, '496301123456', 'die übrigen Angaben bleiben') })) test('Kontakt: ein Fehler des Servers steht am Feld; Speichern ändert nichts an den anderen Formularen', mitDom({}, async (fx) => { const { root } = await oeffne(fx) const [von1] = zeitFelder(fx, tagZeile(fx, root, P1, '5')) await fx.setze(von1, '07:45') await fx.setze(kfeld(fx, root, 'telefon'), 'abc') fx.antworten.push(fehler(400, 'x', { fehler: { 'kontakt.telefon': 'Bitte gib eine gültige Telefonnummer an (nur Ziffern, Leerzeichen, + ( ) / . und -).' } })) await fx.klick(fx.finde(kontaktForm(fx, root), '[data-aktion="kontakt-speichern"]')) assert.match(fx.text(fehlerAn(fx, root, 'kontakt.telefon')), /gültige Telefonnummer/) assert.equal(kfeld(fx, root, 'telefon').getAttribute('aria-invalid'), 'true') assert.equal(zeitFelder(fx, tagZeile(fx, root, P1, '5'))[0].value, '07:45') // Speichern gelingt: nur der Kontakt-Abschnitt wird neu gezeichnet await fx.setze(kfeld(fx, root, 'telefon'), '06301 111') fx.antworten.push(antwort(daten({ kontakt: { ...KONTAKT, telefon: '4963011110' } }), 4)) await fx.klick(fx.finde(kontaktForm(fx, root), '[data-aktion="kontakt-speichern"]')) assert.equal(zeitFelder(fx, tagZeile(fx, root, P1, '5'))[0].value, '07:45', 'der Wochenplan-Entwurf bleibt') })) test('Kontakt in der Kundenansicht (R12): der WhatsApp-Haken ist gesperrt, ein gespeicherter Zeitpunkt geht unverändert zurück, nie ein neuer', mitDom({}, async (fx) => { const { root } = await oeffne(fx, { start: stand(daten(), { kundenansicht: true }), kundenansicht: true }) const haken = kfeld(fx, root, 'whatsapp.bestaetigtAt') assert.equal(haken.checked, true) assert.equal(haken.disabled, true, 'gesperrt') assert.match(fx.text(kontaktForm(fx, root)), /Kundenansicht/) assert.match(fx.text(fx.finde(kontaktForm(fx, root), '[data-hinweis="whatsapp-kundenansicht"]')), /nur der Kunde selbst/) await fx.klick(haken) assert.equal(haken.checked, true, 'ein Klick ändert nichts') await fx.setze(kfeld(fx, root, 'whatsapp.starttext'), 'Hallo zusammen') fx.antworten.push(antwort(daten(), 4, { kundenansicht: true })) await fx.klick(fx.finde(kontaktForm(fx, root), '[data-aktion="kontakt-speichern"]')) assert.equal(puts(fx)[0].body.daten.kontakt.whatsapp.bestaetigtAt, KONTAKT.whatsapp.bestaetigtAt, 'unverändert zurück') assert.equal(puts(fx)[0].body.daten.kontakt.whatsapp.starttext, 'Hallo zusammen') // ohne gespeicherte Bestätigung: WhatsApp einschalten geht in der Kundenansicht nicht (nie ein neuer Zeitpunkt) const ohne = daten({ kontakt: { an: true, telefon: '496301123456', adresse: '', whatsapp: null } }) const zwei = await oeffne(fx, { start: stand(ohne, { kundenansicht: true }), kundenansicht: true }) await fx.klick(kfeld(fx, zwei.root, 'whatsapp')) await fx.setze(kfeld(fx, zwei.root, 'whatsapp.nummer'), '0171 7654321') const haken2 = kfeld(fx, zwei.root, 'whatsapp.bestaetigtAt') assert.equal(haken2.disabled, true) assert.equal(haken2.checked, false) await fx.klick(fx.finde(kontaktForm(fx, zwei.root), '[data-aktion="kontakt-speichern"]')) assert.equal(puts(fx).length, 1, 'es geht nichts hinaus') assert.match(fx.text(fehlerAn(fx, zwei.root, 'kontakt.whatsapp.bestaetigtAt')), /Kunde/) // Kundenansicht ohne WhatsApp: speichern geht, whatsapp bleibt null await fx.klick(kfeld(fx, zwei.root, 'whatsapp')) fx.antworten.push(antwort(daten(), 5, { kundenansicht: true })) await fx.klick(fx.finde(kontaktForm(fx, zwei.root), '[data-aktion="kontakt-speichern"]')) assert.equal(puts(fx)[1].body.daten.kontakt.whatsapp, null) })) test('Kontakt abgeschaltet (nur ansehen): Felder gesperrt, kein Speichern-Knopf', mitDom({}, async (fx) => { const { root } = await oeffne(fx, { state: 'disabled', start: stand(daten(), { state: 'disabled' }) }) const form = kontaktForm(fx, root) assert.equal(fx.finde(form, 'button[type="submit"]'), null) for (const feld of fx.alle(form, 'input, select')) assert.equal(feld.matches(':disabled'), true, `${feld.tagName} ist gesperrt`) })) // ---------------------------------------------------------------------------------------------------------------- Google test('Google: „Für Google kopieren“ legt den googleText des Servers in die Zwischenablage; Link zum Google-Profil, Satz zu den Google-Daten', mitDom({}, async (fx) => { const text = 'Montag: 09:00–12:30, 13:30–18:00\nDienstag: 09:00–18:00\nSonntag: geschlossen' const { root } = await oeffne(fx, { start: stand(daten(), { googleText: text }) }) const el = abschnitt(fx, root, 'google') const link = fx.finde(el, 'a[data-google="profil"]') assert.equal(fx.text(link), 'Google-Profil öffnen') assert.equal(link.getAttribute('href'), 'https://business.google.com/') assert.match(link.getAttribute('rel'), /noopener/) assert.equal(link.getAttribute('target'), '_blank') assert.match(fx.text(el), /Google-Daten im Seitencode laufen automatisch/) const knopf = fx.finde(el, '[data-aktion="google-kopieren"]') assert.equal(fx.text(knopf), 'Für Google kopieren') assert.equal(fx.finde(el, '[data-google="text"]').value, text, 'der Text steht zum Nachlesen da') const kopiert = [] await mitZwischenablage({ writeText: async (t) => { kopiert.push(t) } }, async () => { await fx.klick(knopf) }) assert.deepEqual(kopiert, [text]) assert.match(fx.text(fx.finde(el, '[data-meldung="google"]')), /Kopiert/) // nach einem Speichern gilt der neue Text vom Server const neu = 'Montag: 08:00–12:00\nSonntag: geschlossen' fx.antworten.push(antwort(daten(), 4, { googleText: neu })) await fx.klick(speichernKnopf(fx, root, P1)) assert.equal(fx.finde(el, '[data-google="text"]').value, neu, 'die Anzeige folgt dem Server') await mitZwischenablage({ writeText: async (t) => { kopiert.push(t) } }, async () => { await fx.klick(knopf) }) assert.deepEqual(kopiert, [text, neu]) })) test('Google: fehlt die Zwischenablage oder wird das Kopieren abgelehnt, ist der Text markiert und eine Meldung sagt, wie es weitergeht', mitDom({}, async (fx) => { const text = 'Montag: 09:00–18:00' const { root } = await oeffne(fx, { start: stand(daten(), { googleText: text }) }) const el = abschnitt(fx, root, 'google') const feld = fx.finde(el, '[data-google="text"]') assert.equal(feld.hasAttribute('readonly'), true) let markiert = 0 feld.select = () => { markiert += 1 } await mitZwischenablage(undefined, async () => { await fx.klick(fx.finde(el, '[data-aktion="google-kopieren"]')) }) assert.equal(markiert, 1, 'ohne Zwischenablage: Text markiert') assert.match(fx.text(fx.finde(el, '[data-meldung="google"]')), /markiert/) assert.doesNotMatch(fx.text(fx.finde(el, '[data-meldung="google"]')), /^Kopiert/) await mitZwischenablage({ writeText: async () => { throw new Error('verweigert') } }, async () => { await fx.klick(fx.finde(el, '[data-aktion="google-kopieren"]')) }) assert.equal(markiert, 2, 'abgelehnt: Text markiert') assert.match(fx.text(fx.finde(el, '[data-meldung="google"]')), /markiert/) })) // ---------------------------------------------------------------------------------------------------------------- Vorschau const vorschauText = (fx, root) => fx.text(fx.finde(root, '[data-vorschau="jetzt"]')) const regler = (fx, root, name) => fx.finde(root, `[data-regler="${name}"]`) const vorschauAufrufe = (fx) => fx.fetchLog.filter((a) => /\/vorschau\?/.test(a.url)) test('Vorschau oben: „So steht es gerade auf deiner Seite: …“ aus dem GET, nach dem Speichern aus der Antwort des PUT', mitDom({}, async (fx) => { const { root } = await oeffne(fx, { start: stand(daten(), { vorschau: { offen: true, text: 'Jetzt geöffnet · schließt um 18 Uhr', kurz: 'Geöffnet' } }) }) const zeile = fx.finde(root, '.zt-vorschau') assert.ok(zeile) assert.match(fx.text(zeile), /So steht es gerade auf deiner Seite:/) assert.equal(vorschauText(fx, root), 'Jetzt geöffnet · schließt um 18 Uhr') assert.equal(fx.finde(root, '[data-vorschau="jetzt"]').getAttribute('data-offen'), 'true') assert.equal(root.querySelector('.zt-kopfbereich').contains(zeile), true, 'oben, im Kopfbereich') fx.antworten.push(antwort(daten(), 4, { vorschau: { offen: false, text: 'Geschlossen · öffnet morgen um 9 Uhr', kurz: 'Geschlossen' } })) await fx.klick(speichernKnopf(fx, root, P1)) assert.equal(vorschauText(fx, root), 'Geschlossen · öffnet morgen um 9 Uhr') assert.equal(fx.finde(root, '[data-vorschau="jetzt"]').getAttribute('data-offen'), 'false') assert.equal(vorschauAufrufe(fx).length, 0, 'ohne Regler keine weitere Anfrage') })) test('Vorschau vor dem ersten Speichern: es steht noch nichts da; nach dem ersten Speichern der Status aus der Antwort', mitDom({}, async (fx) => { const { root } = await oeffne(fx, { start: stand(null) }) assert.match(fx.text(fx.finde(root, '.zt-vorschau')), /So steht es gerade auf deiner Seite:/) assert.match(vorschauText(fx, root), /noch keine Öffnungszeiten gespeichert/i) const id = fx.finde(root, 'details[data-plan]').getAttribute('data-plan') fx.antworten.push(antwort(daten({ plaene: [plan(id)] }), 1, { vorschau: { offen: null, text: 'Öffnungszeiten nach Vereinbarung', kurz: 'Nach Vereinbarung' } })) await fx.klick(speichernKnopf(fx, root, id)) assert.equal(vorschauText(fx, root), 'Öffnungszeiten nach Vereinbarung') assert.equal(fx.alle(root, '.zt-vorschau').length, 1) })) test('Vorschau-Regler: fragt /vorschau mit dem nächsten Vorkommen des Wochentags und der Uhrzeit (Berliner Wanduhr) und zeigt die Antwort', mitDom({}, async (fx) => { const { root } = await oeffne(fx, { start: stand(daten(), { vorschau: { offen: true, text: 'Jetzt geöffnet', kurz: 'Geöffnet' } }) }) const tag = regler(fx, root, 'tag') const zeit = regler(fx, root, 'zeit') assert.deepEqual(optionen(fx, tag), ['1', '2', '3', '4', '5', '6', '7']) assert.deepEqual(fx.alle(tag, 'option').map((o) => fx.text(o)), ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag']) assert.equal(tag.value, String(wochentagNr(HEUTE)), 'Vorgabe: heute') assert.equal(zeit.getAttribute('type'), 'time') assert.match(zeit.value, /^\d{2}:\d{2}$/, 'Vorgabe: jetzt (Berliner Zeit)') assert.match(fx.text(root.querySelector('.zt-regler')), /Uhrzeit testen/) const geschlossen = { status: 200, body: { offen: false, text: 'Geschlossen · öffnet morgen um 9 Uhr', kurz: 'Geschlossen' } } fx.antworten.push(geschlossen, geschlossen) // eine Anfrage je Änderung await fx.setze(tag, '3') await fx.setze(zeit, '14:30') const aufrufe = vorschauAufrufe(fx) assert.ok(aufrufe.length >= 1) const letzter = aufrufe[aufrufe.length - 1] assert.equal(letzter.method, 'GET') const url = new URL(letzter.url, 'http://x') assert.equal(url.pathname, '/api/zeiten/p1/vorschau') assert.equal(url.searchParams.get('zeit'), `${naechster(3)}T14:30`) const test = fx.text(fx.finde(root, '[data-vorschau="test"]')) assert.match(test, /Geschlossen · öffnet morgen um 9 Uhr/) assert.ok(test.includes('Mittwoch'), 'der Tag der Anfrage steht dabei') assert.ok(test.includes('14:30')) assert.equal(vorschauText(fx, root), 'Jetzt geöffnet', 'die Vorschau „jetzt“ bleibt') })) test('Vorschau-Regler: 409 keine_zeiten ist ein Hinweis, kein Fehler; ein anderer Fehler steht als Fehler; leere Uhrzeit fragt nichts', mitDom({}, async (fx) => { const { root } = await oeffne(fx, { start: stand(null) }) fx.antworten.push(fehler(409, 'Bitte speichere zuerst deine Öffnungszeiten.', { code: 'keine_zeiten' })) await fx.setze(regler(fx, root, 'zeit'), '10:00') const ergebnis = fx.finde(root, '[data-vorschau="test"]') assert.match(fx.text(ergebnis), /Speichere zuerst deine Öffnungszeiten/) assert.notEqual(ergebnis.getAttribute('role'), 'alert', 'kein Fehler') assert.equal(root.querySelector('[data-meldung="editor"]').textContent, '', 'nichts über dem Editor') // anderer Fehler fx.antworten.push(fehler(500, 'Fehler 500')) await fx.setze(regler(fx, root, 'zeit'), '11:00') assert.equal(fx.finde(root, '[data-vorschau="test"]').getAttribute('role'), 'alert') assert.match(fx.text(fx.finde(root, '[data-vorschau="test"]')), /Fehler 500/) // Uhrzeit leer ⇒ keine Anfrage const n = vorschauAufrufe(fx).length await fx.setze(regler(fx, root, 'zeit'), '') assert.equal(vorschauAufrufe(fx).length, n) })) test('Vorschau-Regler: nur die letzte Anfrage zählt – eine späte Antwort auf eine frühere überschreibt nichts', mitDom({}, async (fx) => { const { root } = await oeffne(fx) const echtes = globalThis.fetch let freigeben const wartet = new Promise((resolve) => { freigeben = resolve }) let nr = 0 globalThis.fetch = async (url, optionen) => { if (!/\/vorschau\?/.test(String(url))) return echtes(url, optionen) nr += 1 const meine = nr if (meine === 1) await wartet // die erste Antwort kommt zuletzt fx.fetchLog.push({ method: 'GET', url: String(url) }) return { ok: true, status: 200, json: async () => ({ offen: meine === 1, text: meine === 1 ? 'Erste Frage' : 'Zweite Frage', kurz: '' }) } } try { const erste = fx.setze(regler(fx, root, 'zeit'), '09:00') await fx.ruhe() await fx.setze(regler(fx, root, 'zeit'), '20:00') assert.match(fx.text(fx.finde(root, '[data-vorschau="test"]')), /Zweite Frage/) freigeben() await erste await fx.ruhe() assert.match(fx.text(fx.finde(root, '[data-vorschau="test"]')), /Zweite Frage/, 'die späte erste Antwort wird verworfen') assert.doesNotMatch(fx.text(fx.finde(root, '[data-vorschau="test"]')), /Erste Frage/) } finally { globalThis.fetch = echtes } })) test('Vorschau-Regler: nach dem Speichern wird dieselbe Uhrzeit mit den neuen Daten erneut gefragt', mitDom({}, async (fx) => { const { root } = await oeffne(fx) fx.antworten.push({ status: 200, body: { offen: true, text: 'Alt', kurz: '' } }) await fx.setze(regler(fx, root, 'zeit'), '10:00') assert.match(fx.text(fx.finde(root, '[data-vorschau="test"]')), /Alt/) fx.antworten.push(antwort(daten(), 4), { status: 200, body: { offen: false, text: 'Neu', kurz: '' } }) await fx.klick(speichernKnopf(fx, root, P1)) await fx.ruhe() assert.match(fx.text(fx.finde(root, '[data-vorschau="test"]')), /Neu/) assert.equal(new URL(vorschauAufrufe(fx).at(-1).url, 'http://x').searchParams.get('zeit').slice(-5), '10:00') })) test('Vorschau abgeschaltet (nur ansehen): keine Vorschau und kein Regler (die Website zeigt nichts mehr)', mitDom({}, async (fx) => { const { root } = await oeffne(fx, { state: 'disabled', start: stand(daten(), { state: 'disabled' }) }) assert.equal(fx.finde(root, '.zt-vorschau'), null) assert.equal(fx.finde(root, '[data-regler]'), null) })) test('naechstesVorkommen: der erste Tag ab heute (einschließlich) mit dem Wochentag', async () => { const { naechstesVorkommen, wochentagVon } = await import('../public/zeiten-view.js') assert.equal(wochentagVon('2026-10-05'), 1, 'Montag') assert.equal(wochentagVon('2026-10-11'), 7, 'Sonntag') assert.equal(naechstesVorkommen('2026-10-05', 1), '2026-10-05', 'heute zählt') assert.equal(naechstesVorkommen('2026-10-05', 3), '2026-10-07') assert.equal(naechstesVorkommen('2026-10-07', 1), '2026-10-12', 'nächste Woche') assert.equal(naechstesVorkommen('2026-10-05', 7), '2026-10-11') assert.equal(naechstesVorkommen('2026-12-30', 5), '2027-01-01', 'über den Jahreswechsel') }) // ---------------------------------------------------------------------------------------------------------------- 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', 'zeiten-leisten.js', 'zeiten-google.js'] test('Quelltext: sechs Editor-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`) }) test('zeiten.css: die Bestätigung des Editors klebt unter der Kopfleiste, solange sie nicht leer ist (undurchsichtig, z-index 5)', () => { const css = lesen('public/zeiten.css') assert.match(css, /\.zt-editor > \.tm-meldung:not\(:empty\) \{ position: sticky; top: calc\(var\(--kb-appbar-h, 0px\) \+ 8px\); z-index: 5;/) assert.match(css, /\.zt-editor > \.tm-meldung-ok:not\(:empty\) \{ background: linear-gradient\(.*\), #0c0c0e; \}/) assert.match(css, /\.zt-editor > \.tm-meldung-fehler:not\(:empty\) \{ background: linear-gradient\(.*\), #0c0c0e; \}/) })