160 lines
9.5 KiB
JavaScript
160 lines
9.5 KiB
JavaScript
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/<id>/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 = '<script>alert(1)</script> & "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)')
|
|
})
|