336 lines
17 KiB
JavaScript
336 lines
17 KiB
JavaScript
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: true,
|
||
versand: { aktiv: true, laender: ['DE'], kostenCent: { DE: 490 }, freiAbCent: null },
|
||
abholung: { aktiv: false, adresse: '', hinweis: '' },
|
||
lieferzeit: '2–4 Werktage',
|
||
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}"]`)
|
||
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')
|
||
assert.equal(feldVon(root, 'kleinunternehmer').checked, true)
|
||
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, '2–4 Werktage')
|
||
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), /Kleinunternehmer nach § 19 UStG/)
|
||
// Ö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(feldVon(root, 'kleinunternehmer').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('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(feldVon(root, 'kleinunternehmer'))
|
||
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/)
|
||
})
|