Files
Webklar-Kundenbereich/test/knAnsichtWechsel.test.js
T
ClaudeandClaude Sonnet 5.5 d4050f870a Kundenansicht neu E-Fix F1: Kerninfo und Knopf der Bestellseiten passen in den nutzbaren Bereich (390x844, 375x667)
Die Messung im Werkzeug nahm innerHeight; nutzbar ist am Handy nur der Bereich zwischen Kopf und fester Leiste (735 px bei 844, 558 px bei 667).
Gemessen vor der Korrektur (Blockanfang bis Knopfunterkante, 375x667): Abo bestellen privat 799 px, Wechsel privat 888 px, Zusatzspeicher 567 px.

- Block knapp: Leistung als eine Zeile mit Link "Leistungen ansehen" auf die Leistungsbeschreibung, Preis, Fällig, Beginn, Laufzeit und Zahlung in je ein bis zwei Zeilen.
- Die ausführlichen Sätze stehen unter dem Knopf und der Kundenart-Zeile (baueEinzelheiten), ohne Klappe.
- Handy: Beschriftung läuft in der Zeile mit, kleinere Überschrift, knappere Abstände.
- Die Wortlaute des Blocks sind gekürzt und gehören in die Rechtsrunde.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:25:48 +00:00

238 lines
15 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'))])
assert.deepEqual(zeilen.map(([l]) => l), ['Leistung', 'Preis', 'Zusatzspeicher', 'Gutschrift', '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[2][1].includes('2 Pakete (+10 GB)'))
assert.ok(zeilen[3][1].startsWith('−15,00'))
assert.ok(zeilen[4][1].startsWith('heute 30,00'))
assert.equal(zeilen[5][1], 'Mindestlaufzeit bis 16.10.2027 (12 Monate), danach monatlich kündbar')
assert.ok(zeilen[6][1].startsWith('sofort mit der Zahlung'))
for (const [l, text] of zeilen) assert.ok(text.length <= 120, `${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', '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\./)
}))
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/)
})