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

225 lines
14 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, 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']
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'])
const t = fx.text(fx.finde(w, '#switch-block'))
for (const s of ['Leistung', 'Wartungs-Abo', 'Zusatzspeicher', '2 Pakete (+10 GB)', 'Gutschrift für nicht genutzte Tage', '−15,00', 'Heute fällig', '30,00', 'Neuer Abbuchungstag', 'Laufzeit', 'Beginn', 'bei deiner Bank']) assert.ok(t.includes(s), s)
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\./)
}))
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'])
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/)
})