274 lines
17 KiB
JavaScript
274 lines
17 KiB
JavaScript
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { installFakeDom } from './support/fakeDom.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')
|
|
const FRIST = '2026-10-30T22:59:59.999Z'
|
|
const OK = { feature: { state: 'setup' }, ticketWoid: '100301' }
|
|
const mit = (fn) => async () => { const fx = installFakeDom(); try { await fn(fx) } finally { fx.restore() } }
|
|
function seite(fx, { antwort = () => OK, ...opt } = {}) {
|
|
const b = fx.document.createElement('div'); fx.document.body.append(b)
|
|
const aufrufe = []; const rueck = []
|
|
const api = async (pfad, o = {}) => { const a = { pfad, method: o.method ?? 'GET', body: o.body }; aufrufe.push(a); return antwort(a) }
|
|
const bei = { fertig: (a) => rueck.push(['fertig', a]), weiter: (w) => rueck.push(['weiter', w]), abbrechen: () => rueck.push(['abbrechen']) }
|
|
const el = renderEinschalten(b, { key: 'news', projectId: 'p1', daten: daten(), kundenartWirksam: 'unternehmen', jetzt: JETZT, api, bei, ...opt })
|
|
return { b, el, aufrufe, rueck }
|
|
}
|
|
const knopf = (fx, b) => fx.finde(b, '[data-aktion="einschalten"]')
|
|
const haken = (fx, b) => fx.alle(b, 'input[type="checkbox"]')
|
|
|
|
test('Betrieb: Fakten, Stundenkonto-Satz, Linkzeile, Knopf, kein Häkchen, kein Preis', mit((fx) => {
|
|
const { b } = seite(fx)
|
|
const t = fx.text(b)
|
|
assert.match(t, /News-Pinnwand einschalten/)
|
|
assert.match(t, /Leistung.*Einrichtung durch WEBklar/)
|
|
assert.match(t, /Dauer.*meist in wenigen Tagen/)
|
|
assert.match(t, /Laufzeit.*läuft, solange dein Wartungs-Abo läuft/)
|
|
assert.match(t, /Die Einrichtung geht von deinem Stundenkonto ab, auch wenn das Guthaben noch nicht reicht\. Extrakosten entstehen keine\./)
|
|
assert.match(t, /Es gelten Leistungsbeschreibung und Auftragsverarbeitung \(Fassung 2026-10-05\)\./)
|
|
assert.deepEqual(fx.alle(b, 'a').map((a) => a.getAttribute('href')), ['/rechtliches/leistungsbeschreibung-news-2026-10-05.html', '/rechtliches/avv-news-2026-10-05.html'])
|
|
assert.deepEqual(fx.alle(b, 'a').map((a) => [a.getAttribute('target'), a.getAttribute('rel')]), [['_blank', 'noopener noreferrer'], ['_blank', 'noopener noreferrer']])
|
|
assert.equal(haken(fx, b).length, 0)
|
|
assert.equal(knopf(fx, b).textContent, 'Einschalten')
|
|
assert.ok(fx.finde(b, '[data-aktion="abbrechen"]'))
|
|
assert.doesNotMatch(t, /€|Euro/)
|
|
}))
|
|
|
|
test('Privatkunde in der Frist: Häkchen sichtbar, Knopf nennt das Datum und bleibt gesperrt, bis das Häkchen sitzt', mit(async (fx) => {
|
|
const { b } = seite(fx, { kundenartWirksam: 'privat', daten: daten({ abo: { widerrufsfristBis: FRIST } }) })
|
|
const h1 = fx.finde(b, 'input[data-haken="beginn"]')
|
|
assert.equal(h1.checked, false)
|
|
assert.match(fx.text(b), /Bitte beginnt sofort mit der Einrichtung\./)
|
|
assert.deepEqual([knopf(fx, b).textContent, knopf(fx, b).disabled], ['Einschalten ab 31.10.', true])
|
|
await fx.klick(h1)
|
|
assert.deepEqual([knopf(fx, b).textContent, knopf(fx, b).disabled], ['Einschalten', false])
|
|
await fx.klick(h1)
|
|
assert.equal(knopf(fx, b).disabled, true)
|
|
}))
|
|
|
|
test('Häkchen nie für Betrieb, nie nach Fristende; leere Kundenart zählt als privat', mit((fx) => {
|
|
const d = daten({ abo: { widerrufsfristBis: FRIST } })
|
|
assert.equal(haken(fx, seite(fx, { daten: d, kundenartWirksam: 'unternehmen' }).b).length, 0)
|
|
assert.equal(haken(fx, seite(fx, { daten: d, kundenartWirksam: 'privat', jetzt: () => new Date('2026-10-31T09:00:00Z') }).b).length, 0)
|
|
assert.equal(haken(fx, seite(fx, { daten: d, kundenartWirksam: '' }).b).length, 1)
|
|
}))
|
|
|
|
test('Register fehlt: kein Absenden, freundlicher Text', mit((fx) => {
|
|
const { b } = seite(fx, { daten: daten({ katalogEdit: { news: { consent: null } } }) })
|
|
assert.match(fx.text(b), /Einschalten ist gerade nicht möglich\. Schreib uns im Chat\./)
|
|
assert.equal(knopf(fx, b), null)
|
|
}))
|
|
|
|
test('Kundenansicht (viewAs): Knopf gesperrt mit Hinweis', mit((fx) => {
|
|
const { b } = seite(fx, { viewAs: true })
|
|
assert.equal(knopf(fx, b).disabled, true)
|
|
assert.match(fx.text(b), /In der Kundenansicht kann nichts eingeschaltet werden\./)
|
|
}))
|
|
|
|
test('Fremdtext: HTML in Funktions- und Websitenamen nur als Text; Websitezeile erst ab zwei Websites', mit((fx) => {
|
|
const d = daten({ katalogEdit: { news: { name: '<img src=x onerror=alert(1)>' } }, projekte: [projekt('p1', '<b>Café</b>'), projekt('p2', 'Kiosk')] })
|
|
const { b } = seite(fx, { daten: d })
|
|
assert.equal(fx.alle(b, 'img').length + fx.alle(b, 'b').length, 0)
|
|
assert.match(fx.text(b), /<img src=x onerror=alert\(1\)> einschalten/)
|
|
assert.match(fx.text(b), /Website.*<b>Café<\/b>/)
|
|
assert.doesNotMatch(fx.text(seite(fx).b), /Website/)
|
|
}))
|
|
|
|
test('Hinweis ohne Haken (N17): über dem Knopf, nach der Linkzeile; Register ohne beginn-Item sperrt nur, wenn das Häkchen nötig ist', mit((fx) => {
|
|
const { b } = seite(fx, { key: 'shop' })
|
|
const hinweis = fx.finde(b, 'p.kn-fn-hinweis')
|
|
assert.match(fx.text(hinweis), /^Du verkaufst in eigenem Namen/)
|
|
const reihe = fx.alle(b, '*').map((el) => el.className)
|
|
assert.ok(reihe.indexOf('kn-fn-recht') < reihe.indexOf('kn-fn-hinweis') && reihe.indexOf('kn-fn-hinweis') < reihe.findIndex((c) => c.includes('kn-fn-knopf')))
|
|
assert.equal(fx.finde(seite(fx).b, 'p.kn-fn-hinweis'), null)
|
|
const ohneBeginn = daten({ katalogEdit: { news: { consent: { version: '2026-10-20', knopf: 'Einschalten', items: [{ id: 'einschalten-zeile', text: 'Es gilt die Leistungsbeschreibung (Fassung 2026-10-05).', haken: false }] } } } })
|
|
assert.ok(knopf(fx, seite(fx, { daten: ohneBeginn }).b)) // Betrieb braucht das Item nicht
|
|
const privat = seite(fx, { daten: { ...ohneBeginn, abo: { ...ohneBeginn.abo, widerrufsfristBis: FRIST } }, kundenartWirksam: 'privat' })
|
|
assert.equal(knopf(fx, privat.b), null) // Privatkunde in der Frist braucht es
|
|
assert.match(fx.text(privat.b), /Einschalten ist gerade nicht möglich/)
|
|
}))
|
|
|
|
test('Abo: Laufzeit nennt das Abo; Hosting-Abo bei einer Funktion des Hosting-Abos', mit((fx) => {
|
|
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(), '')
|
|
})
|