Öffnungszeiten: Editor für Hinweis- und Kontakt-Leiste, Google und Vorschau

Hinweis-Leiste (Liste, „Hinweis-Leiste anlegen“, Art, Knopf, Zeitraum, Preis-Hinweis),
Kontakt-Leiste (Telefon, Adresse, WhatsApp mit Datenschutz-Haken, R12 in der Kundenansicht),
Google (Für Google kopieren, Profil-Link), Live-Vorschau mit Regler „Uhrzeit testen“
und UI-Prüfung test/ui/zeiten-portal.ui.mjs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 06:43:22 +00:00
1 parent 242fbb01e5
commit b3e96d4a56
8 files changed
+1684 -13

No files matched your search

+654 -4
View File
@@ -13,8 +13,9 @@ import * as editor from '../public/zeiten-editor.js'
import * as speichern from '../public/zeiten-speichern.js'
// Editor der Öffnungszeiten live, Teil I (Task 29): Speichern, Zeitpläne, Feiertage, Ausnahmen (public/zeiten-editor.js, zeiten-speichern.js,
// zeiten-plaene.js, zeiten-ausnahmen.js; Spec 3.4 Bereiche 1 bis 3 und der Absatz „Speichern“). Fake-DOM und Fake-fetch aus
// test/support/fakeDom.js: jeder Aufruf steht in fx.fetchLog, die Antworten kommen der Reihe nach aus fx.antworten.
// 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)
@@ -1196,11 +1197,660 @@ test('Hülle: eine andere Website oder ein anderer Schreibstatus (abgeschaltet)
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 = '<img src=x onerror=alert(1)>'
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('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']
const DATEIEN = ['zeiten-speichern.js', 'zeiten-editor.js', 'zeiten-plaene.js', 'zeiten-ausnahmen.js', 'zeiten-leisten.js', 'zeiten-google.js']
test('Quelltext: vier neue Dateien, nur relative Importe, kein HTML aus Daten, keine Browser-Rückfragen, kein Speicher, Du-Form', () => {
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`)