import { test, after } from 'node:test' import assert from 'node:assert/strict' import { analyseSel } from '../public/analyse-view.js' import { KEIN_PREIS, htmlWurzel } from './support/modulAbo.js' // Analyse im Abo-Modell (P4 Task 8): Linkzeile A1 und Zielgruppen-Häkchen A2 statt des langen Zustimmungssatzes, Text A3, audienceConfirmed im // Körper, die Reiter Übersicht, Heatmap und Einstellungen in der Etage (kn:reiter). Ohne modell === 'abo' bleibt alles wie heute. const { initAnalyse } = await import('../public/analyse.js') globalThis.document = { querySelectorAll: () => [] } // pickTab ruft hideChartTip after(() => { delete globalThis.document }) 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' } const offProject = (id = 'p1', analyse = {}) => ({ projectId: id, projectName: `Muster ${id}`, analyse: { state: 'off', eligible: true, accentColor: '#111111', avvVersion: '2026-10-02', legal: LEGAL, controller: CTRL, ...analyse } }) const aktivProjekt = (id = 'p1') => offProject(id, { state: 'active' }) const KEIN_ABO = { eligible: false, reason: 'kein_abo', reasonText: 'Alter Satz des Servers' } async function lauf({ modell, projects = [offProject()], dataset = {}, projectId = 'p1', tab = 'uebersicht' } = {}) { const posts = [] globalThis.fetch = async (url, opts) => { if (opts?.method === 'POST') posts.push({ url: String(url), body: JSON.parse(opts.body) }) if (String(url).startsWith('/api/funktionen/analyse/stats')) return { ok: false, status: 503, json: async () => ({}) } // das Cockpit zeigt die Fehlerkarte; mehr braucht der Reitertest nicht return { ok: true, status: 200, json: async () => ({ ...(modell ? { modell } : {}), projects }) } } const { root, stage, kn } = htmlWurzel('#an-stage') Object.assign(root.dataset, dataset) analyseSel.tab = tab await initAnalyse(root, { projectId }) return { root, stage, posts, kn } } test('Abo: Linkzeile A1 mit beiden Links, ein Häkchen mit A2, kein langer Zustimmungssatz, keine Preise', async () => { const { stage } = await lauf({ modell: 'abo' }) const html = stage.innerHTML assert.match(html, /Es gelten ]*>Leistungsbeschreibung<\/a> und ]*>Auftragsverarbeitung<\/a> \(Fassung 2026-10-02\)\./) assert.ok(html.includes(`href="${LEGAL.lb}"`) && html.includes(`href="${LEGAL.avv}"`)) assert.match(html, /Meine Website richtet sich nicht gezielt an Kinder unter 16 Jahren\.<\/span>/) assert.doesNotMatch(html, /Ich beauftrage WEBklar|id="an-avv"/) assert.doesNotMatch(html, KEIN_PREIS) assert.match(html, /Muster GmbH/, 'Verantwortlicher und Banner-Vorschau bleiben') assert.match(html, /id="an-color"/) }) test('Abo: Knopf gesperrt ohne Zielgruppen-Häkchen und bei unvollständigem Verantwortlichen; Körper mit audienceConfirmed', async () => { const { stage, posts } = await lauf({ modell: 'abo' }) const box = stage.els['#an-zielgruppe'] const knopf = stage.els['#an-on'] box.checked = true box.handlers.change() assert.equal(knopf.disabled, false) box.checked = false box.handlers.change() assert.equal(knopf.disabled, true) box.checked = true box.handlers.change() await knopf.handlers.click() assert.deepEqual(posts, [{ url: '/api/funktionen/analyse/activate', body: { projectId: 'p1', accentColor: '#111111', consent: { accepted: true, avvVersion: '2026-10-02' }, audienceConfirmed: true } }]) const ohne = await lauf({ modell: 'abo', projects: [offProject('p1', { controller: { ok: false, company: '', address: '', email: '', missing: ['firma'] } })] }) ohne.stage.els['#an-zielgruppe'].checked = true ohne.stage.els['#an-zielgruppe'].handlers.change() assert.equal(ohne.stage.els['#an-on'].disabled, true) }) test('Abo: kein_abo nennt Hosting- und Wartungs-Abo mit Link zu Verträge (A3), nicht den Satz des Servers', async () => { const { stage } = await lauf({ modell: 'abo', projects: [offProject('p1', KEIN_ABO)] }) assert.match(stage.innerHTML, /Die Analyse ist im Hosting- und im Wartungs-Abo enthalten\./) assert.match(stage.innerHTML, /data-view="vertraege"/) assert.doesNotMatch(stage.innerHTML, /Alter Satz des Servers/) }) test('Abo: ein anderer Grund zeigt weiter den Satz des Servers, ohne Link', async () => { const { stage } = await lauf({ modell: 'abo', projects: [offProject('p1', { eligible: false, reason: 'keine_website', reasonText: 'Es fehlt die Website.' })] }) assert.match(stage.innerHTML, /Es fehlt die Website\./) assert.doesNotMatch(stage.innerHTML, /data-view="vertraege"/) }) test('Ohne modell (fehlt, „Abo“, „abo “): der lange Zustimmungssatz mit #an-avv, Körper ohne audienceConfirmed, bei kein_abo der Satz des Servers', async () => { for (const modell of [undefined, 'Abo', 'abo ']) { const a = await lauf({ modell }) assert.match(a.stage.innerHTML, /id="an-avv"/) assert.match(a.stage.innerHTML, /Ich beauftrage WEBklar/) assert.doesNotMatch(a.stage.innerHTML, /an-zielgruppe/) a.stage.els['#an-avv'].checked = true a.stage.els['#an-avv'].handlers.change() await a.stage.els['#an-on'].handlers.click() assert.deepEqual(a.posts[0].body, { projectId: 'p1', accentColor: '#111111', consent: { accepted: true, avvVersion: '2026-10-02' } }) const b = await lauf({ modell, projects: [offProject('p1', KEIN_ABO)] }) assert.match(b.stage.innerHTML, /Alter Satz des Servers/) } }) test('wunschProjekt aus dem Behälter wählt das Projekt und wird entfernt', async () => { const { root, stage } = await lauf({ modell: 'abo', projects: [offProject('p1'), offProject('p2')], dataset: { wunschProjekt: 'p2' }, projectId: '' }) assert.match(stage.innerHTML, /Analyse für Muster p2 einschalten/) assert.equal(root.dataset.wunschProjekt, undefined) }) test('Abo, aktive Analyse: keine eigene Leiste, kn:reiter mit den drei Reitern, Panel mit aria-label; bei() wechselt und meldet den neuen aktiven Reiter', async () => { assert.equal(typeof CustomEvent, 'function', 'meldeReiter braucht CustomEvent (Node 20: global)') const { root, stage, kn } = await lauf({ modell: 'abo', projects: [aktivProjekt()] }) assert.doesNotMatch(root.innerHTML, /role="tablist"|an-tabbtn|aria-labelledby/) assert.match(root.innerHTML, /id="an-stage" role="tabpanel" aria-label="Übersicht"/) const erst = kn.reiter.at(-1) assert.deepEqual(erst.eintraege, [{ key: 'uebersicht', label: 'Übersicht' }, { key: 'heatmap', label: 'Heatmap' }, { key: 'einstellungen', label: 'Einstellungen' }]) assert.equal(erst.aktiv, 'uebersicht') erst.bei('heatmap') await new Promise((r) => setImmediate(r)) assert.equal(kn.reiter.at(-1).aktiv, 'heatmap') assert.equal(stage.attribute['aria-label'], 'Heatmap') assert.equal(analyseSel.tab, 'heatmap') }) test('Abo, aktive Analyse mit gemerktem Reiter: das Panel nennt ihn, die Meldung nennt ihn als aktiv', async () => { const { root, kn } = await lauf({ modell: 'abo', projects: [aktivProjekt()], tab: 'heatmap' }) assert.match(root.innerHTML, /id="an-stage" role="tabpanel" aria-label="Heatmap"/) assert.equal(kn.reiter.at(-1).aktiv, 'heatmap') }) test('Abo ohne Cockpit (Einschalten, abgeschaltet, nicht verfügbar, keine Website): leere Reiterliste, „Zurück“ füllt die Zeile', async () => { for (const projects of [[offProject()], [offProject('p1', { state: 'disabled' })], [offProject('p1', KEIN_ABO)], []]) { const { kn } = await lauf({ modell: 'abo', projects }) assert.deepEqual(kn.reiter.at(-1)?.eintraege, [], JSON.stringify(projects.map((p) => p.analyse.state))) } }) test('Ohne modell (fehlt, „Abo“): eigene Leiste mit denselben drei Labels, aria-labelledby, kein kn:reiter', async () => { for (const modell of [undefined, 'Abo']) { const { root, kn } = await lauf({ modell, projects: [aktivProjekt()] }) assert.deepEqual([...root.innerHTML.matchAll(/role="tab"[^>]*>([^<]+) m[1]), ['Übersicht', 'Heatmap', 'Einstellungen']) assert.match(root.innerHTML, /role="tablist"/) assert.match(root.innerHTML, /id="an-stage" role="tabpanel" aria-labelledby="an-tab-uebersicht"/) assert.equal(kn.reiter.length, 0) } })