build-legal-html: Satz auftraege für die Widerrufsbelehrung der Projektaufträge und die AGB, beide Seiten erzeugt

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-07 14:12:23 +00:00
1 parent edccd4d8af
commit aeae17b491
4 files changed
+485 -5

No files matched your search

+123
View File
@@ -426,3 +426,126 @@ test('agb_text_gleich_website: die Kopie ist wortgleich mit der Seite AGB.tsx de
assert.ok(kopie === website, `Die AGB-Kopie weicht von den AGB der Website ab (${ersterUnterschied(kopie, website)}). Ist die Website neu, braucht die Kopie eine neue Fassung (AGB_FASSUNG, agb.md, Seite, PDF, Hash).`)
assert.equal(sha256(website), AGB_TEXT_SHA256)
})
// ---------------------------------------------------------------------------------------------------------- Seiten der Widerrufsbelehrung und der AGB
// jedes Dokument wird in den Tests neu geholt, damit ein fehlender Generator nur die betroffenen Tests scheitern lässt (nicht das Laden der Datei)
const dokumente = () => gen.auftraegeDokumente()
const klartext = (html) =>
html
.replace(/<style>[\s\S]*?<\/style>/g, '')
.replace(/<[^>]+>/g, ' ')
.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&')
.replace(/\s+/g, ' ')
.trim()
const quelleVon = (d) => (d.markdown ? d.markdown() : lies(d.md))
test('auftraegeDokumente: widerruf (aus texte.js) und agb (aus docs/rechtliches/agb.md) mit Quelle, Ziel und Navigation auf die Fassungen', () => {
const DOKUMENTE = dokumente()
assert.deepEqual(DOKUMENTE.map((d) => d.doc), ['widerruf', 'agb'])
assert.deepEqual(
DOKUMENTE.map((d) => [d.md, d.html]),
[
[null, `public/rechtliches/widerrufsbelehrung-projektauftrag-${texte.TEXTE_FASSUNG}.html`],
['docs/rechtliches/agb.md', `public/rechtliches/agb-${AGB_FASSUNG}.html`],
],
)
assert.deepEqual(DOKUMENTE.map((d) => d.meta.fassung), [texte.TEXTE_FASSUNG, AGB_FASSUNG])
const adressen = DOKUMENTE.map((d) => d.html.replace(/^public/, ''))
for (const d of DOKUMENTE) {
const hrefs = d.meta.nav.map((l) => l.href)
for (const a of adressen) assert.ok(hrefs.includes(a), `Navigation von ${d.doc} verlinkt ${a}`)
assert.ok(hrefs.includes('/abo-bedingungen.html') && hrefs.includes('/datenschutz.html'))
}
// andere Fassungen wirken auf Dateinamen und Meta; die Quelle der Belehrung folgt der Fassung
const andere = gen.auftraegeDokumente('2027-01-02', '2027-01-03')
assert.equal(andere[0].html, 'public/rechtliches/widerrufsbelehrung-projektauftrag-2027-01-02.html')
assert.equal(andere[1].html, 'public/rechtliches/agb-2027-01-03.html')
assert.ok(andere[0].markdown().includes('**Fassung 2027-01-02**'))
})
test('auftraege_html_gleich_generator: die eingecheckten Seiten sind genau render(Quelle), deterministisch, ohne Datum außer den Fassungen; --check Exit 0', () => {
const DOKUMENTE = dokumente()
for (const d of DOKUMENTE) {
const md = quelleVon(d)
const html = gen.render(md, d.meta)
assert.equal(lies(d.html), html, `${d.html} weicht vom Generator ab (node scripts/build-legal-html.mjs auftraege)`)
assert.equal(gen.render(md, d.meta), html, 'deterministisch: zweiter Lauf gleiche Bytes')
assert.doesNotMatch(html.replaceAll(texte.TEXTE_FASSUNG, '').replaceAll(AGB_FASSUNG, ''), /\b\d{4}-\d{2}-\d{2}\b/, `${d.html}: Datum außer den Fassungen`)
}
const lauf = spawnSync(process.execPath, ['--max-old-space-size=512', 'scripts/build-legal-html.mjs', 'auftraege', '--check'], { cwd: ROOT, encoding: 'utf8', timeout: 60000 })
assert.equal(lauf.status, 0, lauf.stdout + lauf.stderr)
assert.equal([...lauf.stdout.matchAll(/^ok /gm)].length, 2)
})
test('auftraege_html_text_gleich_quelle: der sichtbare Text jeder Seite ist genau der Text der Quelle (plus Navigation und Marke)', () => {
const DOKUMENTE = dokumente()
for (const d of DOKUMENTE) {
const aus = quelleVon(d).replace(/\r/g, '').split('\n').filter((z) => z.trim() !== '').map((z) => z.replace(/^#{1,3} /, '').replace(/^- /, ''))
const erwartet = norm(aus.join(' ').replace(/\*\*|`/g, ''))
const html = lies(d.html)
const main = html.slice(html.indexOf('<main>'), html.indexOf('</main>')).replace(/<nav class="top">[\s\S]*?<\/nav>/, '').replace('<div class="brand">WEBKLAR</div>', '')
assert.equal(klartext(main), erwartet, `${d.html}: sichtbarer Text weicht von der Quelle ab`)
}
})
test('auftraege_html_sauber: keine Kommentare, Entwurfsmarken, Skripte oder Ereignis-Attribute; Fassung im Kopf; Links nur relativ oder https', () => {
const DOKUMENTE = dokumente()
for (const d of DOKUMENTE) {
const html = lies(d.html)
assert.doesNotMatch(html, /<!--|Entwurf|ENTWURF|Prüfpunkt|<script|<iframe|<img|<form|<link|\sstyle=|\ssrc=/, `${d.html}: verbotener Inhalt`)
assert.doesNotMatch(html, /\son[a-z]+\s*=/i, `${d.html}: Ereignis-Attribut`)
assert.doesNotMatch(html, /⚖|\bQ\d+\b|\{\{|\}\}|undefined|NaN/, `${d.html}: Marker oder Platzhalter`)
assert.ok(html.includes(`<strong>Fassung ${d.meta.fassung}</strong>`), `${d.html}: Fassung fehlt im Kopf`)
assert.ok(html.includes(`<meta name="webklar-fassung" content="${d.meta.fassung}">`))
assert.match(html, /<html lang="de">/)
assert.match(html, /<title>[^<]+ – WEBklar<\/title>/)
const hrefs = [...html.matchAll(/\shref="([^"]*)"/g)].map((m) => m[1])
assert.ok(hrefs.length >= 6)
for (const h of hrefs) assert.ok(/^\/(?!\/)/.test(h) || /^https:\/\//.test(h), `${d.html}: Link ${h}`)
assert.ok(html.includes(`content: "WEBklar · ${d.meta.seitenkopf}"`))
assert.equal([...html.matchAll(/<h1>/g)].length, 1)
assert.ok(html.includes('<div class="brand">WEBKLAR</div>'))
}
})
test('auftraege_lauf: --check meldet fehlende oder abweichende Seiten mit Exit 1 und schreibt nichts; ohne --check werden beide Seiten geschrieben; Aufruf nennt alle Sätze', () => {
const DOKUMENTE = dokumente()
const dir = mkdtempSync(join(tmpdir(), 'legalhtml-auftraege-'))
try {
mkdirSync(join(dir, 'docs/rechtliches'), { recursive: true })
mkdirSync(join(dir, 'public/rechtliches'), { recursive: true })
cpSync(join(ROOT, AGB_MD), join(dir, AGB_MD))
const zeilen = []
const ruf = (...args) => gen.lauf(args, { root: dir, log: (z) => zeilen.push(z), fehler: (z) => zeilen.push(z) })
// noch keine Seiten: --check meldet beide und schreibt nichts
assert.equal(ruf('auftraege', '--check'), 1)
assert.equal(zeilen.filter((z) => z.startsWith('ABWEICHT')).length, 2)
assert.equal(existsSync(join(dir, DOKUMENTE[0].html)), false, '--check schreibt nichts')
// schreiben, dann stimmt --check, und die Bytes sind die der eingecheckten Seiten
assert.equal(ruf('auftraege'), 0)
assert.equal(ruf('auftraege', '--check'), 0)
for (const d of DOKUMENTE) assert.equal(readFileSync(join(dir, d.html), 'utf8'), lies(d.html), 'dieselben Bytes wie die eingecheckte Seite')
// eine Seite von Hand geändert, die andere Quelle geändert: Abweichung, die Dateien bleiben wie sie sind
const ziel = join(dir, DOKUMENTE[1].html)
const handgemacht = readFileSync(ziel, 'utf8').replace('<h2>1. Vertragsgegenstand</h2>', '<h2>1. Vertragsgegenstand (von Hand)</h2>')
assert.notEqual(handgemacht, readFileSync(ziel, 'utf8'))
writeFileSync(ziel, handgemacht)
zeilen.length = 0
assert.equal(ruf('auftraege', '--check'), 1)
assert.deepEqual(zeilen.filter((z) => z.startsWith('ABWEICHT')), [`ABWEICHT ${DOKUMENTE[1].html}`])
assert.equal(readFileSync(ziel, 'utf8'), handgemacht, '--check ändert die Datei nicht')
writeFileSync(ziel, lies(DOKUMENTE[1].html))
writeFileSync(join(dir, AGB_MD), `${lies(AGB_MD)}\n## 15. Neu\n\n(1) Neuer Absatz.\n`)
assert.equal(ruf('auftraege', '--check'), 1)
// falscher Aufruf: Exit 2, die Meldung nennt termine, shop und auftraege
zeilen.length = 0
assert.equal(ruf('analyse'), 2)
assert.equal(ruf('auftraege', '--schreiben'), 2)
assert.equal(ruf(), 2)
assert.match(zeilen[0], /Aufruf: node scripts\/build-legal-html\.mjs termine\|shop \[--check\]/)
assert.match(zeilen[0], /node scripts\/build-legal-html\.mjs auftraege \[--check\]/)
} finally {
rmSync(dir, { recursive: true, force: true })
}
})