Abo-Bedingungen Fassung 6: Seite im neuen Stil (Entwurf)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-16 15:53:49 +00:00
1 parent 7f4e84e0b1
commit 90b7f10f1a
2 files changed
+552

No files matched your search

+87
View File
@@ -39,3 +39,90 @@ test('Entwurf Fassung 6: Stellen in Reihenfolge, jeder Pflichtsatz im Wortlaut u
}
assert.doesNotMatch(md.slice(0, md.indexOf('## Was gleich bleibt')), /€/, 'die Pflichtsätze nennen keinen Preis')
})
// Hilfen aus test/legalTexts.test.js (kein Import möglich)
const strip = (html) => html.replace(/<[^>]+>/g, ' ').replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/\s+/g, ' ').trim()
const between = (s, a, b) => {
const i = s.indexOf(a)
assert.ok(i >= 0, `Text fehlt: ${a}`)
const j = s.indexOf(b, i)
assert.ok(j > i, `Text fehlt: ${b}`)
return s.slice(i, j)
}
const V6 = 'public/neu/rechtliches/abo-bedingungen.html', V5A = 'public/vertragsunterlagen/v5/abo-bedingungen.html'
const abschnitt = (html, von, bis) => {
const a = von ? html.indexOf(`id="${von}"`) : 0
assert.ok(a >= 0, `Anker #${von}`)
const b = bis ? html.indexOf(`id="${bis}"`, a + 1) : html.length
assert.ok(b > a, `Anker #${bis}`)
return strip(html.slice(a, b))
}
test('v6-Seite: gleiche Anker wie Fassung 5, Absatzzahlen der geänderten Paragrafen', () => {
const h = lies(V6), ids = (x) => [...x.matchAll(/ id="([^"]+)"/g)].map((m) => m[1])
assert.deepEqual(ids(h), ids(lies(V5A)))
const n = (von, bis) => [...h.slice(h.indexOf(`id="${von}"`), h.indexOf(`id="${bis}"`)).matchAll(/^ <li>([\s\S]*?)<\/li>$/gm)].length
assert.deepEqual([['p1', 'p2'], ['p2', 'p3'], ['p4', 'p5'], ['p6', 'p7'], ['wechsel', 'zusatzspeicher'], ['p11', 'p12'], ['p14', 'p15']].map(([a, b]) => n(a, b)), [4, 6, 9, 6, 6, 4, 2])
assert.equal((h.slice(h.indexOf('id="p3"'), h.indexOf('id="p4"')).match(/<h3>\(\d\)/g) ?? []).length, 8)
})
test('v6-Seite: Version, Stand, Entwurfsmarke und PDF-Link passen zu termsVersionNeu', () => {
const m = /^(\d{4})-(\d{2})-(\d{2})-v6$/.exec(config.abo.termsVersionNeu); assert.ok(m)
const h = lies(V6), [, y, mo, d] = m
assert.ok(strip(h).includes(`Version 6 · Stand ${d}.${mo}.${y}`))
assert.ok(h.includes(`content: "Version 6 · Stand ${d}.${mo}.${y}"`), 'Kopfzeile im Druck')
assert.ok(!strip(h).includes('Version 5'))
assert.equal(strip(h).includes('Entwurf – Prüfung durch Kanzlei offen'), ENTWURF)
assert.ok(h.includes('href="/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf"') && !h.includes('href="/WEBklar-Abo-Vertragsunterlagen.pdf"'))
})
test('v6-Seite: jede Pflichtstelle steht im Wortlaut an ihrem Platz', () => {
const h = lies(V6)
for (const p of PFLICHT) for (const t of p.texte) assert.ok(abschnitt(h, p.von, p.bis).includes(t), `${p.stelle}: ${t.slice(0, 50)}`)
assert.ok(strip(h).includes(`Funktionen ${PFLICHT[0].texte[0]}`), 'Zeile „Funktionen“ in der Übersichtstabelle')
})
test('v6-Seite: Widerrufsbelehrung und Muster-Formular wortgleich mit Fassung 5, Abo-Preise unverändert, Funktionen ohne Preis', () => {
const h = lies(V6), v5 = lies(V5A)
const muster = (x) => between(abschnitt(x, 'widerruf', 'formular'), 'Widerrufsrecht Sie haben das Recht', 'Ende der Widerrufsbelehrung')
assert.equal(muster(h), muster(v5))
const formular = (x) => between(abschnitt(x, 'formular', 'widerruf-hinweise'), 'Muster-Widerrufsformular', '(*) Unzutreffendes streichen.')
assert.equal(formular(h), formular(v5))
for (const s of ['20,00 €', '50,00 €', '1,00 €', '100 € pro Stunde', 'zwölf Monaten']) assert.ok(strip(h).includes(s), s)
assert.doesNotMatch(abschnitt(h, 'p3', 'p4').split('(8) Funktionen')[1], /€/)
})
test('v6-Seite: neues Aussehen, keine alten Farben, Druckkopf', () => {
const style = lies(V6).match(/<style>([\s\S]*?)<\/style>/)[1]
for (const t of ['#1a1f36', '#5e6678', '#ebeef1', '#d8dde3', '#f8f9fb', '#111', '-apple-system', 'color-scheme: light']) assert.ok(style.includes(t), t)
assert.doesNotMatch(style, /#6d1fd1|#9d3dfe|gradient|prefers-color-scheme|backdrop-filter|#f7f6f3/)
})
test('v6-Seite: Querverweise treffen (16 Paragrafen, mindestens 15 Verweise, Leistungsbeschreibung A/B Nr.)', () => {
const html = lies(V6)
const ids = [...html.matchAll(/ id="([^"]+)"/g)].map((m) => m[1])
for (const [, ref] of html.matchAll(/href="#([^"]+)"/g)) assert.equal(ids.filter((i) => i === ref).length, 1, `#${ref}`)
// Absätze je Paragraf: <li> der ol.abs auf oberster Ebene; bei § 3 die Überschriften "(M)"
const absaetze = {}
for (const [, n, body] of html.matchAll(/<h2 id="[^"]+">§ (\d+) [^<]*<\/h2>([\s\S]*?)(?=<h2 |<p class="anlage-kopf)/g)) {
const h3 = [...body.matchAll(/<h3>\((\d+)\)/g)].map((x) => Number(x[1]))
if (h3.length) { absaetze[n] = Math.max(...h3); continue }
const start = body.indexOf('<ol class="abs">')
let depth = 0, count = 0
if (start >= 0) for (const [, close, tag] of body.slice(start).matchAll(/<(\/?)(ol|ul|li)\b[^>]*>/g)) {
if (tag === 'li') { if (!close && depth === 1) count++; continue }
depth += close ? -1 : 1
if (depth === 0) break
}
absaetze[n] = count
}
assert.equal(Object.keys(absaetze).length, 16)
assert.deepEqual([absaetze[3], absaetze[4], absaetze[6], absaetze[9], absaetze[11], absaetze[14], absaetze[15]], [8, 9, 6, 6, 4, 2, 2])
const text = strip(html)
const refs = [...text.matchAll(/§ (\d+) Abs\. (\d+)/g)]
assert.ok(refs.length >= 15, `Querverweise: ${refs.length}`)
for (const [ref, n, a] of refs) assert.ok(Number(a) <= (absaetze[n] ?? 0), `${ref}: § ${n} hat ${absaetze[n] ?? 0} Absätze`)
// Leistungsbeschreibung A/B Nr. N: nummerierte <h3> der jeweiligen Beschreibung
const nummern = (fromId, toId) => [...html.slice(html.indexOf(`id="${fromId}"`), html.indexOf(`id="${toId}"`)).matchAll(/<h3>(\d+)\. /g)].map((x) => Number(x[1]))
const maxA = Math.max(...nummern('hosting', 'wartung')), maxB = Math.max(...nummern('wartung', 'widerruf'))
for (const [ref, ab, nr] of text.matchAll(/Leistungsbeschreibung ([AB]) Nr\. (\d+)/g)) assert.ok(Number(nr) <= (ab === 'A' ? maxA : maxB), ref)
})
test('v6-Seite: Entwurf ist nicht indexierbar (N45)', () => {
assert.equal(lies(V6).includes('<meta name="robots" content="noindex">'), ENTWURF)
})