diff --git a/public/auftrag-dom.js b/public/auftrag-dom.js
new file mode 100644
index 0000000..6e00d74
--- /dev/null
+++ b/public/auftrag-dom.js
@@ -0,0 +1,126 @@
+/* Auftragsbestätigung im Kundenportal: DOM-Bausteine (Zusammenfassung, Bestätigt-Block, Unterlagen, Bestätigen-Formular).
+ Kundentext und Servertext kommen ausschließlich über textContent in die Seite (h), nie über innerHTML.
+ Alle Texte stehen in auftrag-ansicht.js; die Anrede (Sie auf der öffentlichen Seite, Du im Portal) wählt der Aufrufer. */
+import { ANREDE_SIE, BESTAETIGUNG_FALLBACK, bestaetigtZeilen, fehlerMeldung, texte, zeigtFruehbeginn, zusammenfassung } from './auftrag-ansicht.js'
+
+/** Element bauen: Texte nur als Textknoten, Attribute mit on… als Ereignis, true setzt ein Attribut ohne Wert, false/null/undefined lassen es weg. */
+export function h(tag, attrs, ...kinder) {
+ const el = document.createElement(tag)
+ for (const [name, wert] of Object.entries(attrs || {})) {
+ if (wert === undefined || wert === null || wert === false) continue
+ if (name === 'class') el.className = wert
+ else if (name.startsWith('on')) el.addEventListener(name.slice(2), wert)
+ else el.setAttribute(name, wert === true ? '' : wert)
+ }
+ for (const kind of kinder.flat()) if (kind !== undefined && kind !== null && kind !== false) el.append(kind)
+ return el
+}
+
+const zeile = (label, wert) => h('div', { class: 'ab-zeile' }, h('dt', null, label), h('dd', null, wert))
+const liste = (zeilen) => h('dl', { class: 'ab-liste' }, zeilen.map((z) => zeile(z.label, z.wert)))
+
+/** Zusammenfassung der AB (Kopf, Leistung bzw. Aufwand, Zahlung, Termine); ebene = Überschriftenebene der Abschnitte. */
+export function renderZusammenfassung(auftrag, { ebene = 2 } = {}) {
+ const t = texte(ANREDE_SIE)
+ const z = zusammenfassung(auftrag)
+ const kopfzeile = (text) => h(`h${ebene}`, { class: 'ab-h2' }, text)
+ const abschnitt = (name, titel, ...inhalt) => h('section', { class: 'ab-abschnitt', 'data-abschnitt': name }, kopfzeile(titel), ...inhalt)
+ const hinweis = z.hinweis ? h('p', { class: 'ab-hinweis', 'data-hinweis': true }, z.hinweis) : null
+
+ const teile = []
+ if (z.kopf.length) teile.push(liste(z.kopf))
+ if (z.art === 'aufwand') {
+ teile.push(abschnitt('aufwand', t.aufwand, h('p', { class: 'ab-text', 'data-aufwand': true }, z.aufwand), hinweis))
+ } else {
+ const tabelle = z.positionen.length
+ ? h('table', { class: 'ab-tabelle', 'data-positionen': true },
+ h('thead', null, h('tr', null, t.spalten.map((spalte) => h('th', { scope: 'col' }, spalte)))),
+ h('tbody', null, z.positionen.map((p) => h('tr', { 'data-position': true },
+ [p.beschreibung, p.menge, p.einzelpreis, p.summe].map((wert, i) => h('td', { 'data-label': t.spalten[i] }, wert))))))
+ : null
+ const gesamt = z.summe ? h('p', { class: 'ab-gesamt', 'data-gesamt': true }, h('span', null, t.gesamtpreis), ' ', h('strong', null, z.summe)) : null
+ teile.push(abschnitt('leistung', t.leistung, tabelle, gesamt, hinweis))
+ }
+ if (z.zahlung) teile.push(abschnitt('zahlung', t.zahlung, h('p', { class: 'ab-text', 'data-zahlung': true }, z.zahlung)))
+ if (z.termine.length) teile.push(abschnitt('termine', t.termine, liste(z.termine)))
+ return h('div', { class: 'ab-zusammenfassung', 'data-art': z.art }, teile)
+}
+
+/** Block „bestätigt“ (Zeitpunkt, bei Privatpersonen Zeile zum Beginn); null ohne Bestätigung. */
+export function renderBestaetigtBlock(auftrag, { anrede = ANREDE_SIE } = {}) {
+ const zeilen = bestaetigtZeilen(auftrag, anrede)
+ if (!zeilen.length) return null
+ return h('div', { class: 'ab-bestaetigt', 'data-bestaetigt': true, role: 'status', tabindex: '-1' },
+ zeilen.map((text, i) => h('p', null, i === 0 ? h('strong', null, text) : text)))
+}
+
+/** Links auf die AB als PDF und die AGB (neuer Tab); null ohne Ziele. */
+export function renderDokumente({ pdfHref = '', agbHref = '' } = {}) {
+ const t = texte(ANREDE_SIE)
+ const link = (art, href, text) => h('li', null, h('a', { class: 'ab-link', 'data-link': art, href, target: '_blank', rel: 'noopener noreferrer' }, text))
+ const eintraege = [pdfHref ? link('pdf', pdfHref, t.linkPdf) : null, agbHref ? link('agb', agbHref, t.linkAgb) : null].filter(Boolean)
+ return eintraege.length ? h('ul', { class: 'ab-links' }, eintraege) : null
+}
+
+/**
+ * Formular zum Bestätigen: ein Häkchen, bei Privatpersonen ein zweites, freiwilliges (Frühbeginn). Sendet genau einmal:
+ * senden(koerper) → { ok: true, auftrag } | { ok: false, status, body }; nachErfolg(auftrag); nachEnde(status, body) bei 404/410.
+ * gesperrt (Text) = Ansichtsmodus: Häkchen und Knopf gesperrt, Hinweis sichtbar.
+ */
+export function renderBestaetigung(auftrag, { senden, nachErfolg, nachEnde, gesperrt = '', anrede = ANREDE_SIE } = {}) {
+ const t = texte(anrede)
+ let busy = false
+ const haken1 = h('input', { type: 'checkbox', 'data-feld': 'bestaetigung', required: true, disabled: Boolean(gesperrt) })
+ const haken2 = zeigtFruehbeginn(auftrag) ? h('input', { type: 'checkbox', 'data-feld': 'fruehbeginn', disabled: Boolean(gesperrt) }) : null
+ const fehler = h('p', { class: 'ab-fehler', 'data-fehler': true, role: 'alert', hidden: true })
+ const knopf = h('button', { type: 'submit', class: 'ab-knopf', 'data-aktion': 'bestaetigen', disabled: true }, t.knopf)
+
+ const aktualisiere = () => { knopf.disabled = Boolean(gesperrt) || busy || !haken1.checked }
+
+ const form = h('form', {
+ class: 'ab-bestaetigen',
+ novalidate: true,
+ onsubmit: async (ev) => {
+ ev.preventDefault()
+ if (busy || gesperrt || !haken1.checked) return
+ busy = true
+ fehler.hidden = true
+ knopf.disabled = true
+ knopf.setAttribute('aria-busy', 'true')
+ knopf.textContent = t.sendet
+ let ergebnis
+ try {
+ ergebnis = await senden({ bestaetigt: true, fruehbeginn: Boolean(haken2?.checked) })
+ } catch {
+ ergebnis = { ok: false, status: 0, body: {} }
+ }
+ knopf.removeAttribute('aria-busy')
+ knopf.textContent = t.knopf
+ if (ergebnis?.ok) {
+ nachErfolg?.(ergebnis.auftrag)
+ return
+ }
+ const status = ergebnis?.status ?? 0
+ if (status === 404 || status === 410) {
+ nachEnde?.(status, ergebnis?.body)
+ return
+ }
+ busy = false
+ aktualisiere()
+ fehler.textContent = fehlerMeldung(status, anrede)
+ fehler.hidden = false
+ },
+ },
+ h('fieldset', { class: 'ab-feldgruppe' },
+ h('legend', null, t.formular),
+ h('label', { class: 'ab-haken' }, haken1, h('span', null, auftrag?.texte?.bestaetigung || BESTAETIGUNG_FALLBACK)),
+ haken2 ? h('label', { class: 'ab-haken' }, haken2, h('span', null, auftrag.texte.fruehbeginn, ' ', h('small', { class: 'ab-frei' }, t.freiwillig))) : null),
+ fehler,
+ knopf,
+ h('p', { class: 'ab-notiz' }, t.speicherHinweis),
+ gesperrt ? h('p', { class: 'ab-gesperrt', 'data-gesperrt': true }, gesperrt) : null)
+
+ haken1.addEventListener('change', aktualisiere)
+ aktualisiere()
+ return form
+}
diff --git a/test/auftraegeKundenportalDom.test.js b/test/auftraegeKundenportalDom.test.js
new file mode 100644
index 0000000..e5cd2bb
--- /dev/null
+++ b/test/auftraegeKundenportalDom.test.js
@@ -0,0 +1,249 @@
+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 = '
& fett "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'] })
+})