Files
Webklar-Kundenbereich/test/knAnsichtWechsel.test.js
T

250 lines
17 KiB
JavaScript
Raw 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 { zeigeWechsel, gemerkteFunktion } from '../public/neu/ansicht-wechsel.js'
import { nimmHinweis } from '../public/neu/ansicht-hilfen.js'
import { mit, me, abo, aboAntwort, neueWurzel, VORSCHAU } from './support/knDaten.js'
import { NEU, TERMS_URL, meMit, aboNeu } from './support/knBestellDaten.js'
// Wechselseite im schlanken Muster (Spec 5.6, Fassung 6): Block, Linkzeile, Häkchen nur privat mit Sperrzeile, Knopf, Kundenart-Zeile
const HOSTING = (o = {}) => aboNeu({ subscription: abo({ produkt: 'hosting', canSwitch: true, storage: { units: 2, pendingSetup: false, pendingReduction: null } }), ...o })
const SRV = { 'POST /api/abo/switch/preview': VORSCHAU, 'POST /api/abo/switch': () => ({ body: { paymentUrl: 'https://pay.stripe.test/x', subscription: abo() } }) }
const IDS = ['switch-block', 'switch-linkzeile', 'switch-beginn', 'switch-sperre', 'switch-error', 'switch-buy', 'switch-kundenart', 'switch-einzelheiten']
const reihe = (fx, w) => fx.alle(fx.finde(w, '#switch-order'), (el) => IDS.includes(el.id)).map((el) => el.id)
const ROUTE = { bereich: 'vertraege', etage: 'wechsel', query: '' }
/** Seite zeigen; die gemeldeten Titel (kn:titel) sammelt fx.titel. */
async function zeige(fx, daten, meDaten) {
fx.titel = fx.titel ?? []
const w = neueWurzel(fx)
w.addEventListener('kn:titel', (e) => fx.titel.push(e.detail.text))
await zeigeWechsel(w, { route: ROUTE, me: meDaten, nav: 'push', daten })
await fx.ruhe()
return w
}
test('Betrieb: Block, Linkzeile, Knopf, kein Häkchen, Knopf nach der Vorschau frei, Body ohne earlyStart', mit(SRV, async (fx) => {
const w = await zeige(fx, HOSTING(), meMit('unternehmen'))
assert.deepEqual(fx.titel, ['Wechsel ins Wartungs-Abo'])
assert.equal(fx.alle(w, 'h1').length, 0) // Titel kommt als Ereignis, keine eigene h1
assert.deepEqual(reihe(fx, w), ['switch-block', 'switch-linkzeile', 'switch-error', 'switch-buy', 'switch-kundenart', 'switch-einzelheiten'])
// Block über dem Knopf: knapp (F1); Leistung als eine Zeile mit Link auf die Leistungsbeschreibung B (Wartung)
const zeilen = fx.alle(fx.finde(w, '#switch-block'), '.kn-block-zeile').map((z) => [fx.text(fx.finde(z, 'dt')), fx.text(fx.finde(z, 'dd'))])
// Zusatzspeicher steckt im Preis (ein Block weniger über dem Knopf), die gutgeschriebenen Tage in „Fällig“ (F1: auch mit gebuchtem Speicher auf 375 x 667)
assert.deepEqual(zeilen.map(([l]) => l), ['Leistung', 'Preis', 'Fällig', 'Laufzeit', 'Beginn'])
assert.deepEqual(zeilen[0], ['Leistung', 'Wartungs-Abo statt Hosting-Abo · Leistungen ansehen'])
assert.equal(fx.finde(fx.finde(w, '#switch-block'), 'a').getAttribute('href'), `${TERMS_URL}#wartung`)
assert.ok(zeilen[1][1].includes('mit 2 Paketen Zusatzspeicher (+10 GB)') && zeilen[1][1].includes('gemäß § 19 UStG ohne Umsatzsteuer'), zeilen[1][1])
assert.ok(zeilen[2][1].startsWith('heute 30,00') && zeilen[2][1].includes('Gutschrift von 15,00') && zeilen[2][1].includes('ab 16.11.2026'), zeilen[2][1])
assert.equal(zeilen[3][1], 'Mindestlaufzeit bis 16.10.2027 (12 Monate), danach monatlich kündbar')
assert.ok(zeilen[4][1].startsWith('sofort mit der Zahlung'))
for (const [l, text] of zeilen) assert.ok(text.length <= 140, `${l}: ${text.length} Zeichen`)
// unter dem Knopf: die ausführlichen Sätze, ohne Klappe
assert.equal(fx.alle(w, '#switch-order > *').map((e) => e.id).filter(Boolean).at(-1), 'switch-einzelheiten')
const ein = fx.text(fx.finde(w, '#switch-einzelheiten'))
for (const x of ['Hosting-Abo endet mit dem Wechsel im gegenseitigen Einvernehmen', 'Zusatzspeicher', '2 Pakete (+10 GB) bleiben gebucht', 'Gutschrift für nicht genutzte Tage', '−15,00', 'Neuer Abbuchungstag', 'nächste Abbuchung am', 'Heute fällig', 'bei deiner Bank', 'Einen späteren Beginn gibt es nicht', '12 Monate ab dem Wechsel']) assert.ok(ein.includes(x), x)
assert.equal(fx.finde(w, '#switch-buy').textContent, 'Zahlungspflichtig bestellen')
assert.equal(fx.finde(w, '#switch-buy').hasAttribute('disabled'), false)
assert.equal(fx.alle(w, 'details, dialog, input[type="checkbox"]').length, 0)
assert.equal(fx.text(fx.finde(w, '#switch-kundenart')), 'Du bestellst als Betrieb · Stimmt nicht? Schreib uns im Chat')
await fx.klick(fx.finde(w, '#switch-buy'))
await fx.ruhe()
assert.deepEqual(fx.fetchLog.find((a) => a.url === '/api/abo/switch').body, { acceptTerms: true, termsVersion: NEU, prorationDate: VORSCHAU.prorationDate, amountDueCents: VORSCHAU.amountDueCents })
assert.equal(fx.window.location.href, 'https://pay.stripe.test/x')
assert.match(fx.text(w), /Dein Wechsel wartet auf die Zahlung\. Wir leiten dich zur Zahlung weiter\./)
}))
// E-Fix F1 (Nachkorrektur): nach „mit“ steht der Dativ Plural; „Paket“ im Singular bleibt, „Pakete“ in „… bleiben gebucht“ ist ein Nominativ
test('Preiszeile: „mit 1 Paket“ und „mit 2 Paketen“ Zusatzspeicher (Dativ), die Einzelheiten nennen „1 Paket“ bzw. „2 Pakete“', mit(SRV, async (fx) => {
for (const [einheiten, mitText, einzelText] of [[1, 'mit 1 Paket Zusatzspeicher (+5 GB)', '1 Paket (+5 GB) bleiben gebucht'], [2, 'mit 2 Paketen Zusatzspeicher (+10 GB)', '2 Pakete (+10 GB) bleiben gebucht']]) {
const daten = HOSTING({ subscription: abo({ produkt: 'hosting', canSwitch: true, storage: { units: einheiten, pendingSetup: false, pendingReduction: null } }) })
const w = await zeige(fx, daten, meMit('unternehmen'))
const preis = fx.text(fx.alle(fx.finde(w, '#switch-block'), '.kn-block-zeile')[1])
assert.ok(preis.includes(mitText), preis)
assert.ok(!/mit \d+ Pakete /.test(preis), preis) // nie „mit 2 Pakete Zusatzspeicher“
assert.ok(fx.text(fx.finde(w, '#switch-einzelheiten')).includes(einzelText), einzelText)
}
}))
test('Privat und leer: Knopf gesperrt mit Sperrzeile, nach dem Häkchen frei, Body mit earlyStart true', mit(SRV, async (fx) => {
for (const m of [meMit('privat'), meMit('')]) {
const w = await zeige(fx, HOSTING(), m)
assert.deepEqual(reihe(fx, w), ['switch-block', 'switch-linkzeile', 'switch-beginn', 'switch-sperre', 'switch-error', 'switch-buy', 'switch-kundenart', 'switch-einzelheiten'])
assert.equal(fx.finde(w, '#switch-buy').hasAttribute('disabled'), true)
assert.equal(fx.text(fx.finde(w, '#switch-sperre')), HOSTING().bestelltexte.wechsel.gesperrt)
assert.equal(fx.text(fx.finde(w, '#switch-beginn-text')), HOSTING().bestelltexte.wechsel.haken)
assert.equal(fx.finde(w, '#switch-beginn').checked, false)
await fx.klick(fx.finde(w, '#switch-beginn'))
assert.equal(fx.finde(w, '#switch-buy').hasAttribute('disabled'), false)
assert.equal(fx.finde(w, '#switch-sperre'), null)
await fx.klick(fx.finde(w, '#switch-buy'))
await fx.ruhe()
assert.deepEqual(fx.fetchLog.findLast((a) => a.url === '/api/abo/switch').body, { acceptTerms: true, earlyStart: true, termsVersion: NEU, prorationDate: VORSCHAU.prorationDate, amountDueCents: VORSCHAU.amountDueCents })
}
}))
test('Das Häkchen bleibt beim Neuzeichnen erhalten und lässt sich wieder abwählen', mit(SRV, async (fx) => {
const w = await zeige(fx, HOSTING(), meMit('privat'))
await fx.klick(fx.finde(w, '#switch-beginn'))
assert.equal(fx.finde(w, '#switch-beginn').checked, true)
await fx.klick(fx.finde(w, '#switch-beginn'))
assert.equal(fx.finde(w, '#switch-beginn').checked, false)
assert.equal(fx.finde(w, '#switch-buy').hasAttribute('disabled'), true)
assert.ok(fx.finde(w, '#switch-sperre'))
}))
test('Der Body enthält nie kundenart, unternehmer oder startChoice', mit(SRV, async (fx) => {
for (const m of [meMit('unternehmen'), meMit('privat')]) {
const w = await zeige(fx, HOSTING(), m)
if (fx.finde(w, '#switch-beginn')) await fx.klick(fx.finde(w, '#switch-beginn'))
await fx.klick(fx.finde(w, '#switch-buy'))
await fx.ruhe()
assert.deepEqual(Object.keys(fx.fetchLog.findLast((a) => a.url === '/api/abo/switch').body).filter((k) => /kundenart|startChoice|unternehmer/i.test(k)), [])
}
}))
test('Wechsel nicht möglich: Satz und Rückweg zu Verträgen', mit({}, async (fx) => {
fx.window.history.replaceState = (_s, _t, url) => { fx.window.location.hash = `#${String(url).split('#')[1] ?? ''}` }
const w = await zeige(fx, aboNeu({ subscription: abo({ produkt: 'wartung' }) }), meMit('privat'))
assert.match(fx.text(w), /Ein Wechsel ist derzeit nicht möglich\. Details findest du unter Verträge\./)
assert.equal(fx.window.location.hash, '#vertraege')
}))
test('Ohne Wortlaut oder ohne Sätze des Wechsels: nur ein Satz, kein Knopf, keine Vorschau-Abfrage', mit(SRV, async (fx) => {
const ohneWechsel = HOSTING()
delete ohneWechsel.bestelltexte.wechsel
for (const d of [aboAntwort({ termsVersion: NEU, subscription: abo({ produkt: 'hosting', canSwitch: true }) }), ohneWechsel]) {
const w = await zeige(fx, d, meMit('unternehmen'))
assert.match(fx.text(w), /Die Online-Bestellung wird gerade eingerichtet – schreib uns gern im Chat\./)
assert.equal(fx.finde(w, '#switch-buy'), null)
}
assert.equal(fx.fetchLog.filter((a) => a.url === '/api/abo/switch/preview').length, 0)
}))
test('Unsichere Linkadressen werden Text', mit(SRV, async (fx) => {
const boese = HOSTING()
boese.bestelltexte.wechsel.linkzeile.urls = ['javascript:alert(1)', '//evil.example']
assert.equal(fx.alle(await zeige(fx, boese, meMit('unternehmen')), '#switch-linkzeile a[href]').length, 0)
}))
test('Prüfungen: Body mit gemerkter Funktion, Schlüssel danach weg', mit(SRV, async (fx) => {
const speicher = new Map()
globalThis.sessionStorage = { getItem: (k) => speicher.get(k) ?? null, setItem: (k, v) => speicher.set(k, v), removeItem: (k) => speicher.delete(k) }
try {
const w = await zeige(fx, HOSTING(), meMit('privat'))
await fx.klick(fx.finde(w, '#switch-beginn'))
speicher.set('kn-wechsel-funktion', JSON.stringify({ key: 'termine', projectId: 'p1' }))
await fx.klick(fx.finde(w, '#switch-buy'))
await fx.ruhe()
assert.deepEqual(fx.fetchLog.find((a) => a.url === '/api/abo/switch').body, { acceptTerms: true, earlyStart: true, termsVersion: NEU, prorationDate: 1760000000, amountDueCents: 3000, funktion: { key: 'termine', projectId: 'p1' } })
assert.equal(speicher.has('kn-wechsel-funktion'), false)
assert.equal(fx.window.location.href, 'https://pay.stripe.test/x')
} finally {
delete globalThis.sessionStorage
}
}))
test('gemerkteFunktion: kaputte oder fremde Werte ergeben null', () => {
const wert = (v) => { globalThis.sessionStorage = { getItem: () => v }; try { return gemerkteFunktion() } finally { delete globalThis.sessionStorage } }
assert.deepEqual(wert('{"key":"shop","projectId":"p2"}'), { key: 'shop', projectId: 'p2' })
for (const v of ['kaputt', '{"key":"shop"}', '{"key":1,"projectId":"p"}', null]) assert.equal(wert(v), null)
assert.equal(gemerkteFunktion(), null) // ohne sessionStorage
})
const FUNKTION = { key: 'termine', projectId: 'p1' }
const WOHIN = (fx) => { fx.window.history.replaceState = (_s, _t, url) => { fx.window.location.hash = `#${String(url).split('#')[1] ?? ''}` }; fx.window.history.pushState = fx.window.history.replaceState }
for (const [name, gemerkt, antwort, nachgeladen, ziel] of [
['gemerkt, Funktion steht in der Antwort', true, { wechselFunktion: FUNKTION }, undefined, '#funktionen'],
['gemerkt, Funktion erst im GET /api/abo', true, {}, { wechselFunktion: FUNKTION }, '#funktionen'],
['gemerkt, Wechsel noch nicht erledigt', true, {}, { wechselFunktion: null }, '#vertraege'],
['nicht gemerkt, der Server meldet trotzdem eine Funktion', false, { wechselFunktion: FUNKTION }, { wechselFunktion: FUNKTION }, '#vertraege'],
]) {
test(`Wechsel ohne Zahlungsseite (${name}): Ziel ${ziel} (E14, C5)`, mit({
'POST /api/abo/switch/preview': VORSCHAU, 'POST /api/abo/switch': () => ({ body: { subscription: abo(antwort) } }),
'GET /api/abo': aboNeu({ subscription: abo({ produkt: 'wartung', ...nachgeladen }) }),
}, async (fx) => {
WOHIN(fx)
const speicher = new Map(gemerkt ? [['kn-wechsel-funktion', JSON.stringify(FUNKTION)]] : [])
globalThis.sessionStorage = { getItem: (k) => speicher.get(k) ?? null, setItem: (k, v) => speicher.set(k, v), removeItem: (k) => speicher.delete(k) }
try {
nimmHinweis()
const w = await zeige(fx, HOSTING(), meMit('unternehmen'))
await fx.klick(fx.finde(w, '#switch-buy'))
await fx.ruhe()
assert.equal(fx.window.location.hash, ziel)
assert.equal(speicher.has('kn-wechsel-funktion'), false)
assert.equal(nimmHinweis() !== '', ziel === '#vertraege') // bei #funktionen bleibt kein Hinweis für später liegen
assert.equal(fx.fetchLog.filter((a) => a.url === '/api/abo').length, gemerkt && !antwort.wechselFunktion ? 1 : 0) // höchstens ein Nachladen, nur mit gemerkter Funktion
} finally {
delete globalThis.sessionStorage
}
}))
}
test('Erfolgstexte in du: SEPA und Karte', mit({ 'POST /api/abo/switch/preview': VORSCHAU, 'POST /api/abo/switch': () => ({ body: { subscription: abo({ switchPending: { type: 'wechsel' } }) } }) }, async (fx) => {
WOHIN(fx)
nimmHinweis()
const w = await zeige(fx, HOSTING(), meMit('unternehmen'))
await fx.klick(fx.finde(w, '#switch-buy'))
await fx.ruhe()
assert.equal(nimmHinweis(), 'Dein Wechsel ins Wartungs-Abo gilt ab sofort. Die Lastschrift wird noch eingezogen; wir bestätigen dir den Wechsel per E-Mail.')
fx.tabelle['POST /api/abo/switch'] = () => ({ body: { subscription: abo() } })
const v = await zeige(fx, HOSTING(), meMit('unternehmen'))
await fx.klick(fx.finde(v, '#switch-buy'))
await fx.ruhe()
assert.equal(nimmHinweis(), 'Dein Wechsel ins Wartungs-Abo ist abgeschlossen. Wir bestätigen ihn per E-Mail.')
}))
test('409 lädt die Vorschau neu und behält die Meldung; Ansichtsmodus sperrt Knopf und Häkchen', mit({ 'POST /api/abo/switch/preview': VORSCHAU, 'POST /api/abo/switch': () => ({ status: 409, body: { error: 'Betrag hat sich geändert.' } }) }, async (fx) => {
const w = await zeige(fx, HOSTING(), meMit('privat'))
await fx.klick(fx.finde(w, '#switch-beginn'))
await fx.klick(fx.finde(w, '#switch-buy'))
await fx.ruhe()
assert.match(fx.text(w), /Betrag hat sich geändert\./)
assert.equal(fx.fetchLog.filter((a) => a.url === '/api/abo/switch/preview').length, 2)
assert.equal(fx.finde(w, '#switch-beginn').checked, true, 'das Häkchen bleibt nach dem Neuzeichnen')
const v = await zeige(fx, HOSTING(), meMit('privat', { viewAs: true }))
assert.equal(fx.finde(v, '#switch-buy').hasAttribute('disabled'), true)
assert.equal(fx.finde(v, '#switch-beginn').hasAttribute('disabled'), true)
assert.match(fx.text(v), /Im Ansichtsmodus nicht möglich\./)
}))
test('Vorschau-Fehler, hängende Bestellung nach pageshow; im Ansichtsmodus geht kein POST raus', mit({ 'POST /api/abo/switch/preview': () => ({ status: 500, body: { error: 'Vorschau kaputt.' } }) }, async (fx) => {
const w = await zeige(fx, HOSTING(), meMit('unternehmen'))
assert.match(fx.text(w), /Vorschau kaputt\./)
assert.equal(fx.finde(w, '#switch-buy').hasAttribute('disabled'), true)
fx.tabelle['POST /api/abo/switch/preview'] = VORSCHAU
fx.tabelle['POST /api/abo/switch'] = { paymentUrl: 'https://pay.stripe.test/y', subscription: abo() }
const v = await zeige(fx, HOSTING(), meMit('unternehmen'))
await fx.klick(fx.finde(v, '#switch-buy'))
await fx.ruhe()
assert.equal(fx.finde(v, '#switch-buy').hasAttribute('disabled'), true) // wartet auf die Zahlung
fx.window.dispatchEvent({ type: 'pageshow', persisted: true })
await fx.ruhe()
assert.equal(fx.finde(v, '#switch-buy').hasAttribute('disabled'), false)
assert.equal(fx.finde(v, '#switch-buy').textContent, 'Zahlungspflichtig bestellen')
const vorher = fx.fetchLog.length
const a = await zeige(fx, HOSTING(), meMit('unternehmen', { viewAs: true }))
await fx.klick(fx.finde(a, '#switch-buy'))
assert.deepEqual(fx.fetchLog.slice(vorher).filter((x) => x.url === '/api/abo/switch'), [])
}))
test('Unsichere Zahlungsadresse wird nicht geöffnet und nicht als Erfolg gezeigt', mit({ 'POST /api/abo/switch/preview': VORSCHAU, 'POST /api/abo/switch': () => ({ body: { paymentUrl: 'javascript:alert(1)', subscription: abo() } }) }, async (fx) => {
nimmHinweis()
const w = await zeige(fx, HOSTING(), meMit('unternehmen'))
await fx.klick(fx.finde(w, '#switch-buy'))
await fx.ruhe()
assert.notEqual(fx.window.location.href, 'javascript:alert(1)')
assert.match(fx.text(w), /Die Zahlungsseite konnte nicht geöffnet werden\./)
assert.equal(nimmHinweis(), '')
}))
test('Quelltext: kein Sie, keine Klappe, alte Ids weg, Body ohne kundenart', () => {
const q = readFileSync(new URL('../public/neu/ansicht-wechsel.js', import.meta.url), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '')
assert.doesNotMatch(q, /\b(Sie|Ihr(e[nmrs]?)?|Ihnen)\b/)
assert.doesNotMatch(q, /details|dialog|innerHTML|switch-terms|switch-early/)
const aufruf = q.slice(q.indexOf('const body = {'), q.indexOf("api('/api/abo/switch'"))
assert.ok(aufruf.includes('acceptTerms: true'), 'der Body der Bestellung ist gefunden')
assert.doesNotMatch(aufruf, /startChoice|kundenart|unternehmer/)
})