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>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-16 21:25:48 +00:00
1 parent 779320c080
commit d4050f870a
8 files changed
+148 -41

No files matched your search

+20 -11
View File
@@ -1,6 +1,8 @@
/* Kundenansicht neu (P7): Bestellseite je Abo (#bestellen-hosting, #bestellen-wartung) im schlanken Muster der Spec 5.6, Fassung 6. /* Kundenansicht neu (P7): Bestellseite je Abo (#bestellen-hosting, #bestellen-wartung) im schlanken Muster der Spec 5.6, Fassung 6.
Reihenfolge ohne Klappe und ohne Scrollbereich: Block „Was du bestellst“, eine Linkzeile, bei Privatkunden ein Häkchen, der Knopf Reihenfolge ohne Klappe und ohne Scrollbereich: Block „Was du bestellst“, eine Linkzeile, bei Privatkunden ein Häkchen, der Knopf
„Zahlungspflichtig bestellen“ (§ 312j BGB: Kerninfo und Knopf auf demselben Bildschirm), darunter die Kundenart-Zeile. „Zahlungspflichtig bestellen“ (§ 312j BGB: Kerninfo und Knopf auf demselben Bildschirm), darunter die Kundenart-Zeile.
Der Block ist knapp gehalten (Leistung als eine Zeile mit Link auf die Leistungsbeschreibung), damit er mit dem Knopf in den nutzbaren Bereich
zwischen Kopf und Leiste passt (375 x 667); die ausführlichen Sätze stehen unter dem Knopf (baueEinzelheiten), ohne Klappe.
Die Sätze (Linkzeile, Häkchen, Kundenart-Zeile) kommen vom Server (GET /api/abo → bestelltexte). Der Server entscheidet die Kundenart selbst: Die Sätze (Linkzeile, Häkchen, Kundenart-Zeile) kommen vom Server (GET /api/abo → bestelltexte). Der Server entscheidet die Kundenart selbst:
der Client sendet nie kundenart oder startChoice, bei Privatkunden nur earlyStart (das Häkchen) und den gezeigten Leistungsbeginn. der Client sendet nie kundenart oder startChoice, bei Privatkunden nur earlyStart (das Häkchen) und den gezeigten Leistungsbeginn.
Der Titel kommt von der Hülle (meldeTitel); die Seite hat keine eigene h1. */ Der Titel kommt von der Hülle (meldeTitel); die Seite hat keine eigene h1. */
@@ -8,7 +10,7 @@
import { api } from './api.js' import { api } from './api.js'
import { h, leeren } from './dom.js' import { h, leeren } from './dom.js'
import { K, ladeAbo, sichereAdresse, meldeTitel, fmtEuro, fmtDatum, ABO_NICHT_GELADEN } from './ansicht-hilfen.js' import { K, ladeAbo, sichereAdresse, meldeTitel, fmtEuro, fmtDatum, ABO_NICHT_GELADEN } from './ansicht-hilfen.js'
import { baueBlock, baueKundenartZeile, baueLinkzeile, hatWortlaut, kundenartVon } from './bestell-bausteine.js' import { baueBlock, baueEinzelheiten, baueKundenartZeile, baueLinkzeile, hatWortlaut, kundenartVon, leistungLink } from './bestell-bausteine.js'
const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs
const TEXT_SPERRE = 'Im Ansichtsmodus nicht möglich.' const TEXT_SPERRE = 'Im Ansichtsmodus nicht möglich.'
@@ -35,10 +37,10 @@ function preise(product, offer) {
const mitZahlen = Boolean(offer) && Number.isFinite(offerMonthly) && offer.monthlyPriceCents != null && offerMonths > 0 const mitZahlen = Boolean(offer) && Number.isFinite(offerMonthly) && offer.monthlyPriceCents != null && offerMonths > 0
const preis = fmtEuro(product.amountCents / 100) const preis = fmtEuro(product.amountCents / 100)
const preisZeile = !offer const preisZeile = !offer
? `${preis} pro Monat (Gesamtpreis, ${UST}), keine weiteren Kosten` ? `${preis} pro Monat (Gesamtpreis, ${UST})`
: mitZahlen : mitZahlen
? `${fmtEuro(offerMonthly / 100)} pro Monat für die ersten ${offerMonths} Monate, danach ${fmtEuro(offerRegular / 100)} pro Monat (individuelles Angebot: ${offer.text}; Gesamtpreis, ${UST}), keine weiteren Kosten` ? `${fmtEuro(offerMonthly / 100)} pro Monat für die ersten ${offerMonths} Monate, danach ${fmtEuro(offerRegular / 100)} pro Monat (individuelles Angebot: ${offer.text}; Gesamtpreis, ${UST})`
: `${preis} pro Monat (individuelles Angebot: ${offer.text}; Gesamtpreis, ${UST}), keine weiteren Kosten` : `${preis} pro Monat (individuelles Angebot: ${offer.text}; Gesamtpreis, ${UST})`
return { preisZeile, erste: fmtEuro((mitZahlen ? offerMonthly : product.amountCents) / 100) } return { preisZeile, erste: fmtEuro((mitZahlen ? offerMonthly : product.amountCents) / 100) }
} }
@@ -46,13 +48,13 @@ function preise(product, offer) {
function faelligBeginn({ lage, erste, frist }) { function faelligBeginn({ lage, erste, frist }) {
if (lage === 'frist') { if (lage === 'frist') {
return { return {
faellig: `Erste Abbuchung am ${frist || 'Ende der 14-tägigen Widerrufsfrist'}: ${erste} (${UST}); heute wird nichts abgebucht`, faellig: `erst am ${frist || 'Ende der Widerrufsfrist'}: ${erste}`,
beginn: `nach Ablauf der 14-tägigen Widerrufsfrist${frist ? ` (am ${frist})` : ''}; bis dahin keine Leistung und keine Abbuchung`, beginn: `nach Ablauf der 14-tägigen Widerrufsfrist${frist ? ` (am ${frist})` : ''}`,
} }
} }
return { return {
faellig: `Heute fällig: ${erste} (${UST})`, faellig: `heute ${erste}`,
beginn: lage === 'wunsch' ? 'sofort, auf deinen ausdrücklichen Wunsch vor Ablauf der Widerrufsfrist' : 'sofort', beginn: lage === 'wunsch' ? 'sofort, auf deinen ausdrücklichen Wunsch' : 'sofort',
} }
} }
@@ -82,13 +84,19 @@ function bestellung(daten, productKey, { viewAs, kundenart }) {
const start = faelligBeginn({ lage: privat ? 'frist' : 'sofort', erste, frist }) const start = faelligBeginn({ lage: privat ? 'frist' : 'sofort', erste, frist })
const block = baueBlock([ const block = baueBlock([
{ label: 'Leistung', text: `${product.name}: ${(product.features || []).map(([t]) => t).join(', ')}` }, { label: 'Leistung', text: labelVon(daten, productKey), link: { text: 'Leistungen ansehen', href: leistungLink(T.abo.linkzeile, productKey) } },
{ label: 'Preis', text: preisZeile }, { label: 'Preis', text: preisZeile },
{ id: 'abo-due-summary', label: 'Fällig', text: start.faellig }, { id: 'abo-due-summary', label: 'Fällig', text: start.faellig },
{ id: 'abo-start-summary', label: 'Beginn', text: start.beginn }, { id: 'abo-start-summary', label: 'Beginn', text: start.beginn },
{ label: 'Laufzeit', text: `Mindestlaufzeit ${product.minTermMonths} Monate, kündbar bis 1 Monat vor Ende; danach monatlich` },
{ label: 'Zahlung', text: 'Karte oder SEPA über Stripe' },
], { id: 'abo-block' })
// Die ausführlichen Sätze stehen unter dem Knopf
const einzelheiten = baueEinzelheiten([
{ label: 'Laufzeit', text: `Mindestlaufzeit ${product.minTermMonths} Monate ab Leistungsbeginn, kündbar zum Ende der Mindestlaufzeit bis einen Monat vorher; danach jederzeit zum Ende des Abrechnungsmonats kündbar` }, { label: 'Laufzeit', text: `Mindestlaufzeit ${product.minTermMonths} Monate ab Leistungsbeginn, kündbar zum Ende der Mindestlaufzeit bis einen Monat vorher; danach jederzeit zum Ende des Abrechnungsmonats kündbar` },
{ label: 'Zahlung', text: 'Kreditkarte oder SEPA-Lastschrift über Stripe; die Zahlungsart hinterlegst du im nächsten Schritt, bis zum Abschluss dort kannst du abbrechen' }, { label: 'Zahlung', text: 'Kreditkarte oder SEPA-Lastschrift über Stripe; die Zahlungsart hinterlegst du im nächsten Schritt, bis zum Abschluss dort kannst du abbrechen' },
], { id: 'abo-block' }) { label: 'Kosten', text: 'keine weiteren Kosten' },
], { id: 'abo-einzelheiten' })
const haken = privat ? h('input', { type: 'checkbox', id: 'abo-beginn', disabled: viewAs }) : null const haken = privat ? h('input', { type: 'checkbox', id: 'abo-beginn', disabled: viewAs }) : null
const fehler = h('p', { class: K.fehler, id: 'abo-error', hidden: true }) const fehler = h('p', { class: K.fehler, id: 'abo-error', hidden: true })
@@ -103,7 +111,8 @@ function bestellung(daten, productKey, { viewAs, kundenart }) {
haken ? h('label', { class: K.check }, haken, h('span', { id: 'abo-beginn-text' }, T.abo.haken)) : null, haken ? h('label', { class: K.check }, haken, h('span', { id: 'abo-beginn-text' }, T.abo.haken)) : null,
fehler, fehler,
h('div', { class: 'kn-aktionen' }, kaufen, viewAs ? h('span', { class: K.meldung }, TEXT_SPERRE) : null), h('div', { class: 'kn-aktionen' }, kaufen, viewAs ? h('span', { class: K.meldung }, TEXT_SPERRE) : null),
baueKundenartZeile(kundenart, T.kundenart, { id: 'abo-kundenart' })) baueKundenartZeile(kundenart, T.kundenart, { id: 'abo-kundenart' }),
einzelheiten)
// Das Häkchen schaltet die Zeilen „Fällig“ und „Beginn“ um // Das Häkchen schaltet die Zeilen „Fällig“ und „Beginn“ um
haken?.addEventListener('change', () => { haken?.addEventListener('change', () => {
+24 -7
View File
@@ -4,12 +4,14 @@
der Wechselrechnung (Bedingungen § 9 Abs. 3). Die Sätze kommen vom Server (GET /api/abo → bestelltexte); der Server entscheidet die der Wechselrechnung (Bedingungen § 9 Abs. 3). Die Sätze kommen vom Server (GET /api/abo → bestelltexte); der Server entscheidet die
Kundenart selbst, der Client sendet nie kundenart. Ein Betrieb braucht kein Häkchen. Kundenart selbst, der Client sendet nie kundenart. Ein Betrieb braucht kein Häkchen.
Die gemerkte Funktion (E14, C5): kam der Kunde aus dem Einschalten einer Funktion, geht es danach zurück zu den Funktionen. Die gemerkte Funktion (E14, C5): kam der Kunde aus dem Einschalten einer Funktion, geht es danach zurück zu den Funktionen.
Der Block ist knapp gehalten (Leistung als eine Zeile mit Link auf die Leistungsbeschreibung), damit er mit dem Knopf in den nutzbaren Bereich
zwischen Kopf und Leiste passt (375 x 667); die ausführlichen Sätze stehen unter dem Knopf (baueEinzelheiten), ohne Klappe.
Der Titel kommt von der Hülle (meldeTitel); die Seite hat keine eigene h1. */ Der Titel kommt von der Hülle (meldeTitel); die Seite hat keine eigene h1. */
import { api } from './api.js' import { api } from './api.js'
import { h, leeren } from './dom.js' import { h, leeren } from './dom.js'
import { K, ladeAbo, wechselFunktionAus, sichereAdresse, merkeHinweis, navigiere, meldeTitel, meldeZaehlerNeu, zuAnsicht, fmtEuro, fmtDatum, berlinTag, ABO_NICHT_GELADEN } from './ansicht-hilfen.js' import { K, ladeAbo, wechselFunktionAus, sichereAdresse, merkeHinweis, navigiere, meldeTitel, meldeZaehlerNeu, zuAnsicht, fmtEuro, fmtDatum, berlinTag, ABO_NICHT_GELADEN } from './ansicht-hilfen.js'
import { baueBlock, baueKundenartZeile, baueLinkzeile, hatWortlaut, kundenartVon } from './bestell-bausteine.js' import { baueBlock, baueEinzelheiten, baueKundenartZeile, baueLinkzeile, hatWortlaut, kundenartVon, leistungLink } from './bestell-bausteine.js'
const SCHLUESSEL = 'kn-wechsel-funktion' const SCHLUESSEL = 'kn-wechsel-funktion'
const TEXT_SPERRE = 'Im Ansichtsmodus nicht möglich.' const TEXT_SPERRE = 'Im Ansichtsmodus nicht möglich.'
@@ -75,18 +77,32 @@ export async function zeigeWechsel(wurzel, { route, me, nav, daten } = {}) {
const lauf = { busy: false, preview: null, error: '', loading: true, token: 0, early: false, fehler: '', fehlerKlasse: null, zeichne: null } const lauf = { busy: false, preview: null, error: '', loading: true, token: 0, early: false, fehler: '', fehlerKlasse: null, zeichne: null }
aktiv = lauf aktiv = lauf
/** Zeilen des Blocks aus der Vorschau des Servers (Leistung, Wartungs-Abo, Zusatzspeicher, Gutschrift, fällig, Abbuchungstag, Laufzeit, Beginn). */ /** Zeilen des Blocks aus der Vorschau des Servers, knapp: Leistung, Preis, Zusatzspeicher, Gutschrift, Fällig (mit Abbuchungstag), Laufzeit, Beginn. */
const zeilenVon = (p) => { const zeilenVon = (p) => {
const monthly = Number.isFinite(Number(p.monthlyCents)) && p.monthlyCents != null ? Number(p.monthlyCents) : wartung.amountCents const monthly = Number.isFinite(Number(p.monthlyCents)) && p.monthlyCents != null ? Number(p.monthlyCents) : wartung.amountCents
const tag = berlinTag(p.nextChargeAt) const tag = berlinTag(p.nextChargeAt)
return [ return [
{ label: 'Leistung', text: 'Zusätzlich zum Hosting: Pflege deiner Website, beliebig viele Postfächer und 1 Stunde Arbeitszeit pro Monat. Das Hosting-Abo endet mit dem Wechsel im gegenseitigen Einvernehmen; Domain, Postfächer und gebuchter Speicher bleiben.' }, { label: 'Leistung', text: 'Wartungs-Abo statt Hosting-Abo', link: { text: 'Leistungen ansehen', href: leistungLink(T.wechsel.linkzeile, 'wartung') } },
{ label: 'Wartungs-Abo', text: `${fmtEuro(monthly / 100)} pro Monat (Gesamtpreis, ${UST})` }, { label: 'Preis', text: `${fmtEuro(monthly / 100)} pro Monat (Gesamtpreis, ${UST})` },
...(units > 0 ...(units > 0
? [{ label: 'Zusatzspeicher', text: `${units} Paket${units === 1 ? '' : 'e'} (+${units * (Number(mail.gbPerUnit) || 0)} GB) bleiben gebucht: ${fmtEuro(units * unitCents / 100)} pro Monat zusätzlich (${UST}), insgesamt ${fmtEuro((monthly + units * unitCents) / 100)} pro Monat` }] ? [{ label: 'Zusatzspeicher', text: `${units} Paket${units === 1 ? '' : 'e'} (+${units * (Number(mail.gbPerUnit) || 0)} GB) bleiben gebucht, insgesamt ${fmtEuro((monthly + units * unitCents) / 100)} pro Monat` }]
: []), : []),
...((Number(p.creditCents) || 0) > 0 ...((Number(p.creditCents) || 0) > 0
? [{ label: 'Gutschrift für nicht genutzte Tage', text: `−${fmtEuro(p.creditCents / 100)} aus dem bereits bezahlten Hosting-Abo (${UST})` }] ? [{ label: 'Gutschrift', text: `−${fmtEuro(p.creditCents / 100)} für nicht genutzte Tage des Hosting-Abos` }]
: []),
{ label: 'Fällig', text: `heute ${fmtEuro(p.amountDueCents / 100)}; danach ab ${fmtDatum(p.nextChargeAt)}${tag ? ` jeweils am ${tag.t}.` : ''}` },
{ label: 'Laufzeit', text: `Mindestlaufzeit bis ${fmtDatum(p.minTermEnd)} (12 Monate), danach monatlich kündbar` },
{ label: 'Beginn', text: 'sofort mit der Zahlung; einen späteren Beginn gibt es nicht' },
]
}
/** Die ausführlichen Sätze unter dem Knopf (Wortlaut wie bisher im Block). */
const einzelheitenVon = (p) => {
const tag = berlinTag(p.nextChargeAt)
return [
{ label: 'Leistung', text: 'Zusätzlich zum Hosting: Pflege deiner Website, beliebig viele Postfächer und 1 Stunde Arbeitszeit pro Monat. Das Hosting-Abo endet mit dem Wechsel im gegenseitigen Einvernehmen; Domain, Postfächer und gebuchter Speicher bleiben.' },
...(units > 0
? [{ label: 'Zusatzspeicher', text: `${units} Paket${units === 1 ? '' : 'e'} (+${units * (Number(mail.gbPerUnit) || 0)} GB) bleiben gebucht: ${fmtEuro(units * unitCents / 100)} pro Monat zusätzlich (${UST})` }]
: []), : []),
{ label: 'Heute fällig', text: `${fmtEuro(p.amountDueCents / 100)} (${UST})` }, { label: 'Heute fällig', text: `${fmtEuro(p.amountDueCents / 100)} (${UST})` },
{ label: 'Neuer Abbuchungstag', text: `${tag ? `Ab dem Wechsel jeweils am ${tag.t}. eines Monats; ` : ''}nächste Abbuchung am ${fmtDatum(p.nextChargeAt)}` }, { label: 'Neuer Abbuchungstag', text: `${tag ? `Ab dem Wechsel jeweils am ${tag.t}. eines Monats; ` : ''}nächste Abbuchung am ${fmtDatum(p.nextChargeAt)}` },
@@ -119,7 +135,8 @@ export async function zeigeWechsel(wurzel, { route, me, nav, daten } = {}) {
h('div', { class: 'kn-aktionen' }, h('div', { class: 'kn-aktionen' },
h('button', { type: 'button', class: K.primaer, id: 'switch-buy', disabled: lauf.busy || viewAs || lauf.loading || !p || gesperrt, on: { click: absenden } }, lauf.busy ? 'Wird bestellt …' : (T.knopf || KNOPF)), h('button', { type: 'button', class: K.primaer, id: 'switch-buy', disabled: lauf.busy || viewAs || lauf.loading || !p || gesperrt, on: { click: absenden } }, lauf.busy ? 'Wird bestellt …' : (T.knopf || KNOPF)),
viewAs ? h('span', { class: K.meldung }, TEXT_SPERRE) : null), viewAs ? h('span', { class: K.meldung }, TEXT_SPERRE) : null),
baueKundenartZeile(kundenart, T.kundenart, { id: 'switch-kundenart' }))) baueKundenartZeile(kundenart, T.kundenart, { id: 'switch-kundenart' }),
p ? baueEinzelheiten(einzelheitenVon(p), { id: 'switch-einzelheiten' }) : null))
} }
lauf.zeichne = zeichne lauf.zeichne = zeichne
+14 -2
View File
@@ -336,6 +336,9 @@
.kn-speicher-warnung { margin: 0 0 12px; font-size: 13px; line-height: 20px; color: var(--kn-orange-fg); } .kn-speicher-warnung { margin: 0 0 12px; font-size: 13px; line-height: 20px; color: var(--kn-orange-fg); }
.kn-speicher-warnung a { color: inherit; text-decoration: underline; text-underline-offset: 3px; } .kn-speicher-warnung a { color: inherit; text-decoration: underline; text-underline-offset: 3px; }
.kn-block dd.kn-fehler { color: var(--kn-danger); } .kn-block dd.kn-fehler { color: var(--kn-danger); }
.kn-einzelheiten { margin: 20px 0 0; }
.kn-einzelheiten > h3 { margin: 0 0 8px; font-size: 14px; line-height: 20px; font-weight: 600; color: var(--kn-text-2); }
.kn-einzelheiten .kn-block { color: var(--kn-text-2); }
.kn-gutschrift-bedingungen { max-width: 640px; } .kn-gutschrift-bedingungen { max-width: 640px; }
/* ------------------------------------------------------------------ Handy --- */ /* ------------------------------------------------------------------ Handy --- */
@@ -353,8 +356,17 @@
.kn-pw { max-width: none; } .kn-pw { max-width: none; }
.kn-tk-feld input:not([type="checkbox"]):not([type="file"]) { height: 44px; font-size: 16px; } .kn-tk-feld input:not([type="checkbox"]):not([type="file"]) { height: 44px; font-size: 16px; }
.kn-stepper .kn-knopf { min-width: 44px; } .kn-stepper .kn-knopf { min-width: 44px; }
.kn-block-zeile { grid-template-columns: minmax(0, 1fr); gap: 0; padding: 6px 0; } /* Bestellseiten (F1): Kerninfo und Knopf müssen im nutzbaren Bereich zwischen Kopf und Leiste zusammen stehen (375 x 667: gut 550 px).
.kn-block dt { font-size: 12px; line-height: 16px; } Darum läuft die Beschriftung in der Zeile mit (Label: Text) statt eine eigene Zeile zu brauchen, und der Abstand ist knapp. */
.kn-bestellen > h2 { margin: 0 0 6px; font-size: 17px; line-height: 24px; font-weight: 600; }
.kn-block { margin-bottom: 10px; }
.kn-block-zeile { display: block; padding: 5px 0; }
.kn-block dt, .kn-block dd { display: inline; }
.kn-block dt { font-weight: 600; color: var(--kn-text); }
.kn-block dt::after { content: ": "; }
.kn-linkzeile { margin-bottom: 8px; }
.kn-bestellen .kn-check { margin-bottom: 8px; }
.kn-sperrzeile { margin-bottom: 8px; }
.kn-bestellen .kn-knopf { width: 100%; min-height: 44px; } .kn-bestellen .kn-knopf { width: 100%; min-height: 44px; }
.kn .kn-chat .chat-bubble { max-width: 88%; } .kn .kn-chat .chat-bubble { max-width: 88%; }
.kn .kn-chat .chat-messages { min-height: 240px; max-height: 55vh; } .kn .kn-chat .chat-messages { min-height: 240px; max-height: 55vh; }
+23 -2
View File
@@ -70,9 +70,30 @@ export function baueKundenartZeile(kundenart, texte, { id } = {}) {
return h('p', { id, class: 'kn-kundenart-zeile' }, lage, ' · ', korrektur) return h('p', { id, class: 'kn-kundenart-zeile' }, lage, ' · ', korrektur)
} }
/** Block „Was du bestellst“: zeilen [{ id?, label, text, klasse? }] als Beschriftungsliste. Der Text ist immer Text. */ /**
* Block „Was du bestellst“: zeilen [{ id?, label, text, klasse?, link? }] als Beschriftungsliste. Der Text ist immer Text.
* link { text, href }: hinter dem Text ' · ' und ein Link (nur mit sicherer Adresse; Adressen außerhalb der Seite öffnen in neuem Tab).
*/
export function baueBlock(zeilen, { id } = {}) { export function baueBlock(zeilen, { id } = {}) {
return h('dl', { id, class: 'kn-block' }, (zeilen || []).map((z) => h('div', { class: 'kn-block-zeile' }, h('dt', null, z.label), h('dd', { id: z.id, class: z.klasse }, z.text)))) return h('dl', { id, class: 'kn-block' }, (zeilen || []).map((z) => {
const href = z.link ? sicherLink(z.link.href) : ''
const link = href ? [' · ', h('a', { href, ...(ausserhalb(href) ? { target: '_blank', rel: 'noopener' } : {}) }, z.link.text)] : null
return h('div', { class: 'kn-block-zeile' }, h('dt', null, z.label), h('dd', { id: z.id, class: z.klasse }, z.text, link))
}))
}
/**
* Die ausführlichen Sätze UNTER dem Knopf (und der Kundenart-Zeile): Überschrift „Im Einzelnen“ und dieselbe Beschriftungsliste wie der Block.
* Der Block über dem Knopf trägt die Kerninfo knapp, damit sie mit dem Knopf auf einen Handy-Bildschirm passt; hier steht sie ausführlich.
*/
export function baueEinzelheiten(zeilen, { id } = {}) {
return h('section', { id, class: 'kn-einzelheiten' }, h('h3', null, 'Im Einzelnen'), baueBlock(zeilen))
}
/** Adresse der Leistungsbeschreibung: die Bedingungen (erste Adresse der Linkzeile) mit Anker ('wartung', 'hosting'); unsicher oder fehlend ''. */
export function leistungLink(linkzeile, anker) {
const basis = sicherLink(Array.isArray(linkzeile?.urls) ? linkzeile.urls[0] : '')
return basis && !basis.includes('#') ? `${basis}#${anker}` : ''
} }
const nichtLeer = (x) => typeof x === 'string' && x !== '' const nichtLeer = (x) => typeof x === 'string' && x !== ''
+25 -12
View File
@@ -8,7 +8,7 @@ import { NEU, TERMS_URL, meMit, aboNeu } from './support/knBestellDaten.js'
const route = (produkt) => ({ bereich: 'vertraege', etage: 'bestellen', produkt, query: '' }) const route = (produkt) => ({ bereich: 'vertraege', etage: 'bestellen', produkt, query: '' })
// fmtEuro setzt ein geschütztes Leerzeichen vor das Euro-Zeichen // fmtEuro setzt ein geschütztes Leerzeichen vor das Euro-Zeichen
const eur = (text) => text.replaceAll(' €', '\u00a0€') const eur = (text) => text.replaceAll(' €', '\u00a0€')
const IDS = ['abo-block', 'abo-linkzeile', 'abo-beginn', 'abo-error', 'abo-buy', 'abo-kundenart'] 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. */ /** Seite zeigen; die Titel, die die Ansicht meldet (Ereignis kn:titel), sammelt fx.titel. */
async function zeige(fx, daten, meDaten, produkt = 'wartung') { async function zeige(fx, daten, meDaten, produkt = 'wartung') {
@@ -24,11 +24,11 @@ const reihe = (fx, w) => fx.alle(fx.finde(w, '#abo-order'), (el) => IDS.includes
test('Betrieb: Block, Linkzeile, Knopf, Kundenart-Zeile in dieser Reihenfolge, ohne Häkchen und ohne Klappe', mit({}, async (fx) => { 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')) const w = await zeige(fx, aboNeu(), meMit('unternehmen'))
assert.deepEqual(reihe(fx, w), ['abo-block', 'abo-linkzeile', 'abo-error', 'abo-buy', 'abo-kundenart']) 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.finde(w, '#abo-buy').textContent, 'Zahlungspflichtig bestellen')
assert.equal(fx.alle(w, 'details, dialog, input[type="radio"], input[type="checkbox"]').length, 0) assert.equal(fx.alle(w, 'details, dialog, input[type="radio"], input[type="checkbox"]').length, 0)
const t = fx.text(fx.finde(w, '#abo-block')) 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 fällig: 50,00']) assert.ok(t.includes(s), s) 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“) // 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.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-start-summary')), 'sofort')
@@ -37,14 +37,27 @@ test('Betrieb: Block, Linkzeile, Knopf, Kundenart-Zeile in dieser Reihenfolge, o
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') 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: feste Zeilentexte (Preis, Laufzeit, Zahlung)', mit({}, async (fx) => { 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 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'))]) 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']) assert.deepEqual(zeilen.map(([l]) => l), ['Leistung', 'Preis', 'Fällig', 'Beginn', 'Laufzeit', 'Zahlung'])
assert.deepEqual(zeilen[0], ['Leistung', 'WEBklar Wartungs-Abo: Wartung']) // Leistung: eine kurze Zeile mit Link auf die Leistungsbeschreibung B (Wartung), kein Aufzählungstext
assert.deepEqual(zeilen[1], ['Preis', eur('50,00 € pro Monat (Gesamtpreis, gemäß § 19 UStG ohne Umsatzsteuer), keine weiteren Kosten')]) assert.deepEqual(zeilen[0], ['Leistung', 'Wartungs-Abo · Leistungen ansehen'])
assert.deepEqual(zeilen[4], ['Laufzeit', 'Mindestlaufzeit 12 Monate ab Leistungsbeginn, kündbar zum Ende der Mindestlaufzeit bis einen Monat vorher; danach jederzeit zum Ende des Abrechnungsmonats kündbar']) 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[5], ['Zahlung', 'Kreditkarte oder SEPA-Lastschrift über Stripe; die Zahlungsart hinterlegst du im nächsten Schritt, bis zum Abschluss dort kannst du abbrechen']) 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) => { test('Titel: Produktname als Ereignis, keine eigene h1 (P2, P1 VF11)', mit({}, async (fx) => {
@@ -65,11 +78,11 @@ test('Betrieb: Body ohne earlyStart, lateStartAt, startChoice und Kundenart', mi
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) => { 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('')]) { for (const m of [meMit('privat'), meMit('')]) {
const w = await zeige(fx, aboNeu(), m) const w = await zeige(fx, aboNeu(), m)
assert.deepEqual(reihe(fx, w), ['abo-block', 'abo-linkzeile', 'abo-beginn', 'abo-error', 'abo-buy', 'abo-kundenart']) 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.finde(w, '#abo-beginn').checked, false)
assert.equal(fx.text(fx.finde(w, '#abo-beginn-text')), aboNeu().bestelltexte.abo.haken) 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-start-summary')).startsWith('nach Ablauf der 14-tägigen Widerrufsfrist (am '))
assert.ok(fx.text(fx.finde(w, '#abo-due-summary')).startsWith('Erste Abbuchung 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 · ')) assert.ok(fx.text(fx.finde(w, '#abo-kundenart')).startsWith('Du bestellst privat · '))
await fx.klick(fx.finde(w, '#abo-buy')) await fx.klick(fx.finde(w, '#abo-buy'))
await fx.ruhe() await fx.ruhe()
@@ -77,7 +90,7 @@ test('Privat und leer: ein Häkchen aus dem Wortlaut, ungehakt nach der Frist, g
const v = await zeige(fx, aboNeu(), m) const v = await zeige(fx, aboNeu(), m)
await fx.klick(fx.finde(v, '#abo-beginn')) 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-start-summary')).startsWith('sofort, auf deinen ausdrücklichen Wunsch'))
assert.ok(fx.text(fx.finde(v, '#abo-due-summary')).startsWith('Heute fällig: ')) assert.ok(fx.text(fx.finde(v, '#abo-due-summary')).startsWith('heute '))
await fx.klick(fx.finde(v, '#abo-buy')) await fx.klick(fx.finde(v, '#abo-buy'))
await fx.ruhe() await fx.ruhe()
assert.equal(fx.fetchLog.at(-1).body.earlyStart, true) assert.equal(fx.fetchLog.at(-1).body.earlyStart, true)
@@ -101,7 +114,7 @@ test('Angebot: Angebotsnummer im Body, Text in Abschnitt und Preiszeile', mit({}
assert.ok(fx.text(fx.finde(w, '#abo-angebot')).includes(angebot().text)) 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(`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-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 fällig: 25,00')) assert.ok(fx.text(fx.finde(w, '#abo-due-summary')).startsWith('heute 25,00'))
await fx.klick(fx.finde(w, '#abo-buy')) await fx.klick(fx.finde(w, '#abo-buy'))
await fx.ruhe() await fx.ruhe()
assert.equal(fx.fetchLog.at(-1).body.offerId, angebot().id) assert.equal(fx.fetchLog.at(-1).body.offerId, angebot().id)
+7
View File
@@ -27,3 +27,10 @@ test('Stil: Aufträge-Abschnitt, Chat-Variablen, hidden und Handy-Abfrage', () =
for (const v of ['accent', 'accent-hover', 'accent-soft', 'accent-line', 'accent-text', 'on-accent', 'line', 'muted', 'surface']) assert.match(A, new RegExp(`\\.kn-chat[^{]*\\{[^}]*--chat-${v}:`), v) for (const v of ['accent', 'accent-hover', 'accent-soft', 'accent-line', 'accent-text', 'on-accent', 'line', 'muted', 'surface']) assert.match(A, new RegExp(`\\.kn-chat[^{]*\\{[^}]*--chat-${v}:`), v)
assert.match(A, /\.kn \.hidden\s*\{[^}]*display:\s*none/); assert.ok(A.includes('(max-width: 880px), (pointer: coarse) and (max-height: 500px)')) assert.match(A, /\.kn \.hidden\s*\{[^}]*display:\s*none/); assert.ok(A.includes('(max-width: 880px), (pointer: coarse) and (max-height: 500px)'))
}) })
test('Stil: Bestellseiten am Handy (F1): Beschriftung läuft in der Zeile mit, die Einzelheiten stehen unter dem Knopf', () => {
const block = A.slice(A.lastIndexOf('(max-width: 880px), (pointer: coarse) and (max-height: 500px)'))
assert.match(block, /\.kn-block-zeile\s*\{[^}]*display:\s*block/)
assert.match(block, /\.kn-block dt,\s*\.kn-block dd\s*\{[^}]*display:\s*inline/)
assert.match(block, /\.kn-block dt::after\s*\{[^}]*content:\s*": "/)
assert.match(A, /\.kn-einzelheiten\s*\{/)
})
+19 -6
View File
@@ -4,12 +4,12 @@ import { readFileSync } from 'node:fs'
import { zeigeWechsel, gemerkteFunktion } from '../public/neu/ansicht-wechsel.js' import { zeigeWechsel, gemerkteFunktion } from '../public/neu/ansicht-wechsel.js'
import { nimmHinweis } from '../public/neu/ansicht-hilfen.js' import { nimmHinweis } from '../public/neu/ansicht-hilfen.js'
import { mit, me, abo, aboAntwort, neueWurzel, VORSCHAU } from './support/knDaten.js' import { mit, me, abo, aboAntwort, neueWurzel, VORSCHAU } from './support/knDaten.js'
import { NEU, meMit, aboNeu } from './support/knBestellDaten.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 // 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 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 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 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 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: '' } const ROUTE = { bereich: 'vertraege', etage: 'wechsel', query: '' }
@@ -27,9 +27,22 @@ test('Betrieb: Block, Linkzeile, Knopf, kein Häkchen, Knopf nach der Vorschau f
const w = await zeige(fx, HOSTING(), meMit('unternehmen')) const w = await zeige(fx, HOSTING(), meMit('unternehmen'))
assert.deepEqual(fx.titel, ['Wechsel ins Wartungs-Abo']) assert.deepEqual(fx.titel, ['Wechsel ins Wartungs-Abo'])
assert.equal(fx.alle(w, 'h1').length, 0) // Titel kommt als Ereignis, keine eigene h1 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']) assert.deepEqual(reihe(fx, w), ['switch-block', 'switch-linkzeile', 'switch-error', 'switch-buy', 'switch-kundenart', 'switch-einzelheiten'])
const t = fx.text(fx.finde(w, '#switch-block')) // Block über dem Knopf: knapp (F1); Leistung als eine Zeile mit Link auf die Leistungsbeschreibung B (Wartung)
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) 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').textContent, 'Zahlungspflichtig bestellen')
assert.equal(fx.finde(w, '#switch-buy').hasAttribute('disabled'), false) 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.alle(w, 'details, dialog, input[type="checkbox"]').length, 0)
@@ -44,7 +57,7 @@ test('Betrieb: Block, Linkzeile, Knopf, kein Häkchen, Knopf nach der Vorschau f
test('Privat und leer: Knopf gesperrt mit Sperrzeile, nach dem Häkchen frei, Body mit earlyStart true', mit(SRV, async (fx) => { 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('')]) { for (const m of [meMit('privat'), meMit('')]) {
const w = await zeige(fx, HOSTING(), m) 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.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.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-sperre')), HOSTING().bestelltexte.wechsel.gesperrt)
assert.equal(fx.text(fx.finde(w, '#switch-beginn-text')), HOSTING().bestelltexte.wechsel.haken) assert.equal(fx.text(fx.finde(w, '#switch-beginn-text')), HOSTING().bestelltexte.wechsel.haken)
+16 -1
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs' import { readFileSync } from 'node:fs'
import { mitDom } from './support/knDaten.js' import { mitDom } from './support/knDaten.js'
import { TERMS_URL, meMit, aboNeu } from './support/knBestellDaten.js' import { TERMS_URL, meMit, aboNeu } from './support/knBestellDaten.js'
import { kundenartVon, sicherLink, linkTeile, baueLinkzeile, baueKundenartZeile, baueBlock, hatWortlaut } from '../public/neu/bestell-bausteine.js' import { kundenartVon, sicherLink, linkTeile, baueLinkzeile, baueKundenartZeile, baueBlock, baueEinzelheiten, leistungLink, hatWortlaut } from '../public/neu/bestell-bausteine.js'
test('kundenartVon: nur exakt unternehmen ist Betrieb (E4)', () => { test('kundenartVon: nur exakt unternehmen ist Betrieb (E4)', () => {
for (const m of [undefined, null, {}, { customer: {} }, meMit(''), meMit('privat'), { customer: { kundenartWirksam: 'Unternehmen' } }, { customer: { kundenart: 'unternehmen' } }]) { for (const m of [undefined, null, {}, { customer: {} }, meMit(''), meMit('privat'), { customer: { kundenartWirksam: 'Unternehmen' } }, { customer: { kundenart: 'unternehmen' } }]) {
@@ -52,6 +52,21 @@ test('baueLinkzeile, baueKundenartZeile, baueBlock', () =>
for (const el of [p, dl, baueKundenartZeile('privat', T, { id: 'k' })]) assert.equal(fx.alle(el, 'h1').length, 0) for (const el of [p, dl, baueKundenartZeile('privat', T, { id: 'k' })]) assert.equal(fx.alle(el, 'h1').length, 0)
})) }))
test('baueBlock: Link in der Zeile, nur sichere Adressen; baueEinzelheiten steht unter dem Knopf mit eigener Überschrift', () =>
mitDom((fx) => {
const dl = baueBlock([{ label: 'Leistung', text: 'Wartungs-Abo', link: { text: 'Leistungen ansehen', href: `${TERMS_URL}#wartung` } }, { label: 'Preis', text: '50', link: { text: 'x', href: 'javascript:alert(1)' } }], { id: 'blk' })
const [erste, zweite] = fx.alle(dl, 'dd')
assert.equal(fx.text(erste), 'Wartungs-Abo · Leistungen ansehen')
assert.deepEqual([fx.finde(erste, 'a').getAttribute('href'), fx.finde(erste, 'a').getAttribute('target'), fx.finde(erste, 'a').getAttribute('rel')], [`${TERMS_URL}#wartung`, '_blank', 'noopener'])
assert.equal(fx.text(zweite), '50', 'unsichere Adresse: kein Link, auch kein Trenner')
assert.equal(fx.alle(zweite, 'a').length, 0)
const e = baueEinzelheiten([{ label: 'Zahlung', text: 'Karte' }], { id: 'ein' })
assert.deepEqual([e.tagName, e.id, fx.text(fx.finde(e, 'h3')), fx.alle(e, 'dt').length, fx.alle(e, 'details, dialog, h1').length], ['SECTION', 'ein', 'Im Einzelnen', 1, 0])
// Adresse der Leistungsbeschreibung: die Bedingungen mit Anker; unsichere oder fehlende Adresse ergibt ''
assert.equal(leistungLink({ urls: [TERMS_URL, '/datenschutz.html'] }, 'wartung'), `${TERMS_URL}#wartung`)
for (const schlecht of [undefined, {}, { urls: [] }, { urls: ['javascript:alert(1)'] }, { urls: ['//evil.example/x'] }]) assert.equal(leistungLink(schlecht, 'wartung'), '')
}))
test('hatWortlaut', () => { test('hatWortlaut', () => {
assert.equal(hatWortlaut(aboNeu()), true) assert.equal(hatWortlaut(aboNeu()), true)
for (const d of [null, undefined, {}, { bestelltexte: {} }, { bestelltexte: { status: 'entwurf', knopf: 'x' } }, { bestelltexte: { status: 'entwurf', abo: { linkzeile: { text: 'a' } } } }]) assert.equal(hatWortlaut(d), false) for (const d of [null, undefined, {}, { bestelltexte: {} }, { bestelltexte: { status: 'entwurf', knopf: 'x' } }, { bestelltexte: { status: 'entwurf', abo: { linkzeile: { text: 'a' } } } }]) assert.equal(hatWortlaut(d), false)