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: 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"]')) })) 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\./) }))