Files
Webklar-Kundenbereich/test/funktionenTiles.test.js
T

236 lines
12 KiB
JavaScript

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'
// public/funktionen.js wirklich laden (die absoluten Importe /fn-util.js und /analyse-view.js werden auf die Dateien umgeleitet);
// analyse.js importiert relativ und wird direkt geladen
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("'/fn-util.js'", `'${fileUrl('fn-util.js')}'`)
.replace("'/analyse-view.js'", `'${fileUrl('analyse-view.js')}'`)
const file = join(dir, 'funktionen.mjs')
writeFileSync(file, src)
const { initFunktionen } = await import(pathToFileURL(file).href)
const { initAnalyse, renderAnalyseOff } = await import(fileUrl('analyse.js'))
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('</article>', 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/)
})
// ---- Terminplaner-Kachel: nur mit Schalter (termine.verfuegbar), Zustand je Projekt aus features.termine
const withTermine = (id, state) => ({ projectId: id, projectName: `P ${id}`, liveUrl: '', features: { news, termine: { state } } })
const noAnalyse = { comingSoon: true }
async function renderTermine(projects, { verfuegbar, anfragen = {} } = {}) {
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 }) }
}
return { ok: true, status: 200, json: async () => ({ projects, ...(verfuegbar === undefined ? {} : { termine: { verfuegbar } }) }) }
}
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')
}
})
test('Terminplaner-Kachel: Aus ⇒ „Wir schalten den Terminplaner für dich frei.“ + Chat', async () => {
for (const projects of [[{ ...project('a', noAnalyse), features: { news } }], [withTermine('a', 'off')], [withTermine('a', 'ordered')]]) {
const { html } = await renderTermine(projects, { verfuegbar: true })
const kachel = tile(html, 'termine')
assert.match(kachel, /Terminplaner/)
assert.match(kachel, /fn-pill-off">Aus</)
assert.match(kachel, /Wir schalten den Terminplaner für dich frei\./)
assert.match(kachel, /data-view="chat"[^>]*>Chat öffnen</)
assert.doesNotMatch(kachel, /data-view="termine"/)
}
})
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: Projektname und Zustand kommen nur maskiert in die Kachel', async () => {
const { html } = await renderTermine([{ ...withTermine('a<b>', 'active'), projectName: '<img src=x onerror=1>' }], { verfuegbar: true })
assert.doesNotMatch(html, /<img/)
assert.match(tile(html, 'termine'), /data-project="a&lt;b&gt;"/)
})
// ---- Analyse-Einschalten und Ausschalten (ohne DOM: Attrappen, die innerHTML und Klick-Handler festhalten)
function fakeEl() {
const handlers = {}
return { innerHTML: '', disabled: false, checked: false, value: '', dataset: {}, classList: { add() {}, remove() {}, toggle() {} }, handlers, addEventListener(ev, fn) { handlers[ev] = fn }, querySelector: () => 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<b>&Co' } })
assert.match(stage.innerHTML, /A&lt;b&gt;&amp;Co/)
assert.doesNotMatch(stage.innerHTML, /A<b>/)
})
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)
})