Files
Webklar-Kundenbereich/test/knAnsichtBestellen.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

182 lines
12 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 { zeigeBestellen, produktAusRoute } from '../public/neu/ansicht-bestellen.js'
import { mit, me, aboAntwort, angebot, neueWurzel } from './support/knDaten.js'
import { NEU, TERMS_URL, meMit, aboNeu } from './support/knBestellDaten.js'
// Bestellseite im schlanken Muster (Spec 5.6, Fassung 6): Block, Linkzeile, Häkchen nur privat, Knopf, Kundenart-Zeile
const route = (produkt) => ({ bereich: 'vertraege', etage: 'bestellen', produkt, query: '' })
// fmtEuro setzt ein geschütztes Leerzeichen vor das Euro-Zeichen
const eur = (text) => text.replaceAll(' €', '\u00a0€')
const IDS = ['abo-block', 'abo-linkzeile', 'abo-beginn', 'abo-error', 'abo-buy', 'abo-kundenart', 'abo-einzelheiten']
/** Seite zeigen; die Titel, die die Ansicht meldet (Ereignis kn:titel), sammelt fx.titel. */
async function zeige(fx, daten, meDaten, produkt = 'wartung') {
fx.titel = fx.titel ?? []
const w = neueWurzel(fx)
w.addEventListener('kn:titel', (e) => fx.titel.push(e.detail.text))
await zeigeBestellen(w, { route: route(produkt), me: meDaten, nav: 'push', daten })
await fx.ruhe()
return w
}
/** Die Ids aus IDS in Dokumentreihenfolge unterhalb von #abo-order. */
const reihe = (fx, w) => fx.alle(fx.finde(w, '#abo-order'), (el) => IDS.includes(el.id)).map((el) => el.id)
test('Betrieb: Block, Linkzeile, Knopf, Kundenart-Zeile in dieser Reihenfolge, ohne Häkchen und ohne Klappe', mit({}, async (fx) => {
const w = await zeige(fx, aboNeu(), meMit('unternehmen'))
assert.deepEqual(reihe(fx, w), ['abo-block', 'abo-linkzeile', 'abo-error', 'abo-buy', 'abo-kundenart', 'abo-einzelheiten'])
assert.equal(fx.finde(w, '#abo-buy').textContent, 'Zahlungspflichtig bestellen')
assert.equal(fx.alle(w, 'details, dialog, input[type="radio"], input[type="checkbox"]').length, 0)
const t = fx.text(fx.finde(w, '#abo-block'))
for (const s of ['Leistung', 'Preis', 'Fällig', 'Beginn', 'Laufzeit', 'Zahlung', '50,00', 'gemäß § 19 UStG ohne Umsatzsteuer', 'heute 50,00']) assert.ok(t.includes(s), s)
// die Zeilen „Anbieter“ und „Abrechnung“ entfallen (N37); das Wort steht nur im Satz zur Kündigung („Abrechnungsmonats“)
assert.deepEqual(fx.alle(fx.finde(w, '#abo-block'), 'dt').map((e) => fx.text(e)), ['Leistung', 'Preis', 'Fällig', 'Beginn', 'Laufzeit', 'Zahlung'])
assert.equal(fx.text(fx.finde(w, '#abo-start-summary')), 'sofort')
assert.equal(fx.text(fx.finde(w, '#abo-kundenart')), 'Du bestellst als Betrieb · Stimmt nicht? Schreib uns im Chat')
assert.equal(fx.finde(w, '#abo-linkzeile a').getAttribute('href'), TERMS_URL)
assert.equal(fx.text(fx.finde(w, '#abo-linkzeile')), 'Es gelten die Vertragsbedingungen mit Leistungsbeschreibung, Auftragsverarbeitung und Widerrufsbelehrung (Fassung ' + NEU + '). Alles kommt danach auch als PDF per Mail. Datenschutz')
}))
test('Block: knappe Zeilentexte über dem Knopf, die ausführlichen Sätze unter dem Knopf (F1)', mit({}, async (fx) => {
const w = await zeige(fx, aboNeu(), meMit('unternehmen'))
const zeilen = fx.alle(fx.finde(w, '#abo-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', 'Fällig', 'Beginn', 'Laufzeit', 'Zahlung'])
// Leistung: eine kurze Zeile mit Link auf die Leistungsbeschreibung B (Wartung), kein Aufzählungstext
assert.deepEqual(zeilen[0], ['Leistung', 'Wartungs-Abo · Leistungen ansehen'])
assert.deepEqual([fx.finde(fx.finde(w, '#abo-block'), 'a').getAttribute('href'), fx.finde(fx.finde(w, '#abo-block'), 'a').getAttribute('target')], [`${TERMS_URL}#wartung`, '_blank'])
assert.deepEqual(zeilen[1], ['Preis', eur('50,00 € pro Monat (Gesamtpreis, gemäß § 19 UStG ohne Umsatzsteuer)')])
assert.deepEqual(zeilen[2], ['Fällig', eur('heute 50,00 €')])
assert.deepEqual(zeilen[4], ['Laufzeit', 'Mindestlaufzeit 12 Monate, kündbar bis 1 Monat vor Ende; danach monatlich'])
assert.deepEqual(zeilen[5], ['Zahlung', 'Karte oder SEPA über Stripe'])
// jede Zeile höchstens 110 Zeichen: sonst passt die Kerninfo am Handy nicht mehr auf den Bildschirm
for (const [l, t] of zeilen) assert.ok(t.length <= 110, `${l}: ${t.length} Zeichen`)
// unter dem Knopf und der Kundenart-Zeile: die ausführlichen Sätze, ohne Klappe
const ein = fx.finde(w, '#abo-einzelheiten')
assert.equal(fx.alle(w, '#abo-order > *').map((e) => e.id).filter(Boolean).at(-1), 'abo-einzelheiten')
const z2 = fx.alle(ein, '.kn-block-zeile').map((z) => [fx.text(fx.finde(z, 'dt')), fx.text(fx.finde(z, 'dd'))])
assert.deepEqual(z2.map(([l]) => l), ['Laufzeit', 'Zahlung', 'Kosten'])
assert.equal(z2[0][1], 'Mindestlaufzeit 12 Monate ab Leistungsbeginn, kündbar zum Ende der Mindestlaufzeit bis einen Monat vorher; danach jederzeit zum Ende des Abrechnungsmonats kündbar')
assert.equal(z2[1][1], 'Kreditkarte oder SEPA-Lastschrift über Stripe; die Zahlungsart hinterlegst du im nächsten Schritt, bis zum Abschluss dort kannst du abbrechen')
assert.equal(z2[2][1], 'keine weiteren Kosten')
}))
test('Titel: Produktname als Ereignis, keine eigene h1 (P2, P1 VF11)', mit({}, async (fx) => {
const w = await zeige(fx, aboNeu(), meMit('unternehmen'))
assert.deepEqual(fx.titel, ['Wartungs-Abo'])
assert.equal(fx.alle(w, 'h1').length, 0)
assert.deepEqual(fx.alle(w, 'h2').map((e) => fx.text(e)), ['Was du bestellst'])
}))
test('Betrieb: Body ohne earlyStart, lateStartAt, startChoice und Kundenart', mit({ 'POST /api/abo/checkout': { url: 'https://checkout.stripe.test/x' } }, async (fx) => {
const w = await zeige(fx, aboNeu(), meMit('unternehmen'))
await fx.klick(fx.finde(w, '#abo-buy'))
await fx.ruhe()
assert.deepEqual(fx.fetchLog.at(-1), { method: 'POST', url: '/api/abo/checkout', body: { productKey: 'wartung', acceptTerms: true, termsVersion: NEU, offerId: '' } })
assert.equal(fx.window.location.href, 'https://checkout.stripe.test/x')
}))
test('Privat und leer: ein Häkchen aus dem Wortlaut, ungehakt nach der Frist, gehakt sofort', mit({ 'POST /api/abo/checkout': { url: 'https://checkout.stripe.test/x' } }, async (fx) => {
for (const m of [meMit('privat'), meMit('')]) {
const w = await zeige(fx, aboNeu(), m)
assert.deepEqual(reihe(fx, w), ['abo-block', 'abo-linkzeile', 'abo-beginn', 'abo-error', 'abo-buy', 'abo-kundenart', 'abo-einzelheiten'])
assert.equal(fx.finde(w, '#abo-beginn').checked, false)
assert.equal(fx.text(fx.finde(w, '#abo-beginn-text')), aboNeu().bestelltexte.abo.haken)
assert.ok(fx.text(fx.finde(w, '#abo-start-summary')).startsWith('nach Ablauf der 14-tägigen Widerrufsfrist (am '))
assert.ok(fx.text(fx.finde(w, '#abo-due-summary')).startsWith('erst am '))
assert.ok(fx.text(fx.finde(w, '#abo-kundenart')).startsWith('Du bestellst privat · '))
await fx.klick(fx.finde(w, '#abo-buy'))
await fx.ruhe()
assert.deepEqual(fx.fetchLog.at(-1).body, { productKey: 'wartung', acceptTerms: true, earlyStart: false, termsVersion: NEU, offerId: '', lateStartAt: aboNeu().lateStartAt })
const v = await zeige(fx, aboNeu(), m)
await fx.klick(fx.finde(v, '#abo-beginn'))
assert.ok(fx.text(fx.finde(v, '#abo-start-summary')).startsWith('sofort, auf deinen ausdrücklichen Wunsch'))
assert.ok(fx.text(fx.finde(v, '#abo-due-summary')).startsWith('heute '))
await fx.klick(fx.finde(v, '#abo-buy'))
await fx.ruhe()
assert.equal(fx.fetchLog.at(-1).body.earlyStart, true)
await fx.klick(fx.finde(v, '#abo-beginn')) // Häkchen wieder weg: die Zeilen folgen
assert.ok(fx.text(fx.finde(v, '#abo-start-summary')).startsWith('nach Ablauf der 14-tägigen Widerrufsfrist'))
}
}))
test('Der Body enthält nie kundenart oder startChoice', mit({ 'POST /api/abo/checkout': { url: 'https://checkout.stripe.test/x' } }, async (fx) => {
for (const m of [meMit('unternehmen'), meMit('privat'), meMit('')]) {
const w = await zeige(fx, aboNeu(), m)
await fx.klick(fx.finde(w, '#abo-buy'))
await fx.ruhe()
assert.deepEqual(Object.keys(fx.fetchLog.at(-1).body).filter((k) => /kundenart|startChoice|unternehmer/i.test(k)), [])
}
}))
test('Angebot: Angebotsnummer im Body, Text in Abschnitt und Preiszeile', mit({}, async (fx) => {
const d = aboNeu({ offers: { hosting: null, wartung: angebot() } })
const w = await zeige(fx, d, meMit('unternehmen'))
assert.ok(fx.text(fx.finde(w, '#abo-angebot')).includes(angebot().text))
assert.ok(fx.text(fx.finde(w, '#abo-block')).includes(`individuelles Angebot: ${angebot().text}`))
assert.ok(fx.text(fx.finde(w, '#abo-block')).includes(eur('25,00 € pro Monat für die ersten 2 Monate, danach 50,00 € pro Monat')))
assert.ok(fx.text(fx.finde(w, '#abo-due-summary')).startsWith('heute 25,00'))
await fx.klick(fx.finde(w, '#abo-buy'))
await fx.ruhe()
assert.equal(fx.fetchLog.at(-1).body.offerId, angebot().id)
}))
test('Ohne Wortlaut, nicht verfügbar oder unbekanntes Produkt: nur der Satz, kein Knopf; unsichere Linkadressen werden Text', mit({}, async (fx) => {
const satz = 'Die Online-Bestellung wird gerade eingerichtet – schreib uns gern im Chat.'
for (const d of [aboAntwort({ termsVersion: NEU }), aboNeu({ available: false })]) {
const w = await zeige(fx, d, meMit('unternehmen'))
assert.ok(fx.text(w).includes(satz))
assert.equal(fx.finde(w, '#abo-buy'), null)
}
assert.equal(fx.finde(await zeige(fx, aboNeu(), meMit('unternehmen'), 'gibtsnicht'), '#abo-buy'), null)
const boese = aboNeu()
boese.bestelltexte.abo.linkzeile.urls = ['javascript:alert(1)', '//evil.example']
assert.equal(fx.alle(await zeige(fx, boese, meMit('unternehmen')), '#abo-linkzeile a[href]').length, 0)
}))
test('produktAusRoute', () => {
assert.equal(produktAusRoute({ produkt: 'wartung' }, ''), 'wartung')
assert.equal(produktAusRoute({}, '#bestellen-hosting'), 'hosting')
assert.equal(produktAusRoute({}, '#abo'), '')
})
test('Fehler der Zahlungsseite: Text in #abo-error, Knopf zurück; unsichere Zahlungsadresse leitet nicht weiter', mit({ 'POST /api/abo/checkout': () => ({ status: 400, body: { error: 'Kaputt.' } }) }, async (fx) => {
const w = await zeige(fx, aboNeu(), meMit('unternehmen'))
await fx.klick(fx.finde(w, '#abo-buy'))
await fx.ruhe()
assert.equal(fx.text(fx.finde(w, '#abo-error')), 'Kaputt.')
assert.equal(fx.finde(w, '#abo-error').hasAttribute('hidden'), false)
assert.equal(fx.finde(w, '#abo-buy').hasAttribute('disabled'), false)
assert.equal(fx.finde(w, '#abo-buy').textContent, 'Zahlungspflichtig bestellen')
fx.tabelle['POST /api/abo/checkout'] = () => ({ body: { url: 'javascript:alert(1)' } })
await fx.klick(fx.finde(w, '#abo-buy'))
await fx.ruhe()
assert.notEqual(fx.window.location.href, 'javascript:alert(1)')
assert.equal(fx.text(fx.finde(w, '#abo-error')), 'Die Zahlungsseite konnte nicht geöffnet werden. Bitte versuche es später noch einmal.')
assert.equal(fx.finde(w, '#abo-buy').hasAttribute('disabled'), false)
}))
test('Ansichtsmodus sperrt Knopf und Häkchen, Testkonto-Hinweis in du', mit({}, async (fx) => {
const v = await zeige(fx, aboNeu({ testAccount: true }), meMit('privat', { viewAs: true }))
assert.equal(fx.finde(v, '#abo-buy').hasAttribute('disabled'), true)
assert.equal(fx.finde(v, '#abo-beginn').hasAttribute('disabled'), true)
assert.match(fx.text(v), /Im Ansichtsmodus nicht möglich\./)
assert.match(fx.text(v), /Testkonto: Bei Stripe wird ein Test-Rabatt verrechnet, abgebucht werden nur 0,50 € pro Monat\./) // Literal im Quelltext mit normalem Leerzeichen
await fx.klick(fx.finde(v, '#abo-buy'))
assert.equal(fx.fetchLog.length, 0)
}))
test('Kein Abo geladen: nur der Hinweis', mit({ 'GET /api/abo': () => ({ status: 500, body: { error: 'x' } }) }, async (fx) => {
const v = await zeige(fx, undefined, me())
assert.match(fx.text(v), /Deine Verträge konnten gerade nicht geladen werden\./)
}))
test('Quelltext: kein Sie, keine Klappe, kein startChoice', async () => {
const { readFileSync } = await import('node:fs')
const q = readFileSync(new URL('../public/neu/ansicht-bestellen.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/)
const aufruf = q.slice(q.indexOf("api('/api/abo/checkout'"), q.indexOf('const ziel'))
assert.ok(aufruf.includes('acceptTerms: true'), 'der Aufruf der Bestellung ist gefunden')
assert.doesNotMatch(aufruf, /startChoice|kundenart|unternehmer/)
})