Files
Webklar-Kundenbereich/test/knHuelleStart.test.js
T
ClaudeandClaude Sonnet 5.5 524ac64d20 Kundenansicht der Mitarbeiter: Einschalten gesperrt statt 403-Text nach dem Tipp
Die Hülle reicht viewAs der Sitzung an die Einschalt-Option der Module durch
(News, Terminplaner, Webshop, Buchungskalender, Zeiten), auch wenn ein Modul es
nicht nennt. In Entdecken ist der Einschalten-Knopf gesperrt, daneben steht
"In der Kundenansicht nicht möglich." Meine bleibt bei gesperrtem Knopf auf der
Bestätigungsseite (jetzt gepinnt).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 19:30:26 +00:00

139 lines
12 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('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`)
}
})