import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync, writeFileSync, mkdtempSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { pathToFileURL } from 'node:url' import { aboMeldung } from '../server/services/featureOrder.js' import { featureConfig } from '../server/services/featureRegistry.js' // public/funktionen.js wirklich laden (die absoluten Importe /analyse-view.js und /geraete.js werden auf die Dateien umgeleitet) const dir = mkdtempSync(join(tmpdir(), 'fn-tiles-')) const fileUrl = (name) => pathToFileURL(new URL(`../public/${name}`, import.meta.url).pathname).href const src = readFileSync(new URL('../public/funktionen.js', import.meta.url), 'utf8') .replace("'/analyse-view.js'", `'${fileUrl('analyse-view.js')}'`) .replace("'/geraete.js'", `'${fileUrl('geraete.js')}'`) const file = join(dir, 'funktionen.mjs') writeFileSync(file, src) const { initFunktionen, initNews, initAnalyse, renderAnalyseOff } = await import(pathToFileURL(file).href) const news = { state: 'off' } const project = (id, analyse) => ({ projectId: id, projectName: `P ${id}`, liveUrl: '', features: { news }, analyse }) async function render(projects) { globalThis.fetch = async () => ({ ok: true, status: 200, json: async () => ({ projects }) }) const root = { innerHTML: '', querySelector: () => null } await initFunktionen(root) return root.innerHTML } const tile = (html, key) => html.slice(html.indexOf(`data-fn-tile="${key}"`), html.indexOf('', html.indexOf(`data-fn-tile="${key}"`))) test('Kacheln rendern ohne Fehler: News, Analyse comingSoon/aus/aktiv/gesperrt', async () => { const soon = await render([project('a', { comingSoon: true })]) assert.match(tile(soon, 'news'), /News-Pinnwand/) assert.match(soon, /Kommt bald/) assert.doesNotMatch(soon, /data-fn-tile="analyse"/) const off = await render([project('a', { state: 'off', eligible: true, devMode: false })]) assert.match(tile(off, 'analyse'), /Einrichten/) assert.doesNotMatch(off, /Dev-Modus/) const dev = await render([project('a', { state: 'active', eligible: true, devMode: true })]) assert.match(tile(dev, 'analyse'), /Auswertung ansehen/) assert.match(tile(dev, 'analyse'), /Im Dev-Modus wird nicht gemessen/) assert.doesNotMatch(tile(dev, 'news'), /Dev-Modus/) const live = await render([project('a', { state: 'active', eligible: true, devMode: false })]) assert.match(tile(live, 'analyse'), /Auswertung ansehen/) assert.doesNotMatch(live, /Dev-Modus/) const disabled = await render([project('a', { state: 'disabled' })]) assert.match(tile(disabled, 'analyse'), /Bitte melde dich bei uns/) assert.doesNotMatch(disabled, /Dev-Modus/) }) // ---- News-Kachel und -Buchungsseite: Preis und Zustimmungstext aus dem Register (features.news), Rückfall ohne features async function renderMit(projects, extra) { globalThis.fetch = async () => ({ ok: true, status: 200, json: async () => ({ projects, ...extra }) }) const root = { innerHTML: '', querySelector: () => null } await initFunktionen(root) return root.innerHTML } test('News-Kachel: Rückfall ohne features ⇒ bisheriger Preis 30 €', async () => { const html = await render([project('a', { comingSoon: true })]) assert.match(tile(html, 'news'), /Einrichten lassen · 30 €/) // auch bei leerem oder unvollständigem features-Objekt for (const features of [{}, { news: {} }, { news: { priceLabel: '' } }, { news: { priceLabel: 31 } }, { termine: { priceLabel: '149 €' } }]) { const rueck = await renderMit([project('a', { comingSoon: true })], { features }) assert.match(tile(rueck, 'news'), /Einrichten lassen · 30 €/, JSON.stringify(features)) } }) test('News-Kachel: Preis aus dem Register ⇒ „Einrichten lassen · 31 €“', async () => { const html = await renderMit([project('a', { comingSoon: true })], { features: { news: { priceLabel: '31 €' } } }) assert.match(tile(html, 'news'), /Einrichten lassen · 31 €/) assert.doesNotMatch(tile(html, 'news'), /30 €/) // der Preis kommt maskiert in die Kachel const bose = await renderMit([project('a', { comingSoon: true })], { features: { news: { priceLabel: '' } } }) assert.doesNotMatch(bose, / { calls.push({ url: String(url), method: opts?.method, body: opts?.body ? JSON.parse(opts.body) : undefined }) if (String(url) === '/api/funktionen/news/checkout') return { ok: true, status: 200, json: async () => ({ url: 'https://checkout.example.test/s1' }) } return { ok: true, status: 200, json: async () => antwort } } const stage = fakeStage() const root = fakeEl() root.querySelector = (sel) => (sel === '#news-stage' ? stage : null) try { await initNews(root, { projectId: 'p1' }) // Haken setzen und bestellen const box = stage.els['#news-consent'] box.checked = true box.handlers.change() const button = stage.els['#news-order'] const knopfAktiv = button.disabled === false await button.handlers.click() return { stage, calls, knopfAktiv, href: globalThis.window.location.href } } finally { delete globalThis.window } } test('News-Buchungsseite: Preis, Knopf und Zustimmungstext aus features.news, sendet weiter consent: true', async () => { const register = { priceLabel: '31 €', consent: { version: 'v', knopf: 'Jetzt kaufen · 31 €', items: [{ id: 'beginn', text: 'Registertext der Zustimmung', haken: true }] } } const { stage, calls, knopfAktiv, href } = await renderBuchung({ projects: [freiesProjekt], setupEstimate: 'meist innerhalb von 1–2 Wochen', consentText: 'Alter Text', features: { news: register }, }) assert.match(stage.innerHTML, /31 €<\/strong>/) assert.match(stage.innerHTML, /inklusive Mehrwertsteuer/, 'der Wortlaut bleibt bis Block F') assert.match(stage.innerHTML, /id="news-order" disabled>Jetzt kaufen · 31 €) assert.equal(stage.els['#news-consent-text'].textContent, 'Registertext der Zustimmung') assert.equal(knopfAktiv, true) const bestellung = calls.filter((c) => c.url === '/api/funktionen/news/checkout') assert.equal(bestellung.length, 1) assert.equal(bestellung[0].method, 'POST') assert.deepEqual(bestellung[0].body, { projectId: 'p1', consent: true }) assert.equal(href, 'https://checkout.example.test/s1') }) test('News-Buchungsseite: Rückfall ohne features ⇒ 30 €, Standardknopf, consentText der Antwort', async () => { const { stage, calls } = await renderBuchung({ projects: [freiesProjekt], setupEstimate: '', consentText: 'Alter Text' }) assert.match(stage.innerHTML, /30 €<\/strong>/) assert.match(stage.innerHTML, /id="news-order" disabled>Zahlungspflichtig bestellen · 30 €) assert.equal(stage.els['#news-consent-text'].textContent, 'Alter Text') assert.deepEqual(calls.find((c) => c.url === '/api/funktionen/news/checkout').body, { projectId: 'p1', consent: true }) }) test('News-Buchungsseite: Preis ohne Knopftext ⇒ Standardknopf mit Registerpreis, Knopftext kommt maskiert', async () => { const nurPreis = await renderBuchung({ projects: [freiesProjekt], consentText: 'Alter Text', features: { news: { priceLabel: '31 €' } } }) assert.match(nurPreis.stage.innerHTML, /id="news-order" disabled>Zahlungspflichtig bestellen · 31 €) assert.equal(nurPreis.stage.els['#news-consent-text'].textContent, 'Alter Text') const bose = await renderBuchung({ projects: [freiesProjekt], features: { news: { priceLabel: '31 €', consent: { knopf: '', items: [] } } } }) assert.doesNotMatch(bose.stage.innerHTML, / { assert.match(src, /request\('\/api\/funktionen\/news\/checkout',\s*\{[^}]*body:\s*\{[^}]*consent: true/) }) // ---- Terminplaner-Kachel: nur mit Schalter (termine.verfuegbar), Zustand je Projekt aus features.termine const withTermine = (id, state, extra = {}) => ({ projectId: id, projectName: `P ${id}`, liveUrl: '', features: { news, termine: { state, ...extra } } }) const noAnalyse = { comingSoon: true } // Standardantwort bei verfuegbar: true wie GET /api/funktionen (Kunde mit Wartungs-Abo); abo ueberschreibt einzelne Felder const ABO_OK = { bestellbar: true, grund: null, preis: '149 €', einrichtung: 'meist innerhalb von 1–2 Wochen' } async function renderTermine(projects, { verfuegbar, anfragen = {}, abo = {} } = {}) { const calls = [] globalThis.fetch = async (url) => { calls.push(String(url)) if (String(url).startsWith('/api/termine/einstellungen')) { const id = new URL(url, 'http://x').searchParams.get('projectId') return { ok: true, status: 200, json: async () => ({ anfragenOffen: anfragen[id] ?? 0 }) } } const termine = verfuegbar === undefined ? {} : { termine: verfuegbar ? { verfuegbar, ...ABO_OK, ...abo } : { verfuegbar } } return { ok: true, status: 200, json: async () => ({ projects, ...termine }) } } const count = { textContent: '', hidden: true } const root = { innerHTML: '', querySelector: (sel) => (sel === '[data-termine-count]' ? count : null) } await initFunktionen(root) return { html: root.innerHTML, count, calls } } test('Terminplaner-Kachel: fehlt termine.verfuegbar oder ist false ⇒ „Kommt bald“ ohne Knopf', async () => { for (const verfuegbar of [undefined, false]) { const { html, calls } = await renderTermine([{ ...withTermine('a', 'active'), analyse: noAnalyse }], { verfuegbar }) assert.doesNotMatch(html, /data-fn-tile="termine"/) assert.match(html, /fn-tile-soon[\s\S]*Terminplaner/) assert.match(html, /Kommt bald/) assert.doesNotMatch(html, /data-view="termine"/) assert.deepEqual(calls, ['/api/funktionen'], 'ohne Schalter keine Abfrage an /api/termine') } }) // Die Abo-Hinweise der Kachel kommen aus aboMeldung (server/services/featureOrder.js, einzige Quelle der Abo-Hinweistexte); // die Kachel steht im Browser und kennt den Text nur als Kopie: dieser Test haelt beide gleich. const TERMINE_REGISTER = featureConfig('termine') const KEIN_ABO_HINWEIS = /Im Wartungs-Abo enthalten\. Zu Verträge<\/button>/ const knopfDerKachel = (kachel) => kachel.match(/]*fn-tile-cta[^>]*>([^<]*)<\/button>/) test('Terminplaner-Kachel, Tabelle: jede Zeile mit Pille, Pillenklasse, Knopftext und Ziel', async () => { const zeilen = [ // [Name, Projekte, Abo-Felder, Pillenklasse, Pille, Knopf, data-view] ['Aus mit Abo', [withTermine('a', 'off')], {}, 'off', 'Aus', 'Terminplaner einrichten · 149 €', 'termine'], ['Aus mit Abo, Entwurf (Dokument und siteKey)', [withTermine('a', 'off', { siteKey: 'abcdefghijklmnop' })], {}, 'off', 'Aus', 'Fragebogen fortsetzen', 'termine'], ['Aus, Projekt ohne Funktionsdokument (kein features.termine)', [{ ...project('a', noAnalyse), features: { news } }], {}, 'off', 'Aus', 'Terminplaner einrichten · 149 €', 'termine'], ['Zahlung offen', [withTermine('a', 'ordered')], {}, 'setup', 'Zahlung offen', 'Status ansehen', 'termine'], ['In Einrichtung', [withTermine('a', 'setup')], {}, 'setup', 'In Einrichtung', 'Status ansehen', 'termine'], ['Aktiv', [withTermine('a', 'active')], {}, 'on', 'Aktiv', 'Termine öffnen', 'termine'], ['Abgeschaltet', [withTermine('a', 'disabled')], {}, 'off', 'Abgeschaltet', 'Termine ansehen', 'termine'], // das Abo betrifft nur die Bestellung: wer schon bestellt hat oder nutzt, behaelt seinen Weg in die Ansicht ['Zahlung offen, Abo gekündigt', [withTermine('a', 'ordered')], { bestellbar: false, grund: 'gekuendigt' }, 'setup', 'Zahlung offen', 'Status ansehen', 'termine'], ['In Einrichtung, Abo nicht lesbar', [withTermine('a', 'setup')], { bestellbar: null, grund: null }, 'setup', 'In Einrichtung', 'Status ansehen', 'termine'], ['Aktiv, Abo gekündigt', [withTermine('a', 'active')], { bestellbar: false, grund: 'gekuendigt' }, 'on', 'Aktiv', 'Termine öffnen', 'termine'], ['Abgeschaltet, Abo nicht aktiv', [withTermine('a', 'disabled')], { bestellbar: false, grund: 'nicht_aktiv' }, 'off', 'Abgeschaltet', 'Termine ansehen', 'termine'], ] for (const [name, projects, abo, klasse, pille, knopfText, ziel] of zeilen) { const { html } = await renderTermine(projects, { verfuegbar: true, abo }) const kachel = tile(html, 'termine') assert.ok(kachel.includes(`${pille}`), `${name}: Pille ${pille} (${klasse}) in ${kachel}`) const knopf = knopfDerKachel(kachel) assert.ok(knopf, `${name}: Knopf`) assert.equal(knopf[1], knopfText, name) assert.ok(knopf[0].includes(`data-view="${ziel}" data-project="a"`), `${name}: Ziel ${knopf[0]}`) assert.doesNotMatch(kachel, /Wir schalten den Terminplaner für dich frei/, name) assert.doesNotMatch(kachel, /data-view="chat"/, `${name}: kein Chat-Knopf mehr`) } }) test('Terminplaner-Kachel: Aus ohne Abo ⇒ kein Knopf und kein Weg in die Ansicht, Hinweis je Grund', async () => { const hinweise = [ ['kein_abo', { bestellbar: false, grund: 'kein_abo' }, KEIN_ABO_HINWEIS], ['gekuendigt', { bestellbar: false, grund: 'gekuendigt' }, new RegExp(`${aboMeldung(TERMINE_REGISTER, 'gekuendigt')}`)], ['nicht_aktiv', { bestellbar: false, grund: 'nicht_aktiv' }, new RegExp(aboMeldung(TERMINE_REGISTER, 'nicht_aktiv').replace(/[()]/g, '\\$&'))], ['null (Lesefehler)', { bestellbar: null, grund: null }, /Das Abo konnte gerade nicht geprüft werden\./], ['Feld fehlt (ältere Antwort)', { bestellbar: undefined, grund: undefined }, /Das Abo konnte gerade nicht geprüft werden\./], ['unbekannter Grund', { bestellbar: false, grund: 'etwas_neues' }, /Das Abo konnte gerade nicht geprüft werden\./], ] // mit und ohne Entwurf: auch ein vorhandener Entwurf oeffnet ohne Abo keinen Weg for (const projekte of [[withTermine('a', 'off')], [withTermine('a', 'off', { siteKey: 'abcdefghijklmnop' })]]) { for (const [name, abo, hinweis] of hinweise) { const { html, calls } = await renderTermine(projekte, { verfuegbar: true, abo }) const kachel = tile(html, 'termine') assert.match(kachel, /fn-pill-off">Aus, name) assert.match(kachel, hinweis, `${name}: ${kachel}`) assert.doesNotMatch(kachel, /data-view="termine"/, name) assert.doesNotMatch(kachel, /fn-tile-cta/, `${name}: kein Knopf`) assert.doesNotMatch(html, /data-view="termine"/, `${name}: nirgends ein Weg in die Ansicht`) assert.deepEqual(calls, ['/api/funktionen'], name) } } // nur kein_abo verweist auf die Vertraege; die uebrigen Hinweise sind reiner Text const gekuendigt = tile((await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { bestellbar: false, grund: 'gekuendigt' } })).html, 'termine') assert.doesNotMatch(gekuendigt, /data-view=/) }) test('Terminplaner-Kachel: die Abo-Hinweise stimmen wortgleich mit aboMeldung überein (gekündigt, nicht aktiv, nicht lesbar)', async () => { for (const grund of ['gekuendigt', 'nicht_aktiv']) { const { html } = await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { bestellbar: false, grund } }) assert.ok(tile(html, 'termine').includes(`${aboMeldung(TERMINE_REGISTER, grund)}`), grund) } const { html } = await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { bestellbar: null, grund: null } }) assert.ok(tile(html, 'termine').includes(`${aboMeldung(TERMINE_REGISTER, 'unbekannt')}`)) assert.equal(aboMeldung(TERMINE_REGISTER, 'unbekannt'), 'Das Abo konnte gerade nicht geprüft werden.') }) test('Terminplaner-Kachel: ordered und setup fragen /api/termine nicht nach einer Zahl (nur aktive Websites)', async () => { for (const state of ['ordered', 'setup']) { const { html, calls, count } = await renderTermine([withTermine('a', state)], { verfuegbar: true }) assert.deepEqual(calls, ['/api/funktionen'], state) assert.doesNotMatch(tile(html, 'termine'), /data-termine-count/, state) assert.equal(count.textContent, '') } }) test('Terminplaner-Kachel: Preis kommt aus der Antwort und maskiert; ohne Preis steht der Knopf ohne Zahl', async () => { const mit = await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { preis: '150 €' } }) assert.equal(knopfDerKachel(tile(mit.html, 'termine'))[1], 'Terminplaner einrichten · 150 €') const bose = await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { preis: '' } }) assert.doesNotMatch(bose.html, / { const { html } = await renderTermine([], { verfuegbar: true }) const kachel = tile(html, 'termine') assert.match(kachel, /fn-pill-off">Aus) assert.doesNotMatch(kachel, /fn-tile-cta|data-view=/) assert.match(kachel, /Sobald deine Website angelegt ist/) }) test('Terminplaner-Kachel: Aktiv ⇒ „Termine öffnen“ mit Projekt, Anfragen aus anfragenOffen', async () => { const { html, count, calls } = await renderTermine([withTermine('a', 'off'), withTermine('b', 'active')], { verfuegbar: true, anfragen: { b: 3 } }) const kachel = tile(html, 'termine') assert.match(kachel, /fn-pill-on">Aktiv) assert.match(kachel, /data-view="termine" data-project="b"[^>]*>Termine öffnen) assert.deepEqual(calls, ['/api/funktionen', '/api/termine/einstellungen?projectId=b'], 'nur aktive Projekte werden gefragt') assert.equal(count.textContent, '3 Anfragen warten') assert.equal(count.hidden, false) }) test('Terminplaner-Kachel: Anfragen zählen über alle aktiven Websites, 1 ⇒ Einzahl, 0 ⇒ nichts', async () => { const zwei = await renderTermine([withTermine('a', 'active'), withTermine('b', 'active')], { verfuegbar: true, anfragen: { a: 1, b: 2 } }) assert.equal(zwei.count.textContent, '3 Anfragen warten') const eine = await renderTermine([withTermine('a', 'active')], { verfuegbar: true, anfragen: { a: 1 } }) assert.equal(eine.count.textContent, '1 Anfrage wartet') const keine = await renderTermine([withTermine('a', 'active')], { verfuegbar: true, anfragen: {} }) assert.equal(keine.count.textContent, '') assert.equal(keine.count.hidden, true) }) test('Terminplaner-Kachel: Fehler bei der Anfragen-Zahl lässt die Kachel stehen', async () => { globalThis.fetch = async (url) => { if (String(url).startsWith('/api/termine/')) throw new Error('offline') return { ok: true, status: 200, json: async () => ({ projects: [withTermine('a', 'active')], termine: { verfuegbar: true } }) } } const count = { textContent: '', hidden: true } const root = { innerHTML: '', querySelector: (sel) => (sel === '[data-termine-count]' ? count : null) } await initFunktionen(root) assert.match(tile(root.innerHTML, 'termine'), /Termine öffnen/) assert.equal(count.textContent, '') assert.equal(count.hidden, true) }) test('Terminplaner-Kachel: Abgeschaltet ⇒ Pille, Hinweis, Termine nur ansehen', async () => { const { html, calls } = await renderTermine([withTermine('a', 'disabled')], { verfuegbar: true }) const kachel = tile(html, 'termine') assert.match(kachel, /fn-pill-off">Abgeschaltet) assert.match(kachel, /abgeschaltet/) assert.match(kachel, /data-view="termine" data-project="a"[^>]*>Termine ansehen) assert.deepEqual(calls, ['/api/funktionen'], 'abgeschaltet: keine Anfragen-Zahl') }) test('Terminplaner-Kachel: mehrere Websites, Hinweis „aktiv für n von m“, Aktive zuerst', async () => { const { html } = await renderTermine([withTermine('a', 'disabled'), withTermine('b', 'off'), withTermine('c', 'active')], { verfuegbar: true }) const kachel = tile(html, 'termine') assert.match(kachel, /aktiv für 1 von 3 Websites/) assert.match(kachel, /data-project="c"/) }) test('Terminplaner-Kachel: Zielprojekt = aktiv, dann ordered/setup, dann abgeschaltet, dann Entwurf, dann das erste', async () => { const entwurf = { siteKey: 'abcdefghijklmnop' } const faelle = [ ['aktiv vor allem', [withTermine('a', 'off', entwurf), withTermine('b', 'disabled'), withTermine('c', 'setup'), withTermine('d', 'ordered'), withTermine('e', 'active')], 'e', 'Aktiv', 'Termine öffnen'], ['ordered/setup vor abgeschaltet und Entwurf (setup zuerst in der Liste)', [withTermine('a', 'off', entwurf), withTermine('b', 'disabled'), withTermine('c', 'setup'), withTermine('d', 'ordered')], 'c', 'In Einrichtung', 'Status ansehen'], ['ordered/setup vor abgeschaltet und Entwurf (ordered zuerst in der Liste)', [withTermine('a', 'off', entwurf), withTermine('b', 'disabled'), withTermine('c', 'ordered'), withTermine('d', 'setup')], 'c', 'Zahlung offen', 'Status ansehen'], ['abgeschaltet vor Entwurf', [withTermine('a', 'off', entwurf), withTermine('b', 'off'), withTermine('c', 'disabled')], 'c', 'Abgeschaltet', 'Termine ansehen'], ['Entwurf vor dem ersten', [withTermine('a', 'off'), withTermine('b', 'off'), withTermine('c', 'off', entwurf)], 'c', 'Aus', 'Fragebogen fortsetzen'], ['sonst das erste', [withTermine('a', 'off'), withTermine('b', 'off')], 'a', 'Aus', 'Terminplaner einrichten · 149 €'], ] for (const [name, projekte, ziel, pille, knopf] of faelle) { const { html } = await renderTermine(projekte, { verfuegbar: true }) const kachel = tile(html, 'termine') assert.ok(kachel.includes(`data-view="termine" data-project="${ziel}"`), `${name}: Ziel ${ziel} in ${kachel}`) assert.match(kachel, new RegExp(`${pille}`), name) assert.equal(knopfDerKachel(kachel)[1], knopf, name) } }) test('Terminplaner-Kachel: mehrere Websites ohne Abo ⇒ die Pille bleibt „Aus“, nirgends ein Knopf (auch mit Entwurf)', async () => { const { html } = await renderTermine([withTermine('a', 'off'), withTermine('b', 'off', { siteKey: 'abcdefghijklmnop' })], { verfuegbar: true, abo: { bestellbar: false, grund: 'kein_abo' } }) const kachel = tile(html, 'termine') assert.match(kachel, /fn-pill-off">Aus) assert.doesNotMatch(kachel, /fn-tile-cta|data-view="termine"/) assert.match(kachel, KEIN_ABO_HINWEIS) }) test('Terminplaner-Kachel: Projektname und Zustand kommen nur maskiert in die Kachel', async () => { const { html } = await renderTermine([{ ...withTermine('a', 'active'), projectName: '' }], { verfuegbar: true }) assert.doesNotMatch(html, /', state, { siteKey: 'abcdefghijklmnop' }), projectName: '' }], { verfuegbar: true }) assert.doesNotMatch(bose.html, / null, querySelectorAll: () => [] } } function fakeStage() { const els = {} const get = (sel) => (els[sel] ||= fakeEl()) return { els, innerHTML: '', querySelector: get, querySelectorAll: () => [] } } const offProject = (analyse) => ({ projectId: 'p1', projectName: 'Muster', analyse: { state: 'off', eligible: true, accentColor: '#111111', avvVersion: '2026-10-02', ...analyse } }) const LEGAL = { avv: '/rechtliches/avv-website-analyse-2026-10-02.html', lb: '/rechtliches/leistungsbeschreibung-website-analyse-2026-10-02.html' } const CTRL = { ok: true, company: 'Muster GmbH', address: 'Weg 1, 12345 Ort', email: 'info@muster.de' } async function renderActivation(analyse) { globalThis.fetch = async () => ({ ok: true, status: 200, json: async () => ({ projects: [offProject(analyse)] }) }) const root = fakeEl() const stage = fakeStage() root.querySelector = (sel) => (sel === '#an-stage' ? stage : null) await initAnalyse(root) return stage } test('Einschalten: zwei Links aus legal, Fassung, Verantwortlicher, Kinder-Satz, Banner-Vorschau', async () => { const stage = await renderActivation({ legal: LEGAL, controller: CTRL }) const html = stage.innerHTML assert.ok(html.includes(`href="${LEGAL.avv}"`)) assert.ok(html.includes(`href="${LEGAL.lb}"`)) assert.match(html, /Fassung 2026-10-02/) assert.match(html, /Muster GmbH/) assert.match(html, /info@muster\.de/) assert.match(html, /Stimmt etwas nicht\? Schreib uns im Chat, bevor Du einschaltest\./) assert.match(html, /nicht gezielt an Kinder unter 16 Jahren/) assert.match(html, /Ohne Zustimmung der Besucher zählen wir nur Seitenaufrufe mit Land und Geräteklasse/) assert.doesNotMatch(html, /anonym/) const preview = stage.els['#an-preview'].innerHTML assert.match(preview, /Ablehnen/) assert.match(preview, /Annehmen/) assert.match(preview, /Datenschutz-Einstellungen/) // Haken schaltet den Knopf frei const box = stage.els['#an-avv'] box.checked = true box.handlers.change() assert.equal(stage.els['#an-on'].disabled, false) }) test('Einschalten: Firma und Anschrift werden maskiert', async () => { const stage = await renderActivation({ legal: LEGAL, controller: { ...CTRL, company: 'A&Co' } }) assert.match(stage.innerHTML, /A<b>&Co/) assert.doesNotMatch(stage.innerHTML, /A/) }) test('Einschalten: ohne Verantwortlichen-Angaben gesperrt, Hinweis sichtbar, kein Schreib-uns-Hinweis der Vorschau', async () => { const stage = await renderActivation({ legal: LEGAL, controller: { ok: false, company: '', address: '', email: '', missing: ['firma'] } }) assert.match(stage.innerHTML, /fehlen in Deinen Kundendaten Firma oder Name und eine gültige E-Mail-Adresse/) assert.doesNotMatch(stage.innerHTML, /bevor Du einschaltest/) const box = stage.els['#an-avv'] box.checked = true box.handlers.change() assert.equal(stage.els['#an-on'].disabled, true) }) test('Ausschalten: Hinweis vorher, nach "Ja, ausschalten" Bestätigung, erst "OK" lädt neu', async () => { const calls = [] globalThis.fetch = async (url) => { calls.push(url); return { ok: true, status: 200, json: async () => ({}) } } const box = fakeEl() const actions = fakeEl() const yes = fakeEl() const no = fakeEl() const ok = fakeEl() const ask = fakeEl() box.querySelector = (sel) => ({ '#an-off-actions': actions, '#an-off-ask': ask, '#an-off-ok': ok, '#an-off-error': fakeEl() })[sel] actions.querySelector = (sel) => ({ '#an-off-yes': yes, '#an-off-no': no })[sel] let reloads = 0 renderAnalyseOff(box, 'p1', () => { reloads++ }) assert.match(box.innerHTML, /Skript und Zusatzseite entfernen wir mit der nächsten Veröffentlichung Deiner Website/) assert.match(box.innerHTML, /bis zum 46\. Tag kannst Du eine Kopie anfordern/) ask.handlers.click() await yes.handlers.click({ currentTarget: yes }) assert.deepEqual(calls, ['/api/funktionen/analyse/deactivate']) assert.match(box.innerHTML, /Die Website-Analyse ist ausgeschaltet\. Ab jetzt werden keine Daten mehr gespeichert\./) assert.equal(reloads, 0) ok.handlers.click() assert.equal(reloads, 1) })
${aboMeldung(TERMINE_REGISTER, grund)}
${aboMeldung(TERMINE_REGISTER, 'unbekannt')}