From 57112cc51fa706404b6f20877a812fa458691961 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 18:07:30 +0000 Subject: [PATCH] =?UTF-8?q?Funktionen=20neu:=20Einschalten=20senden,=20409?= =?UTF-8?q?-Folgen,=20Wechsel=20mit=20gemerkter=20Funktion,=20zeigeEinscha?= =?UTF-8?q?lten=20f=C3=BCr=20die=20Module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- public/neu/funktion-einschalten.js | 155 ++++++++++++++++++++++++- test/knEinschaltenSeite.test.js | 179 ++++++++++++++++++++++++++++- 2 files changed, 327 insertions(+), 7 deletions(-) diff --git a/public/neu/funktion-einschalten.js b/public/neu/funktion-einschalten.js index 54fe2bd..4ca8604 100644 --- a/public/neu/funktion-einschalten.js +++ b/public/neu/funktion-einschalten.js @@ -23,6 +23,7 @@ export const TEXTE = Object.freeze({ nurBetrieb: (name) => `${name} gibt es für Betriebe. Stimmt das nicht? Schreib uns im Chat.`, registerFehlt: 'Einschalten ist gerade nicht möglich. Schreib uns im Chat.', kundenansicht: 'In der Kundenansicht kann nichts eingeschaltet werden.', + laden: 'Die Angaben konnten gerade nicht geladen werden. Bitte versuch es gleich noch einmal.', }) /** Die Erklärung der Funktion aus dem Katalog der Antwort (einzige Stelle für das Register); fehlt sie, null. */ @@ -213,16 +214,29 @@ export function fehlerAktion(fehler, { eintrag }) { * @returns {HTMLElement} section.kn-fn-einschalten */ export function renderEinschalten(behaelter, o) { - const { key, projectId, viewAs = false, imModul = false, jetzt = () => new Date(), bei = {} } = o + const { key, projectId, viewAs = false, imModul = false, jetzt = () => new Date() } = o let daten = o.daten + const api = o.api ?? apiStandard + const geheZu = o.geheZu ?? geheZuStandard const kundenart = () => (o.kundenartWirksam === undefined ? daten?.kundenart : o.kundenartWirksam) - const z = { haken: false, serverAb: '', sperreAb: '', sendet: false, meldung: '' } + const z = { haken: false, serverAb: '', sperreAb: '', erzwingeHaken: false, sendet: false, meldung: '' } + // Standards der Rückmeldungen (Task 4): Erfolg merkt die Meldung und geht zu Meine, Abbrechen zu Entdecken + const bei = { + fertig: (antwort) => { merkeMeldung(erfolgText(String(eintragVon()?.name ?? key), antwort)); geheZu('#funktionen', { ersetzen: true }) }, + weiter: (weg) => navigiere(weg, { key, projectId }, { geheZu, speicher: o.speicher }), + abbrechen: () => geheZu('#entdecken', { ersetzen: true }), + neuLaden: () => api('/api/funktionen'), + ...o.bei, + } const abschnitt = h('section', { class: 'kn-fn-einschalten', 'data-key': key, 'data-projekt': projectId }) let knopf = null let meldungEl = null const eintragVon = () => (Array.isArray(daten?.katalog) ? daten.katalog.find((e) => e?.key === key) ?? null : null) - const lageJetzt = () => beginnLage({ kundenart: kundenart(), abo: daten?.abo, jetzt: jetzt(), serverAb: z.serverAb }) + const lageJetzt = () => { + const lage = beginnLage({ kundenart: kundenart(), abo: daten?.abo, jetzt: jetzt(), serverAb: z.serverAb }) + return z.erzwingeHaken && !lage.noetig ? { noetig: true, ab: '' } : lage // beginn_noetig ohne lesbares Datum: Häkchen trotzdem zeigen + } function aktualisiereKnopf() { if (!knopf) return @@ -278,18 +292,147 @@ export function renderEinschalten(behaelter, o) { viewAs ? h('p', { class: 'kn-fn-info' }, TEXTE.kundenansicht) : null, meldungEl, h('div', { class: 'kn-fn-aktionen' }, - knopf ?? h('button', { type: 'button', class: 'kn-fn-knopf', 'data-aktion': 'chat', on: { click: () => (o.geheZu ?? geheZuStandard)('#chat') } }, 'Im Chat schreiben'), - imModul ? null : h('button', { type: 'button', class: 'kn-fn-knopf kn-fn-knopf--zweit', 'data-aktion': 'abbrechen', on: { click: () => bei.abbrechen?.() } }, 'Abbrechen')), + knopf ?? h('button', { type: 'button', class: 'kn-fn-knopf', 'data-aktion': 'chat', on: { click: () => geheZu('#chat') } }, 'Im Chat schreiben'), + imModul ? null : h('button', { type: 'button', class: 'kn-fn-knopf kn-fn-knopf--zweit', 'data-aktion': 'abbrechen', on: { click: () => bei.abbrechen() } }, 'Abbrechen')), ] abschnitt.replaceChildren(...kinder.flat(Infinity).filter(Boolean)) aktualisiereKnopf() } async function senden() { - // folgt in Task 4 + if (z.sendet || viewAs || z.sperreAb) return + const eintrag = eintragVon() + const consent = consentVon(daten, key) + const lage = lageJetzt() + if (!eintrag || !consent || (lage.noetig && !z.haken)) return + z.sendet = true + setzeMeldung('') + aktualisiereKnopf() + let antwort + try { + antwort = await api(`/api/funktionen/${encodeURIComponent(key)}/einschalten`, { + method: 'POST', + body: einschaltKoerper({ projectId, consent, beginnGesetzt: lage.noetig && z.haken }), + }) + } catch (fehler) { + z.sendet = false + await behandleFehler(fehler, eintrag) + return + } + bei.fertig(antwort) // der Knopf bleibt gesperrt: nach Erfolg gibt es nichts mehr zu senden + } + + async function behandleFehler(fehler, eintrag) { + const aktion = fehlerAktion(fehler, { eintrag }) + switch (aktion.art) { + case 'wechsel': + case 'bestellen': + aktualisiereKnopf() + bei.weiter(aktion) + break + case 'etage': // im Modul steht der Servertext (die Etage ist schon offen), sonst führt die Seite hin + if (imModul) { setzeMeldung(fehler?.message || TEXTE.allgemein); aktualisiereKnopf() } else { aktualisiereKnopf(); bei.weiter(aktion) } + break + case 'schon': + bei.fertig(null) + break + case 'beginn': // Häkchen zeigen, Knopf bis dahin gesperrt mit Datum + z.serverAb = typeof aktion.ab === 'string' ? aktion.ab : '' + z.erzwingeHaken = true + z.haken = false + fuelle() + abschnitt.querySelector('input[data-haken="beginn"]')?.focus?.() + break + case 'spaeter': // das Abo beginnt später: Text und gesperrter Knopf mit Datum, kein Häkchen + z.sperreAb = berlinTag(aktion.ab) + setzeMeldung(aktion.text) + aktualisiereKnopf() + break + case 'neu-laden': + try { + daten = await bei.neuLaden() + z.haken = false + z.serverAb = '' + z.erzwingeHaken = false + setzeMeldung(aktion.text) + fuelle() + } catch { + setzeMeldung(TEXTE.allgemein) + aktualisiereKnopf() + } + break + default: + setzeMeldung(aktion.text) + aktualisiereKnopf() + } } leeren(behaelter).append(abschnitt) fuelle() return abschnitt } + +/* ------------------------------------------------------------------------------------ Navigation und Meldungen --- */ + +/** + * Wohin nach einem Weg von einschaltWeg oder fehlerAktion? ziel = { key, projectId }; 'seite' behandelt die Ansicht selbst. + * etage → #?projekt=, bestellen → #bestellen-, wechsel → die gewünschte Funktion merken (V11), dann #wechsel. + * Der Wechsel geht auch, wenn sessionStorage fehlt oder voll ist. + */ +export async function navigiere(weg, ziel, { geheZu = geheZuStandard, speicher } = {}) { + switch (weg?.art) { + case 'etage': + geheZu(`#${weg.etage}?projekt=${encodeURIComponent(ziel.projectId)}`) + break + case 'bestellen': + geheZu(`#bestellen-${weg.produkt}`) + break + case 'wechsel': + try { + const ablage = speicher ?? globalThis.sessionStorage + ablage.setItem('kn-wechsel-funktion', JSON.stringify({ key: ziel.key, projectId: ziel.projectId })) + } catch { /* ohne Speicher geht der Wechsel trotzdem; nur die Rückkehr zur Funktion entfällt */ } + geheZu('#wechsel') + break + default: + } +} + +/** Text nach dem Einschalten; antwort null heißt: war schon eingeschaltet. */ +export function erfolgText(name, antwort) { + if (!antwort) return `${name} ist schon eingeschaltet.` + const ticket = antwort.ticketWoid ? ` Ticket #${antwort.ticketWoid}.` : '' + return `${name} ist eingeschaltet. Die Einrichtung läuft jetzt.${ticket}` +} + +let gemerkteMeldung = '' +/** Merkt genau eine Meldung für den nächsten Aufbau von Meine oder Entdecken. */ +export function merkeMeldung(text) { gemerkteMeldung = String(text ?? '') } +/** Gibt die gemerkte Meldung zurück und vergisst sie. */ +export function nimmMeldung() { + const text = gemerkteMeldung + gemerkteMeldung = '' + return text +} + +/** + * Vertrag mit den Funktionsmodulen (P4, VF1): die Bestätigungsseite in `platz`, ohne „Abbrechen“. Lädt GET /api/funktionen, wenn + * `daten` fehlt. onFertig({ feature, ticketWoid }) nach 200, onFertig({ feature: null, ticketWoid: null }) nach 409 bereits_eingeschaltet; + * alle anderen Fehler zeigt die Seite selbst. Liefert die gezeichnete Seite (oder die Fehlerzeile beim Laden). + */ +export async function zeigeEinschalten(platz, { key, projectId, onFertig, daten, kundenartWirksam, viewAs = false, api = apiStandard, geheZu, speicher, jetzt } = {}) { + let liste = daten + if (!liste) { + leeren(platz).append(h('p', { class: 'kn-leer' }, 'Wird geladen …')) + try { + liste = await api('/api/funktionen') + } catch { + return leeren(platz).appendChild(h('p', { class: 'kn-fn-info', role: 'alert' }, TEXTE.laden, + h('button', { type: 'button', class: 'kn-fn-knopf kn-fn-knopf--zweit', 'data-aktion': 'nochmal', on: { click: () => zeigeEinschalten(platz, { key, projectId, onFertig, kundenartWirksam, viewAs, api, geheZu, speicher, jetzt }) } }, 'Nochmal versuchen'))) + } + } + return renderEinschalten(platz, { + key, projectId, daten: liste, kundenartWirksam, viewAs, imModul: true, api, geheZu, speicher, jetzt, + bei: { fertig: (antwort) => onFertig?.(antwort ?? { feature: null, ticketWoid: null }) }, + }) +} diff --git a/test/knEinschaltenSeite.test.js b/test/knEinschaltenSeite.test.js index 1e9f799..8f476e2 100644 --- a/test/knEinschaltenSeite.test.js +++ b/test/knEinschaltenSeite.test.js @@ -1,7 +1,7 @@ import { test } from 'node:test' import assert from 'node:assert/strict' import { installFakeDom } from './support/fakeDom.js' -import { renderEinschalten } from '../public/neu/funktion-einschalten.js' +import { renderEinschalten, navigiere, erfolgText, merkeMeldung, nimmMeldung, zeigeEinschalten } from '../public/neu/funktion-einschalten.js' import { daten, projekt } from './support/knFunktionenDaten.js' const JETZT = () => new Date('2026-10-20T10:00:00Z') @@ -94,3 +94,180 @@ test('Abo: Laufzeit nennt das Abo; Hosting-Abo bei einer Funktion des Hosting-Ab const d = daten({ abo: { produkt: 'hosting' }, katalogEdit: { news: { enthaltenIn: 'hosting' } } }) assert.match(fx.text(seite(fx, { daten: d }).b), /Laufzeit.*läuft, solange dein Hosting-Abo läuft/) })) + +/* ------------------------------------------------------------------ Teil 2: Senden, Fehlerfolgen, Navigation --- */ +const fehler = (status, code, body = {}, message = 'Servertext') => Object.assign(new Error(message), { status, code, body: { code, ...body } }) +const posts = (s) => s.aufrufe.filter((a) => a.method === 'POST') + +test('Erfolg: genau ein POST mit dem Körper aus dem Register, ohne Kundenart; fertig bekommt die Antwort', mit(async (fx) => { + const s = seite(fx) + await fx.klick(knopf(fx, s.b)) + assert.deepEqual(posts(s), [{ pfad: '/api/funktionen/news/einschalten', method: 'POST', body: { projectId: 'p1', zustimmung: { version: '2026-10-20', haken: [] } } }]) + assert.deepEqual(s.rueck, [['fertig', OK]]) +})) + +test('zweiter Tap: höchstens ein POST', mit(async (fx) => { + const s = seite(fx) + await Promise.all([fx.klick(knopf(fx, s.b)), fx.klick(knopf(fx, s.b))]) + assert.equal(posts(s).length, 1) +})) + +test('Privatkunde: ohne Häkchen geht nichts raus, mit Häkchen ["beginn"]', mit(async (fx) => { + const s = seite(fx, { kundenartWirksam: 'privat', daten: daten({ abo: { widerrufsfristBis: FRIST } }) }) + await fx.klick(knopf(fx, s.b)) + assert.equal(s.aufrufe.length, 0) + await fx.klick(fx.finde(s.b, 'input[data-haken="beginn"]')) + await fx.klick(knopf(fx, s.b)) + assert.deepEqual(posts(s)[0].body.zustimmung.haken, ['beginn']) +})) + +test('beginn_noetig vom Server: Häkchen erscheint, Knopf nennt das Datum, zweiter Versuch mit Häkchen', mit(async (fx) => { + let n = 0 + const s = seite(fx, { antwort: () => { if (++n === 1) throw fehler(409, 'beginn_noetig', { ab: '2026-11-02T23:00:00.000Z' }); return OK } }) + await fx.klick(knopf(fx, s.b)) + assert.deepEqual([knopf(fx, s.b).textContent, knopf(fx, s.b).disabled], ['Einschalten ab 03.11.', true]) + await fx.klick(fx.finde(s.b, 'input[data-haken="beginn"]')) + await fx.klick(knopf(fx, s.b)) + assert.deepEqual(posts(s).map((p) => p.body.zustimmung.haken), [[], ['beginn']]) +})) + +test('beginn_noetig ohne lesbares Datum: Häkchen erscheint trotzdem, der Knopf bleibt bis dahin gesperrt', mit(async (fx) => { + let n = 0 + const s = seite(fx, { antwort: () => { if (++n === 1) throw fehler(409, 'beginn_noetig', {}); return OK } }) + await fx.klick(knopf(fx, s.b)) + assert.deepEqual([knopf(fx, s.b).textContent, knopf(fx, s.b).disabled], ['Einschalten', true]) + assert.ok(fx.finde(s.b, 'input[data-haken="beginn"]')) +})) + +test('abo_fehlt beginnt_spaeter (N20): Text und Datum, Knopf gesperrt mit Datum, kein Häkchen', mit(async (fx) => { + const s = seite(fx, { antwort: () => { throw fehler(409, 'abo_fehlt', { grund: 'beginnt_spaeter', ab: '2026-11-02T23:00:00.000Z' }) } }) + await fx.klick(knopf(fx, s.b)) + assert.match(fx.text(fx.finde(s.b, '[data-meldung]')), /^Dein Abo beginnt am 03\.11\.2026\. Ab dann kannst du einschalten\.$/) + assert.deepEqual([knopf(fx, s.b).textContent, knopf(fx, s.b).disabled], ['Einschalten ab 03.11.', true]) + assert.equal(haken(fx, s.b).length, 0) + assert.deepEqual(s.rueck, []) +})) + +test('weitere 409-Codes führen an den richtigen Ort', mit(async (fx) => { + const faelle = [ + [fehler(409, 'abo_fehlt', { grund: 'falsches_abo', wechsel: true }), ['weiter', { art: 'wechsel' }]], + [fehler(409, 'abo_fehlt', { grund: 'kein_abo' }), ['weiter', { art: 'bestellen', produkt: 'wartung' }]], + [fehler(409, 'bestellung_offen', { offen: [] }), ['weiter', { art: 'etage', etage: 'news' }]], + [fehler(409, 'bereits_eingeschaltet'), ['fertig', null]], + ] + for (const [f, erwartet] of faelle) { + const s = seite(fx, { antwort: () => { throw f } }) + await fx.klick(knopf(fx, s.b)) + assert.deepEqual(s.rueck, [erwartet], f.code) + } +})) + +test('fassung_veraltet: lädt neu, zeigt die Zeile erneut und sendet mit der neuen Fassung', mit(async (fx) => { + const neu = daten(); neu.katalog[0].consent.version = '2026-11-01' + let n = 0 + const s = seite(fx, { antwort: () => { if (++n === 1) throw fehler(409, 'fassung_veraltet'); return OK }, bei: { neuLaden: async () => neu, fertig: () => {} } }) + await fx.klick(knopf(fx, s.b)) + assert.match(fx.text(s.b), /Die Bedingungen wurden aktualisiert\. Bitte lies die Zeile noch einmal\./) + await fx.klick(knopf(fx, s.b)) + assert.equal(posts(s).at(-1).body.zustimmung.version, '2026-11-01') +})) + +test('fassung_veraltet: scheitert das Neuladen, steht die allgemeine Meldung da und der Knopf ist frei', mit(async (fx) => { + const s = seite(fx, { antwort: () => { throw fehler(409, 'fassung_veraltet') }, bei: { neuLaden: async () => { throw new Error('weg') }, fertig: () => {} } }) + await fx.klick(knopf(fx, s.b)) + assert.match(fx.text(fx.finde(s.b, '[data-meldung]')), /Das Einschalten hat gerade nicht geklappt\./) + assert.equal(knopf(fx, s.b).disabled, false) +})) + +test('Netzfehler: Meldung, Knopf wieder frei, zweiter Versuch geht durch', mit(async (fx) => { + let n = 0 + const s = seite(fx, { antwort: () => { if (++n === 1) throw Object.assign(new Error('Keine Verbindung. Bitte versuch es gleich noch einmal.'), { status: 0, body: {} }); return OK } }) + await fx.klick(knopf(fx, s.b)) + assert.match(fx.text(fx.finde(s.b, '[data-meldung]')), /Keine Verbindung/) + assert.equal(knopf(fx, s.b).disabled, false) + await fx.klick(knopf(fx, s.b)) + assert.equal(posts(s).length, 2) + assert.deepEqual(s.rueck, [['fertig', OK]]) +})) + +test('Kundenansicht: auch ein erzwungener Klick sendet nichts', mit(async (fx) => { + const s = seite(fx, { viewAs: true }) + await knopf(fx, s.b).click() + assert.equal(s.aufrufe.length, 0) +})) + +test('Standards ohne bei: Erfolg merkt die Meldung und geht ersetzend zu Meine, Abbrechen zu Entdecken, Wechsel merkt die Funktion', mit(async (fx) => { + nimmMeldung() + const gegangen = []; const gespeichert = {} + const standard = (antwort) => { + const b = fx.document.createElement('div'); fx.document.body.append(b) + renderEinschalten(b, { key: 'news', projectId: 'p1', daten: daten(), kundenartWirksam: 'unternehmen', jetzt: JETZT, api: async () => antwort(), geheZu: (hash, o) => gegangen.push(o ? [hash, o] : [hash]), speicher: { setItem: (k, v) => { gespeichert[k] = v } } }) + return b + } + await fx.klick(knopf(fx, standard(() => OK))) + assert.deepEqual(gegangen, [['#funktionen', { ersetzen: true }]]) + assert.equal(nimmMeldung(), 'News-Pinnwand ist eingeschaltet. Die Einrichtung läuft jetzt. Ticket #100301.') + gegangen.length = 0 + await fx.klick(fx.finde(standard(() => OK), '[data-aktion="abbrechen"]')) + assert.deepEqual(gegangen, [['#entdecken', { ersetzen: true }]]) + gegangen.length = 0 + await fx.klick(knopf(fx, standard(() => { throw fehler(409, 'abo_fehlt', { grund: 'falsches_abo', wechsel: true }) }))) + assert.deepEqual(gegangen, [['#wechsel']]) + assert.deepEqual(JSON.parse(gespeichert['kn-wechsel-funktion']), { key: 'news', projectId: 'p1' }) +})) + +test('zeigeEinschalten (Vertrag mit P4): lädt die Liste, kein Abbrechen, onFertig nach 200 und nach bereits_eingeschaltet, Servertext bei bestellung_offen', mit(async (fx) => { + const lauf = async (antwort) => { + const platz = fx.document.createElement('div'); fx.document.body.append(platz) + const log = { onFertig: [], gegangen: [], pfade: [] } + const api = async (pfad, o = {}) => { log.pfade.push(`${o.method ?? 'GET'} ${pfad}`); if (!o.method) return daten(); return antwort() } + await zeigeEinschalten(platz, { key: 'news', projectId: 'p1', onFertig: (a) => log.onFertig.push(a), api, geheZu: (h) => log.gegangen.push(h), jetzt: JETZT }) + assert.equal(fx.finde(platz, '[data-aktion="abbrechen"]'), null) + await fx.klick(knopf(fx, platz)) + return { platz, log } + } + const ok = await lauf(() => OK) + assert.deepEqual([ok.log.pfade, ok.log.onFertig], [['GET /api/funktionen', 'POST /api/funktionen/news/einschalten'], [OK]]) + const schon = await lauf(() => { throw fehler(409, 'bereits_eingeschaltet') }) + assert.deepEqual(schon.log.onFertig, [{ feature: null, ticketWoid: null }]) + const offen = await lauf(() => { throw fehler(409, 'bestellung_offen', { offen: [] }, 'Es fehlen noch Angaben.') }) + assert.match(fx.text(offen.platz), /Es fehlen noch Angaben\./) + assert.deepEqual([offen.log.gegangen, offen.log.onFertig], [[], []]) +})) + +test('zeigeEinschalten: Ladefehler zeigt Text und "Nochmal versuchen"; mit übergebenen Daten kein Abruf', mit(async (fx) => { + const platz = fx.document.createElement('div'); fx.document.body.append(platz) + let n = 0; const pfade = [] + const api = async (pfad, o = {}) => { pfade.push(`${o.method ?? 'GET'} ${pfad}`); if (++n === 1) throw Object.assign(new Error('x'), { status: 500, body: {} }); return daten() } + await zeigeEinschalten(platz, { key: 'news', projectId: 'p1', api, jetzt: JETZT }) + assert.match(fx.text(platz), /Die Angaben konnten gerade nicht geladen werden\./) + await fx.klick(fx.finde(platz, '[data-aktion="nochmal"]')) + assert.ok(knopf(fx, platz)) + const eigener = fx.document.createElement('div'); fx.document.body.append(eigener) + pfade.length = 0 + await zeigeEinschalten(eigener, { key: 'news', projectId: 'p1', daten: daten(), api, jetzt: JETZT }) + assert.deepEqual(pfade, []) + assert.ok(knopf(fx, eigener)) +})) + +test('navigiere: Ziele je Weg; der Wechsel merkt die Funktion und geht auch ohne beschreibbaren Speicher', async () => { + const gegangen = []; const geheZu = (hash, o) => { gegangen.push(o ? [hash, o] : [hash]) } + const gespeichert = {}; const speicher = { setItem: (k, v) => { gespeichert[k] = v } } + const ziel = { key: 'termine', projectId: 'p1' } + await navigiere({ art: 'etage', etage: 'webshop' }, { key: 'shop', projectId: 'p2' }, { geheZu, speicher }) + await navigiere({ art: 'bestellen', produkt: 'hosting' }, { key: 'analyse', projectId: 'p1' }, { geheZu, speicher }) + assert.deepEqual(gegangen, [['#webshop?projekt=p2'], ['#bestellen-hosting']]) + gegangen.length = 0 + await navigiere({ art: 'wechsel' }, ziel, { geheZu, speicher }) + assert.deepEqual(JSON.parse(gespeichert['kn-wechsel-funktion']), ziel) + await navigiere({ art: 'wechsel' }, ziel, { geheZu, speicher: { setItem() { throw new Error('voll') } } }) + await navigiere({ art: 'wechsel' }, ziel, { geheZu }) // ohne sessionStorage (Node) + assert.deepEqual(gegangen, [['#wechsel'], ['#wechsel'], ['#wechsel']]) +}) + +test('erfolgText und Meldung: eine Meldung, einmal gelesen', () => { + assert.equal(erfolgText('News-Pinnwand', { ticketWoid: '100301' }), 'News-Pinnwand ist eingeschaltet. Die Einrichtung läuft jetzt. Ticket #100301.') + assert.equal(erfolgText('News-Pinnwand', { ticketWoid: null }), 'News-Pinnwand ist eingeschaltet. Die Einrichtung läuft jetzt.') + assert.equal(erfolgText('News-Pinnwand', null), 'News-Pinnwand ist schon eingeschaltet.') + merkeMeldung('x'); assert.equal(nimmMeldung(), 'x'); assert.equal(nimmMeldung(), '') +})