import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { installFakeDom } from './support/fakeDom.js' import { h } from '../public/auftrag-dom.js' import { initAuftraege } from '../public/auftrag-portal.js' import { ab, privat, bestaetigt, eintrag, pruefeQuelltext } from './auftraegeKundenportalDaten.js' const NB = '\u{a0}' const mit = (fn) => async () => { const fx = installFakeDom() try { await fn(fx) } finally { fx.restore() } } function aufbau(fx) { const body = h('div', { id: 'orders-body' }) const section = h('section', { id: 'orders-section', class: 'ab-portal hidden' }, body) const invoicesTitle = h('h2', { id: 'invoices-title', class: 'hidden' }, 'Rechnungen') fx.document.body.append(section, invoicesTitle) return { section, body, invoicesTitle } } /** request wie api() in app.js: gibt die nächste Antwort zurück oder wirft sie (Error mit status). */ function fakeRequest(antworten) { const aufrufe = [] const request = async (pfad, optionen = {}) => { aufrufe.push({ pfad, methode: optionen.method || 'GET', body: optionen.body ? JSON.parse(optionen.body) : undefined }) const antwort = antworten.shift() if (antwort instanceof Error) throw antwort return antwort } return { request, aufrufe } } const fehler = (status, text = `Fehler ${status}`) => Object.assign(new Error(text), { status }) const sichtbar = (el) => !el.classList.contains('hidden') const liste = (...auftraege) => ({ aktiv: true, auftraege }) const A_OFFEN = eintrag({ id: 'ab1', nummer: 'AB-2026-0002' }) const A_OHNE_RECHT = eintrag({ id: 'ab4', nummer: 'AB-2026-0004', kannBestaetigen: false }) // Status offen, der Server erlaubt das Bestätigen nicht const A_FERTIG = eintrag({ id: 'ab2', ...bestaetigt(privat({ fruehbeginn: true, fruehesterBeginn: null })), nummer: 'AB-2026-0003', kannBestaetigen: false }) const A_STORNO = eintrag({ id: 'ab0', nummer: 'AB-2026-0001', status: 'storniert', kannBestaetigen: false, art: 'aufwand', positionen: [], summeCents: null, aufwandText: '100,00 € pro Stunde in Schritten von 15 Minuten.', }) const bestaetige = async (fx, wurzel) => { await fx.klick(fx.finde(wurzel, '[data-feld="bestaetigung"]')) await fx.klick(fx.finde(wurzel, '[data-aktion="bestaetigen"]')) } test('Der Abschnitt bleibt weg: Schalter aus, keine Aufträge, Ladefehler', mit(async (fx) => { for (const antwort of [{ aktiv: false, auftraege: [] }, { aktiv: false, auftraege: [A_OFFEN] }, liste(), fehler(500), fehler(401)]) { const els = aufbau(fx) const { request, aufrufe } = fakeRequest([antwort]) assert.deepEqual(await initAuftraege({ ...els, request, viewAs: false }), { sichtbar: false }) assert.equal(sichtbar(els.section), false) assert.equal(sichtbar(els.invoicesTitle), false) assert.deepEqual(aufrufe, [{ pfad: '/api/auftraege', methode: 'GET', body: undefined }]) els.section.remove() els.invoicesTitle.remove() } })) test('Liste: Bestätigbares zuerst, Status, Zeile, PDF-Link; nur der offene Auftrag ist aufgeklappt und hat das Formular', mit(async (fx) => { const els = aufbau(fx) const { request } = fakeRequest([liste(A_STORNO, A_FERTIG, A_OHNE_RECHT, A_OFFEN)]) assert.deepEqual(await initAuftraege({ ...els, request, viewAs: false }), { sichtbar: true }) assert.equal(sichtbar(els.section), true) assert.equal(sichtbar(els.invoicesTitle), true) const karten = fx.alle(els.body, '.ab-karte') assert.deepEqual(karten.map((k) => k.getAttribute('data-auftrag')), ['ab1', 'ab4', 'ab2', 'ab0']) const [offen, ohneRecht, fertig, storno] = karten assert.equal(fx.finde(offen, '.ab-pille').textContent, 'Bitte bestätigen') assert.equal(fx.finde(ohneRecht, '.ab-pille').textContent, 'Versendet') assert.equal(fx.finde(ohneRecht, 'form'), null, 'ohne kannBestaetigen vom Server gibt es kein Formular') assert.equal(fx.finde(fertig, '.ab-pille').textContent, 'Bestätigt') assert.equal(fx.finde(storno, '.ab-pille').textContent, 'Storniert') assert.equal(fx.finde(offen, '.ab-karte-titel').textContent, 'Website Anelia Wolf') assert.equal(fx.finde(offen, '.ab-karte-meta').textContent, `vom 06.10.2026 · 800,00${NB}€`) assert.equal(fx.finde(storno, '.ab-karte-meta').textContent, 'vom 06.10.2026 · nach Aufwand') assert.equal(fx.finde(offen, '[data-link="pdf"]').getAttribute('href'), '/api/auftraege/ab1/pdf') assert.equal(fx.finde(offen, '[data-link="pdf"]').textContent, 'PDF ansehen') assert.equal(fx.finde(offen, 'summary').textContent, 'Details ansehen und bestätigen') assert.equal(fx.finde(fertig, 'summary').textContent, 'Details ansehen') assert.equal(fx.finde(offen, 'details').hasAttribute('open'), true) assert.equal(fx.finde(fertig, 'details').hasAttribute('open'), false) assert.ok(fx.finde(offen, 'form [data-feld="bestaetigung"]')) assert.equal(fx.finde(fertig, 'form'), null) assert.equal(fx.finde(storno, 'form'), null) assert.deepEqual(fx.alle(fx.finde(fertig, '[data-bestaetigt]'), 'p').map((p) => p.textContent), ['Bestätigt am 07.10.2026, 10:15 Uhr', 'Wir beginnen auf deinen Wunsch schon vor Ablauf der Widerrufsfrist.']) assert.equal(fx.text(fx.finde(offen, 'legend')), 'Deine Bestätigung', 'Portal: Du-Form') assert.equal(fx.text(fx.finde(offen, '.ab-notiz')), 'Deine Bestätigung wird mit Datum und Uhrzeit gespeichert.') assert.deepEqual(fx.alle(offen, 'h4').map((x) => x.textContent), ['Leistungsumfang', 'Zahlung', 'Termine']) assert.equal(fx.finde(offen, '[data-link="agb"]').getAttribute('href'), '/rechtliches/agb-2026-09-30.pdf') })) test('Bestätigen: POST an /api/auftraege//bestaetigen, die Karte zeigt danach „Bestätigt“ (auch ohne id in der Antwort)', mit(async (fx) => { const els = aufbau(fx) const { request, aufrufe } = fakeRequest([liste(A_OFFEN), { auftrag: bestaetigt(ab()) }]) await initAuftraege({ ...els, request, viewAs: false }) await bestaetige(fx, els.body) assert.deepEqual(aufrufe[1], { pfad: '/api/auftraege/ab1/bestaetigen', methode: 'POST', body: { bestaetigt: true, fruehbeginn: false } }) const karte = fx.finde(els.body, '.ab-karte') assert.equal(karte.getAttribute('data-auftrag'), 'ab1') assert.equal(fx.finde(karte, '.ab-pille').textContent, 'Bestätigt') assert.equal(fx.finde(karte, 'form'), null) assert.equal(fx.finde(karte, 'details').hasAttribute('open'), true) assert.equal(fx.document.activeElement, fx.finde(karte, '[data-bestaetigt]')) })) test('Ansichtsmodus: Häkchen und Knopf gesperrt, Hinweis sichtbar, kein POST; der Abschnitt bleibt lesbar', mit(async (fx) => { const els = aufbau(fx) // wie L5 es liefert: in der Kundenansicht ist kannBestaetigen immer false; die Oberfläche zeigt die offene AB trotzdem mit gesperrtem Formular const { request, aufrufe } = fakeRequest([liste(eintrag({ ...privat(), id: 'ab1', kannBestaetigen: false }))]) await initAuftraege({ ...els, request, viewAs: true }) assert.equal(fx.finde(els.body, '.ab-pille').textContent, 'Bitte bestätigen') const form = fx.finde(els.body, 'form') assert.equal(fx.alle(form, 'input').every((i) => i.disabled), true) assert.equal(fx.finde(form, '[data-aktion="bestaetigen"]').disabled, true) assert.equal(fx.finde(form, '[data-gesperrt]').textContent, 'Im Ansichtsmodus kann der Auftrag nicht bestätigt werden. Die Bestätigung gibt der Kunde selbst ab.') await fx.sende(form, 'submit') assert.equal(aufrufe.length, 1, 'nur das Laden der Liste') assert.ok(fx.finde(els.body, '[data-link="pdf"]')) assert.ok(fx.finde(els.body, '[data-position]')) })) test('Fehler beim Bestätigen: feste Meldung statt Servertext; ein 410 lädt die Liste neu', mit(async (fx) => { const els = aufbau(fx) const storniert = { ...A_OFFEN, status: 'storniert', kannBestaetigen: false } const { request, aufrufe } = fakeRequest([liste(A_OFFEN), fehler(500), fehler(410, 'Diese Auftragsbestätigung wurde storniert.'), liste(storniert)]) await initAuftraege({ ...els, request, viewAs: false }) await bestaetige(fx, els.body) assert.equal(fx.finde(els.body, '[data-fehler]').textContent, 'Die Bestätigung konnte nicht gespeichert werden. Bitte versuche es später noch einmal.') assert.ok(!fx.text(els.body).includes('Fehler 500')) await fx.klick(fx.finde(els.body, '[data-aktion="bestaetigen"]')) assert.equal(aufrufe.length, 4, 'Liste, POST, POST, Liste neu') assert.equal(fx.finde(els.body, '.ab-pille').textContent, 'Storniert') assert.equal(fx.finde(els.body, 'form'), null) })) test('Fremdtext bleibt Text; die ID im PDF-Link wird kodiert', mit(async (fx) => { const els = aufbau(fx) const boese = ' & "Test"' const { request } = fakeRequest([liste(eintrag({ id: 'a/b', titel: boese }))]) await initAuftraege({ ...els, request, viewAs: false }) assert.equal(fx.alle(els.body, 'script').length, 0) assert.equal(fx.finde(els.body, '.ab-karte-titel').textContent, boese) assert.equal(fx.finde(els.body, '[data-link="pdf"]').getAttribute('href'), '/api/auftraege/a%2Fb/pdf') })) test('Quelltext und CSS: kein eigenes fetch, Karten und Abschnitt sind gestaltet', () => { pruefeQuelltext('auftrag-portal.js', { importe: ['./auftrag-ansicht.js', './auftrag-dom.js'] }) const css = readFileSync(new URL('../public/auftrag.css', import.meta.url), 'utf8') for (const klasse of ['ab-portal', 'ab-karte', 'ab-karte-kopf', 'ab-karte-titel', 'ab-karte-meta', 'ab-karte-aktionen', 'ab-details']) { assert.match(css, new RegExp(`\\.${klasse}[\\s{,.:\\[>]`), `.${klasse} ist gestaltet`) } assert.match(css, /\.ab-portal\s*\{[^}]*--ab-/, 'der Portal-Abschnitt setzt seine eigenen Farbschlüssel (dunkles Portal)') })