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

145 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { useTestEnv } from './support/testEnv.js'
import { beispielAuftrag, beispielSettings } from './auftraegePdfHilfe.js'
useTestEnv({ appwriteUrl: 'http://127.0.0.1:1', stripeUrl: 'http://127.0.0.1:1' })
const { auftragPdfDaten, paginiere, baueAtome, BEREICHE, SEITE, SPALTEN, INHALT_BREITE } = await import('../server/services/auftraege/pdf.js')
// Kleine Seiten: erste Seite 0–200 pt, Folgeseiten 20–200 pt
const SEITEN = { erste: { oben: 0, unten: 200 }, folge: { oben: 20, unten: 200 } }
const baustein = (id, hoehe, extra = {}) => ({ id, typ: 'absatz', hoehe, abstandDavor: 0, haengtAnNaechstem: false, umbruchDavor: false, kopf: null, ...extra })
const lage = (seiten) => seiten.map((s) => s.posten.map((p) => `${p.atom.id}${p.wiederholt ? '*' : ''}@${p.y}`))
test('Maße: A4, Inhaltsbreite und Spaltenbreiten passen zusammen', () => {
assert.equal(SEITE.breite, 595.28)
assert.equal(SEITE.hoehe, 841.89)
assert.ok(Math.abs(INHALT_BREITE - 481.88) < 1e-9)
assert.ok(Math.abs(SPALTEN.reduce((a, b) => a + b, 0) - INHALT_BREITE) < 1e-9)
assert.ok(BEREICHE.erste.oben < BEREICHE.folge.oben && BEREICHE.erste.unten === BEREICHE.folge.unten)
assert.ok(BEREICHE.erste.unten < SEITE.hoehe - 80, 'Platz für den Fuß bleibt frei')
})
test('paginiere: alles passt auf eine Seite, Abstände nur zwischen Bausteinen', () => {
const seiten = paginiere([baustein('a', 30, { abstandDavor: 9 }), baustein('b', 20, { abstandDavor: 8 }), baustein('c', 10, { abstandDavor: 8 })], SEITEN)
assert.deepEqual(lage(seiten), [['a@0', 'b@38', 'c@66']])
assert.deepEqual(paginiere([], SEITEN).map((s) => s.posten), [[]], 'auch ohne Inhalt gibt es eine Seite')
})
test('paginiere: was nicht mehr passt, beginnt die nächste Seite ohne Abstand davor', () => {
const seiten = paginiere([baustein('a', 170), baustein('b', 40, { abstandDavor: 5 }), baustein('c', 40, { abstandDavor: 5 })], SEITEN)
assert.deepEqual(lage(seiten), [['a@0'], ['b@20', 'c@65']])
assert.deepEqual(seiten.map((s) => s.nummer), [1, 2])
})
test('paginiere: Überschrift (haengtAnNaechstem) bleibt bei ihrem Absatz', () => {
const seiten = paginiere([baustein('a', 150), baustein('kopf', 15, { abstandDavor: 5, haengtAnNaechstem: true }), baustein('text', 40)], SEITEN)
assert.deepEqual(lage(seiten), [['a@0'], ['kopf@20', 'text@35']])
})
test('paginiere: umbruchDavor beginnt eine neue Seite, aber nie eine leere', () => {
assert.deepEqual(lage(paginiere([baustein('a', 30), baustein('b', 30, { umbruchDavor: true })], SEITEN)), [['a@0'], ['b@20']])
assert.deepEqual(lage(paginiere([baustein('a', 30, { umbruchDavor: true })], SEITEN)), [['a@0']])
assert.deepEqual(lage(paginiere([baustein('a', 200), baustein('b', 30, { umbruchDavor: true })], SEITEN)), [['a@0'], ['b@20']])
})
const tabelle = (zeilen, extra = {}) => {
const kopf = baustein('kopf', 20, { typ: 'tabellenkopf', haengtAnNaechstem: true })
const rows = Array.from({ length: zeilen }, (_, i) => baustein(`z${i + 1}`, 30, { typ: 'zeile', kopf, haengtAnNaechstem: i === zeilen - 1 && extra.mitSumme }))
return [kopf, ...rows, ...(extra.mitSumme ? [baustein('summe', 30, { haengtAnNaechstem: true }), baustein('hinweis', 10)] : [])]
}
test('paginiere: lange Tabelle läuft über mehrere Seiten, der Kopf wiederholt sich, keine Zeile wird geteilt', () => {
const seiten = paginiere(tabelle(20), SEITEN)
assert.deepEqual(lage(seiten).map((s) => s.length), [7, 6, 6, 5], 'Seite 1: Kopf + 6 Zeilen; Folgeseiten: wiederholter Kopf + je 5 Zeilen, Rest auf der letzten')
assert.equal(seiten.length, 4)
for (const seite of seiten.slice(1)) {
assert.equal(seite.posten[0].atom.id, 'kopf')
assert.equal(seite.posten[0].wiederholt, true)
assert.equal(seite.posten[0].y, 20)
}
const zeilen = seiten.flatMap((s) => s.posten.filter((p) => !p.wiederholt).map((p) => p.atom.id))
assert.deepEqual(zeilen, ['kopf', ...Array.from({ length: 20 }, (_, i) => `z${i + 1}`)])
for (const seite of seiten) for (const p of seite.posten) assert.ok(p.y + p.atom.hoehe <= seite.unten, `${p.atom.id} ragt über die Seite`)
})
test('paginiere: letzte Zeile, Summe und Hinweis bleiben zusammen, auch wenn die Zeile allein noch gepasst hätte', () => {
// Seite 1: Kopf (20) + 6 Zeilen (180) = 200 – Zeile 6 ist die letzte und hängt an Summe und Hinweis
const seiten = paginiere(tabelle(6, { mitSumme: true }), SEITEN)
assert.deepEqual(lage(seiten), [['kopf@0', 'z1@20', 'z2@50', 'z3@80', 'z4@110', 'z5@140'], ['kopf*@20', 'z6@40', 'summe@70', 'hinweis@100']])
})
test('paginiere: Kette größer als eine Seite wird zwischen Bausteinen getrennt, zu hoher Baustein steht allein', () => {
const kette = [baustein('a', 120, { haengtAnNaechstem: true }), baustein('b', 120, { haengtAnNaechstem: true }), baustein('c', 120)]
assert.deepEqual(lage(paginiere(kette, SEITEN)), [['a@0'], ['b@20'], ['c@20']])
assert.deepEqual(lage(paginiere([baustein('a', 50), baustein('riesig', 500), baustein('b', 50)], SEITEN)), [['a@0'], ['riesig@20'], ['b@20']])
})
// Messfunktion ohne pdfkit: 10 pt je angefangene 60 Zeichen
const messe = (text) => 10 * Math.max(1, Math.ceil(String(text).length / 60))
const bausteine = (extra = {}, optionen = {}) => baueAtome(auftragPdfDaten(beispielAuftrag(extra), { settings: beispielSettings, ...optionen }), messe)
const ids = (atome) => atome.map((a) => a.id)
test('baueAtome Festpreis: Reihenfolge, Zeilenhöhen und Zusammenhalt der Tabelle', () => {
const atome = bausteine()
assert.deepEqual(ids(atome), [
'briefkopf', 'einleitung', 'leistung', 'tabellenkopf', 'zeile-1', 'zeile-2', 'summe', 'kleinunternehmer',
'zahlung-kopf', 'zahlung', 'termine-kopf', 'termine', 'mitwirkung-kopf', 'mitwirkung', 'agb', 'gruss',
])
const nach = (id) => atome.find((a) => a.id === id)
assert.equal(nach('briefkopf').typ, 'briefkopf')
assert.ok(nach('briefkopf').hoehe > 150)
assert.equal(nach('briefkopf').masse.infoZeilen.length, 3)
assert.equal(nach('zeile-1').hoehe, 10 + 12, 'eine Zeile Text plus 2 × 6 pt Rand')
assert.deepEqual([nach('leistung'), nach('tabellenkopf'), nach('zeile-1'), nach('zeile-2'), nach('summe'), nach('kleinunternehmer')].map((a) => a.haengtAnNaechstem), [true, true, false, true, true, false])
assert.equal(nach('zeile-1').kopf, nach('tabellenkopf'))
assert.equal(nach('zeile-2').kopf, nach('tabellenkopf'))
assert.equal(nach('summe').label, 'Gesamtpreis')
assert.ok(atome.every((a) => a.hoehe > 0 && Number.isFinite(a.hoehe)))
})
test('baueAtome Aufwand, ersetzt und Privatperson: nur die passenden Bausteine', () => {
const aufwand = bausteine({ art: 'aufwand', positionen: [], summeCents: null, stundensatzCents: 10000, schaetzungMinuten: 600, zahlungsplan: { art: 'monatlich', prozent: null, text: '' } })
assert.ok(ids(aufwand).includes('aufwand') && !ids(aufwand).includes('tabellenkopf') && !ids(aufwand).includes('summe'))
assert.equal(aufwand.find((a) => a.id === 'aufwand').kasten, true)
const mitErsetzt = bausteine({}, { ersetzt: { nummer: 'AB-2026-0003', datum: '2026-10-01' } })
assert.ok(ids(mitErsetzt).indexOf('ersetzt') > ids(mitErsetzt).indexOf('agb') && ids(mitErsetzt).indexOf('ersetzt') < ids(mitErsetzt).indexOf('gruss'))
assert.equal(ids(bausteine()).some((id) => /^(widerruf|formular)/.test(id)), false)
const privat = bausteine({ kunde: { ...beispielAuftrag().kunde, kundenart: 'privat' } })
const titel = privat.find((a) => a.id === 'widerruf-titel')
assert.equal(titel.umbruchDavor, true)
assert.ok(ids(privat).indexOf('formular-titel') > ids(privat).indexOf('widerruf-titel'))
assert.equal(privat.find((a) => a.id === 'formular-titel').haengtAnNaechstem, true)
assert.equal(privat.at(-1).haengtAnNaechstem, false)
})
test('20 Positionen mit je 200 Zeichen ohne Leerzeichen: alle Zeilen da, mehrere Seiten, Tabellenkopf auf jeder Folgeseite', () => {
const positionen = Array.from({ length: 20 }, (_, i) => ({ beschreibung: 'X'.repeat(200), menge: i + 1, einzelpreisCents: 1000 }))
const summe = positionen.reduce((s, p) => s + p.menge * p.einzelpreisCents, 0)
const seiten = paginiere(bausteine({ positionen, summeCents: summe }), BEREICHE)
assert.ok(seiten.length >= 2)
const zeilen = seiten.flatMap((s) => s.posten.filter((p) => p.atom.typ === 'zeile').map((p) => p.atom.id))
assert.deepEqual(zeilen, Array.from({ length: 20 }, (_, i) => `zeile-${i + 1}`))
for (const seite of seiten.slice(1)) {
const ersteZeile = seite.posten.findIndex((p) => p.atom.typ === 'zeile')
if (ersteZeile >= 0) assert.equal(seite.posten[0].atom.typ, 'tabellenkopf', `Seite ${seite.nummer} beginnt mit dem Tabellenkopf`)
}
const letzte = seiten.at(-1).posten.map((p) => p.atom.id)
assert.ok(letzte.indexOf('summe') > letzte.indexOf('zeile-20'), 'Summe steht bei der letzten Zeile')
for (const seite of seiten) for (const p of seite.posten) assert.ok(p.y + p.atom.hoehe <= seite.unten + 1e-9)
})
test('lange Widerrufsbelehrung: beginnt auf einer neuen Seite, das Formular bleibt zusammen', () => {
const absatz = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '.repeat(12)
const d = auftragPdfDaten(beispielAuftrag({ kunde: { ...beispielAuftrag().kunde, kundenart: 'privat' } }), { settings: beispielSettings })
d.widerruf = { ...d.widerruf, belehrung: { titel: 'Widerrufsbelehrung', abschnitte: Array.from({ length: 6 }, (_, i) => ({ ueberschrift: `Abschnitt ${i + 1}`, absaetze: [absatz, absatz] })) } }
const seiten = paginiere(baueAtome(d, messe), BEREICHE)
const seiteVon = (id) => seiten.find((s) => s.posten.some((p) => p.atom.id === id)).nummer
assert.ok(seiten.length >= 4)
assert.ok(seiteVon('widerruf-titel') >= 2)
assert.equal(seiten[seiteVon('widerruf-titel') - 1].posten.find((p) => !p.wiederholt).atom.id, 'widerruf-titel', 'Belehrung beginnt oben auf einer neuen Seite')
const formular = seiten.flatMap((s) => s.posten.filter((p) => p.atom.id.startsWith('formular')).map(() => s.nummer))
assert.ok(formular.length >= 2 && new Set(formular).size === 1, 'alle Zeilen des Formulars stehen auf derselben Seite')
for (const seite of seiten) for (const p of seite.posten) assert.ok(p.y + p.atom.hoehe <= seite.unten + 1e-9)
})