From 1b66f6aa81f1b9fa3a09490a88b1a199ae5d95e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 17:09:08 +0000 Subject: [PATCH] Kundenansicht neu P2: Wechsel im Wortlaut der Fassung 5 mit gemerkter Funktion Co-Authored-By: Claude Sonnet 5.5 --- public/neu/ansicht-wechsel.js | 184 +++++++++++++++++++++++++++++++++- test/knAnsichtWechsel.test.js | 79 +++++++++++++++ 2 files changed, 259 insertions(+), 4 deletions(-) create mode 100644 test/knAnsichtWechsel.test.js diff --git a/public/neu/ansicht-wechsel.js b/public/neu/ansicht-wechsel.js index 272f6ed..978a4b2 100644 --- a/public/neu/ansicht-wechsel.js +++ b/public/neu/ansicht-wechsel.js @@ -1,7 +1,183 @@ -/* Kundenansicht neu: Platzhalter, den das nächste Paket füllt (Stub aus P1). */ +/* Kundenansicht neu (P2): Wechsel vom Hosting-Abo ins Wartungs-Abo (#wechsel), Text und Logik 1:1 aus app.js renderSwitchView + (Fassung 5, Sie-Form, C14). Vorschau vom Server, Bestellübersicht unmittelbar über dem Knopf (§ 312j BGB), Zustimmung, Bestellung. + Der Wechsel wirkt erst mit der Zahlung der Wechselrechnung (Bedingungen § 9 Abs. 3). Das Wortlaut-Orakel prüft den sichtbaren Text. + Neu: die gemerkte Funktion (E14, C5): kam der Kunde aus dem Einschalten einer Funktion, geht es danach zurück zu den Funktionen. */ -import { h } from './dom.js' +import { api } from './api.js' +import { h, leeren } from './dom.js' +import { K, ladeAbo, wechselFunktionAus, safeUrl, merkeHinweis, navigiere, meldeTitel, meldeZaehlerNeu, zuAnsicht, fmtEuro, fmtDatum, berlinTag, ABO_NICHT_GELADEN } from './ansicht-hilfen.js' -export async function zeigeWechsel(wurzel, { route, me, nav }) { - wurzel.replaceChildren(h('p', { class: 'kn-leer' }, 'Abo wechseln (folgt)')) +const SCHLUESSEL = 'kn-wechsel-funktion' +const BEDINGUNGEN = '/abo-bedingungen.html' +const TEXT_SPERRE = 'Im Ansichtsmodus nicht möglich.' +const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs +const link = (href, text) => h('a', { href, target: '_blank', rel: 'noopener' }, text) +const reihe = (titel, ...inhalt) => h('tr', null, h('td', null, titel), h('td', null, ...inhalt)) + +/** Die von P3 gemerkte Funktion { key, projectId } oder null (kaputter oder fremder Wert, kein sessionStorage). */ +export function gemerkteFunktion() { + try { + const wert = JSON.parse(globalThis.sessionStorage?.getItem(SCHLUESSEL) ?? 'null') + return wert && typeof wert.key === 'string' && wert.key && typeof wert.projectId === 'string' && wert.projectId ? { key: wert.key, projectId: wert.projectId } : null + } catch { + return null + } +} +function vergissFunktion() { + try { globalThis.sessionStorage?.removeItem(SCHLUESSEL) } catch { /* ohne Speicher bleibt nichts liegen */ } +} + +// Zurück aus dem Bfcache (z. B. von der Zahlungsseite): eine hängende Bestellung nicht gesperrt lassen +let aktiv = null +const mitPageshow = new WeakSet() +function registrierePageshow() { + const win = window + if (mitPageshow.has(win)) return + mitPageshow.add(win) + win.addEventListener('pageshow', () => { if (aktiv?.busy) { aktiv.busy = false; aktiv.zeichne() } }) +} + +export async function zeigeWechsel(wurzel, { route, me, nav, daten } = {}) { + const nr = (laeufe.get(wurzel) ?? 0) + 1 + laeufe.set(wurzel, nr) + const aktuell = () => laeufe.get(wurzel) === nr + const viewAs = me?.viewAs === true + meldeTitel(wurzel, 'Wechsel ins Wartungs-Abo') + leeren(wurzel).append(h('p', { class: K.leer }, 'Wird geladen …')) + registrierePageshow() + + const stand = daten ?? await ladeAbo() + if (!aktuell()) return + if (!stand) { leeren(wurzel).append(h('p', { class: K.fehler }, ABO_NICHT_GELADEN)); return } + const sub = stand.subscription + const wartung = (stand.products || []).find((p) => p.key === 'wartung') || null + if (!sub?.active || sub.productKey !== 'hosting' || !sub.canSwitch || !wartung) { + leeren(wurzel).append(h('div', { class: 'kn-panel' }, h('p', { class: 'kn-hinweis' }, 'Ein Wechsel ist derzeit nicht möglich. Details finden Sie unter ', h('a', zuAnsicht('vertraege'), 'Verträge'), '.'))) + navigiere('#vertraege', { ersetzen: true }) + return + } + + const units = Number(sub.storage?.units) || 0 + const mail = stand.mailStorage || {} + const unitCents = Number(mail.unitPriceCents) || 0 + const lauf = { busy: false, preview: null, error: '', loading: true, token: 0, terms: false, early: false, fehler: '', zeichne: null } + aktiv = lauf + + const zeichne = () => { + const p = lauf.preview + const rows = [] + if (p) { + const monthly = Number.isFinite(Number(p.monthlyCents)) && p.monthlyCents != null ? Number(p.monthlyCents) : wartung.amountCents + rows.push(reihe('Wartungs-Abo', h('strong', null, `${fmtEuro(monthly / 100)} pro Monat`), ' (Gesamtpreis, gemäß § 19 UStG ohne Umsatzsteuer); enthalten sind Pflege Ihrer Website, beliebig viele Postfächer und 1 Stunde Arbeitszeit pro Monat')) + if (units > 0) { + rows.push(reihe('Zusatzspeicher', `${units} Paket${units === 1 ? '' : 'e'} (+${units * (Number(mail.gbPerUnit) || 0)} GB) bleiben gebucht: ${fmtEuro(units * unitCents / 100)} pro Monat zusätzlich (gemäß § 19 UStG ohne Umsatzsteuer), insgesamt ${fmtEuro((monthly + units * unitCents) / 100)} pro Monat`)) + } + if ((Number(p.creditCents) || 0) > 0) { + rows.push(reihe('Gutschrift für nicht genutzte Tage', `−${fmtEuro(p.creditCents / 100)} aus dem bereits bezahlten Hosting-Abo (gemäß § 19 UStG ohne Umsatzsteuer)`)) + } + rows.push(reihe('Heute fällig', h('strong', null, fmtEuro(p.amountDueCents / 100)), ' (gemäß § 19 UStG ohne Umsatzsteuer)')) + const tag = berlinTag(p.nextChargeAt) + rows.push(reihe('Neuer Abbuchungstag', `${tag ? `Ab dem Wechsel jeweils am ${tag.t}. eines Monats; ` : ''}nächste Abbuchung am ${fmtDatum(p.nextChargeAt)}`)) + rows.push(reihe('Laufzeit', h('strong', null, `Mindestlaufzeit bis ${fmtDatum(p.minTermEnd)}`), ' (12 Monate ab dem Wechsel), danach monatlich kündbar')) + rows.push(reihe('Beginn', 'Der Wechsel wird erst mit der Zahlung wirksam. Bei Kreditkarte, sobald die Zahlung erfolgreich ist (ggf. nach Bestätigung bei Ihrer Bank); bei SEPA-Lastschrift sofort mit der Bestellung, wird die Lastschrift nicht eingelöst, gilt der Wechsel als nicht erfolgt. Einen späteren Beginn gibt es nicht.')) + } + const uebersicht = lauf.error + ? h('p', { class: K.fehler }, lauf.error) + : lauf.loading || !p ? h('p', { class: 'kn-hinweis' }, 'Ihre Übersicht wird berechnet …') + : h('table', { class: 'kn-zusammenfassung' }, h('tbody', null, rows)) + const sperre = lauf.busy || viewAs + const terms = h('input', { type: 'checkbox', id: 'switch-terms', checked: lauf.terms, disabled: sperre, on: { change: (e) => { lauf.terms = e.currentTarget.checked } } }) + const early = h('input', { type: 'checkbox', id: 'switch-early', checked: lauf.early, disabled: sperre, on: { change: (e) => { lauf.early = e.currentTarget.checked } } }) + leeren(wurzel).append(h('div', { class: 'kn-panel', id: 'switch-order' }, + h('h3', null, 'Ihre Bestellung'), + h('p', { class: 'kn-hinweis' }, 'Zusätzlich zum Hosting: Pflege Ihrer Website, beliebig viele Postfächer und 1 Stunde Arbeitszeit pro Monat. Das Hosting-Abo endet mit dem Wechsel im gegenseitigen Einvernehmen; Domain, Postfächer und gebuchter Speicher bleiben.'), + h('label', { class: 'kn-check' }, terms, + h('span', null, 'Ich habe die ', link(BEDINGUNGEN, 'Vertragsbedingungen mit Leistungsbeschreibung und Auftragsverarbeitungsvertrag'), ' gelesen und stimme ihnen zu. Die ', link(`${BEDINGUNGEN}#widerruf`, 'Widerrufsbelehrung'), ' habe ich zur Kenntnis genommen.')), + h('label', { class: 'kn-check' }, early, + h('span', null, h('strong', null, 'Sofortbeginn.'), ' Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Wartungs-Abo beginnt. Mir ist bekannt, dass ich bei einem Widerruf einen angemessenen Betrag für die bis dahin erbrachten Leistungen zahle (§ 357a Abs. 2 BGB; siehe Hinweise zum Widerruf). Firmenkunden haben kein Widerrufsrecht.')), + h('div', { class: 'kn-uebersicht' }, uebersicht, + h('p', { class: 'kn-hinweis' }, 'Es gilt das gesetzliche Mängelhaftungsrecht. Einzelheiten: ', link(BEDINGUNGEN, 'Vertragsbedingungen'), ' · ', link('/datenschutz.html', 'Datenschutz'))), + h('p', { class: lauf.fehlerKlasse ?? K.fehler, id: 'switch-error', hidden: !lauf.fehler }, lauf.fehler), + h('div', { class: 'kn-aktionen' }, + h('button', { type: 'button', class: K.primaer, id: 'switch-buy', disabled: lauf.busy || viewAs || lauf.loading || !p, on: { click: absenden } }, lauf.busy ? 'Wird bestellt …' : 'Zahlungspflichtig bestellen'), + h('span', { class: 'kn-hinweis' }, 'Bezahlt wird mit der bei Stripe hinterlegten Zahlungsart. Bis zum Klick können Sie abbrechen.'), + viewAs ? h('span', { class: K.meldung }, TEXT_SPERRE) : null))) + } + lauf.zeichne = zeichne + + const ladeVorschau = async () => { + const token = ++lauf.token + lauf.loading = true + lauf.error = '' + zeichne() + try { + const p = await api('/api/abo/switch/preview', { method: 'POST', body: {} }) + if (token !== lauf.token || !aktuell()) return + lauf.preview = p + } catch (f) { + if (token !== lauf.token || !aktuell()) return + lauf.preview = null + lauf.error = f.message + } + lauf.loading = false + zeichne() + } + + const zeigeFehler = (text, klasse) => { lauf.fehler = text; lauf.fehlerKlasse = klasse; zeichne() } + + async function absenden() { + if (viewAs || lauf.busy || !lauf.preview) return + lauf.fehler = '' + if (!lauf.terms) { zeigeFehler('Bitte stimmen Sie den Vertragsbedingungen zu.'); return } + if (!lauf.early) { zeigeFehler('Bitte bestätigen Sie den Sofortbeginn, sonst können wir den Wechsel nicht ausführen.'); return } + const p = lauf.preview + const funktion = gemerkteFunktion() + const gesendet = funktion !== null + lauf.busy = true + zeichne() + try { + const body = { acceptTerms: true, earlyStart: true, termsVersion: stand.termsVersion, prorationDate: p.prorationDate, amountDueCents: p.amountDueCents } + if (gesendet) body.funktion = funktion + const res = await api('/api/abo/switch', { method: 'POST', body }) + if (gesendet) vergissFunktion() + if (res.paymentUrl) { + const ziel = safeUrl(res.paymentUrl) + if (!ziel) { + lauf.busy = false + zeigeFehler('Die Zahlungsseite konnte nicht geöffnet werden. Der Wechsel wartet auf die Zahlung; Näheres steht unter Verträge.') + return + } + const knopf = wurzel.querySelector('#switch-buy') + if (knopf) knopf.textContent = 'Weiter zur Zahlung …' + zeigeFehler('Ihr Wechsel wartet auf die Zahlung. Wir leiten Sie zur Zahlung weiter.', 'kn-meldung') + window.location.href = ziel + return + } + lauf.busy = false + const sepa = Boolean(res.subscription?.switchPending) + const hinweis = sepa + ? 'Ihr Wechsel ins Wartungs-Abo gilt ab sofort. Die Lastschrift wird noch eingezogen; wir bestätigen Ihnen den Wechsel per E-Mail.' + : 'Ihr Wechsel ins Wartungs-Abo ist abgeschlossen. Wir bestätigen ihn per E-Mail.' + meldeZaehlerNeu('vertraege') + // Mit gemerkter Funktion zurück zu den Funktionen (dort öffnet P3 den Einschalt-Ablauf), sonst wie bisher zu den Verträgen + if (gesendet) { + let zurueck = wechselFunktionAus(res.subscription) + if (!zurueck) zurueck = wechselFunktionAus((await ladeAbo())?.subscription) + if (zurueck) { navigiere('#funktionen', { ersetzen: true }); return } + } + merkeHinweis(hinweis) + navigiere('#vertraege') + } catch (f) { + lauf.busy = false + zeigeFehler(f.message) + // Betrag oder Stichtag haben sich geändert: neue Vorschau laden, Fehlerhinweis bleibt stehen + if (f.status === 409) { + const msg = f.message + await ladeVorschau() + zeigeFehler(msg) + } + } + } + + await ladeVorschau() } diff --git a/test/knAnsichtWechsel.test.js b/test/knAnsichtWechsel.test.js new file mode 100644 index 0000000..77394a0 --- /dev/null +++ b/test/knAnsichtWechsel.test.js @@ -0,0 +1,79 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { zeigeWechsel, gemerkteFunktion } from '../public/neu/ansicht-wechsel.js' +import { nimmHinweis } from '../public/neu/ansicht-hilfen.js' +import { sichtbarerText, ohneKopf, ladeAlteApp } from './support/knWortlautOrakel.js' +import { mit, me, abo, aboAntwort, neueWurzel, VORSCHAU } from './support/knDaten.js' + +const alt = ladeAlteApp() +const HOSTING = aboAntwort({ subscription: abo({ produkt: 'hosting', canSwitch: true, storage: { units: 2, pendingSetup: false, pendingReduction: null } }) }) +const zeigeNeu = async (fx, daten = HOSTING, m = me(), titel = []) => { const w = neueWurzel(fx); w.addEventListener('kn:titel', (e) => titel.push(e.detail.text)); await zeigeWechsel(w, { route: { bereich: 'vertraege', etage: 'wechsel', query: '' }, me: m, nav: 'push', daten }); await fx.ruhe(); return w } +test('Wechsel: sichtbarer Text wie in app.js, Titel wird gemeldet', mit({ 'POST /api/abo/switch/preview': VORSCHAU }, async (fx) => { + const titel = []; assert.equal(sichtbarerText(await zeigeNeu(fx, HOSTING, me(), titel)), sichtbarerText(ohneKopf(await alt.wechsel(HOSTING, VORSCHAU)))); assert.deepEqual(titel, ['Wechsel ins Wartungs-Abo']) +})) +test('Wechsel nicht möglich: Satz und Rückweg zu Verträgen', mit({}, async (fx) => { + fx.window.history.replaceState = (_s, _t, url) => { fx.window.location.hash = `#${String(url).split('#')[1] ?? ''}` } + const w = await zeigeNeu(fx, aboAntwort({ subscription: abo({ produkt: 'wartung' }) })) + assert.match(fx.text(w), /Ein Wechsel ist derzeit nicht möglich\. Details finden Sie unter Verträge\./); assert.equal(fx.window.location.hash, '#vertraege') +})) +test('Wechsel: Prüfungen wörtlich, Body mit gemerkter Funktion, Schlüssel danach weg', mit({ 'POST /api/abo/switch/preview': VORSCHAU, 'POST /api/abo/switch': () => ({ body: { paymentUrl: 'https://pay.stripe.com/x', subscription: abo() } }) }, async (fx) => { + const speicher = new Map(); globalThis.sessionStorage = { getItem: (k) => speicher.get(k) ?? null, setItem: (k, v) => speicher.set(k, v), removeItem: (k) => speicher.delete(k) } + try { + const w = await zeigeNeu(fx) + await fx.klick(fx.finde(w, '#switch-buy')); assert.match(fx.text(w), /Bitte stimmen Sie den Vertragsbedingungen zu\./) + await fx.klick(fx.finde(w, '#switch-terms')); await fx.klick(fx.finde(w, '#switch-buy')); assert.match(fx.text(w), /Bitte bestätigen Sie den Sofortbeginn, sonst können wir den Wechsel nicht ausführen\./) + await fx.klick(fx.finde(w, '#switch-early')); speicher.set('kn-wechsel-funktion', JSON.stringify({ key: 'termine', projectId: 'p1' })) + await fx.klick(fx.finde(w, '#switch-buy')); await fx.ruhe() + assert.deepEqual(fx.fetchLog.find((a) => a.url === '/api/abo/switch').body, { acceptTerms: true, earlyStart: true, termsVersion: '2026-09-30-v5', prorationDate: 1760000000, amountDueCents: 3000, funktion: { key: 'termine', projectId: 'p1' } }) + assert.equal(speicher.has('kn-wechsel-funktion'), false); assert.equal(fx.window.location.href, 'https://pay.stripe.com/x'); assert.match(fx.text(w), /Ihr Wechsel wartet auf die Zahlung\. Wir leiten Sie zur Zahlung weiter\./) + } finally { delete globalThis.sessionStorage } +})) +test('gemerkteFunktion: kaputte oder fremde Werte ergeben null', () => { + const wert = (v) => { globalThis.sessionStorage = { getItem: () => v }; try { return gemerkteFunktion() } finally { delete globalThis.sessionStorage } } + assert.deepEqual(wert('{"key":"shop","projectId":"p2"}'), { key: 'shop', projectId: 'p2' }) + for (const v of ['kaputt', '{"key":"shop"}', '{"key":1,"projectId":"p"}', null]) assert.equal(wert(v), null) + assert.equal(gemerkteFunktion(), null) // ohne sessionStorage +}) +const FUNKTION = { key: 'termine', projectId: 'p1' } +const WOHIN = (fx) => { fx.window.history.replaceState = (_s, _t, url) => { fx.window.location.hash = `#${String(url).split('#')[1] ?? ''}` }; fx.window.history.pushState = fx.window.history.replaceState } +for (const [name, gemerkt, antwort, nachgeladen, ziel] of [ + ['gemerkt, Funktion steht in der Antwort', true, { wechselFunktion: FUNKTION }, undefined, '#funktionen'], + ['gemerkt, Funktion erst im GET /api/abo', true, {}, { wechselFunktion: FUNKTION }, '#funktionen'], + ['gemerkt, Wechsel noch nicht erledigt', true, {}, { wechselFunktion: null }, '#vertraege'], + ['nicht gemerkt, der Server meldet trotzdem eine Funktion', false, { wechselFunktion: FUNKTION }, { wechselFunktion: FUNKTION }, '#vertraege'], +]) { + test(`Wechsel ohne Zahlungsseite (${name}): Ziel ${ziel} (E14, C5)`, mit({ + 'POST /api/abo/switch/preview': VORSCHAU, 'POST /api/abo/switch': () => ({ body: { subscription: abo(antwort) } }), + 'GET /api/abo': aboAntwort({ subscription: abo({ produkt: 'wartung', ...nachgeladen }) }), + }, async (fx) => { + WOHIN(fx); const speicher = new Map(gemerkt ? [['kn-wechsel-funktion', JSON.stringify(FUNKTION)]] : []) + globalThis.sessionStorage = { getItem: (k) => speicher.get(k) ?? null, setItem: (k, v) => speicher.set(k, v), removeItem: (k) => speicher.delete(k) } + try { + nimmHinweis() + const w = await zeigeNeu(fx); await fx.klick(fx.finde(w, '#switch-terms')); await fx.klick(fx.finde(w, '#switch-early')); await fx.klick(fx.finde(w, '#switch-buy')); await fx.ruhe() + assert.equal(fx.window.location.hash, ziel); assert.equal(speicher.has('kn-wechsel-funktion'), false) + assert.equal(nimmHinweis() !== '', ziel === '#vertraege') // bei #funktionen bleibt kein Hinweis für später liegen + assert.equal(fx.fetchLog.filter((a) => a.url === '/api/abo').length, gemerkt && !antwort.wechselFunktion ? 1 : 0) // höchstens ein Nachladen, nur mit gemerkter Funktion + } finally { delete globalThis.sessionStorage } + })) +} +test('Wechsel: 409 lädt die Vorschau neu und behält die Meldung; Ansichtsmodus sperrt', mit({ 'POST /api/abo/switch/preview': VORSCHAU, 'POST /api/abo/switch': () => ({ status: 409, body: { error: 'Betrag hat sich geändert.' } }) }, async (fx) => { + const w = await zeigeNeu(fx); await fx.klick(fx.finde(w, '#switch-terms')); await fx.klick(fx.finde(w, '#switch-early')); await fx.klick(fx.finde(w, '#switch-buy')); await fx.ruhe() + assert.match(fx.text(w), /Betrag hat sich geändert\./); assert.equal(fx.fetchLog.filter((a) => a.url === '/api/abo/switch/preview').length, 2) + const v = await zeigeNeu(fx, HOSTING, me({ viewAs: true })); assert.equal(fx.finde(v, '#switch-buy').hasAttribute('disabled'), true); assert.match(fx.text(v), /Im Ansichtsmodus nicht möglich\./) +})) +test('Wechsel: Vorschau-Fehler, hängende Bestellung nach pageshow, SEPA-Hinweis; im Ansichtsmodus geht kein POST raus', mit({ + 'POST /api/abo/switch/preview': () => ({ status: 500, body: { error: 'Vorschau kaputt.' } }) }, async (fx) => { + const w = await zeigeNeu(fx); assert.match(fx.text(w), /Vorschau kaputt\./); assert.equal(fx.finde(w, '#switch-buy').hasAttribute('disabled'), true) + fx.tabelle['POST /api/abo/switch/preview'] = VORSCHAU; fx.tabelle['POST /api/abo/switch'] = { paymentUrl: 'https://pay.stripe.com/y', subscription: abo() } + const v = await zeigeNeu(fx); await fx.klick(fx.finde(v, '#switch-terms')); await fx.klick(fx.finde(v, '#switch-early')); await fx.klick(fx.finde(v, '#switch-buy')); await fx.ruhe() + assert.equal(fx.finde(v, '#switch-buy').hasAttribute('disabled'), true) // wartet auf die Zahlung + fx.window.dispatchEvent({ type: 'pageshow', persisted: true }); await fx.ruhe() + assert.equal(fx.finde(v, '#switch-buy').hasAttribute('disabled'), false); assert.equal(fx.finde(v, '#switch-buy').textContent, 'Zahlungspflichtig bestellen') + const vorher = fx.fetchLog.length; const a = await zeigeNeu(fx, HOSTING, me({ viewAs: true })); await fx.klick(fx.finde(a, '#switch-buy')) + assert.deepEqual(fx.fetchLog.slice(vorher).filter((x) => x.url === '/api/abo/switch'), []) +})) +test('Wechsel: unsichere Zahlungsadresse wird nicht geöffnet und nicht als Erfolg gezeigt', mit({ 'POST /api/abo/switch/preview': VORSCHAU, 'POST /api/abo/switch': () => ({ body: { paymentUrl: 'javascript:alert(1)', subscription: abo() } }) }, async (fx) => { + nimmHinweis(); const w = await zeigeNeu(fx); await fx.klick(fx.finde(w, '#switch-terms')); await fx.klick(fx.finde(w, '#switch-early')); await fx.klick(fx.finde(w, '#switch-buy')); await fx.ruhe() + assert.notEqual(fx.window.location.href, 'javascript:alert(1)'); assert.match(fx.text(w), /Die Zahlungsseite konnte nicht geöffnet werden\./); assert.equal(nimmHinweis(), '') +}))