Files
Webklar-Kundenbereich/test/auftraegeKundenportalDom.test.js

250 lines
13 KiB
JavaScript

import { test } from 'node:test'
import assert from 'node:assert/strict'
import { installFakeDom } from './support/fakeDom.js'
import { renderZusammenfassung, renderBestaetigtBlock, renderDokumente, renderBestaetigung } from '../public/auftrag-dom.js'
import { KLEINUNTERNEHMER, ab, privat, bestaetigt, pruefeQuelltext } from './auftraegeKundenportalDaten.js'
import { ANREDE_DU } from '../public/auftrag-ansicht.js'
const NB = '\u{a0}'
const mit = (fn) => async () => {
const fx = installFakeDom()
try {
await fn(fx)
} finally {
fx.restore()
}
}
const paare = (fx, baum) => fx.alle(baum, '.ab-zeile').map((z) => [fx.finde(z, 'dt').textContent, fx.finde(z, 'dd').textContent])
test('Zusammenfassung (Festpreis): Kopfzeilen, Positionen mit Spaltenmarken, Gesamtpreis, § 19, Zahlung, Termine', mit(async (fx) => {
const el = renderZusammenfassung(ab())
assert.equal(el.getAttribute('data-art'), 'festpreis')
assert.deepEqual(paare(fx, el).slice(0, 3), [['Nummer', 'AB-2026-0002'], ['Datum', '06.10.2026'], ['Projekt', 'Website Anelia Wolf']])
const zeilen = fx.alle(el, '[data-position]')
assert.equal(zeilen.length, 2)
assert.deepEqual(fx.alle(zeilen[1], 'td').map((td) => td.textContent), ['Texte und Bildbearbeitung', '2', `100,00${NB}€`, `200,00${NB}€`])
assert.deepEqual(fx.alle(zeilen[1], 'td').map((td) => td.getAttribute('data-label')), ['Beschreibung', 'Menge', 'Einzelpreis', 'Summe'])
assert.deepEqual(fx.alle(el, 'th').map((th) => th.textContent), ['Beschreibung', 'Menge', 'Einzelpreis', 'Summe'])
assert.equal(fx.finde(el, '[data-gesamt] strong').textContent, `800,00${NB}€`)
assert.equal(fx.finde(el, '[data-hinweis]').textContent, KLEINUNTERNEHMER)
assert.equal(fx.finde(el, '[data-zahlung]').textContent, ab().zahlungsplanText)
assert.deepEqual(paare(fx, fx.finde(el, '[data-abschnitt="termine"]')), [['Start', '12.10.2026'], ['Voraussichtliche Fertigstellung', '30.11.2026']])
}))
test('Zusammenfassung (Aufwand): Aufwandstext statt Tabelle und Gesamtpreis; ohne Termine kein Abschnitt', mit(async (fx) => {
const a = ab({ art: 'aufwand', positionen: [], summeCents: null, aufwandText: '100,00 € pro Stunde in Schritten von 15 Minuten.', startAm: '', fertigBis: '' })
const el = renderZusammenfassung(a)
assert.equal(el.getAttribute('data-art'), 'aufwand')
assert.equal(fx.finde(el, 'table'), null)
assert.equal(fx.finde(el, '[data-gesamt]'), null)
assert.equal(fx.finde(el, '[data-aufwand]').textContent, '100,00 € pro Stunde in Schritten von 15 Minuten.')
assert.equal(fx.finde(el, '[data-hinweis]').textContent, KLEINUNTERNEHMER)
assert.equal(fx.finde(el, '[data-abschnitt="termine"]'), null)
}))
test('Zusammenfassung: der § 19-Satz kommt nur aus der API; fehlt er, gibt es keinen Hinweis', mit(async (fx) => {
assert.equal(fx.finde(renderZusammenfassung(ab({ texte: { bestaetigung: 'x', fruehbeginn: null } })), '[data-hinweis]'), null)
}))
test('Zusammenfassung: Überschriftenebene wählbar (Seite h2, Portalkarte h4); leere Eingabe wirft nicht', mit(async (fx) => {
assert.deepEqual(fx.alle(renderZusammenfassung(ab()), 'h2').map((x) => x.textContent), ['Leistungsumfang', 'Zahlung', 'Termine'])
const karte = renderZusammenfassung(ab(), { ebene: 4 })
assert.equal(fx.alle(karte, 'h2').length, 0)
assert.deepEqual(fx.alle(karte, 'h4').map((x) => x.textContent), ['Leistungsumfang', 'Zahlung', 'Termine'])
assert.doesNotThrow(() => renderZusammenfassung({}))
}))
test('Fremdtext bleibt Text: Titel, Positionen und Zahlungsplan werden nie als HTML gelesen', mit(async (fx) => {
const boese = '<img src=x onerror=alert(1)> & <b>fett</b> "Test"'
const lang = 'x'.repeat(200)
const el = renderZusammenfassung(ab({
titel: boese, zahlungsplanText: boese, summeCents: 200,
positionen: [{ beschreibung: boese, menge: 1, einzelpreisCents: 100, summeCents: 100 }, { beschreibung: lang, menge: 1, einzelpreisCents: 100, summeCents: 100 }],
}))
assert.equal(fx.finde(el, 'img'), null)
assert.equal(fx.finde(el, 'b'), null)
assert.ok(fx.text(el).includes(boese))
assert.ok(fx.text(el).includes(lang))
}))
test('Bestätigt-Block: Zeitpunkt in Berliner Zeit und Zeile zum Beginn; ohne Bestätigung null', mit(async (fx) => {
assert.equal(renderBestaetigtBlock(ab()), null)
const block = renderBestaetigtBlock(bestaetigt(privat()))
assert.equal(block.getAttribute('role'), 'status')
assert.equal(block.getAttribute('tabindex'), '-1')
assert.equal(block.hasAttribute('data-bestaetigt'), true)
assert.deepEqual(fx.alle(block, 'p').map((p) => p.textContent), ['Bestätigt am 07.10.2026, 10:15 Uhr', 'Wir beginnen frühestens am 21.10.2026.'])
}))
test('Unterlagen: PDF und AGB als Links in neuem Tab, leere Ziele entfallen', mit(async (fx) => {
const links = fx.alle(renderDokumente({ pdfHref: '/api/auftrag/T/pdf', agbHref: '/rechtliches/agb-2026-09-30.pdf' }), 'a')
assert.deepEqual(links.map((a) => [a.getAttribute('data-link'), a.getAttribute('href'), a.textContent]), [
['pdf', '/api/auftrag/T/pdf', 'Auftragsbestätigung als PDF ansehen'],
['agb', '/rechtliches/agb-2026-09-30.pdf', 'Allgemeine Geschäftsbedingungen (PDF)'],
])
for (const a of links) {
assert.equal(a.getAttribute('target'), '_blank')
assert.equal(a.getAttribute('rel'), 'noopener noreferrer')
}
assert.equal(fx.alle(renderDokumente({ agbHref: '/rechtliches/agb-2026-09-30.pdf' }), 'a').length, 1)
assert.equal(renderDokumente({}), null)
}))
/* ------------------------------------------------------------ Bestätigen-Formular --- */
const OK = () => Promise.resolve({ ok: true, auftrag: bestaetigt(ab()) })
function monte(fx, a, { antwort = OK, gesperrt = '', anrede } = {}) {
const aufrufe = []
const erfolg = []
const ende = []
const form = renderBestaetigung(a, {
senden: (koerper) => { aufrufe.push(koerper); return antwort(koerper) },
nachErfolg: (x) => erfolg.push(x),
nachEnde: (status, body) => ende.push([status, body]),
gesperrt,
anrede,
})
fx.document.body.append(form)
return {
form, aufrufe, erfolg, ende,
knopf: fx.finde(form, '[data-aktion="bestaetigen"]'),
haken1: fx.finde(form, '[data-feld="bestaetigung"]'),
haken2: fx.finde(form, '[data-feld="fruehbeginn"]'),
fehler: fx.finde(form, '[data-fehler]'),
}
}
test('Bestätigen (Unternehmen): ein Häkchen mit dem Text vom Server, Knopf erst nach dem Haken, Antwort geht an nachErfolg', mit(async (fx) => {
const t = monte(fx, ab())
assert.equal(t.haken2, null)
assert.equal(fx.text(fx.finde(t.form, 'label')), 'Ich bestätige den Auftrag zu diesen Bedingungen.')
assert.equal(t.knopf.textContent, 'Auftrag bestätigen')
assert.equal(t.knopf.disabled, true)
await fx.klick(t.haken1)
assert.equal(t.knopf.disabled, false)
await fx.klick(t.haken1)
assert.equal(t.knopf.disabled, true)
await fx.klick(t.haken1)
await fx.klick(t.knopf)
assert.deepEqual(t.aufrufe, [{ bestaetigt: true, fruehbeginn: false }])
assert.equal(t.erfolg.length, 1)
assert.equal(t.erfolg[0].status, 'bestaetigt')
assert.equal(fx.text(fx.finde(t.form, '.ab-notiz')), 'Ihre Bestätigung wird mit Datum und Uhrzeit gespeichert.')
}))
test('Bestätigen: fehlt der Text vom Server, gilt der feste Wortlaut aus der Übersicht (V7)', mit(async (fx) => {
const t = monte(fx, ab({ texte: {} }))
assert.equal(fx.text(fx.finde(t.form, 'label')), 'Ich bestätige den Auftrag zu diesen Bedingungen.')
}))
test('Bestätigen (Privatperson): zweites, freiwilliges Häkchen; ohne Haken fruehbeginn false, mit Haken true', mit(async (fx) => {
const a = privat()
for (const [angehakt, soll] of [[false, false], [true, true]]) {
const t = monte(fx, a)
const zweites = fx.alle(t.form, 'label')[1]
assert.equal(fx.text(zweites).startsWith(a.texte.fruehbeginn), true)
assert.match(fx.text(zweites), /\(freiwillig\)$/)
assert.equal(t.haken2.checked, false)
await fx.klick(t.haken1)
assert.equal(t.knopf.disabled, false, 'das zweite Häkchen ist nicht nötig')
if (angehakt) await fx.klick(t.haken2)
await fx.klick(t.knopf)
assert.deepEqual(t.aufrufe, [{ bestaetigt: true, fruehbeginn: soll }])
t.form.remove()
}
}))
test('Bestätigen: Privatperson ohne Frühbeginn-Text vom Server bekommt kein zweites Häkchen und sendet false', mit(async (fx) => {
const t = monte(fx, privat({ texte: { bestaetigung: 'Ich bestätige den Auftrag zu diesen Bedingungen.', fruehbeginn: null } }))
assert.equal(t.haken2, null)
await fx.klick(t.haken1)
await fx.klick(t.knopf)
assert.deepEqual(t.aufrufe, [{ bestaetigt: true, fruehbeginn: false }])
}))
test('Doppelter Absenden-Versuch sendet genau einmal; Knopf zeigt „Wird gesendet …“ und bleibt nach Erfolg gesperrt', mit(async (fx) => {
let fertig
const t = monte(fx, ab(), { antwort: () => new Promise((resolve) => { fertig = resolve }) })
await fx.klick(t.haken1)
const erster = fx.klick(t.knopf)
assert.equal(t.knopf.textContent, 'Wird gesendet …')
assert.equal(t.knopf.disabled, true)
assert.equal(t.knopf.getAttribute('aria-busy'), 'true')
await fx.sende(t.form, 'submit') // zweiter Versuch (z. B. Enter), während der erste läuft
assert.equal(t.aufrufe.length, 1)
fertig({ ok: true, auftrag: bestaetigt(ab()) })
await erster
assert.equal(t.erfolg.length, 1)
assert.equal(t.knopf.disabled, true)
await fx.sende(t.form, 'submit')
assert.equal(t.aufrufe.length, 1, 'auch nach dem Erfolg geht nichts mehr raus')
}))
test('Netzfehler: Meldung, Häkchen bleiben stehen, erneutes Senden möglich', mit(async (fx) => {
const antworten = [{ ok: false, status: 0, body: {} }, { ok: true, auftrag: bestaetigt(privat()) }]
const t = monte(fx, privat(), { antwort: async () => antworten.shift() })
await fx.klick(t.haken1)
await fx.klick(t.haken2)
await fx.klick(t.knopf)
assert.equal(t.fehler.hidden, false)
assert.equal(t.fehler.textContent, 'Keine Verbindung. Bitte prüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.')
assert.equal(t.haken1.checked, true)
assert.equal(t.haken2.checked, true)
assert.equal(t.knopf.disabled, false)
assert.equal(t.knopf.textContent, 'Auftrag bestätigen')
assert.equal(t.erfolg.length, 0)
await fx.klick(t.knopf)
assert.deepEqual(t.aufrufe, [{ bestaetigt: true, fruehbeginn: true }, { bestaetigt: true, fruehbeginn: true }])
assert.equal(t.erfolg.length, 1)
}))
test('Fehlertexte nach Status; 404 und 410 gehen an nachEnde statt in die Fehlerzeile', mit(async (fx) => {
for (const [status, text] of [
[429, 'Zu viele Anfragen. Bitte versuchen Sie es in einigen Minuten noch einmal.'],
[500, 'Die Bestätigung konnte nicht gespeichert werden. Bitte versuchen Sie es später noch einmal.'],
[400, 'Bitte setzen Sie das Häkchen, um den Auftrag zu bestätigen.'],
]) {
const t = monte(fx, ab(), { antwort: async () => ({ ok: false, status, body: { error: 'Servertext, der nie erscheint' } }) })
await fx.klick(t.haken1)
await fx.klick(t.knopf)
assert.equal(t.fehler.textContent, text, String(status))
t.form.remove()
}
const body = { error: 'Diese Auftragsbestätigung wurde ersetzt.' }
const t = monte(fx, ab(), { antwort: async () => ({ ok: false, status: 410, body }) })
await fx.klick(t.haken1)
await fx.klick(t.knopf)
assert.deepEqual(t.ende, [[410, body]])
assert.equal(t.fehler.hidden, true)
}))
test('Gesperrt (Ansichtsmodus): Häkchen und Knopf gesperrt, Hinweis sichtbar, kein Senden', mit(async (fx) => {
const hinweis = 'Im Ansichtsmodus kann der Auftrag nicht bestätigt werden. Die Bestätigung gibt der Kunde selbst ab.'
const t = monte(fx, privat(), { gesperrt: hinweis })
assert.equal(fx.alle(t.form, 'input').every((i) => i.disabled), true)
assert.equal(t.knopf.disabled, true)
assert.equal(fx.finde(t.form, '[data-gesperrt]').textContent, hinweis)
await fx.sende(t.form, 'submit')
assert.equal(t.aufrufe.length, 0)
}))
test('Anrede: im Portal (Du) stehen Überschrift, Hinweis, Fehlertext und die Zeile zum Wunsch in der Du-Form, auf der öffentlichen Seite (Sie) in der Sie-Form', mit(async (fx) => {
const sie = monte(fx, ab())
assert.equal(fx.text(fx.finde(sie.form, 'legend')), 'Ihre Bestätigung')
assert.equal(fx.text(fx.finde(sie.form, '.ab-notiz')), 'Ihre Bestätigung wird mit Datum und Uhrzeit gespeichert.')
sie.form.remove()
const du = monte(fx, ab(), { anrede: ANREDE_DU, antwort: async () => ({ ok: false, status: 0, body: {} }) })
assert.equal(fx.text(fx.finde(du.form, 'legend')), 'Deine Bestätigung')
assert.equal(fx.text(fx.finde(du.form, '.ab-notiz')), 'Deine Bestätigung wird mit Datum und Uhrzeit gespeichert.')
await fx.klick(du.haken1)
await fx.klick(du.knopf)
assert.equal(du.fehler.textContent, 'Keine Verbindung. Bitte prüfe deine Internetverbindung und versuche es erneut.')
const wunsch = bestaetigt(privat({ fruehbeginn: true, fruehesterBeginn: null }))
assert.deepEqual(fx.alle(renderBestaetigtBlock(wunsch), 'p').map((p) => p.textContent), ['Bestätigt am 07.10.2026, 10:15 Uhr', 'Wir beginnen auf Ihren Wunsch schon vor Ablauf der Widerrufsfrist.'])
assert.deepEqual(fx.alle(renderBestaetigtBlock(wunsch, { anrede: ANREDE_DU }), 'p').map((p) => p.textContent), ['Bestätigt am 07.10.2026, 10:15 Uhr', 'Wir beginnen auf deinen Wunsch schon vor Ablauf der Widerrufsfrist.'])
}))
test('Quelltext: nur Text, nur ./auftrag-ansicht.js, kein fetch, keine Kundentexte im Modul', () => {
pruefeQuelltext('auftrag-dom.js', { importe: ['./auftrag-ansicht.js'] })
})