Kundenportal Aufträge: reine Ansichtslogik (Token, Beträge, Zustände, Status, AB-Nummer)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
987268e2f0
commit
e6a0fc9acf
3 files changed
+536
No files matched your search
@@ -0,0 +1,211 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import {
|
||||
TOKEN_MUSTER, KONTAKT, ANREDE_SIE, ANREDE_DU, TEXTE_GEMEINSAM, TEXTE_SIE, TEXTE_DU, texte, tokenAusAdresse, euroDe, datumDe, zeitpunktDe,
|
||||
pdfPfad, agbPfad, zeigtFruehbeginn, darfBestaetigen, statusAnzeige, kartenMeta, zusammenfassung, bestaetigtZeilen, zustandAusAntwort,
|
||||
fehlerMeldung, sortiereAuftraege, abNummerNormalisiert,
|
||||
} from '../public/auftrag-ansicht.js'
|
||||
import { TOKEN, BESTAETIGT_AM, KLEINUNTERNEHMER, ab, privat, bestaetigt, hatDuForm, hatSieForm, pruefeQuelltext } from './auftraegeKundenportalDaten.js'
|
||||
|
||||
const NB = '\u{a0}' // geschütztes Leerzeichen vor „€“ (wie euroDe auf dem Server)
|
||||
|
||||
test('Token: 43 Zeichen base64url, zuerst aus #t= (so verlinkt K31), sonst aus ?t=, nie mehrdeutig', () => {
|
||||
assert.equal(TOKEN.length, 43)
|
||||
assert.match(TOKEN, TOKEN_MUSTER)
|
||||
assert.equal(tokenAusAdresse('', `#t=${TOKEN}`), TOKEN)
|
||||
assert.equal(tokenAusAdresse(`?t=${TOKEN}`, ''), TOKEN)
|
||||
assert.equal(tokenAusAdresse(`?x=1&t=${TOKEN}`, ''), TOKEN)
|
||||
const anderer = 'y'.repeat(43)
|
||||
assert.equal(tokenAusAdresse(`?t=${TOKEN}`, `#t=${anderer}`), anderer, 'das Fragment gewinnt')
|
||||
assert.equal(tokenAusAdresse(`?t=${TOKEN}`, '#t=abc'), TOKEN, 'ungültiges Fragment: weiter mit der Suche')
|
||||
for (const hash of ['', '#', '#t=', '#t=abc', `#t=${TOKEN}x`, `#t=${TOKEN.slice(0, 42)}!`, `#t=${TOKEN}&t=${TOKEN}`, `#token=${TOKEN}`]) {
|
||||
assert.equal(tokenAusAdresse('', hash), '', hash)
|
||||
}
|
||||
assert.equal(tokenAusAdresse(`?t=${TOKEN}&t=${TOKEN}`, ''), '')
|
||||
assert.equal(tokenAusAdresse(undefined, undefined), '')
|
||||
})
|
||||
|
||||
test('Beträge: ganze Cent als deutscher Euro-Betrag wie auf dem Server', () => {
|
||||
assert.equal(euroDe(80000), `800,00${NB}€`)
|
||||
assert.equal(euroDe(123456), `1.234,56${NB}€`)
|
||||
assert.equal(euroDe(5), `0,05${NB}€`)
|
||||
assert.equal(euroDe(0), `0,00${NB}€`)
|
||||
assert.equal(euroDe(-449), `−4,49${NB}€`)
|
||||
for (const kaputt of [1.5, '800', null, undefined, Number.NaN]) assert.equal(euroDe(kaputt), '–')
|
||||
})
|
||||
|
||||
test('Datum und Zeit: Berliner Zeit, unabhängig von der Zeitzone des Browsers', () => {
|
||||
assert.equal(datumDe('2026-10-06'), '06.10.2026')
|
||||
assert.equal(datumDe('06.10.2026'), '06.10.2026')
|
||||
assert.equal(datumDe('2026-10-06T22:30:00.000Z'), '07.10.2026') // 00:30 MESZ am Folgetag
|
||||
for (const kaputt of ['', null, undefined, 'morgen', '2026-10', 20261006]) assert.equal(datumDe(kaputt), '')
|
||||
assert.equal(zeitpunktDe(BESTAETIGT_AM), '07.10.2026, 10:15 Uhr')
|
||||
assert.equal(zeitpunktDe('2026-10-07T08:15:00.000+00:00'), '07.10.2026, 10:15 Uhr') // so liefert Appwrite
|
||||
assert.equal(zeitpunktDe('2026-12-01T08:15:00Z'), '01.12.2026, 09:15 Uhr') // Winterzeit
|
||||
assert.equal(zeitpunktDe('2026-10-06T22:05:00Z'), '07.10.2026, 00:05 Uhr') // Mitternacht ohne „24“
|
||||
for (const kaputt of ['', null, undefined, 'bald']) assert.equal(zeitpunktDe(kaputt), '')
|
||||
})
|
||||
|
||||
test('Links: PDF-Pfad aus dem Token, AGB nur als PDF unter /rechtliches/', () => {
|
||||
assert.equal(pdfPfad(TOKEN), `/api/auftrag/${TOKEN}/pdf`)
|
||||
assert.equal(agbPfad('/rechtliches/agb-2026-09-30.pdf'), '/rechtliches/agb-2026-09-30.pdf')
|
||||
for (const boese of ['https://evil.example/agb.pdf', '//evil.example/agb.pdf', '/rechtliches/../x.pdf', '/rechtliches/agb.pdf?x=1', '/rechtliches/agb.html', 'javascript:alert(1)', '', null, undefined, 5]) {
|
||||
assert.equal(agbPfad(boese), '', String(boese))
|
||||
}
|
||||
})
|
||||
|
||||
test('Häkchen: Frühbeginn nur mit Text vom Server; Bestätigen nur, solange nicht bestätigt', () => {
|
||||
assert.equal(zeigtFruehbeginn(privat()), true)
|
||||
assert.equal(zeigtFruehbeginn(ab()), false)
|
||||
assert.equal(zeigtFruehbeginn(privat({ texte: { bestaetigung: 'x', fruehbeginn: ' ' } })), false)
|
||||
assert.equal(zeigtFruehbeginn({}), false)
|
||||
assert.equal(darfBestaetigen(ab()), true)
|
||||
assert.equal(darfBestaetigen(ab({ status: 'abgerechnet' })), true) // Schlussrechnung vor der Bestätigung (Übersicht V2)
|
||||
assert.equal(darfBestaetigen(bestaetigt(ab())), false)
|
||||
assert.equal(darfBestaetigen(ab({ status: 'abgerechnet', bestaetigtAm: BESTAETIGT_AM })), false)
|
||||
for (const status of ['storniert', 'ersetzt', 'widerrufen', 'entwurf']) assert.equal(darfBestaetigen(ab({ status })), false, status)
|
||||
})
|
||||
|
||||
test('Status: Beschriftung und Ton; „Bitte bestätigen“ nur mit kannBestaetigen und ohne Bestätigung', () => {
|
||||
assert.deepEqual(statusAnzeige({ status: 'versendet', kannBestaetigen: true, bestaetigtAm: null }), { text: 'Bitte bestätigen', ton: 'warn' })
|
||||
assert.deepEqual(statusAnzeige({ status: 'abgerechnet', kannBestaetigen: true, bestaetigtAm: null }), { text: 'Bitte bestätigen', ton: 'warn' })
|
||||
assert.deepEqual(statusAnzeige({ status: 'versendet' }), { text: 'Versendet', ton: 'muted' })
|
||||
assert.deepEqual(statusAnzeige({ status: 'bestaetigt', bestaetigtAm: BESTAETIGT_AM, kannBestaetigen: false }), { text: 'Bestätigt', ton: 'ok' })
|
||||
assert.deepEqual(statusAnzeige({ status: 'abgerechnet', bestaetigtAm: BESTAETIGT_AM }), { text: 'Abgerechnet', ton: 'ok' })
|
||||
assert.deepEqual(statusAnzeige({ status: 'storniert' }), { text: 'Storniert', ton: 'muted' })
|
||||
assert.deepEqual(statusAnzeige({ status: 'ersetzt' }), { text: 'Ersetzt', ton: 'muted' })
|
||||
assert.deepEqual(statusAnzeige({ status: 'widerrufen' }), { text: 'Widerrufen', ton: 'rot' })
|
||||
assert.deepEqual(statusAnzeige({ status: 'unbekannt' }), { text: '–', ton: 'muted' })
|
||||
assert.deepEqual(statusAnzeige(undefined), { text: '–', ton: 'muted' })
|
||||
})
|
||||
|
||||
test('Karten-Zeile und Sortierung der Portalliste', () => {
|
||||
assert.equal(kartenMeta(ab()), `vom 06.10.2026 · 800,00${NB}€`)
|
||||
assert.equal(kartenMeta(ab({ art: 'aufwand', positionen: [], summeCents: null })), 'vom 06.10.2026 · nach Aufwand')
|
||||
assert.equal(kartenMeta(ab({ datum: '' })), `800,00${NB}€`)
|
||||
assert.equal(kartenMeta({}), '')
|
||||
const liste = [
|
||||
{ nummer: 'AB-2026-0001', kannBestaetigen: false }, { nummer: 'AB-2026-0003', kannBestaetigen: false },
|
||||
{ nummer: 'AB-2026-0002', kannBestaetigen: true }, { nummer: 'AB-2025-0009', kannBestaetigen: false },
|
||||
]
|
||||
assert.deepEqual(sortiereAuftraege(liste).map((a) => a.nummer), ['AB-2026-0002', 'AB-2026-0003', 'AB-2026-0001', 'AB-2025-0009'])
|
||||
assert.equal(liste[0].nummer, 'AB-2026-0001', 'die Eingabe bleibt unverändert')
|
||||
assert.deepEqual(sortiereAuftraege(undefined), [])
|
||||
})
|
||||
|
||||
test('Zusammenfassung (Festpreis): Kopf, Positionen, Summe, Zahlungsplan, Termine, § 19 aus der API', () => {
|
||||
assert.deepEqual(zusammenfassung(ab()), {
|
||||
kopf: [{ label: 'Nummer', wert: 'AB-2026-0002' }, { label: 'Datum', wert: '06.10.2026' }, { label: 'Projekt', wert: 'Website Anelia Wolf' }],
|
||||
art: 'festpreis',
|
||||
positionen: [
|
||||
{ beschreibung: 'Erstellung der Website anelia-wolf.com', menge: '1', einzelpreis: `600,00${NB}€`, summe: `600,00${NB}€` },
|
||||
{ beschreibung: 'Texte und Bildbearbeitung', menge: '2', einzelpreis: `100,00${NB}€`, summe: `200,00${NB}€` },
|
||||
],
|
||||
summe: `800,00${NB}€`,
|
||||
aufwand: '',
|
||||
zahlung: ab().zahlungsplanText,
|
||||
termine: [{ label: 'Start', wert: '12.10.2026' }, { label: 'Voraussichtliche Fertigstellung', wert: '30.11.2026' }],
|
||||
hinweis: KLEINUNTERNEHMER,
|
||||
})
|
||||
})
|
||||
|
||||
test('Zusammenfassung (Aufwand): Aufwandstext statt Positionen; Termine nur, wenn gesetzt; ohne Satz der API kein § 19-Hinweis; leere Eingabe wirft nicht', () => {
|
||||
const z = zusammenfassung(ab({
|
||||
art: 'aufwand', positionen: [], summeCents: null, aufwandText: '100,00 € pro Stunde in Schritten von 15 Minuten.',
|
||||
zahlungsplanText: 'Monatlich nach Aufwand.', startAm: '2026-10-12', fertigBis: '',
|
||||
}))
|
||||
assert.equal(z.art, 'aufwand')
|
||||
assert.deepEqual(z.positionen, [])
|
||||
assert.equal(z.summe, '')
|
||||
assert.equal(z.aufwand, '100,00 € pro Stunde in Schritten von 15 Minuten.')
|
||||
assert.equal(z.zahlung, 'Monatlich nach Aufwand.')
|
||||
assert.deepEqual(z.termine, [{ label: 'Start', wert: '12.10.2026' }])
|
||||
assert.equal(z.hinweis, KLEINUNTERNEHMER)
|
||||
assert.equal(zusammenfassung(ab({ texte: { bestaetigung: 'x', fruehbeginn: null } })).hinweis, '', 'keine eigene Ersatzkonstante: der Satz kommt von der API')
|
||||
assert.deepEqual(zusammenfassung({}), { kopf: [], art: 'festpreis', positionen: [], summe: '', aufwand: '', zahlung: '', termine: [], hinweis: '' })
|
||||
assert.doesNotThrow(() => zusammenfassung(undefined))
|
||||
})
|
||||
|
||||
test('Bestätigt-Zeilen: Zeitpunkt, bei Privatpersonen Frühbeginn bzw. frühester Beginn, in beiden Anreden', () => {
|
||||
assert.deepEqual(bestaetigtZeilen(ab()), [])
|
||||
assert.deepEqual(bestaetigtZeilen(bestaetigt(ab())), ['Bestätigt am 07.10.2026, 10:15 Uhr'])
|
||||
assert.deepEqual(bestaetigtZeilen(bestaetigt(ab({ fruehesterBeginn: '2026-10-21' }))), ['Bestätigt am 07.10.2026, 10:15 Uhr'], 'Unternehmen: keine Zeile zum Beginn')
|
||||
assert.deepEqual(bestaetigtZeilen(bestaetigt(privat())), ['Bestätigt am 07.10.2026, 10:15 Uhr', 'Wir beginnen frühestens am 21.10.2026.'])
|
||||
const wunsch = bestaetigt(privat({ fruehbeginn: true, fruehesterBeginn: null }))
|
||||
assert.deepEqual(bestaetigtZeilen(wunsch), ['Bestätigt am 07.10.2026, 10:15 Uhr', 'Wir beginnen auf Ihren Wunsch schon vor Ablauf der Widerrufsfrist.'])
|
||||
assert.deepEqual(bestaetigtZeilen(wunsch, ANREDE_DU), ['Bestätigt am 07.10.2026, 10:15 Uhr', 'Wir beginnen auf deinen Wunsch schon vor Ablauf der Widerrufsfrist.'])
|
||||
assert.deepEqual(bestaetigtZeilen(bestaetigt(privat({ fruehesterBeginn: null }))), ['Bestätigt am 07.10.2026, 10:15 Uhr'])
|
||||
assert.deepEqual(bestaetigtZeilen({ bestaetigtAm: 'kaputt' }), ['Bestätigt'])
|
||||
})
|
||||
|
||||
test('Zustände aus der Serverantwort: feste Texte, der 410-Grund nur aus dem Stichwort', () => {
|
||||
const kontakt = 'Bitte melden Sie sich bei uns. Wir senden Ihnen gern einen neuen Link.'
|
||||
assert.deepEqual(zustandAusAntwort(404, { error: 'Dieser Link ist ungültig oder abgelaufen.' }), { zustand: 'ungueltig', titel: 'Dieser Link ist ungültig oder abgelaufen.', text: kontakt })
|
||||
assert.deepEqual(zustandAusAntwort(410, { error: 'Diese Auftragsbestätigung wurde ersetzt.' }), {
|
||||
zustand: 'beendet', titel: 'Diese Auftragsbestätigung wurde ersetzt.', text: 'Bitte verwenden Sie den Link in der neuesten Auftragsbestätigung, die wir Ihnen geschickt haben.',
|
||||
})
|
||||
for (const grund of ['storniert', 'widerrufen']) {
|
||||
assert.deepEqual(zustandAusAntwort(410, { error: `Diese Auftragsbestätigung wurde ${grund}.` }), {
|
||||
zustand: 'beendet', titel: `Diese Auftragsbestätigung wurde ${grund}.`, text: 'Bei Fragen melden Sie sich gern bei uns.',
|
||||
})
|
||||
}
|
||||
for (const body of [{ error: `Irgendwas mit ${TOKEN}` }, {}, undefined, null]) { // fremder Text (auch mit Token) wird nie angezeigt
|
||||
const z = zustandAusAntwort(410, body)
|
||||
assert.equal(z.titel, 'Diese Auftragsbestätigung ist nicht mehr gültig.')
|
||||
assert.equal(z.text, 'Bei Fragen melden Sie sich gern bei uns.')
|
||||
}
|
||||
assert.deepEqual(zustandAusAntwort(429, { error: 'Zu viele Anfragen. Bitte später erneut versuchen.' }), { zustand: 'limit', titel: 'Zu viele Anfragen', text: 'Bitte versuchen Sie es in einigen Minuten noch einmal.' })
|
||||
for (const status of [0, 400, 401, 500, 502, 503]) {
|
||||
assert.deepEqual(zustandAusAntwort(status, { error: 'Interner Fehler.' }), {
|
||||
zustand: 'fehler', titel: 'Das hat leider nicht geklappt', text: 'Ihre Auftragsbestätigung konnte gerade nicht geladen werden. Bitte versuchen Sie es in einigen Minuten noch einmal.',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test('Fehlermeldungen beim Bestätigen: feste eigene Texte nach Status, Sie auf der Seite und Du im Portal', () => {
|
||||
assert.equal(fehlerMeldung(0), 'Keine Verbindung. Bitte prüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.')
|
||||
assert.equal(fehlerMeldung(400), 'Bitte setzen Sie das Häkchen, um den Auftrag zu bestätigen.')
|
||||
assert.equal(fehlerMeldung(401), 'Ihre Anmeldung ist abgelaufen. Bitte laden Sie die Seite neu und melden Sie sich erneut an.')
|
||||
for (const status of [403, 409]) assert.equal(fehlerMeldung(status), 'Das Bestätigen ist gerade nicht möglich. Bitte laden Sie die Seite neu.')
|
||||
assert.equal(fehlerMeldung(429), 'Zu viele Anfragen. Bitte versuchen Sie es in einigen Minuten noch einmal.')
|
||||
for (const status of [500, 502, 503, undefined]) assert.equal(fehlerMeldung(status), 'Die Bestätigung konnte nicht gespeichert werden. Bitte versuchen Sie es später noch einmal.')
|
||||
assert.equal(fehlerMeldung(0, ANREDE_DU), 'Keine Verbindung. Bitte prüfe deine Internetverbindung und versuche es erneut.')
|
||||
assert.equal(fehlerMeldung(400, ANREDE_DU), 'Bitte setze das Häkchen, um den Auftrag zu bestätigen.')
|
||||
assert.equal(fehlerMeldung(401, ANREDE_DU), 'Deine Anmeldung ist abgelaufen. Bitte lade die Seite neu und melde dich erneut an.')
|
||||
for (const status of [403, 409]) assert.equal(fehlerMeldung(status, ANREDE_DU), 'Das Bestätigen ist gerade nicht möglich. Bitte lade die Seite neu.')
|
||||
assert.equal(fehlerMeldung(429, ANREDE_DU), 'Zu viele Anfragen. Bitte versuche es in einigen Minuten noch einmal.')
|
||||
for (const status of [500, 502, 503, undefined]) assert.equal(fehlerMeldung(status, ANREDE_DU), 'Die Bestätigung konnte nicht gespeichert werden. Bitte versuche es später noch einmal.')
|
||||
})
|
||||
|
||||
const werte = (objekt) => Object.values(objekt).flatMap((wert) => (wert && typeof wert === 'object' ? werte(wert) : [wert])).filter((wert) => typeof wert === 'string')
|
||||
|
||||
test('Anrede: gemeinsame Texte neutral, öffentliche Seite Sie, Portal Du; jede Du-Variante hat ihr Sie-Gegenstück', () => {
|
||||
assert.equal(ANREDE_SIE, 'sie')
|
||||
assert.equal(ANREDE_DU, 'du')
|
||||
for (const text of werte(TEXTE_GEMEINSAM)) assert.ok(!hatDuForm(text) && !hatSieForm(text), text)
|
||||
for (const text of werte(TEXTE_SIE)) assert.ok(!hatDuForm(text), text)
|
||||
for (const text of werte(TEXTE_DU)) assert.ok(!hatSieForm(text), text)
|
||||
assert.deepEqual(Object.keys(TEXTE_DU).sort(), ['fehler', 'formular', 'fruehbeginnWunsch', 'speicherHinweis'])
|
||||
for (const schluessel of Object.keys(TEXTE_DU)) assert.ok(schluessel in TEXTE_SIE, schluessel)
|
||||
assert.deepEqual(Object.keys(TEXTE_DU.fehler), Object.keys(TEXTE_SIE.fehler))
|
||||
assert.equal(texte().formular, 'Ihre Bestätigung', 'ohne Angabe gilt Sie')
|
||||
assert.equal(texte(ANREDE_SIE).speicherHinweis, 'Ihre Bestätigung wird mit Datum und Uhrzeit gespeichert.')
|
||||
assert.equal(texte(ANREDE_DU).formular, 'Deine Bestätigung')
|
||||
assert.equal(texte(ANREDE_DU).speicherHinweis, 'Deine Bestätigung wird mit Datum und Uhrzeit gespeichert.')
|
||||
assert.equal(texte(ANREDE_DU).knopf, 'Auftrag bestätigen', 'gemeinsame Texte gibt es in beiden Anreden')
|
||||
assert.equal(texte(ANREDE_DU).ansichtsmodus, 'Im Ansichtsmodus kann der Auftrag nicht bestätigt werden. Die Bestätigung gibt der Kunde selbst ab.')
|
||||
})
|
||||
|
||||
test('AB-Nummer: Schreibweisen der Praxis werden zu AB-JJJJ-NNNN, alles andere zu leer', () => {
|
||||
assert.equal(abNummerNormalisiert('AB-2026-0001'), 'AB-2026-0001')
|
||||
assert.equal(abNummerNormalisiert(' ab-2026-0001 '), 'AB-2026-0001')
|
||||
assert.equal(abNummerNormalisiert('ab 2026 0001'), 'AB-2026-0001')
|
||||
assert.equal(abNummerNormalisiert('AB – 2026 – 0001'), 'AB-2026-0001') // Halbgeviertstrich mit Leerzeichen
|
||||
assert.equal(abNummerNormalisiert('AB−2026−0001'), 'AB-2026-0001') // Minuszeichen
|
||||
for (const kaputt of ['', ' ', 'AB-2026-01', 'AB-26-0001', 'RE-2026-0001', 'AB-2026-0001-1', 'AB20260001', undefined, null]) assert.equal(abNummerNormalisiert(kaputt), '', String(kaputt))
|
||||
})
|
||||
|
||||
test('Quelltext und Kontakt: reines Modul ohne Importe, fetch und DOM; Kontaktdaten wie in den Rechtstexten', () => {
|
||||
pruefeQuelltext('auftrag-ansicht.js', { mitTexten: true })
|
||||
assert.deepEqual(KONTAKT, { email: 'support@webklar.com', telefon: '0176 23726355' })
|
||||
})
|
||||
@@ -0,0 +1,65 @@
|
||||
// Gemeinsame Testdaten und Quelltext-Regeln des Strangs L6 (kein Test, daher nicht *.test.js).
|
||||
// Formen laut Übersicht V3 (AuftragOeffentlich) und V5 (Liste im Portal: plus id und kannBestaetigen).
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
export const TOKEN = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG' // 43 Zeichen wie ein echter Token (base64url)
|
||||
export const BESTAETIGT_AM = '2026-10-07T08:15:00.000Z' // = 07.10.2026, 10:15 Uhr Berliner Zeit (MESZ)
|
||||
export const KLEINUNTERNEHMER = 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.' // wie AB_TEXTE.kleinunternehmer; die API liefert ihn als texte.kleinunternehmer
|
||||
|
||||
/** Festpreis-AB eines Unternehmens, gesendet, noch nicht bestätigt. */
|
||||
export const ab = (over = {}) => ({
|
||||
nummer: 'AB-2026-0002', datum: '2026-10-06', titel: 'Website Anelia Wolf', art: 'festpreis',
|
||||
positionen: [
|
||||
{ beschreibung: 'Erstellung der Website anelia-wolf.com', menge: 1, einzelpreisCents: 60000, summeCents: 60000 },
|
||||
{ beschreibung: 'Texte und Bildbearbeitung', menge: 2, einzelpreisCents: 10000, summeCents: 20000 },
|
||||
],
|
||||
summeCents: 80000, stundensatzCents: null, schaetzungMinuten: null, aufwandText: null,
|
||||
zahlungsplanText: 'Anzahlung 50 % (400,00 €) nach Auftragsbestätigung, Rest (400,00 €) nach Abnahme; zahlbar jeweils innerhalb von 14 Tagen nach Rechnung.',
|
||||
startAm: '2026-10-12', fertigBis: '2026-11-30', kundenart: 'unternehmen', status: 'versendet',
|
||||
bestaetigtAm: null, fruehbeginn: false, fruehesterBeginn: null, agbUrl: '/rechtliches/agb-2026-09-30.pdf',
|
||||
texte: { bestaetigung: 'Ich bestätige den Auftrag zu diesen Bedingungen.', fruehbeginn: null, kleinunternehmer: KLEINUNTERNEHMER },
|
||||
...over,
|
||||
})
|
||||
|
||||
/** Dieselbe AB für eine Privatperson (zweites, freiwilliges Häkchen; Texte sind Testtexte, nicht die der Rechtsrunde). */
|
||||
export const privat = (over = {}) => ab({
|
||||
kundenart: 'privat', fruehesterBeginn: '2026-10-21',
|
||||
texte: {
|
||||
bestaetigung: 'Ich bestätige den Auftrag zu diesen Bedingungen.',
|
||||
fruehbeginn: 'Ich verlange, dass Sie schon vor Ablauf der Widerrufsfrist mit der Arbeit beginnen.',
|
||||
kleinunternehmer: KLEINUNTERNEHMER,
|
||||
},
|
||||
...over,
|
||||
})
|
||||
|
||||
/** Die übergebene AB im Zustand „bestätigt“. */
|
||||
export const bestaetigt = (auftrag) => ({ ...auftrag, status: 'bestaetigt', bestaetigtAm: BESTAETIGT_AM })
|
||||
|
||||
/** Eintrag der Portal-Liste (GET /api/auftraege): AB plus id und kannBestaetigen. */
|
||||
export const eintrag = (over = {}) => ({ id: 'ab1', ...ab(), kannBestaetigen: true, ...over })
|
||||
|
||||
const ohneKommentare = (quelle) => quelle.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|\s)\/\/.*$/gm, '$1')
|
||||
const DU = /\b(?:du|dein(?:e[nmrs]?)?|dich|dir)\b/i
|
||||
const SIE = /\b(?:Sie|Ihr(?:e[nmrs]?)?|Ihnen)\b/
|
||||
/** Steht eine Du- bzw. Sie-Form im Text? (Anrede-Prüfungen der Tests) */
|
||||
export const hatDuForm = (text) => DU.test(String(text))
|
||||
export const hatSieForm = (text) => SIE.test(String(text))
|
||||
|
||||
/**
|
||||
* Quelltext-Regeln für jedes Modul public/auftrag*.js; gibt den Quelltext ohne Kommentare zurück.
|
||||
* mitTexten: nur auftrag-ansicht.js enthält Kundentexte (ihre Anrede prüft der Test selbst); alle anderen Module enthalten weder Du- noch Sie-Formen.
|
||||
*/
|
||||
export function pruefeQuelltext(datei, { fetchAnzahl = 0, importe = [], mitTexten = false } = {}) {
|
||||
const quelle = ohneKommentare(readFileSync(new URL(`../public/${datei}`, import.meta.url), 'utf8'))
|
||||
assert.doesNotMatch(quelle, /innerHTML|insertAdjacentHTML|outerHTML|document\.write|\beval\(|new Function/, `${datei}: Daten nie als HTML`)
|
||||
assert.doesNotMatch(quelle, /localStorage|sessionStorage|document\.cookie|indexedDB/, `${datei}: kein Browserspeicher`)
|
||||
assert.doesNotMatch(quelle, /\bconsole\./, `${datei}: nichts ins Protokoll (der Token!)`)
|
||||
assert.doesNotMatch(quelle, /\bconfirm\(|\balert\(|\bprompt\(/, `${datei}: keine Browser-Rückfragen`)
|
||||
assert.doesNotMatch(quelle, /import\s*\(/, `${datei}: kein dynamischer Import`)
|
||||
assert.ok(!quelle.includes(String.fromCodePoint(0xa0)), `${datei}: kein verstecktes geschütztes Leerzeichen im Quelltext`)
|
||||
if (!mitTexten) assert.ok(!DU.test(quelle) && !hatSieForm(quelle), `${datei}: keine Kundentexte im Modul (sie stehen in auftrag-ansicht.js; Anrede über ANREDE_SIE/ANREDE_DU)`)
|
||||
assert.equal([...quelle.matchAll(/\bfetch\(/g)].length, fetchAnzahl, `${datei}: Anzahl fetch`)
|
||||
assert.deepEqual([...quelle.matchAll(/\bfrom\s+'([^']+)'/g)].map((m) => m[1]), importe, `${datei}: Importe`)
|
||||
return quelle
|
||||
}
|
||||
Reference in new issue
Block a user