250 lines
13 KiB
JavaScript
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'] })
|
|
})
|