Auf Abo bestellen, Wechsel, Zusatzspeicher und Übernahmegutschrift fehlt der feste Widerrufsstreifen, damit Kerninfo und Knopf "Zahlungspflichtig bestellen" auch bei Privatkunden in laufender Frist auf 375x667 auf einem Bildschirm stehen (Spec Abschnitt 4). Auf den Hauptseiten bleibt er (Spec 5.2), "Vertrag widerrufen" bleibt im Konto und unter Verträge erreichbar. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
176 lines
15 KiB
JavaScript
176 lines
15 KiB
JavaScript
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { installFakeDom } from './support/fakeDom.js'
|
|
import { h } from '../public/neu/dom.js'
|
|
import { routeAusHash } from '../public/neu/routen.js'
|
|
import { leiteAb, scrollZiel, GERUEST_IDS, starteHuelle } from '../public/neu/huelle.js'
|
|
|
|
const ME_KUNDE = { authenticated: true, role: 'customer', customer: { id: 'c1', name: 'Anna', companyName: '', email: 'a@b.de' } }
|
|
const ME_VIEW = { ...ME_KUNDE, viewAs: true, viewId: 'v1', admin: { name: 'Kenso' } }
|
|
const KARTE_KUNDE = { '/api/auth/me': ME_KUNDE, '/api/chat/unread-count': { unread: 0 }, '/api/invoices': { invoices: [] }, '/api/abo': { subscription: null }, '/api/abo/takeover-credits': { requests: [] } }
|
|
|
|
// Hilfen: jede ID des Gerüsts als div am Körper, ein Fenster aus dem fakeDom, ein api mit Karte, ein Router zum Mitschreiben
|
|
function baueGeruest(fx) { for (const id of GERUEST_IDS) fx.document.body.append(h('div', { id })) }
|
|
async function boot({ karte, hash = '', lade = async () => new Proxy({}, { get: () => async () => {} }) } = {}) {
|
|
const fx = installFakeDom(); baueGeruest(fx); const log = []
|
|
const win = Object.assign(fx.window, { matchMedia: () => ({ matches: true, addEventListener() {} }), KbView: { setId: (id) => log.push(['setId', id]) } })
|
|
win.location.hash = hash
|
|
const ziel = new EventTarget()
|
|
const doc = Object.assign(fx.document, { addEventListener: (...a) => ziel.addEventListener(...a), removeEventListener: (...a) => ziel.removeEventListener(...a), dispatchEvent: (ev) => ziel.dispatchEvent(ev) })
|
|
const api = async (pfad, opt = {}) => {
|
|
log.push(['api', pfad, opt.method ?? 'GET'])
|
|
if (!(pfad in karte)) throw Object.assign(new Error('Nicht gefunden'), { status: 404 })
|
|
if (karte[pfad] instanceof Error) throw karte[pfad]
|
|
return karte[pfad]
|
|
}
|
|
let zeigeFn
|
|
const router = {
|
|
starteRouter: ({ zeige }) => { zeigeFn = zeige; zeige(routeAusHash(hash), { nav: 'tab' }); return { beende() {} } },
|
|
geheZu: (hashNeu) => log.push(['geheZu', hashNeu]), stempeln: () => log.push(['stempeln']), zurueck() {}, oeffneOverlay() {}, beendeOverlay() {},
|
|
}
|
|
await starteHuelle({ win, doc, api, lade, router, hash }); await fx.ruhe()
|
|
return { fx, win, doc, log, el: (id) => fx.document.getElementById(id), zeige: async (hashNeu, nav) => { zeigeFn(routeAusHash(hashNeu), { nav }); await fx.ruhe() } }
|
|
}
|
|
|
|
test('leiteAb: Anmeldung, Mitarbeiter, abgelaufen, Passwort geändert, Kundenansicht (wie app.js:2043-2064)', () => {
|
|
assert.deepEqual(leiteAb({ authenticated: false }, { hash: '' }), { art: 'weiter', ziel: '/login.html' })
|
|
assert.deepEqual(leiteAb({ authenticated: false }, { hash: '#chat' }), { art: 'weiter', ziel: '/login.html?next=%2Fdashboard.html%23chat' })
|
|
assert.deepEqual(leiteAb({ authenticated: false, reason: 'staff', redirect: 'https://ticket.webklar.com' }, {}), { art: 'weiter', ziel: 'https://ticket.webklar.com' })
|
|
assert.equal(leiteAb({ authenticated: false, reason: 'view_expired', message: 'Bitte neu anmelden.' }, { hash: '#chat' }).ziel, '/login.html?next=%2Fdashboard.html%23chat&error=Bitte+neu+anmelden.')
|
|
assert.equal(leiteAb({ authenticated: false, reason: 'password_changed', message: 'Bitte neu anmelden.' }, {}).ziel, '/login.html?error=Bitte+neu+anmelden.')
|
|
assert.equal(leiteAb({ authenticated: true }, {}).ziel, '/login.html')
|
|
const z = leiteAb(ME_VIEW, {}); assert.deepEqual([z.art, z.viewAs, z.viewId, z.anzeigename], ['zeigen', true, 'v1', 'Anna'])
|
|
})
|
|
|
|
test('scrollZiel: push beginnt oben, same scrollt weich nach oben, back und tab stellen wieder her', () => {
|
|
assert.deepEqual([scrollZiel('push', 120), scrollZiel('same', 120), scrollZiel('back', 120), scrollZiel('tab', undefined)], [{ top: 0, weich: false }, { top: 0, weich: true }, { top: 120, weich: false }, { top: 0, weich: false }])
|
|
})
|
|
|
|
test('Kundenansicht: setId vor jedem weiteren API-Aufruf, Banner, Beenden', async () => {
|
|
const b = await boot({ hash: '#chat', karte: { ...KARTE_KUNDE, '/api/auth/me': ME_VIEW, '/api/auth/kundenansicht/ende': { redirect: 'https://ticket.webklar.com/customers/c1' } } })
|
|
assert.deepEqual(b.log.slice(0, 2), [['api', '/api/auth/me', 'GET'], ['setId', 'v1']])
|
|
assert.equal(b.fx.text(b.el('kn-banner')), 'Kundenansicht: Anna · geöffnet von KensoBeenden')
|
|
await b.fx.klick(b.fx.finde(b.el('kn-banner'), 'button')); assert.equal(b.win.location.href, 'https://ticket.webklar.com/customers/c1')
|
|
})
|
|
|
|
test('abgemeldet leitet um ohne weitere Aufrufe; me 500 zeigt eine Meldung statt leerer Seite (Review Focus 5)', async () => {
|
|
const a = await boot({ hash: '#chat', karte: { '/api/auth/me': { authenticated: false } } })
|
|
assert.equal(a.win.location.href, '/login.html?next=%2Fdashboard.html%23chat'); assert.equal(a.log.filter((l) => l[0] === 'api').length, 1)
|
|
const f = await boot({ hash: '', karte: { '/api/auth/me': Object.assign(new Error('Fehler 500'), { status: 500 }) } })
|
|
assert.deepEqual([f.el('kn-fehler').hidden, f.el('kn-fehler').textContent], [false, 'Fehler 500'])
|
|
})
|
|
|
|
test('Route Chat: Ansicht bekommt Wurzel, route, me, nav; Navigation, Titel, Ansage folgen', async () => {
|
|
const gerufen = []
|
|
const b = await boot({ hash: '#chat', karte: KARTE_KUNDE, lade: async (p) => ({ zeigeChat: (w, c) => gerufen.push([p, w.id, c.route, c.nav, c.me.customer.id]) }) })
|
|
assert.deepEqual(gerufen, [['/neu/ansicht-chat.js', 'chat-body', { bereich: 'chat', query: '' }, 'tab', 'c1']])
|
|
assert.deepEqual([b.el('bereich-chat').hidden, b.el('bereich-website').hidden, b.el('kn-blatt').hidden, b.el('kn-kopf-titel').textContent, b.el('kn-ansage').textContent], [false, true, true, 'Chat', 'Chat'])
|
|
})
|
|
|
|
test('Ladezustand als Text, solange die Ansicht lädt', async () => {
|
|
const b = await boot({ hash: '#chat', karte: KARTE_KUNDE, lade: () => new Promise(() => {}) })
|
|
assert.equal(b.el('chat-body').textContent, 'Wird geladen …')
|
|
})
|
|
|
|
test('Etage Terminplaner: wunschProjekt aus ?projekt=, Modul auf #termine-body mit Option einschalten, danach stempeln, Chat-Punkt am Zurück; Ladefehler zeigt Text', async () => {
|
|
const seen = []
|
|
const b = await boot({ hash: '#termine?bestellt=1&projekt=p7', karte: { ...KARTE_KUNDE, '/api/chat/unread-count': { unread: 3 } }, lade: async () => ({ initTermine: (root, opt) => seen.push([root.id, root.dataset.wunschProjekt, typeof opt?.einschalten]) }) })
|
|
assert.deepEqual(seen, [['termine-body', 'p7', 'function']]); assert.ok(b.log.some((l) => l[0] === 'stempeln'))
|
|
assert.deepEqual([b.el('kn-blatt').hidden, b.el('kn-etage-titel').textContent, Boolean(b.fx.finde(b.el('kn-zeile-etage'), '.kn-punkt'))], [false, 'Terminplaner', true])
|
|
const x = await boot({ hash: '#webshop', karte: KARTE_KUNDE, lade: async () => { throw new Error('404') } })
|
|
assert.equal(x.el('webshop-body').textContent, 'Konnte nicht geladen werden.')
|
|
})
|
|
|
|
test('Option einschalten: news, termine, webshop, buchungen und zeiten bekommen sie, analyse nicht; der Aufruf lädt funktion-einschalten.js erst dann', async () => {
|
|
const MODULE = { news: ['/funktionen.js', 'initNews'], termine: ['/termine.js', 'initTermine'], webshop: ['/shop.js', 'initShop'], buchungen: ['/buchungen.js', 'initBuchungen'], zeiten: ['/zeiten.js', 'initZeiten'], analyse: ['/analyse.js', 'initAnalyse'] }
|
|
for (const [etage, [pfad, init]] of Object.entries(MODULE)) {
|
|
const geladen = []; const aufrufe = []
|
|
const lade = async (p) => { geladen.push(p); return p === '/neu/funktion-einschalten.js' ? { zeigeEinschalten: (platz, o) => aufrufe.push(['zeige', platz, o]) } : { [init]: (root, opt) => aufrufe.push(['init', root.id, opt]) } }
|
|
await boot({ hash: `#${etage}`, karte: KARTE_KUNDE, lade })
|
|
const aufruf = aufrufe.find((a) => a[0] === 'init'); assert.ok(geladen.includes(pfad), pfad); assert.equal(aufruf[1], `${etage}-body`)
|
|
if (etage === 'analyse') { assert.equal(aufruf[2], undefined, 'analyse bekommt keine Option'); continue }
|
|
assert.equal(typeof aufruf[2].einschalten, 'function', etage); assert.equal(geladen.includes('/neu/funktion-einschalten.js'), false, `${etage}: lazy`)
|
|
const platz = { id: 'platz' }; await aufruf[2].einschalten(platz, { key: etage, projectId: 'p1' })
|
|
assert.ok(geladen.includes('/neu/funktion-einschalten.js')); assert.deepEqual(aufrufe.at(-1), ['zeige', platz, { key: etage, projectId: 'p1' }])
|
|
}
|
|
let versprechen // Ladefehler der Datei geht an das Modul (es zeigt E4), die Hülle schluckt ihn nicht
|
|
await boot({ hash: '#termine', karte: KARTE_KUNDE, lade: async (p) => { if (p.endsWith('funktion-einschalten.js')) throw new Error('404'); return { initTermine: (_w, o) => { versprechen = o.einschalten({}, {}); versprechen.catch(() => {}) } } } })
|
|
await assert.rejects(versprechen, /404/)
|
|
})
|
|
|
|
test('data-view-Brücke der Module: Name zu Hash, data-project wird ?projekt=', async () => {
|
|
const b = await boot({ hash: '#termine', karte: KARTE_KUNDE, lade: async () => ({ initTermine() {} }) })
|
|
const chat = h('button', { 'data-view': 'chat' }); const shop = h('button', { 'data-view': 'webshop', 'data-project': 'p1' }); b.el('termine-body').append(chat, shop)
|
|
await b.fx.klick(chat); await b.fx.klick(shop)
|
|
assert.deepEqual(b.log.filter((l) => l[0] === 'geheZu').map((l) => l[1]), ['#chat', '#webshop?projekt=p1'])
|
|
})
|
|
|
|
test('Fristen 404 (Server ohne P5): kein Streifen, "Vertrag widerrufen" bleibt ohne Pille; mit Frist Streifen und Pille', async () => {
|
|
const b = await boot({ hash: '', karte: KARTE_KUNDE })
|
|
assert.equal(b.el('kn-streifen').hidden, true)
|
|
const konto = b.fx.text(b.el('kn-konto-wurzel')); assert.match(konto, /Vertrag widerrufen/); assert.doesNotMatch(konto, /Frist läuft/)
|
|
const p = await boot({ hash: '', karte: { ...KARTE_KUNDE, '/api/fristen': { fristen: [{ quelle: 'abschluss', bezeichnung: 'Abo', bis: '2026-10-21T21:59:59.999Z' }], fruehestesEnde: '2026-10-21T21:59:59.999Z', anzeigen: true } } })
|
|
assert.deepEqual([p.el('kn-streifen').hidden, p.fx.text(p.el('kn-streifen'))], [false, 'Widerrufsfrist bis 21.10. · Widerrufen']); assert.match(p.fx.text(p.el('kn-konto-wurzel')), /Frist läuft/)
|
|
})
|
|
|
|
test('M1: Widerrufsstreifen und Pille werden nach Kauf, Wechsel oder Speicher-Plus in der Sitzung neu geladen (kn:zaehler-neu vertraege/alle, Sichtbarkeit); ein Fehler lässt den Stand stehen', async () => {
|
|
const frist = { fristen: [{ quelle: 'wechsel', bezeichnung: 'Wechsel', bis: '2026-10-21T21:59:59.999Z' }], fruehestesEnde: '2026-10-21T21:59:59.999Z', anzeigen: true }
|
|
const karte = { ...KARTE_KUNDE }
|
|
const b = await boot({ hash: '', karte }); const n = () => b.log.filter((l) => l[1] === '/api/fristen').length
|
|
assert.equal(b.el('kn-streifen').hidden, true); assert.doesNotMatch(b.fx.text(b.el('kn-konto-wurzel')), /Frist läuft/)
|
|
const n0 = n()
|
|
// der Kunde wechselt oder bestellt Speicher: die Bestellseite meldet kn:zaehler-neu, jetzt gibt es eine Frist
|
|
karte['/api/fristen'] = frist
|
|
b.doc.dispatchEvent(new CustomEvent('kn:zaehler-neu', { detail: { was: 'chat' } })); await b.fx.ruhe(); assert.equal(n(), n0, 'Chat-Zähler laden die Fristen nicht')
|
|
b.doc.dispatchEvent(new CustomEvent('kn:zaehler-neu', { detail: { was: 'vertraege' } })); await b.fx.ruhe()
|
|
assert.equal(n(), n0 + 1); assert.deepEqual([b.el('kn-streifen').hidden, b.fx.text(b.el('kn-streifen'))], [false, 'Widerrufsfrist bis 21.10. · Widerrufen']); assert.match(b.fx.text(b.el('kn-konto-wurzel')), /Frist läuft/)
|
|
// ein Netzfehler lässt Streifen und Pille stehen
|
|
karte['/api/fristen'] = new Error('Netz weg'); b.doc.dispatchEvent(new CustomEvent('kn:zaehler-neu', { detail: { was: 'alle' } })); await b.fx.ruhe()
|
|
assert.equal(n(), n0 + 2); assert.equal(b.el('kn-streifen').hidden, false)
|
|
// die Frist ist abgelaufen (Server: anzeigen false): der Streifen verschwindet, auch beim Zurückkehren in den Tab
|
|
karte['/api/fristen'] = { fristen: [], fruehestesEnde: null, anzeigen: false }; b.doc.visibilityState = 'visible'; b.doc.dispatchEvent(new Event('visibilitychange')); await b.fx.ruhe()
|
|
assert.equal(n(), n0 + 3); assert.equal(b.el('kn-streifen').hidden, true); assert.doesNotMatch(b.fx.text(b.el('kn-konto-wurzel')), /Frist läuft/)
|
|
})
|
|
|
|
test('E-Fix F1 (Nachkorrektur): auf den Bestellseiten (Abo bestellen, Wechsel, Zusatzspeicher, Gutschrift) ist der Widerrufsstreifen weg, auf den Hauptseiten da; ein Neuladen der Fristen ändert daran nichts', async () => {
|
|
const frist = { fristen: [{ quelle: 'abschluss', bezeichnung: 'Abo', bis: '2026-10-21T21:59:59.999Z' }], fruehestesEnde: '2026-10-21T21:59:59.999Z', anzeigen: true }
|
|
const karte = { ...KARTE_KUNDE, '/api/fristen': frist }
|
|
const b = await boot({ hash: '#wechsel', karte }) // Einstieg direkt auf der Bestellseite
|
|
const zustand = () => [b.el('kn-streifen').hidden, b.fx.document.body.classList.contains('kn-hat-streifen')]
|
|
assert.deepEqual(zustand(), [true, false])
|
|
await b.zeige('#vertraege', 'push'); assert.deepEqual(zustand(), [false, true]); assert.equal(b.fx.text(b.el('kn-streifen')), 'Widerrufsfrist bis 21.10. · Widerrufen')
|
|
for (const hash of ['#bestellen-wartung', '#bestellen-hosting', '#wechsel', '#speicher', '#gutschrift']) {
|
|
await b.zeige('#abo', 'push'); assert.deepEqual(zustand(), [false, true], `Hauptseite vor ${hash}`)
|
|
await b.zeige(hash, 'push'); assert.deepEqual(zustand(), [true, false], hash)
|
|
b.doc.dispatchEvent(new CustomEvent('kn:zaehler-neu', { detail: { was: 'vertraege' } })); await b.fx.ruhe() // Frist neu geladen (z. B. nach Zusatzspeicher)
|
|
assert.deepEqual(zustand(), [true, false], `${hash} nach dem Neuladen der Fristen`)
|
|
}
|
|
await b.zeige('#chat', 'push'); assert.deepEqual(zustand(), [false, true])
|
|
// "Vertrag widerrufen" bleibt im Konto erreichbar (mit der Pille "Frist läuft")
|
|
assert.match(b.fx.text(b.el('kn-konto-wurzel')), /Vertrag widerrufen/)
|
|
})
|
|
|
|
test('Zähler: Verträge laden beim Betreten und bei kn:zaehler-neu (document) neu; der Chat-Zähler fällt beim Öffnen des Chats weg', async () => {
|
|
const b = await boot({ hash: '', karte: { ...KARTE_KUNDE, '/api/chat/unread-count': { unread: 3 } } }); const n = (pfad) => b.log.filter((l) => l[1] === pfad).length
|
|
const i0 = n('/api/invoices'); const chatTab = () => b.fx.finde(b.el('kn-zeile-haupt'), '[role=tab][aria-label="Chat, 3 ungelesen"]')
|
|
assert.ok(chatTab())
|
|
await b.zeige('#rechnungen', 'tab'); assert.equal(n('/api/invoices'), i0 + 1)
|
|
b.doc.dispatchEvent(new CustomEvent('kn:zaehler-neu', { detail: { was: 'vertraege' } })); await b.fx.ruhe(); assert.equal(n('/api/invoices'), i0 + 2)
|
|
await b.zeige('#chat', 'tab'); assert.equal(chatTab(), null)
|
|
})
|
|
|
|
test('Option einschalten in der Mitarbeiter-Kundenansicht: die Hülle reicht viewAs true durch, auch wenn das Modul es nicht nennt; sonst bleibt die Option unverändert', async () => {
|
|
const MODULE = { news: ['/funktionen.js', 'initNews'], termine: ['/termine.js', 'initTermine'], webshop: ['/shop.js', 'initShop'], buchungen: ['/buchungen.js', 'initBuchungen'], zeiten: ['/zeiten.js', 'initZeiten'] }
|
|
for (const [etage, [, init]] of Object.entries(MODULE)) {
|
|
const aufrufe = []
|
|
const lade = async (p) => (p === '/neu/funktion-einschalten.js' ? { zeigeEinschalten: (platz, o) => aufrufe.push(['zeige', o]) } : { [init]: (root, opt) => aufrufe.push(['init', opt]) })
|
|
await boot({ hash: `#${etage}`, karte: { ...KARTE_KUNDE, '/api/auth/me': ME_VIEW }, lade })
|
|
const einschalten = aufrufe.find((a) => a[0] === 'init')[1].einschalten
|
|
const platz = { id: 'platz' }
|
|
await einschalten(platz, { key: etage, projectId: 'p1' })
|
|
assert.deepEqual(aufrufe.at(-1), ['zeige', { key: etage, projectId: 'p1', viewAs: true }], `${etage}: viewAs fehlt im Aufruf`)
|
|
await einschalten(platz, { key: etage, projectId: 'p1', viewAs: false })
|
|
assert.equal(aufrufe.at(-1)[1].viewAs, true, `${etage}: ein viewAs false des Moduls darf die Sperre nicht aufheben`)
|
|
}
|
|
})
|