„Ja, anlegen“ zeichnet nur noch die Hinweis-Liste neu (offene, ungespeicherte Formulare und Zeilen bleiben), statt den ganzen Abschnitt zu ersetzen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1945 lines
125 KiB
JavaScript
1945 lines
125 KiB
JavaScript
import { test } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import { readFileSync, readdirSync } from 'node:fs'
|
||
import { installFakeDom } from './support/fakeDom.js'
|
||
import { config } from '../server/config.js'
|
||
import { oeffentlicheAnsicht } from '../server/services/featureRegistry.js'
|
||
import { h } from '../public/termine-dom.js'
|
||
import { heuteBerlin, tagPlus } from '../public/termine-view.js'
|
||
import { ZEITEN_LIMITS, hinweisVorschlag } from '../public/zeiten-view.js'
|
||
import { initZeiten } from '../public/zeiten.js'
|
||
// Namensraeume statt benannter Importe: fehlt ein Export, scheitert nur der Test, der ihn braucht
|
||
import * as editor from '../public/zeiten-editor.js'
|
||
import * as speichern from '../public/zeiten-speichern.js'
|
||
|
||
// Editor der Öffnungszeiten live, Teil I (Task 29): Speichern, Zeitpläne, Feiertage, Ausnahmen (public/zeiten-editor.js, zeiten-speichern.js,
|
||
// zeiten-plaene.js, zeiten-ausnahmen.js; Spec 3.4 Bereiche 1 bis 3 und der Absatz „Speichern“).
|
||
// 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.<id>.tage.3), Fokus dorthin, Fremdes allgemein mit dem Namen des Plans', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
fx.antworten.push(fehler(400, 'Bitte prüfe deine Eingaben.', { fehler: {
|
||
[`plaene.${P1}.tage.3`]: 'Die Zeitfenster überschneiden sich.',
|
||
[`plaene.${P1}.name`]: 'Höchstens 60 Zeichen.',
|
||
'ausnahmen.x_fremd001.von': 'Bitte gib ein gültiges Datum an.',
|
||
daten: 'Zu viele Einträge – bitte lösche alte Ausnahmen oder Hinweise.',
|
||
} }))
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
assert.equal(fx.text(fehlerAn(fx, root, `plaene.${P1}.tage.3`)), 'Die Zeitfenster überschneiden sich.')
|
||
assert.equal(fx.text(fehlerAn(fx, root, `plaene.${P1}.name`)), 'Höchstens 60 Zeichen.')
|
||
const feld = fx.finde(root, `[data-feld="plaene.${P1}.tage.3"]`)
|
||
assert.equal(feld.getAttribute('aria-invalid'), 'true')
|
||
assert.equal(fx.finde(root, `[data-feld="plaene.${P1}.name"]`).getAttribute('aria-invalid'), 'true')
|
||
assert.ok(planKarte(fx, root, P1).contains(fx.finde(root, '[data-fehler="_"]')), 'Platz für allgemeine Fehler')
|
||
assert.match(fx.text(fx.finde(planKarte(fx, root, P1), '[data-fehler="_"]')), /Zu viele Einträge/)
|
||
assert.match(fx.text(fx.finde(planKarte(fx, root, P1), '[data-fehler="_"]')), /gültiges Datum/)
|
||
assert.equal(document.activeElement, fx.finde(root, '[aria-invalid="true"]'), 'der Fokus geht zum ersten Feld mit Fehler')
|
||
// ein Speichern mit Erfolg räumt die Fehler ab
|
||
fx.antworten.push(antwort(daten(), 4))
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
assert.equal(fx.finde(root, '[aria-invalid="true"]'), null)
|
||
}))
|
||
|
||
test('400 in Ausnahmen und Feiertagen: Fehler an Datum, Fenster und Tag (Pfade mit ID)', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
await fx.klick(fx.finde(root, '[data-werkzeug="sondertag-neu"]'))
|
||
const form = fx.finde(root, 'form[aria-label="Sondertag eintragen"]')
|
||
await fx.setze(feldIn(fx, form, 'von'), tagPlus(HEUTE, 3))
|
||
await fx.setze(modusAuswahl(fx, form), 'zeiten')
|
||
await fx.setze(zeitFelder(fx, form)[0], '09:00')
|
||
await fx.setze(zeitFelder(fx, form)[1], '12:00')
|
||
fx.antworten.push(fehler(400, 'Bitte prüfe deine Eingaben.', { fehler: {} }))
|
||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||
const id = puts(fx)[0].body.daten.ausnahmen[0].id
|
||
fx.antworten.push(fehler(400, 'Bitte prüfe deine Eingaben.', { fehler: { [`ausnahmen.${id}.von`]: 'Bitte gib ein gültiges Datum an.', [`ausnahmen.${id}.fenster`]: 'Die Zeitfenster überschneiden sich.' } }))
|
||
await fx.klick(fx.finde(form, 'button[type="submit"]'))
|
||
assert.equal(fx.text(fehlerAn(fx, form, `ausnahmen.${id}.von`)), 'Bitte gib ein gültiges Datum an.')
|
||
assert.equal(fx.text(fehlerAn(fx, form, `ausnahmen.${id}.fenster`)), 'Die Zeitfenster überschneiden sich.')
|
||
// Feiertag: Pfad feiertage.<tag>
|
||
await fx.setze(fx.finde(fx.finde(root, '[data-feiertag="2026-12-25"]'), 'select'), 'rund')
|
||
fx.antworten.push(fehler(400, 'Bitte prüfe deine Eingaben.', { fehler: { 'feiertage.2026-12-25': 'Bitte wähle „geschlossen“, „geöffnet“, „rund um die Uhr“ oder „nach Vereinbarung“.' } }))
|
||
await fx.klick(fx.finde(root, 'form[aria-label="Feiertage"] button[type="submit"]'))
|
||
assert.match(fx.text(fehlerAn(fx, root, 'feiertage.2026-12-25')), /Bitte wähle/)
|
||
}))
|
||
|
||
test('Sonstige Fehler (429, 500, offline) stehen als Meldung über dem Editor, nie als HTML', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
const bose = '<img src=x onerror=alert(1)>'
|
||
fx.antworten.push(fehler(429, bose))
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
assert.equal(meldung(fx, root), bose)
|
||
assert.equal(fx.alle(root, 'img').length, 0)
|
||
assert.equal(fx.finde(root, '[data-meldung="editor"]').getAttribute('role'), 'alert')
|
||
fx.antworten.push(() => { throw new TypeError('x') })
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
assert.match(meldung(fx, root), /Fehler|Verbindung|Interner/)
|
||
// ein Erfolg ersetzt die Fehlermeldung durch eine ruhige Meldung
|
||
fx.antworten.push(antwort(daten(), 4))
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
assert.equal(fx.finde(root, '[data-meldung="editor"]').getAttribute('role'), 'status')
|
||
}))
|
||
|
||
test('Laden schlägt fehl: Fehlerkarte mit „Noch einmal versuchen“; eine unbrauchbare Antwort ebenso', mitDom({}, async (fx) => {
|
||
const root = h('div', { class: 'zt-editor', 'data-state': 'active' })
|
||
document.body.append(root)
|
||
fx.antworten.push(fehler(500, 'Interner Serverfehler'))
|
||
await editor.renderEditor(root, { projectId: 'p1' })
|
||
assert.match(fx.text(root), /Interner Serverfehler/)
|
||
const nochmal = fx.finde(root, 'button')
|
||
assert.match(fx.text(nochmal), /Noch einmal versuchen/)
|
||
fx.antworten.push({ status: 200, body: { irgendwas: true } })
|
||
await fx.klick(nochmal)
|
||
assert.match(fx.text(root), /konnten gerade nicht geladen werden/)
|
||
fx.antworten.push({ status: 200, body: stand(daten()) })
|
||
await fx.klick(fx.finde(root, 'button'))
|
||
assert.ok(planKarte(fx, root, P1), 'der dritte Versuch zeigt den Editor')
|
||
}))
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------- Kundenansicht, nur lesen, R12
|
||
test('Kundenansicht: Hinweis-Box, Speichern ist erlaubt', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx, { start: stand(daten(), { kundenansicht: true }) })
|
||
const box = fx.finde(root, '[data-hinweis="kundenansicht"]')
|
||
assert.ok(box)
|
||
assert.match(fx.text(box), /Kundenansicht/)
|
||
assert.match(fx.text(box), /Protokoll/)
|
||
assert.equal(speichernKnopf(fx, root, P1).disabled, false)
|
||
fx.antworten.push(antwort(daten(), 4))
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
assert.equal(puts(fx).length, 1)
|
||
// ohne Kundenansicht keine Box
|
||
const kunde = await oeffne(fx)
|
||
assert.equal(fx.finde(kunde.root, '[data-hinweis="kundenansicht"]'), null)
|
||
// auch die Angabe der Hülle genügt, falls die Antwort sie nicht trägt
|
||
const huelle = await oeffne(fx, { kundenansicht: true })
|
||
assert.ok(fx.finde(huelle.root, '[data-hinweis="kundenansicht"]'))
|
||
}))
|
||
|
||
test('abgeschaltet (disabled mit Daten): alles gesperrt, keine Knöpfe zum Ändern, Hinweis „nur ansehen“', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx, { state: 'disabled', start: stand(daten({ plaene: [plan(P1), plan(P2)], ausnahmen: [{ id: 'x_aaaaaaa1', art: 'urlaub', von: tagPlus(HEUTE, 3), bis: tagPlus(HEUTE, 4), text: 'Urlaub', modus: 'zu', fenster: [], plan: null }] }), { state: 'disabled' }) })
|
||
assert.ok(fx.finde(root, '[data-hinweis="nurlesen"]'))
|
||
assert.equal(fx.finde(root, '[data-aktion="speichern"]'), null)
|
||
assert.equal(fx.finde(root, '[data-aktion="haupt"]'), null)
|
||
assert.equal(fx.finde(root, '[data-aktion="entfernen"]'), null)
|
||
assert.equal(fx.finde(root, '[data-werkzeug]'), null)
|
||
assert.equal(fx.finde(root, 'button[type="submit"]'), null)
|
||
for (const feld of fx.alle(root, 'input, select')) assert.equal(feld.matches(':disabled'), true, `${feld.tagName} ist gesperrt`)
|
||
assert.equal(puts(fx).length, 0)
|
||
}))
|
||
|
||
test('R12: die Kontaktdaten gehen unverändert zurück (kein neues bestaetigtAt), auch wenn nur ein Plan gespeichert wird', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
fx.antworten.push(antwort(daten(), 4))
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
assert.deepEqual(puts(fx)[0].body.daten.kontakt, KONTAKT)
|
||
// keine Kontaktdaten: der Editor erfindet keine
|
||
const ohne = daten()
|
||
delete ohne.kontakt
|
||
const zwei = await oeffne(fx, { start: stand(ohne) })
|
||
fx.antworten.push(antwort(daten(), 4))
|
||
await fx.klick(speichernKnopf(fx, zwei.root, P1))
|
||
assert.equal(Object.hasOwn(puts(fx)[1].body.daten, 'kontakt'), false)
|
||
}))
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------- Robustheit
|
||
test('Fremdtext (Plan-, Feiertags-, Ausnahme- und Hinweisnamen) kommt nur als Text in die Seite', mitDom({}, async (fx) => {
|
||
const bose = '<img src=x onerror=alert(1)>'
|
||
const tag = tagPlus(HEUTE, 9)
|
||
const d = daten({
|
||
plaene: [plan(P1, { name: bose })],
|
||
ausnahmen: [{ id: 'x_bose0001', art: 'urlaub', von: tag, bis: tag, text: bose, modus: 'zu', fenster: [], plan: null }],
|
||
})
|
||
const { root } = await oeffne(fx, { start: stand(d, { feiertage: [{ tag: '2026-12-25', name: bose, art: 'feiertag' }] }) })
|
||
assert.equal(fx.alle(root, 'img').length, 0)
|
||
assert.ok(fx.text(root).includes(bose))
|
||
assert.equal(fx.finde(root, `[data-feld="plaene.${P1}.name"]`).value, bose)
|
||
fx.antworten.push(fehler(400, 'x', { fehler: { [`ausnahmen.x_bose0001.text`]: bose } }))
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
assert.equal(fx.alle(root, 'img').length, 0)
|
||
}))
|
||
|
||
test('IDs wie „__proto__“ und „constructor“ sind gewöhnliche Texte: Speichern, Fehler und Löschen finden den richtigen Plan', mitDom({}, async (fx) => {
|
||
const alt = daten({ plaene: [plan('__proto__', { name: 'Eins' }), plan('constructor', { haupt: false, kurz: 'zwei', name: 'Zwei' })] })
|
||
const { root } = await oeffne(fx, { start: stand(alt) })
|
||
const karte = fx.alle(root, 'details[data-plan]').find((k) => k.getAttribute('data-plan') === 'constructor')
|
||
assert.ok(karte)
|
||
karte.setAttribute('open', '')
|
||
await fx.setze(fx.finde(karte, '[data-feld="plaene.constructor.name"]'), 'Zwei neu')
|
||
fx.antworten.push(fehler(400, 'x', { fehler: { 'plaene.constructor.tage.3': 'Die Zeitfenster überschneiden sich.' } }))
|
||
await fx.klick(fx.finde(karte, '[data-aktion="speichern"]'))
|
||
assert.deepEqual(puts(fx)[0].body.daten.plaene.map((p) => [p.id, p.name]), [['__proto__', 'Eins'], ['constructor', 'Zwei neu']])
|
||
assert.equal(fx.text(fehlerAn(fx, karte, 'plaene.constructor.tage.3')), 'Die Zeitfenster überschneiden sich.')
|
||
// Löschen
|
||
await fx.klick(fx.finde(karte, '[data-aktion="entfernen"]'))
|
||
fx.antworten.push(antwort(daten({ plaene: [plan('__proto__', { name: 'Eins' })] }), 4))
|
||
await fx.klick(fx.finde(fx.finde(karte, '.tm-frage-box'), 'button[type="submit"]'))
|
||
assert.deepEqual(puts(fx)[1].body.daten.plaene.map((p) => p.id), ['__proto__'])
|
||
}))
|
||
|
||
test('Ein Speichern zur Zeit: während es läuft sind die Knöpfe gesperrt und ein zweiter Aufruf geht nicht hinaus', mitDom({}, async (fx) => {
|
||
const { root } = await oeffne(fx)
|
||
const echtes = globalThis.fetch
|
||
let freigeben
|
||
const wartet = new Promise((resolve) => { freigeben = resolve })
|
||
globalThis.fetch = async (url, optionen) => {
|
||
if (optionen?.method === 'PUT') await wartet
|
||
return echtes(url, optionen)
|
||
}
|
||
try {
|
||
fx.antworten.push(antwort(daten(), 4))
|
||
const erster = fx.klick(speichernKnopf(fx, root, P1))
|
||
await fx.ruhe()
|
||
assert.equal(planKarte(fx, root, P1).closest('[aria-busy="true"]') !== null || root.getAttribute('aria-busy') === 'true', true, 'der Editor ist beschäftigt')
|
||
assert.equal(speichernKnopf(fx, root, P1).disabled, true)
|
||
await fx.klick(speichernKnopf(fx, root, P1))
|
||
freigeben()
|
||
await erster
|
||
assert.equal(puts(fx).length, 1, 'nur ein Speichervorgang')
|
||
assert.equal(root.hasAttribute('aria-busy'), false)
|
||
} finally {
|
||
globalThis.fetch = echtes
|
||
}
|
||
}))
|
||
|
||
test('neuerZeitenSpeicher: speichere ohne laufende Hülle (ctx mit Stand) – Erfolg liefert true, der Stand wird übernommen', mitDom({}, async (fx) => {
|
||
const root = h('div')
|
||
document.body.append(root)
|
||
const form = h('form', null, h('p', { 'data-fehler': '_' }))
|
||
root.append(form)
|
||
const ctx = {
|
||
projectId: 'p1',
|
||
stand: { daten: daten(), version: 3 },
|
||
aktuell: () => true,
|
||
neuLaden: async () => {},
|
||
meldung: () => {},
|
||
uebernimm(a) { this.stand = { daten: a.daten, version: a.version } },
|
||
}
|
||
const sp = speichern.neuerZeitenSpeicher(ctx, root, () => {})
|
||
assert.equal(typeof sp.speichere, 'function')
|
||
assert.ok(sp.konflikt)
|
||
assert.equal(typeof sp.nochHier, 'function')
|
||
fx.antworten.push(antwort(daten({ land: 'BY' }), 4))
|
||
assert.equal(await sp.speichere((entwurf) => { entwurf.land = 'BY' }, { container: form }), true)
|
||
assert.deepEqual(puts(fx)[0], { method: 'PUT', url: '/api/zeiten/p1', body: { version: 3, daten: daten({ land: 'BY' }) } })
|
||
assert.equal(ctx.stand.version, 4)
|
||
assert.equal(await sp.speichere(() => false, { container: form }), false, 'aendere kann abbrechen (Fehler am Feld schon gezeigt)')
|
||
assert.equal(puts(fx).length, 1)
|
||
}))
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------- Hülle: Editor bleibt stehen
|
||
const zeitenFeature = (state = 'off', extra = {}) => ({
|
||
state, siteKey: null, ticketWoid: null, orderedAt: null, activatedAt: null, invoiceUrl: null, disabledReason: null, disabledAt: null, loeschungAm: null,
|
||
unlockedBy: state === 'off' ? null : 'abo', aboEndeAt: null, einbau: null, ...extra,
|
||
})
|
||
const projekt = (id, state, extra = {}) => ({ projectId: id, projectName: `Website ${id}`, liveUrl: '', features: { news: { state: 'off' }, zeiten: zeitenFeature(state) }, zeitenStatus: null, zeitenLeer: true, ...extra })
|
||
const angebot = { verfuegbar: true, inklusive: true, grund: null, preis: '30 €', einschalten: { consent: {}, leistungsbeschreibung: {} }, belehrung: '' }
|
||
const liste = (projects) => ({ status: 200, body: { projects, features: { zeiten: oeffentlicheAnsicht('zeiten') }, ustHinweis: config.ustHinweis, zeiten: angebot } })
|
||
|
||
test('Hülle: ein Wechsel des Standes (zeitenLeer, setup → active) zeichnet nur den Kopf neu; der Editor und seine ungespeicherten Eingaben bleiben', mitDom({}, async (fx) => {
|
||
const root = h('div', { id: 'zeiten-body' })
|
||
document.body.append(root)
|
||
fx.antworten.push(liste([projekt('p1', 'setup', { zeitenLeer: true })]), { status: 200, body: stand(null, { state: 'setup' }) })
|
||
await initZeiten(root, { projectId: 'p1' })
|
||
await fx.ruhe()
|
||
const platz = fx.finde(root, '.zt-editor')
|
||
assert.ok(platz)
|
||
assert.equal(platz.getAttribute('data-state'), 'setup')
|
||
const id = fx.finde(platz, 'details[data-plan]').getAttribute('data-plan')
|
||
const name = fx.finde(platz, `[data-feld="plaene.${id}.name"]`)
|
||
await fx.setze(name, 'Praxis Dr. Muster')
|
||
const kopf = fx.finde(root, '.zt-kopf')
|
||
|
||
// Stand ändert sich (nach dem ersten Speichern ist zeitenLeer false, der Zustand wechselt auf aktiv): neuer Kopf, derselbe Editor
|
||
fx.antworten.push(liste([projekt('p1', 'active', { zeitenLeer: false })]))
|
||
await initZeiten(root, { projectId: 'p1' })
|
||
assert.equal(fx.finde(root, '.zt-editor'), platz, 'derselbe Editorplatz')
|
||
assert.equal(platz.getAttribute('data-state'), 'active', 'der Zustand wird nachgeführt')
|
||
assert.equal(fx.finde(platz, `[data-feld="plaene.${id}.name"]`), name, 'dasselbe Eingabefeld')
|
||
assert.equal(name.value, 'Praxis Dr. Muster', 'die ungespeicherte Eingabe steht noch da')
|
||
assert.notEqual(fx.finde(root, '.zt-kopf'), kopf, 'der Kopf (Statuskarte) ist neu')
|
||
assert.equal(fx.finde(root, '.zt-status').getAttribute('data-state'), 'active')
|
||
assert.equal(gets(fx).filter((a) => a.url === '/api/zeiten/p1').length, 1, 'der Editor lädt nicht noch einmal')
|
||
assert.equal(platz.hidden, false)
|
||
}))
|
||
|
||
test('Hülle: erstes Speichern im Editor holt die Liste neu (zeitenLeer) und zeichnet nur den Kopf neu, der Editor bleibt', mitDom({}, async (fx) => {
|
||
const root = h('div', { id: 'zeiten-body' })
|
||
document.body.append(root)
|
||
fx.antworten.push(liste([projekt('p1', 'active', { zeitenLeer: true })]), { status: 200, body: stand(null) })
|
||
await initZeiten(root, { projectId: 'p1' })
|
||
await fx.ruhe()
|
||
const platz = fx.finde(root, '.zt-editor')
|
||
const id = fx.finde(platz, 'details[data-plan]').getAttribute('data-plan')
|
||
const gespeichert = daten({ plaene: [plan(id)], kontakt: { an: false, telefon: '', adresse: '', whatsapp: null } })
|
||
fx.antworten.push(antwort(gespeichert, 1), liste([projekt('p1', 'active', { zeitenLeer: false })]))
|
||
await fx.klick(fx.finde(platz, '[data-aktion="speichern"]'))
|
||
await fx.ruhe()
|
||
assert.deepEqual(fx.fetchLog.map((a) => `${a.method} ${a.url}`), ['GET /api/funktionen', 'GET /api/zeiten/p1', 'PUT /api/zeiten/p1', 'GET /api/funktionen'])
|
||
assert.equal(fx.finde(root, '.zt-editor'), platz, 'derselbe Editorplatz nach dem Neuzeichnen der Hülle')
|
||
assert.ok(abschnitt(fx, platz, 'feiertage'), 'der Editor zeigt jetzt Feiertage und Ausnahmen')
|
||
assert.doesNotMatch(fx.text(fx.finde(root, '.zt-kopf')), /Trag jetzt deine Zeiten ein/)
|
||
}))
|
||
|
||
test('Hülle: eine andere Website oder ein anderer Schreibstatus (abgeschaltet) bekommt einen frischen Editor', mitDom({}, async (fx) => {
|
||
const root = h('div', { id: 'zeiten-body' })
|
||
document.body.append(root)
|
||
fx.antworten.push(liste([projekt('p1', 'active', { zeitenLeer: false }), projekt('p2', 'active', { zeitenLeer: false })]), { status: 200, body: stand(daten()) })
|
||
await initZeiten(root, { projectId: 'p1' })
|
||
await fx.ruhe()
|
||
const erster = fx.finde(root, '.zt-editor')
|
||
// Website wechseln
|
||
fx.antworten.push({ status: 200, body: stand(daten({ plaene: [plan(P1, { name: 'Zweite Seite' })] })) })
|
||
await fx.setze(fx.finde(root, '#zeiten-project'), 'p2')
|
||
await fx.ruhe()
|
||
const zweiter = fx.finde(root, '.zt-editor')
|
||
assert.notEqual(zweiter, erster)
|
||
assert.equal(fx.finde(zweiter, `[data-feld="plaene.${P1}.name"]`).value, 'Zweite Seite')
|
||
assert.equal(erster.isConnected, false)
|
||
assert.deepEqual(gets(fx).filter((a) => a.url.startsWith('/api/zeiten/')).map((a) => a.url), ['/api/zeiten/p1', '/api/zeiten/p2'])
|
||
}))
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------- 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('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`)
|
||
})
|