Files
Webklar-Kundenbereich/test/shopTabEinstellungen.test.js
ClaudeandClaude Opus 5.5 1f56b86791 Shop Review B I-4: Steuerart und Lieferzeit ohne Vorbelegung
kleinunternehmer ist ab jetzt null (nicht gewaehlt), lieferzeit leer. istVollstaendig verlangt eine
bewusste Wahl (true oder false), der Shop kann sonst nicht geoeffnet werden. Im Reiter
Einstellungen gibt es zwei Pflicht-Radiobuttons ohne Vorauswahl; Preise, Artikel und Muster
rechnen bei null wie bei Regelbesteuerung. Tests und Fixtures angepasst.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:50:54 +00:00

360 lines
18 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 { readFileSync } from 'node:fs'
import { installFakeDom } from './support/fakeDom.js'
import { h, request } from '../public/shop-dom.js'
import { ANBIETER_HINWEIS, PAYPAL_HINWEIS, SEPA_WARNUNG, STRIPE_HINWEIS, render } from '../public/shop-tab-einstellungen.js'
// Task G6: Reiter „Einstellungen“ mit Fake-DOM und Fake-fetch. Die Antworten des Servers sind von Hand gebaut
// (Form: server/routes/shop.js und server/services/shop/einstellungen.js).
const STANDARD = {
shopname: '',
anbieter: { name: '', strasse: '', plz: '', ort: '', land: 'DE', email: '', telefon: '', ustId: '' },
kleinunternehmer: null,
versand: { aktiv: true, laender: ['DE'], kostenCent: { DE: 490 }, freiAbCent: null },
abholung: { aktiv: false, adresse: '', hinweis: '' },
lieferzeit: '',
zahlungsarten: ['card'],
akzent: '#111111',
mindestbestellCent: null,
ruecksendekosten: 'kaeufer',
}
const GEFUELLT = {
shopname: 'Mützenlädchen',
anbieter: { name: 'Erika Muster', strasse: 'Hauptstraße 1', plz: '67655', ort: 'Kaiserslautern', land: 'DE', email: 'erika@example.com', telefon: '0631 123456', ustId: 'DE123456789' },
kleinunternehmer: false,
versand: { aktiv: true, laender: ['DE', 'AT'], kostenCent: { DE: 490, AT: 990 }, freiAbCent: 5000 },
abholung: { aktiv: true, adresse: 'Hinterhof 2\n67655 Kaiserslautern', hinweis: 'Mo bis Fr 10 bis 18 Uhr' },
lieferzeit: '1–2 Werktage',
zahlungsarten: ['card', 'klarna', 'sepa_debit'],
akzent: '#ff8800',
mindestbestellCent: 1500,
ruecksendekosten: 'haendler',
}
const ANTWORT = (einstellungen, version = 3) => ({ einstellungen, version, vollstaendig: true })
/** Fake-Server: GET /api/shop/einstellungen antwortet aus `welt.get()`, PUT aus `welt.put(aufruf)` (Standard: Echo mit Version + 1). */
function startFx(welt) {
const route = (aufruf) => {
if (aufruf.method === 'GET' && aufruf.url.startsWith('/api/shop/einstellungen?projectId=')) return { body: typeof welt.get === 'function' ? welt.get() : welt.get }
if (aufruf.method === 'PUT' && aufruf.url === '/api/shop/einstellungen') {
if (welt.put) return welt.put(aufruf)
return { body: ANTWORT(aufruf.body.einstellungen, aufruf.body.version + 1) }
}
return { status: 404, body: { error: 'unbekannt' } }
}
return installFakeDom({ antworten: Array.from({ length: 40 }, () => route) })
}
const mitDom = (welt, fn) => async () => {
const fx = startFx(welt)
try {
await fn(fx)
} finally {
fx.restore()
}
}
function ctxVon(extra = {}) {
const log = { meldungen: [], neuGeladen: 0 }
const ctx = {
projectId: 'p1',
nurLesen: false,
request,
zeigeMeldung: (text, art) => log.meldungen.push([text, art]),
neuLaden: async () => { log.neuGeladen += 1 },
...extra,
}
return { ctx, log }
}
async function oeffne(fx, ctxExtra) {
const root = h('div', { id: 'panel' })
fx.document.body.append(root)
const { ctx, log } = ctxVon(ctxExtra)
await render(root, ctx)
await fx.ruhe()
return { root, ctx, log }
}
const feldVon = (root, name) => root.querySelector(`[data-feld="${name}"]`)
// Steuerart: zwei Radiobuttons ohne Vorauswahl
const steuerRadio = (root, wert) => root.querySelector(`[data-feld="kleinunternehmer"][value="${wert}"]`)
const fehlerVon = (root, name) => root.querySelector(`[data-fehler="${name}"]`)
const speichernKnopf = (root) => root.querySelector('[data-aktion="einstellungen-speichern"]')
const puts = (fx) => fx.fetchLog.filter((a) => a.method === 'PUT')
/* --------------------------------------------------------------- Anzeige --- */
test('Standardwerte: Felder gefüllt, Länder nur DE, Karte fest, Hinweise zu Klarna, Lastschrift und PayPal', mitDom({ get: ANTWORT(STANDARD, 0) }, async (fx) => {
const { root } = await oeffne(fx)
assert.equal(fx.fetchLog[0].url, '/api/shop/einstellungen?projectId=p1')
for (const titel of ['Shop', 'Anbieter', 'Steuer', 'Versand', 'Abholung', 'Zahlungsarten']) {
assert.ok(fx.finde(root, (el) => el.tagName === 'H3' && el.textContent === titel), titel)
}
assert.equal(feldVon(root, 'shopname').value, '')
assert.equal(feldVon(root, 'akzent').type, 'color')
assert.equal(feldVon(root, 'akzent').value, '#111111')
assert.equal(feldVon(root, 'anbieter.land').value, 'DE')
// I-4: keine Vorauswahl bei der Steuerart, Lieferzeit leer
assert.equal(steuerRadio(root, 'ja').type, 'radio')
assert.equal(steuerRadio(root, 'nein').type, 'radio')
assert.equal(steuerRadio(root, 'ja').checked, false)
assert.equal(steuerRadio(root, 'nein').checked, false)
assert.equal(steuerRadio(root, 'ja').getAttribute('name'), steuerRadio(root, 'nein').getAttribute('name'))
assert.equal(feldVon(root, 'versand.aktiv').checked, true)
assert.equal(feldVon(root, 'versand.laender').value, 'DE')
assert.equal(feldVon(root, 'versand.kostenCent.DE').value, '4,90')
assert.equal(feldVon(root, 'versand.kostenCent.DE').type, 'text', 'Betrag als Text, Komma bleibt erlaubt')
assert.equal(feldVon(root, 'versand.freiAbCent').value, '')
assert.equal(feldVon(root, 'lieferzeit').value, '')
assert.equal(feldVon(root, 'mindestbestellCent').value, '')
assert.equal(feldVon(root, 'ruecksendekosten').value, 'kaeufer')
assert.equal(fx.text(root).includes(ANBIETER_HINWEIS), true)
assert.equal(fx.text(root).includes(SEPA_WARNUNG), true)
assert.equal(fx.text(root).includes(STRIPE_HINWEIS), true)
assert.equal(fx.text(root).includes(PAYPAL_HINWEIS), true)
assert.match(SEPA_WARNUNG, /^Bei Lastschrift ist das Geld erst nach einigen Tagen sicher\. Versende erst, wenn die Bestellung als bezahlt markiert ist\.$/)
assert.match(fx.text(root), /Ich bin Kleinunternehmer nach § 19 UStG/)
assert.match(fx.text(root), /Ich weise Umsatzsteuer aus/)
// Österreich und Abholung sind zu: ihre Felder sind versteckt
assert.equal(feldVon(root, 'versand.kostenCent.AT').closest('.sh-versand-at').hidden, true)
assert.equal(feldVon(root, 'abholung.adresse').closest('.sh-abhol-details').hidden, true)
}))
test('Zahlungsarten: Karte immer gesetzt und gesperrt, Klarna und Lastschrift frei wählbar', mitDom({ get: ANTWORT(STANDARD) }, async (fx) => {
const { root } = await oeffne(fx)
const karte = feldVon(root, 'zahlung.card')
assert.equal(karte.checked, true)
assert.equal(karte.disabled, true)
assert.match(fx.text(root), /inklusive Apple Pay und Google Pay/)
assert.equal(feldVon(root, 'zahlung.klarna').checked, false)
assert.equal(feldVon(root, 'zahlung.klarna').disabled, false)
assert.equal(feldVon(root, 'zahlung.sepa_debit').checked, false)
await fx.klick(karte)
assert.equal(karte.checked, true, 'ein Klick ändert nichts')
}))
test('gespeicherte Werte werden angezeigt, Österreich und Abholung sind sichtbar', mitDom({ get: ANTWORT(GEFUELLT) }, async (fx) => {
const { root } = await oeffne(fx)
assert.equal(feldVon(root, 'shopname').value, 'Mützenlädchen')
assert.equal(feldVon(root, 'akzent').value, '#ff8800')
assert.equal(feldVon(root, 'anbieter.plz').value, '67655')
assert.equal(feldVon(root, 'anbieter.ustId').value, 'DE123456789')
assert.equal(steuerRadio(root, 'nein').checked, true)
assert.equal(steuerRadio(root, 'ja').checked, false)
assert.equal(feldVon(root, 'versand.laender').value, 'DE,AT')
assert.equal(feldVon(root, 'versand.kostenCent.AT').value, '9,90')
assert.equal(feldVon(root, 'versand.kostenCent.AT').closest('.sh-versand-at').hidden, false)
assert.equal(feldVon(root, 'versand.freiAbCent').value, '50,00')
assert.equal(feldVon(root, 'abholung.aktiv').checked, true)
assert.equal(feldVon(root, 'abholung.adresse').value, 'Hinterhof 2\n67655 Kaiserslautern')
assert.equal(feldVon(root, 'abholung.adresse').closest('.sh-abhol-details').hidden, false)
assert.equal(feldVon(root, 'mindestbestellCent').value, '15,00')
assert.equal(feldVon(root, 'ruecksendekosten').value, 'haendler')
assert.equal(feldVon(root, 'zahlung.klarna').checked, true)
assert.equal(feldVon(root, 'zahlung.sepa_debit').checked, true)
}))
test('Schalter blenden Österreich, Versand- und Abholfelder ein und aus', mitDom({ get: ANTWORT(STANDARD) }, async (fx) => {
const { root } = await oeffne(fx)
const at = feldVon(root, 'versand.kostenCent.AT').closest('.sh-versand-at')
await fx.setze(feldVon(root, 'versand.laender'), 'DE,AT')
assert.equal(at.hidden, false)
await fx.setze(feldVon(root, 'versand.laender'), 'DE')
assert.equal(at.hidden, true)
const versandDetails = feldVon(root, 'versand.laender').closest('.sh-versand-details')
await fx.klick(feldVon(root, 'versand.aktiv'))
assert.equal(versandDetails.hidden, true)
const abhol = feldVon(root, 'abholung.adresse').closest('.sh-abhol-details')
await fx.klick(feldVon(root, 'abholung.aktiv'))
assert.equal(abhol.hidden, false)
}))
/* ------------------------------------------------------------- Speichern --- */
test('Steuerart I-4: Radiobuttons schließen sich aus, ohne Wahl wird null gespeichert, mit Wahl true oder false', mitDom({ get: ANTWORT(STANDARD, 1) }, async (fx) => {
const { root } = await oeffne(fx)
await fx.klick(speichernKnopf(root))
assert.equal(puts(fx)[0].body.einstellungen.kleinunternehmer, null)
await fx.klick(steuerRadio(root, 'ja'))
assert.equal(steuerRadio(root, 'ja').checked, true)
assert.equal(steuerRadio(root, 'nein').checked, false)
await fx.klick(speichernKnopf(root))
assert.equal(puts(fx)[1].body.einstellungen.kleinunternehmer, true)
await fx.klick(steuerRadio(root, 'nein'))
assert.equal(steuerRadio(root, 'ja').checked, false)
await fx.klick(speichernKnopf(root))
assert.equal(puts(fx)[2].body.einstellungen.kleinunternehmer, false)
}))
test('Speichern: Körper mit ganzen Cent, Version, Meldung und neuLaden; der Stand des Servers kommt zurück ins Formular', mitDom({
get: ANTWORT(STANDARD, 2),
put: (aufruf) => ({ body: ANTWORT({ ...aufruf.body.einstellungen, anbieter: { ...aufruf.body.einstellungen.anbieter, email: 'erika@example.com' } }, 3) }),
}, async (fx) => {
const { root, log } = await oeffne(fx)
await fx.setze(feldVon(root, 'shopname'), ' Mützenlädchen ')
await fx.setze(feldVon(root, 'akzent'), '#ff8800')
await fx.setze(feldVon(root, 'anbieter.name'), 'Erika Muster')
await fx.setze(feldVon(root, 'anbieter.strasse'), 'Hauptstraße 1')
await fx.setze(feldVon(root, 'anbieter.plz'), '67655')
await fx.setze(feldVon(root, 'anbieter.ort'), 'Kaiserslautern')
await fx.setze(feldVon(root, 'anbieter.email'), 'Erika@Example.com')
await fx.setze(feldVon(root, 'anbieter.ustId'), 'DE123456789')
await fx.klick(steuerRadio(root, 'nein'))
await fx.setze(feldVon(root, 'versand.laender'), 'DE,AT')
await fx.setze(feldVon(root, 'versand.kostenCent.DE'), '4,9')
await fx.setze(feldVon(root, 'versand.kostenCent.AT'), '9,90')
await fx.setze(feldVon(root, 'versand.freiAbCent'), '50')
await fx.klick(feldVon(root, 'abholung.aktiv'))
await fx.setze(feldVon(root, 'abholung.adresse'), 'Hinterhof 2')
await fx.setze(feldVon(root, 'abholung.hinweis'), 'Mo bis Fr')
await fx.setze(feldVon(root, 'lieferzeit'), '1–2 Werktage')
await fx.setze(feldVon(root, 'mindestbestellCent'), '15')
await fx.setze(feldVon(root, 'ruecksendekosten'), 'haendler')
await fx.klick(feldVon(root, 'zahlung.klarna'))
await fx.klick(feldVon(root, 'zahlung.sepa_debit'))
await fx.klick(speichernKnopf(root))
assert.equal(puts(fx).length, 1)
assert.deepEqual(puts(fx)[0].body, {
projectId: 'p1',
version: 2,
einstellungen: {
shopname: 'Mützenlädchen',
akzent: '#ff8800',
anbieter: { name: 'Erika Muster', strasse: 'Hauptstraße 1', plz: '67655', ort: 'Kaiserslautern', land: 'DE', email: 'Erika@Example.com', telefon: '', ustId: 'DE123456789' },
kleinunternehmer: false,
versand: { aktiv: true, laender: ['DE', 'AT'], kostenCent: { DE: 490, AT: 990 }, freiAbCent: 5000 },
abholung: { aktiv: true, adresse: 'Hinterhof 2', hinweis: 'Mo bis Fr' },
lieferzeit: '1–2 Werktage',
mindestbestellCent: 1500,
ruecksendekosten: 'haendler',
zahlungsarten: ['card', 'klarna', 'sepa_debit'],
},
})
assert.deepEqual(log.meldungen, [['Die Einstellungen sind gespeichert.', 'ok']])
assert.equal(log.neuGeladen, 1)
assert.equal(feldVon(root, 'anbieter.email').value, 'erika@example.com', 'normalisierter Stand des Servers')
// zweites Speichern schickt die neue Version
await fx.klick(speichernKnopf(root))
assert.equal(puts(fx)[1].body.version, 3)
}))
test('Speichern ohne Österreich schickt nur Deutschland; ohne Versand dürfen die Kosten leer sein', mitDom({ get: ANTWORT({ ...STANDARD, versand: { ...STANDARD.versand, aktiv: false }, abholung: { aktiv: true, adresse: 'Laden', hinweis: '' } }) }, async (fx) => {
const { root } = await oeffne(fx)
await fx.setze(feldVon(root, 'versand.kostenCent.DE'), '')
await fx.klick(speichernKnopf(root))
const e = puts(fx)[0].body.einstellungen
assert.deepEqual(e.versand, { aktiv: false, laender: ['DE'], kostenCent: { DE: null }, freiAbCent: null })
assert.deepEqual(e.zahlungsarten, ['card'])
assert.equal(e.mindestbestellCent, null)
}))
test('unlesbare oder fehlende Beträge werden im Browser gemeldet, es geht nichts an den Server', mitDom({ get: ANTWORT(STANDARD) }, async (fx) => {
const { root } = await oeffne(fx)
await fx.setze(feldVon(root, 'versand.kostenCent.DE'), 'viel')
await fx.setze(feldVon(root, 'mindestbestellCent'), '1,234')
await fx.klick(speichernKnopf(root))
assert.equal(puts(fx).length, 0)
assert.match(fehlerVon(root, 'versand.kostenCent.DE').textContent, /Betrag wie 4,90/)
assert.match(fehlerVon(root, 'mindestbestellCent').textContent, /Betrag wie 4,90/)
assert.equal(feldVon(root, 'versand.kostenCent.DE').getAttribute('aria-invalid'), 'true')
await fx.setze(feldVon(root, 'versand.kostenCent.DE'), '')
await fx.setze(feldVon(root, 'mindestbestellCent'), '')
await fx.klick(speichernKnopf(root))
assert.equal(puts(fx).length, 0)
assert.match(fehlerVon(root, 'versand.kostenCent.DE').textContent, /kostenlosen Versand trage 0,00 ein/)
assert.equal(fehlerVon(root, 'mindestbestellCent').textContent, '')
await fx.setze(feldVon(root, 'versand.kostenCent.DE'), '0')
await fx.klick(speichernKnopf(root))
assert.equal(puts(fx).length, 1)
assert.equal(puts(fx)[0].body.einstellungen.versand.kostenCent.DE, 0)
}))
test('Feldfehler des Servers stehen am Feld, Unbekanntes allgemein; der Knopf ist danach wieder frei', mitDom({
get: ANTWORT(STANDARD),
put: () => ({ status: 400, body: { error: 'Die Einstellungen sind nicht gültig.', fehler: { 'anbieter.plz': 'Bitte gib eine deutsche Postleitzahl mit 5 Ziffern an.', 'versand.aktiv': 'Bitte biete Versand oder Abholung an.', 'dings.unbekannt': 'Etwas stimmt nicht.' } } }),
}, async (fx) => {
const { root, log } = await oeffne(fx)
await fx.klick(speichernKnopf(root))
assert.equal(fehlerVon(root, 'anbieter.plz').textContent, 'Bitte gib eine deutsche Postleitzahl mit 5 Ziffern an.')
assert.equal(feldVon(root, 'anbieter.plz').getAttribute('aria-invalid'), 'true')
assert.equal(fehlerVon(root, 'versand.aktiv').textContent, 'Bitte biete Versand oder Abholung an.')
assert.equal(fehlerVon(root, '_').textContent, 'Etwas stimmt nicht.')
assert.equal(speichernKnopf(root).disabled, false)
assert.deepEqual(log.meldungen, [])
assert.equal(log.neuGeladen, 0)
}))
test('409 (Einstellungen veraltet): Hinweis mit „Neu laden“, das den neuen Stand holt', async () => {
let stand = ANTWORT(STANDARD, 1)
const fx = startFx({
get: () => stand,
put: () => ({ status: 409, body: { error: 'Die Einstellungen wurden inzwischen geändert. Bitte lade die Seite neu.', code: 'EINSTELLUNG_VERALTET' } }),
})
try {
const { root } = await oeffne(fx)
await fx.klick(speichernKnopf(root))
const konflikt = root.querySelector('[data-konflikt]')
assert.ok(konflikt)
assert.match(konflikt.textContent, /inzwischen geändert/)
assert.equal(fehlerVon(root, '_').textContent, '')
stand = ANTWORT({ ...STANDARD, shopname: 'Von anderer Seite' }, 2)
await fx.klick(root.querySelector('[data-aktion="neu-laden"]'))
await fx.ruhe()
assert.equal(feldVon(root, 'shopname').value, 'Von anderer Seite')
assert.equal(root.querySelector('[data-konflikt]'), null)
await fx.klick(speichernKnopf(root))
assert.equal(puts(fx).at(-1).body.version, 2)
} finally {
fx.restore()
}
})
test('Netzwerkfehler beim Speichern: Meldung allgemein, nichts gespeichert', mitDom({
get: ANTWORT(STANDARD),
put: () => ({ status: 500, body: { error: 'Der Server hat Probleme.' } }),
}, async (fx) => {
const { root, log } = await oeffne(fx)
await fx.klick(speichernKnopf(root))
assert.equal(fehlerVon(root, '_').textContent, 'Der Server hat Probleme.')
assert.deepEqual(log.meldungen, [])
}))
/* -------------------------------------------------------------- nur lesen --- */
test('nurLesen: alle Felder gesperrt, kein Speichern-Knopf, kein Versand', mitDom({ get: ANTWORT(GEFUELLT) }, async (fx) => {
const { root } = await oeffne(fx, { nurLesen: true })
const steuerungen = fx.alle(root, (el) => ['INPUT', 'SELECT', 'TEXTAREA'].includes(el.tagName))
assert.ok(steuerungen.length > 20)
assert.deepEqual(steuerungen.filter((el) => !el.disabled).map((el) => el.dataset.feld), [])
assert.equal(speichernKnopf(root), null)
assert.equal(fx.alle(root, (el) => el.tagName === 'BUTTON').filter((b) => b.getAttribute('type') === 'submit').length, 0)
assert.equal(feldVon(root, 'shopname').value, 'Mützenlädchen', 'Werte bleiben sichtbar')
// auch ein abgesendetes Formular schreibt nichts
await fx.sende(root.querySelector('form'), 'submit')
assert.equal(puts(fx).length, 0)
}))
test('Ladefehler wird geworfen (die Hülle zeigt ihn mit „Noch einmal versuchen“)', async () => {
const fx = installFakeDom({ antworten: [{ status: 500, body: { error: 'Kaputt.' } }] })
try {
const root = h('div')
await assert.rejects(render(root, ctxVon().ctx), /Kaputt\./)
} finally {
fx.restore()
}
})
/* ---------------------------------------------------------------- statisch --- */
test('statisch: kein innerHTML, outerHTML, insertAdjacentHTML, document.write und kein Import aus termine*.js', () => {
const quelle = readFileSync(new URL('../public/shop-tab-einstellungen.js', import.meta.url), 'utf8')
const ohneKommentare = quelle.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '')
assert.doesNotMatch(ohneKommentare, /innerHTML|outerHTML|insertAdjacentHTML|document\.write|confirm\(/)
assert.doesNotMatch(ohneKommentare, /from ['"]\.\/termine/)
})