Files
tickte-system/tests/unit/zeitenView.test.mjs
T
ClaudeandClaude Opus 5.5 3d67aa88cf Öffnungszeiten: Team-Panel, Ersatz-HTML und Rechtliches-Karte
Panel unter der Zeile Öffnungszeiten live (Header-Selektor, Kontakt-/Hinweis-Leiste aus, Speichern mit teamVersion,
Ersatz-HTML kopieren je Plan), Meldung zum Ersatz-Ticket nach dem Abschalten, Hindernis an aktionen, Statustext der Zeiten,
Team-API (GET ersatz, PATCH team), Karte Leistungsbeschreibung Öffnungszeiten live in setup-legal (Schlüssel höchstens 36 Zeichen).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:12:04 +00:00

335 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
// Öffnungszeiten live im Funktionen-Reiter: Statustexte, Prüfung des Navigations-Selektors (Spiegel des Portals) und Rumpf der Team-Einstellungen
const {
zeitenStatusText, navSelektorFehler, NAV_SELEKTOR_MAX, NAV_SELEKTOR_FEHLER, teamBody,
ersatzPfad, zeitenTeamPfad, teamFormular, teamGeaendert, mergeTeam, teamSpeichernFehler,
ersatzKnoepfe, ersatzKopieren, ersatzMeldung, abschaltMeldung, KEINE_ZEITEN_TEXT, ZWISCHENABLAGE_FEHLT,
} = await loadModule('src/lib/zeitenView.js')
const EINBAU = 'Leisten live – Einbau läuft'
const WARTET = 'wartet auf die Zeiten des Kunden'
test('zeitenStatusText: Einrichtung ohne Hindernis – Leisten sind live, der Einbau läuft', () => {
assert.equal(zeitenStatusText({ state: 'setup', hindernis: null }), EINBAU)
assert.equal(zeitenStatusText({ state: 'setup' }), EINBAU, 'ohne Feld hindernis')
assert.equal(zeitenStatusText({ state: 'setup', hindernis: '' }), EINBAU)
})
test('zeitenStatusText: Einrichtung mit hindernis "zeiten_fehlen" – wartet auf die Zeiten des Kunden', () => {
assert.equal(zeitenStatusText({ state: 'setup', hindernis: 'zeiten_fehlen' }), WARTET)
})
test('zeitenStatusText: unbekanntes Hindernis zeigt nichts Erfundenes, sondern den Einbau-Text', () => {
assert.equal(zeitenStatusText({ state: 'setup', hindernis: 'irgendwas_neues' }), EINBAU)
})
test('zeitenStatusText: Abo-Ende nennt die Frist (Berliner Tag des Abo-Endes plus 30 Tage)', () => {
const abo = (state, aboEndeAt) => zeitenStatusText({ state, hindernis: null, aboEndeAt })
const hinweis = (datum) => `Abo beendet – Abschaltung am ${datum}, falls der Kunde die Funktion nicht behält`
assert.equal(abo('active', '2026-10-04T10:00:00.000Z'), hinweis('03.11.2026'))
// 22:30 UTC ist in Berlin schon der nächste Tag (Sommerzeit, +2 h): 05.10. -> 04.11.
assert.equal(abo('active', '2026-10-04T22:30:00.000Z'), hinweis('04.11.2026'))
// Monats- und Jahreswechsel, Zeitumstellung am 25.10. spielt bei Kalendertagen keine Rolle
assert.equal(abo('active', '2026-10-20T12:00:00.000Z'), hinweis('19.11.2026'))
assert.equal(abo('active', '2026-12-15T12:00:00.000Z'), hinweis('14.01.2027'))
// Appwrite liefert auch +00:00
assert.equal(abo('active', '2026-10-04T10:00:00.000+00:00'), hinweis('03.11.2026'))
})
test('zeitenStatusText: Abo-Ende in der Einrichtung steht zusätzlich zum Einbau- bzw. Warte-Text', () => {
const aboEndeAt = '2026-10-04T10:00:00.000Z'
const frist = 'Abo beendet – Abschaltung am 03.11.2026, falls der Kunde die Funktion nicht behält'
assert.equal(zeitenStatusText({ state: 'setup', hindernis: null, aboEndeAt }), `${EINBAU} · ${frist}`)
assert.equal(zeitenStatusText({ state: 'setup', hindernis: 'zeiten_fehlen', aboEndeAt }), `${WARTET} · ${frist}`)
})
test('zeitenStatusText: Abo-Ende mit unlesbarem Datum nennt 30 Tage statt eines erfundenen Tages', () => {
const text = zeitenStatusText({ state: 'active', aboEndeAt: 'kein-datum' })
assert.equal(text, 'Abo beendet – Abschaltung nach 30 Tagen, falls der Kunde die Funktion nicht behält')
})
test('zeitenStatusText: aktiv ohne Abo-Ende, aus, bestellt, abgeschaltet, ohne Ansicht – leer', () => {
assert.equal(zeitenStatusText({ state: 'active', hindernis: null, aboEndeAt: null }), '')
assert.equal(zeitenStatusText({ state: 'active' }), '')
assert.equal(zeitenStatusText({ state: 'off' }), '')
assert.equal(zeitenStatusText({ state: 'ordered' }), '')
// abgeschaltet: Grund und Löschdatum zeigt abgeschaltetText, nichts doppelt
assert.equal(zeitenStatusText({ state: 'disabled', disabledReason: 'abo_ende', loeschungAm: '2026-11-04', aboEndeAt: '2026-10-04T10:00:00.000Z' }), '')
assert.equal(zeitenStatusText({ state: 'off', aboEndeAt: '2026-10-04T10:00:00.000Z' }), '')
assert.equal(zeitenStatusText(null), '')
assert.equal(zeitenStatusText(undefined), '')
})
test('zeitenStatusText: kein Text nennt "24 Uhr"', () => {
for (const feature of [
{ state: 'setup' },
{ state: 'setup', hindernis: 'zeiten_fehlen' },
{ state: 'active', aboEndeAt: '2026-10-04T10:00:00.000Z' },
{ state: 'setup', hindernis: 'zeiten_fehlen', aboEndeAt: '2026-10-04T22:30:00.000Z' },
]) {
assert.doesNotMatch(zeitenStatusText(feature), /24 Uhr/)
}
})
test('navSelektorFehler: leer ist erlaubt (kein Selektor)', () => {
for (const wert of [undefined, null, '', ' ', '\n\t ']) {
assert.equal(navSelektorFehler(wert), null, JSON.stringify(wert))
}
})
test('navSelektorFehler: übliche Selektoren sind gültig', () => {
for (const wert of [
'header',
'#nav',
'.site-header',
'.site-header > nav',
'header#main-nav.fixed',
'nav[role="navigation"]',
"a[href='x']",
'ul.menu:first-child',
'div_wrap .a-b',
'ABC xyz 019',
' header ',
]) {
assert.equal(navSelektorFehler(wert), null, wert)
}
})
test('navSelektorFehler: erlaubt sind nur A–Z a–z 0–9, Leerzeichen und # . - _ > : [ ] = " \'', () => {
const zeichen = 'AZaz09 #.-_>:[]="\''
assert.equal(navSelektorFehler(zeichen), null)
assert.equal(navSelektorFehler('a'.repeat(NAV_SELEKTOR_MAX)), null)
})
test('navSelektorFehler: < { } ; und alle übrigen Zeichen werden abgelehnt', () => {
for (const zeichen of ['<', '{', '}', ';', '(', ')', ',', '*', '+', '~', '/', '\\', '@', '!', '|', '$', '%', '^', '&', '?', '`', 'ä', 'ß', '€', '\n', '\t']) {
assert.equal(navSelektorFehler(`a${zeichen}b`), NAV_SELEKTOR_FEHLER, JSON.stringify(zeichen))
}
assert.equal(navSelektorFehler('header { display: none }'), NAV_SELEKTOR_FEHLER)
assert.equal(navSelektorFehler('</style><script>'), NAV_SELEKTOR_FEHLER)
assert.equal(navSelektorFehler('a\nb'), NAV_SELEKTOR_FEHLER, 'Zeilenumbruch mitten im Text')
})
test('navSelektorFehler: höchstens 120 Zeichen, gezählt nach dem Trimmen', () => {
assert.equal(NAV_SELEKTOR_MAX, 120)
assert.equal(navSelektorFehler('a'.repeat(120)), null)
assert.equal(navSelektorFehler(` ${'a'.repeat(120)} `), null, 'Leerraum außen zählt nicht')
assert.equal(navSelektorFehler('a'.repeat(121)), NAV_SELEKTOR_FEHLER)
assert.equal(navSelektorFehler(`${'a'.repeat(60)} ${'b'.repeat(60)}`), NAV_SELEKTOR_FEHLER, '121 mit Leerzeichen innen')
})
test('navSelektorFehler: kein Text wird abgelehnt wie auf dem Server (Zahl, Objekt, Wahrheitswert)', () => {
for (const wert of [5, true, {}, []]) {
assert.equal(navSelektorFehler(wert), NAV_SELEKTOR_FEHLER, JSON.stringify(wert))
}
})
test('NAV_SELEKTOR_FEHLER: wörtlich wie die Meldung des Portals', () => {
assert.equal(NAV_SELEKTOR_FEHLER, 'Der Selektor darf höchstens 120 Zeichen lang sein und nur A–Z, 0–9, Leerzeichen und # . - _ > : [ ] = " \' enthalten.')
})
test('teamBody: genau die fünf Felder der Team-Route, Selektor getrimmt, leer wird null', () => {
assert.deepEqual(
teamBody({ projectId: 'p1', teamVersion: 3, navSelektor: ' header > nav ', kontaktleisteAus: true, hinweisleisteAus: false }),
{ projectId: 'p1', teamVersion: 3, navSelektor: 'header > nav', kontaktleisteAus: true, hinweisleisteAus: false }
)
for (const leer of ['', ' ', null, undefined]) {
assert.equal(teamBody({ projectId: 'p1', teamVersion: 0, navSelektor: leer, kontaktleisteAus: false, hinweisleisteAus: false }).navSelektor, null, JSON.stringify(leer))
}
})
test('teamBody: Schalter sind immer echte Wahrheitswerte, nur true gilt als an', () => {
const body = (kontaktleisteAus, hinweisleisteAus) => teamBody({ projectId: 'p1', teamVersion: 1, navSelektor: '', kontaktleisteAus, hinweisleisteAus })
assert.deepEqual(body(true, true), { projectId: 'p1', teamVersion: 1, navSelektor: null, kontaktleisteAus: true, hinweisleisteAus: true })
assert.deepEqual(body(undefined, undefined), { projectId: 'p1', teamVersion: 1, navSelektor: null, kontaktleisteAus: false, hinweisleisteAus: false })
assert.deepEqual(body('true', 1), { projectId: 'p1', teamVersion: 1, navSelektor: null, kontaktleisteAus: false, hinweisleisteAus: false })
})
test('teamBody: die Version wird unverändert durchgereicht (das Portal prüft sie), auch 0', () => {
assert.equal(teamBody({ projectId: 'p1', teamVersion: 0, navSelektor: '', kontaktleisteAus: false, hinweisleisteAus: false }).teamVersion, 0)
assert.equal(teamBody({ projectId: 'p1', teamVersion: 41, navSelektor: '', kontaktleisteAus: false, hinweisleisteAus: false }).teamVersion, 41)
})
test('teamBody: ein ungültiger Selektor wird nicht "repariert", sondern unverändert gesendet (das Formular sperrt vorher, das Portal prüft noch einmal)', () => {
assert.equal(teamBody({ projectId: 'p1', teamVersion: 1, navSelektor: 'a{b}', kontaktleisteAus: false, hinweisleisteAus: false }).navSelektor, 'a{b}')
})
// --- Panel: Pfade, Formular, Speichern, Ersatz-HTML, Abschalt-Meldung ---
test('ersatzPfad: GET .../zeiten/ersatz mit kodierten Parametern projectId und plan', () => {
assert.equal(ersatzPfad('c1', 'p1', 'haupt'), '/funktionen/c1/zeiten/ersatz?projectId=p1&plan=haupt')
assert.equal(ersatzPfad('c/1', 'p 1', 'a&b=c'), '/funktionen/c%2F1/zeiten/ersatz?projectId=p%201&plan=a%26b%3Dc')
})
test('ersatzPfad: ohne plan fehlt der Parameter (Hauptplan), ein leerer plan bleibt stehen (das Portal antwortet dann 404)', () => {
assert.equal(ersatzPfad('c1', 'p1'), '/funktionen/c1/zeiten/ersatz?projectId=p1')
assert.equal(ersatzPfad('c1', 'p1', null), '/funktionen/c1/zeiten/ersatz?projectId=p1')
assert.equal(ersatzPfad('c1', 'p1', ''), '/funktionen/c1/zeiten/ersatz?projectId=p1&plan=')
})
test('zeitenTeamPfad: PATCH .../zeiten/team', () => {
assert.equal(zeitenTeamPfad('c1'), '/funktionen/c1/zeiten/team')
assert.equal(zeitenTeamPfad('c/1'), '/funktionen/c%2F1/zeiten/team')
})
test('teamFormular: Startwerte aus feature.team, null-Selektor wird ein leeres Feld', () => {
assert.deepEqual(
teamFormular({ team: { navSelektor: 'header > nav', kontaktleisteAus: true, hinweisleisteAus: false } }),
{ navSelektor: 'header > nav', kontaktleisteAus: true, hinweisleisteAus: false }
)
assert.deepEqual(
teamFormular({ team: { navSelektor: null, kontaktleisteAus: false, hinweisleisteAus: true } }),
{ navSelektor: '', kontaktleisteAus: false, hinweisleisteAus: true }
)
})
test('teamFormular: ohne Team-Feld (ältere Antwort, keine Ansicht) alles aus und leer', () => {
const leer = { navSelektor: '', kontaktleisteAus: false, hinweisleisteAus: false }
assert.deepEqual(teamFormular({}), leer)
assert.deepEqual(teamFormular({ team: null }), leer)
assert.deepEqual(teamFormular(null), leer)
assert.deepEqual(teamFormular(undefined), leer)
// Schalter nur bei true an, Selektor nur als Text
assert.deepEqual(teamFormular({ team: { navSelektor: 5, kontaktleisteAus: 'true', hinweisleisteAus: 1 } }), leer)
})
test('teamGeaendert: nur ein echter Unterschied zum gespeicherten Stand zählt, Leerraum um den Selektor nicht', () => {
const feature = { team: { navSelektor: 'header', kontaktleisteAus: false, hinweisleisteAus: true } }
const gleich = { navSelektor: 'header', kontaktleisteAus: false, hinweisleisteAus: true }
assert.equal(teamGeaendert(gleich, feature), false)
assert.equal(teamGeaendert({ ...gleich, navSelektor: ' header ' }, feature), false)
assert.equal(teamGeaendert({ ...gleich, navSelektor: 'header nav' }, feature), true)
assert.equal(teamGeaendert({ ...gleich, kontaktleisteAus: true }, feature), true)
assert.equal(teamGeaendert({ ...gleich, hinweisleisteAus: false }, feature), true)
// gespeichert ohne Selektor: leeres oder leeres-mit-Leerraum Feld ist keine Änderung
const ohne = { team: { navSelektor: null, kontaktleisteAus: false, hinweisleisteAus: false } }
assert.equal(teamGeaendert({ navSelektor: ' ', kontaktleisteAus: false, hinweisleisteAus: false }, ohne), false)
assert.equal(teamGeaendert({ navSelektor: 'x', kontaktleisteAus: false, hinweisleisteAus: false }, ohne), true)
})
test('mergeTeam: übernimmt team und teamVersion in genau dieses Projekt, alles andere bleibt', () => {
const zeiten = { state: 'active', plaene: [{ kurz: 'a', name: 'A', haupt: true }], team: { navSelektor: null, kontaktleisteAus: false, hinweisleisteAus: false }, teamVersion: 2 }
const projects = [
{ projectId: 'p1', projectName: 'Eins', features: { news: { state: 'off' }, zeiten } },
{ projectId: 'p2', projectName: 'Zwei', features: { zeiten: { ...zeiten, teamVersion: 7 } } },
]
const neu = mergeTeam(projects, 'p1', { team: { navSelektor: 'header', kontaktleisteAus: true, hinweisleisteAus: false }, teamVersion: 3 })
assert.notEqual(neu, projects, 'neue Liste')
assert.deepEqual(neu[0].features.zeiten, { ...zeiten, team: { navSelektor: 'header', kontaktleisteAus: true, hinweisleisteAus: false }, teamVersion: 3 })
assert.deepEqual(neu[0].features.news, { state: 'off' })
assert.equal(neu[0].projectName, 'Eins')
assert.equal(neu[1], projects[1], 'anderes Projekt unverändert')
assert.equal(projects[0].features.zeiten.teamVersion, 2, 'Eingabe nicht verändert')
})
test('mergeTeam: unbrauchbare Antwort oder Projekt ohne Zeiten ändert nichts', () => {
const projects = [{ projectId: 'p1', features: { zeiten: { state: 'active', teamVersion: 1, team: {} } } }, { projectId: 'p2', features: {} }]
assert.equal(mergeTeam(projects, 'p1', null), projects)
assert.equal(mergeTeam(projects, 'p1', {}), projects)
assert.equal(mergeTeam(projects, 'p1', { team: { navSelektor: null }, teamVersion: 'x' }), projects)
assert.equal(mergeTeam(projects, 'p1', { teamVersion: 2 }), projects)
assert.deepEqual(mergeTeam(projects, 'p2', { team: {}, teamVersion: 1 }), projects, 'p2 hat keine Zeiten: nichts erfunden')
assert.deepEqual(mergeTeam(projects, 'p9', { team: {}, teamVersion: 1 }), projects)
})
test('teamSpeichernFehler: 409 ZEITEN_VERALTET – „Inzwischen geändert – bitte neu laden“, danach neu laden', () => {
const err = Object.assign(new Error('Inzwischen geändert – bitte neu laden.'), { status: 409, code: 'ZEITEN_VERALTET' })
assert.deepEqual(teamSpeichernFehler(err), { text: 'Inzwischen geändert – bitte neu laden.', neuLaden: true })
// der feste Text gilt auch, wenn der Server-Text anders lautet
const anders = Object.assign(new Error('irgendwas'), { status: 409, code: 'ZEITEN_VERALTET' })
assert.equal(teamSpeichernFehler(anders).text, 'Inzwischen geändert – bitte neu laden.')
})
test('teamSpeichernFehler: anderer 409 (z. B. nicht eingeschaltet) zeigt den Text des Portals, die Ansicht ist trotzdem veraltet', () => {
const err = Object.assign(new Error('Öffnungszeiten live ist für dieses Projekt nicht eingeschaltet.'), { status: 409 })
assert.deepEqual(teamSpeichernFehler(err), { text: 'Öffnungszeiten live ist für dieses Projekt nicht eingeschaltet.', neuLaden: true })
})
test('teamSpeichernFehler: 400 und Netzfehler zeigen ihren Text unverändert, ohne Neuladen; ohne Text eine Ersatzmeldung', () => {
assert.deepEqual(teamSpeichernFehler(Object.assign(new Error(NAV_SELEKTOR_FEHLER), { status: 400 })), { text: NAV_SELEKTOR_FEHLER, neuLaden: false })
assert.deepEqual(teamSpeichernFehler(Object.assign(new Error('Das Kundenportal ist gerade nicht erreichbar.'), { status: 0 })), { text: 'Das Kundenportal ist gerade nicht erreichbar.', neuLaden: false })
assert.deepEqual(teamSpeichernFehler(new Error('')), { text: 'Speichern fehlgeschlagen.', neuLaden: false })
assert.deepEqual(teamSpeichernFehler(null), { text: 'Speichern fehlgeschlagen.', neuLaden: false })
})
test('ersatzKnoepfe: ein Plan – „Ersatz-HTML kopieren“, der Hauptplan zuerst bleibt in der Reihenfolge des Portals', () => {
assert.deepEqual(ersatzKnoepfe({ plaene: [{ kurz: 'haupt', name: 'Öffnungszeiten', haupt: true }] }), [
{ kurz: 'haupt', label: 'Ersatz-HTML kopieren', bezeichnung: 'Öffnungszeiten (haupt)' },
])
})
test('ersatzKnoepfe: mehrere Pläne – je Plan ein Knopf mit Name und Kurzname', () => {
const knoepfe = ersatzKnoepfe({
plaene: [
{ kurz: 'laden', name: 'Laden', haupt: true },
{ kurz: 'cafe', name: 'Café', haupt: false },
{ kurz: 'lieferung', name: '', haupt: false },
{ kurz: 'abholung', name: 'abholung', haupt: false },
],
})
assert.deepEqual(knoepfe.map((k) => k.kurz), ['laden', 'cafe', 'lieferung', 'abholung'])
assert.deepEqual(knoepfe.map((k) => k.label), [
'Ersatz-HTML kopieren: Laden (laden)',
'Ersatz-HTML kopieren: Café (cafe)',
'Ersatz-HTML kopieren: lieferung',
'Ersatz-HTML kopieren: abholung',
])
})
test('ersatzKnoepfe: Pläne ohne Kurzname und Unbrauchbares werden übergangen, ohne Pläne [] (Hinweis statt Knopf)', () => {
assert.deepEqual(ersatzKnoepfe({ plaene: [{ kurz: '', name: 'x' }, null, { name: 'y' }, { kurz: 5 }] }), [])
assert.deepEqual(ersatzKnoepfe({ plaene: [] }), [])
assert.deepEqual(ersatzKnoepfe({}), [])
assert.deepEqual(ersatzKnoepfe(null), [])
assert.equal(KEINE_ZEITEN_TEXT, 'Der Kunde hat noch keine Zeiten eingetragen.')
})
test('ersatzKopieren: schreibt das HTML in die Zwischenablage', async () => {
const geschrieben = []
const clipboard = { writeText: async (text) => { geschrieben.push(text) } }
assert.equal(await ersatzKopieren('<table>&auml;</table>', clipboard), 'kopiert')
assert.deepEqual(geschrieben, ['<table>&auml;</table>'])
})
test('ersatzKopieren: ohne Zwischenablage-API oder bei Ablehnung – manuell kopieren; leeres HTML wird nie kopiert', async () => {
assert.equal(await ersatzKopieren('<p>x</p>', undefined), 'manuell')
assert.equal(await ersatzKopieren('<p>x</p>', null), 'manuell')
assert.equal(await ersatzKopieren('<p>x</p>', {}), 'manuell')
assert.equal(await ersatzKopieren('<p>x</p>', { writeText: 'kein Funktion' }), 'manuell')
assert.equal(await ersatzKopieren('<p>x</p>', { writeText: async () => { throw new Error('NotAllowedError') } }), 'manuell')
assert.equal(await ersatzKopieren('<p>x</p>', { writeText: () => { throw new Error('sofort') } }), 'manuell')
let aufgerufen = 0
const clipboard = { writeText: async () => { aufgerufen++ } }
for (const leer of ['', undefined, null, 5]) assert.equal(await ersatzKopieren(leer, clipboard), 'leer', String(leer))
assert.equal(aufgerufen, 0)
})
test('ersatzMeldung: bestätigt das Kopieren sichtbar, nennt den Plan und warnt, wenn die Zwischenablage fehlt', () => {
assert.deepEqual(ersatzMeldung('kopiert'), { ok: true, text: 'Ersatz-HTML kopiert.' })
assert.deepEqual(ersatzMeldung('kopiert', 'Café (cafe)'), { ok: true, text: 'Ersatz-HTML kopiert: Café (cafe).' })
assert.deepEqual(ersatzMeldung('manuell'), { ok: false, text: ZWISCHENABLAGE_FEHLT })
assert.match(ZWISCHENABLAGE_FEHLT, /Zwischenablage/)
assert.equal(ersatzMeldung('leer').ok, false)
assert.equal(ersatzMeldung('irgendwas').ok, false)
})
test('abschaltMeldung: angelegt, aktuell, fehler und ohne Angabe', () => {
assert.deepEqual(abschaltMeldung('angelegt'), { ok: true, text: 'Gespeichert. Ticket „Ersatz-Tabelle aktualisieren“ angelegt.' })
assert.deepEqual(abschaltMeldung('aktuell'), { ok: true, text: 'Gespeichert. Die Ersatz-Tabelle ist aktuell, kein Ticket nötig.' })
assert.deepEqual(abschaltMeldung(undefined), { ok: true, text: 'Gespeichert.' })
assert.deepEqual(abschaltMeldung('etwas-neues'), { ok: true, text: 'Gespeichert.' })
})
test('abschaltMeldung: bei „fehler“ eine sichtbare Warnung – Ticket von Hand anlegen, ein zweites Abschalten gibt es nicht', () => {
const m = abschaltMeldung('fehler')
assert.equal(m.ok, false)
assert.match(m.text, /^Gespeichert, aber/)
assert.match(m.text, /„Ersatz-Tabelle aktualisieren“/)
assert.match(m.text, /konnte nicht angelegt werden/)
assert.match(m.text, /von Hand/)
assert.doesNotMatch(m.text, /24 Uhr/)
})