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

197 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 { 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.match(fx.text(fx.finde(w, '#abo-due-summary')), /^erst am \d{2}\.\d{2}\.\d{4} \(50,00\u00a0€\)$/)
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('M6: zurück von der Zahlungsseite (Wischgeste, Bfcache) ist der Knopf wieder bedienbar; ein später entfernter Knopf bekommt nichts mehr', mit({ 'POST /api/abo/checkout': { url: 'https://checkout.stripe.test/x' } }, async (fx) => {
const w = await zeige(fx, aboNeu(), meMit('privat'))
const knopf = fx.finde(w, '#abo-buy')
await fx.klick(knopf); await fx.ruhe()
assert.deepEqual([knopf.hasAttribute('disabled'), knopf.textContent], [true, 'Weiter zur Zahlung…'])
fx.window.dispatchEvent({ type: 'pageshow', persisted: false }); assert.equal(knopf.hasAttribute('disabled'), true, 'ein normales Laden (nicht persisted) ändert nichts')
fx.window.dispatchEvent({ type: 'pageshow', persisted: true })
assert.deepEqual([knopf.hasAttribute('disabled'), knopf.textContent], [false, 'Zahlungspflichtig bestellen'])
// der Knopf kann wieder bestellen
await fx.klick(knopf); await fx.ruhe(); assert.equal(fx.fetchLog.filter((a) => a.url === '/api/abo/checkout').length, 2)
// die Seite wurde ersetzt: der Hörer entfernt sich und fasst den alten Knopf nicht mehr an
w.replaceChildren(); knopf.disabled = true
fx.window.dispatchEvent({ type: 'pageshow', persisted: true }); assert.equal(knopf.hasAttribute('disabled'), true)
}))
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/)
})