import { test } from 'node:test'
import assert from 'node:assert/strict'
import { zeigeWebsite, projektStatus, vorschauBereit } from '../public/neu/ansicht-website.js'
import { mit, me, projekt, rechnung, neueWurzel } from './support/knDaten.js'
const HAUPT = { 'GET /api/projects': { projects: [projekt({ id: 'p1', projectName: 'Bäckerei Roth' })] }, 'GET /api/activity': { tickets: [] }, 'GET /api/invoices': { invoices: [] } }
const zeige = async (fx, route = { bereich: 'website', query: '' }, m = me()) => { const w = neueWurzel(fx); await zeigeWebsite(w, { route, me: m, nav: 'tab' }); await fx.ruhe(); return w }
test('projektStatus', () => {
const s = (o) => projektStatus(projekt(o))
assert.deepEqual(s({}), { ton: 'gruen', text: 'Vorschau online' })
assert.deepEqual(s({ status: 'building' }), { ton: 'orange', text: 'Vorschau wird vorbereitet' })
assert.deepEqual(s({ provisioningStatus: 'failed' }), { ton: 'rot', text: 'Vorschau gestört' })
assert.deepEqual(s({ previewUrl: '' }), { ton: 'grau', text: 'Noch keine Website' })
assert.equal(vorschauBereit(projekt({ status: 'deployed' })), true); assert.equal(vorschauBereit(projekt({ status: 'building' })), false)
})
test('Website: leere Platzhalter erscheinen nie als Text „null“', mit(HAUPT, async (fx) => { assert.doesNotMatch(fx.text(await zeige(fx)), /null|undefined/) }))
test('Website: Ladezustand ist Text und verschwindet', mit(HAUPT, async (fx) => {
const w = neueWurzel(fx); const lauf = zeigeWebsite(w, { route: { bereich: 'website', query: '' }, me: me(), nav: 'tab' })
assert.match(fx.text(w), /Wird geladen …/); await lauf; await fx.ruhe(); assert.doesNotMatch(fx.text(w), /Wird geladen …/)
}))
test('Website: Zeile je Projekt mit Frame, Status, Link über die Brücke; Name bleibt Text', mit(HAUPT, async (fx) => {
const w = await zeige(fx)
assert.equal(fx.finde(w, 'iframe').getAttribute('src'), 'https://baeckerei.project.webklar.com')
assert.match(fx.text(w), /Vorschau online/); assert.match(fx.text(w), /Bäckerei Roth<\/b>/); assert.equal(fx.finde(w, 'b'), null)
const a = fx.finde(w, '[data-projekt="p1"] a[data-view="projekt"]'); assert.deepEqual([a.textContent, a.getAttribute('href'), a.getAttribute('data-project')], ['Projektdetails', '#projekt?projekt=p1', 'p1'])
}))
test('Website: unsichere Vorschau-Adresse ergibt weder Frame noch Link', mit({ ...HAUPT, 'GET /api/projects': { projects: [projekt({ previewUrl: 'javascript:alert(1)' })] } }, async (fx) => {
const w = await zeige(fx)
assert.equal(fx.finde(w, 'iframe'), null); assert.ok(fx.alle(w, 'a').every((a) => !/^javascript/i.test(a.getAttribute('href') || '')))
}))
test('Website: Aktivität fällt aus, die Projekte bleiben', mit({ ...HAUPT, 'GET /api/activity': () => ({ status: 500, body: { error: 'Server kaputt' } }) }, async (fx) => {
const w = await zeige(fx)
assert.match(fx.text(w), /Aktivität konnte nicht geladen werden: Server kaputt/); assert.ok(fx.finde(w, '[data-projekt="p1"]'))
}))
const ZEIT_AUFTRAEGE = [
{ number: '100207', topic: 'Öffnungszeiten aktualisieren', createdAt: '2026-10-02T10:00:00.000Z', status: { label: 'Erledigt', tone: 'done', step: 4 }, entries: [{ kind: 'online', label: 'Online erledigt', date: '2026-10-04', minutes: 50, person: 'Lena Vogel', text: 'Neue Öffnungszeiten eingetragen.' }] },
{ number: '100231', topic: 'Neue Fotos für die Startseite', createdAt: '2026-10-13T10:00:00.000Z', status: { label: 'Wartet auf deine Rückmeldung', tone: 'waiting', step: 2 }, entries: [] },
]
test('Website: „Was wir zuletzt erledigt haben“ ist eine Zeitleiste, laufende Aufträge zuerst, Rückfrage mit Link in den Chat', mit({ ...HAUPT, 'GET /api/activity': { tickets: ZEIT_AUFTRAEGE } }, async (fx) => {
const w = await zeige(fx)
const abschnitt = fx.finde(w, '[data-abschnitt="aktivitaet"]')
assert.equal(fx.finde(abschnitt, 'h2').textContent, 'Was wir zuletzt erledigt haben')
assert.deepEqual(fx.alle(abschnitt, '[data-auftrag]').map((b) => b.getAttribute('data-auftrag')), ['100231', '100207'])
assert.equal(fx.finde(abschnitt, '.kn-az-info').textContent, 'Wir brauchen eine Info von dir · Im Chat antworten')
assert.equal(fx.finde(abschnitt, '.kn-az-info a').getAttribute('href'), '#chat')
assert.match(fx.finde(abschnitt, '[data-auftrag="100207"]').textContent, /04\.10\.Neue Öffnungszeiten eingetragen\./)
}))
test('Website: ohne Aufträge steht der ruhige Satz', mit(HAUPT, async (fx) => {
assert.match(fx.text(fx.finde(await zeige(fx), '[data-abschnitt="aktivitaet"]')), /Es wurden noch keine Arbeiten dokumentiert\./)
}))
test('Website: leer', mit({ ...HAUPT, 'GET /api/projects': { projects: [] } }, async (fx) => { assert.match(fx.text(await zeige(fx)), /Noch keine Website-Projekte zugewiesen\./) }))
test('Website: offene Rechnung steht als erste Zeile', mit({ ...HAUPT, 'GET /api/invoices': { invoices: [rechnung({ number: 'RE-2026-0042', balance: 50 })] } }, async (fx) => {
const w = await zeige(fx); assert.match(fx.finde(w, '.kn-rechnung-offen').textContent, /^Rechnung offen · RE-2026-0042/)
}))
test('Website: route.scroll scrollt genau einmal zur Aktivität', mit(HAUPT, async (fx) => {
let n = 0; const create = fx.document.createElement.bind(fx.document)
fx.document.createElement = (t) => { const el = create(t); if (t === 'section') el.scrollIntoView = () => { n += 1 }; return el }
await zeige(fx, { bereich: 'website', scroll: 'aktivitaet', query: '' }); assert.equal(n, 1)
}))
test('Etage projekt: Titel, drei Abschnitte, nur das gewählte Projekt (projekt= und id=)', mit({
...HAUPT, 'GET /api/projects': { projects: [projekt({ id: 'p1', projectName: 'Eins' }), projekt({ id: 'p2', projectName: 'Zwei' })] },
'GET /api/projects/p1/git': { changes: [] }, 'GET /api/projects/p1/files': { totalFiles: 0 }, 'GET /api/projects/p1/work': { order: null, entries: [] },
'GET /api/projects/p2/git': { changes: [] }, 'GET /api/projects/p2/files': { totalFiles: 0 }, 'GET /api/projects/p2/work': { order: null, entries: [] },
}, async (fx) => {
const titel = []; const w = neueWurzel(fx); w.addEventListener('kn:titel', (e) => titel.push(e.detail.text))
await zeigeWebsite(w, { route: { bereich: 'website', etage: 'projekt', query: 'projekt=p2' }, me: me(), nav: 'push' }); await fx.ruhe()
assert.deepEqual(titel, ['Zwei']); for (const t of ['Letzte Änderungen', 'Arbeiten am Projekt', 'Dateien']) assert.match(fx.text(w), new RegExp(t))
assert.ok(fx.fetchLog.some((a) => a.url === '/api/projects/p2/git')); assert.ok(!fx.fetchLog.some((a) => a.url === '/api/projects/p1/git'))
await zeigeWebsite(w, { route: { bereich: 'website', etage: 'projekt', query: 'id=p1' }, me: me(), nav: 'push' }); await fx.ruhe(); assert.deepEqual(titel, ['Zwei', 'Eins'])
}))
test('Website: Installationshinweis nur im Handy-Layout und noch nicht installiert', mit(HAUPT, async (fx) => {
fx.window.matchMedia = (q) => ({ matches: q.includes('max-width: 880px') }); fx.window.navigator = { userAgent: 'iPhone', maxTouchPoints: 5 }
assert.match(fx.text(await zeige(fx)), /WEBklar als App/)
}))
test('Website: ohne Handy-Layout kein Installationshinweis; späte Antwort eines früheren Aufrufs wird verworfen', mit(HAUPT, async (fx) => {
const w = await zeige(fx); assert.doesNotMatch(fx.text(w), /WEBklar als App/)
let frei; const gesperrt = new Promise((r) => { frei = r })
fx.tabelle['GET /api/projects'] = async () => { await gesperrt; return { status: 200, body: { projects: [projekt({ id: 'alt', projectName: 'Alt' })] } } }
const w2 = neueWurzel(fx)
const erster = zeigeWebsite(w2, { route: { bereich: 'website', query: '' }, me: me(), nav: 'tab' })
fx.tabelle['GET /api/projects'] = { projects: [projekt({ id: 'neu', projectName: 'Neu' })] }
await zeigeWebsite(w2, { route: { bereich: 'website', query: '' }, me: me(), nav: 'tab' }); frei(); await erster; await fx.ruhe()
assert.match(fx.text(w2), /Neu/); assert.doesNotMatch(fx.text(w2), /Alt/)
}))
test('Etage projekt ohne Projekte oder mit unbekannter ID: Text statt Fehler', mit({ ...HAUPT, 'GET /api/projects': { projects: [projekt({ id: 'p1', projectName: 'Eins' })] } }, async (fx) => {
const w = neueWurzel(fx)
await zeigeWebsite(w, { route: { bereich: 'website', etage: 'projekt', query: 'projekt=nope' }, me: me(), nav: 'push' }); await fx.ruhe()
assert.match(fx.text(w), /Dieses Projekt wurde nicht gefunden\./)
}))