diff --git a/public/neu/ansicht-hilfen.js b/public/neu/ansicht-hilfen.js index f9757f4..1abe5d3 100644 --- a/public/neu/ansicht-hilfen.js +++ b/public/neu/ansicht-hilfen.js @@ -2,7 +2,7 @@ Kein DOM-Zugriff beim Import; document und window werden erst beim Aufruf gelesen. */ import { api } from './api.js' -import { geheZu } from './router.js' +import { geheZu, aktuelleRoute } from './router.js' import { hashFuerAnsicht } from './routen.js' import { safeUrl } from '../fn-util.js' @@ -36,6 +36,12 @@ export const K = Object.freeze({ export const ABO_NICHT_GELADEN = 'Deine Verträge konnten gerade nicht geladen werden. Bitte versuche es später noch einmal.' +/** Inhalt von el ersetzen; null, undefined und false (leere Platzhalter) werden übersprungen, nie als Text "null" angehängt. */ +export function ersetze(el, ...kinder) { + el.replaceChildren(...kinder.flat(Infinity).filter((k) => k !== null && k !== undefined && k !== false)) + return el +} + /* ---------- Formate ---------- */ const ZAHL_2 = new Intl.NumberFormat('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) @@ -118,9 +124,10 @@ export function legacyRequest(apiFn) { } } -/** Programmatisch navigieren (nur über den Router); wirft der Router, setzt der Hash die Adresse. */ +/** Programmatisch navigieren (nur über den Router). Läuft kein Router oder wirft er, trägt der Hash die Adresse. */ export function navigiere(hash, { ersetzen = false } = {}) { try { + if (aktuelleRoute() === null) { window.location.hash = hash; return } geheZu(hash, { ersetzen }) } catch { window.location.hash = hash diff --git a/public/neu/ansicht-speicher.js b/public/neu/ansicht-speicher.js new file mode 100644 index 0000000..9a1577d --- /dev/null +++ b/public/neu/ansicht-speicher.js @@ -0,0 +1,193 @@ +/* Kundenansicht neu (P2): Reiter E-Mail in Verträge, der Zusatzspeicher. Text und Logik 1:1 aus app.js (renderStoragePanel, + Fassung 5), die Sie-Form bleibt (C14, N36). Ein geänderter Satz wäre eine unbemerkte Rechtsänderung: Das Wortlaut-Orakel + (test/support/knWortlautOrakel.js) vergleicht den sichtbaren Text Zeichen für Zeichen. Nur "Postfach anfragen" und die Sperrhinweise sind neu (du). */ + +import { api } from './api.js' +import { h, leeren } from './dom.js' +import { K, ersetze, fmtEuro, fmtDatum, fmtTagMonat, ladeAbo, navigiere, setzeChatVorgabe, ABO_NICHT_GELADEN } from './ansicht-hilfen.js' + +const laeufe = new WeakMap() // Reiter-Inhalt → Nummer des jüngsten Aufrufs +const TEXT_ANSICHTSMODUS = 'Im Ansichtsmodus nicht möglich.' +const BEDINGUNGEN = '/abo-bedingungen.html' +const link = (href, text) => h('a', { href, target: '_blank', rel: 'noopener' }, text) +const paket = (n) => `Paket${n === 1 ? '' : 'e'}` +const fakt = (titel, wert, klasse) => h('div', null, h('dt', null, titel), h('dd', { class: klasse }, wert)) + +// Der Zusatzspeicher: Plus/Minus mit Vorschau vom Server, Bestellübersicht (§ 312j BGB) und Speichern +function baueSpeicherPanel(daten, { viewAs, notice, nachErfolg }) { + const sub = daten.subscription + const conf = daten.mailStorage || {} + const includedGb = Number(conf.includedGb) || 0 + const gbPer = Number(conf.gbPerUnit) || 0 + const unitCents = Number(conf.unitPriceCents) || 0 + const maxUnits = Number(conf.maxUnits) || 10 + const gbOf = (u) => includedGb + u * gbPer + const storage = sub.storage || {} + const pendingReduction = storage.pendingReduction || null + const canChange = sub.canChangeStorage === true + const current = Number(storage.units || 0) + const start = pendingReduction && Number.isInteger(pendingReduction.units) ? pendingReduction.units : current + const state = { target: start, preview: null, previewError: '', loading: start !== current, busy: false, error: '', token: 0 } + const gekuendigt = Boolean(sub.cancelAtPeriodEnd || sub.cancelAt) + const lockedReason = sub.switchPending ? 'Wechsel wartet auf die Zahlung' + : gekuendigt ? 'Abo gekündigt' + : sub.paymentProblem ? 'Zahlung offen' : 'bei Fragen schreiben Sie uns gern im Chat' + const panel = h('div', { class: 'kn-panel', 'data-panel': 'speicher' }) + let fehlerText = '' + let haken = false + + const paint = () => { + const delta = state.target - current + const p = state.preview + const sperre = viewAs || state.busy + let summary = null + if (canChange && delta !== 0) { + const plus = delta > 0 + const totalCents = state.target * unitCents + const facts = [] + const proBad = plus && p && !state.loading && !(p.prorationCents != null && p.prorationCents !== '' && Number.isFinite(Number(p.prorationCents))) + facts.push(fakt('Neue Gesamtgröße', `${gbOf(state.target)} GB`)) + facts.push(fakt('Preis', `${fmtEuro(totalCents / 100)} pro Monat für ${state.target} ${paket(state.target)} (${plus ? 'Mehrpreis' : 'Ersparnis'} ${fmtEuro(Math.abs(delta) * unitCents / 100)} pro Monat; Gesamtpreis, gemäß § 19 UStG ohne Umsatzsteuer)`)) + if (state.previewError || proBad) { + facts.push(fakt('Vorschau', state.previewError || 'Der Betrag konnte nicht berechnet werden. Bitte versuchen Sie es später erneut.', 'kn-fehler')) + } else if (!p || state.loading) { + facts.push(fakt('Vorschau', 'wird berechnet …')) + } else if (plus) { + const pro = Number(p.prorationCents) || 0 + facts.push(fakt('Zusätzlich jetzt', pro > 0 + ? `${fmtEuro(pro / 100)} anteilig für den Rest des laufenden Abrechnungsmonats (gemäß § 19 UStG ohne Umsatzsteuer)` + : 'keine zusätzlichen Kosten, die Pakete sind für den laufenden Abrechnungsmonat schon bezahlt')) + facts.push(fakt('Nächste Abbuchung', `${fmtDatum(p.nextChargeAt)}, danach monatlich ${fmtEuro(totalCents / 100)}`)) + facts.push(pro === 0 && pendingReduction + ? fakt('Geplante Reduzierung', 'wird aufgehoben oder angepasst; es ist nichts neu einzurichten') + : fakt('Einrichtung', 'in der Regel innerhalb eines Werktags; bis dahin steht „wird eingerichtet“ da')) + } else { + facts.push(fakt('Wirkt zum', `${fmtDatum(p.effectiveAt)}, zum Ende des Abrechnungsmonats`)) + } + facts.push(fakt('Laufzeit', 'monatlich, zum Ende jedes Abrechnungsmonats kündbar, endet spätestens mit dem Abo')) + const hinweis = plus + ? h('label', { class: 'kn-check' }, + h('input', { type: 'checkbox', id: 'storage-terms', checked: haken, disabled: sperre }), + h('span', null, 'Ich habe die ', link(`${BEDINGUNGEN}#zusatzspeicher`, 'Regeln zum Zusatzspeicher'), ' gelesen und stimme ihnen zu. Die ', link(`${BEDINGUNGEN}#widerruf`, 'Widerrufsbelehrung'), ' habe ich zur Kenntnis genommen.')) + : h('p', { class: 'kn-meldung kn-meldung-warn' }, 'Wichtig: Passen die gespeicherten E-Mails nach der Verkleinerung nicht mehr in den verbleibenden Speicher, nehmen die Postfächer keine neuen E-Mails mehr an. Bitte räumen Sie vorher auf. Mehr dazu in den ', + link(`${BEDINGUNGEN}#zusatzspeicher`, 'Regeln zum Zusatzspeicher'), ' und in der ', link(`${BEDINGUNGEN}#widerruf`, 'Widerrufsbelehrung'), '.') + const gesperrt = sperre || state.loading || !p || state.previewError || proBad + summary = h('div', { class: 'kn-speicher-zusammenfassung' }, + h('dl', { class: 'kn-fakten' }, facts), + hinweis, + h('p', { class: 'kn-fehler', id: 'storage-error', hidden: !fehlerText }, fehlerText), + h('button', { type: 'button', class: K.primaer, id: 'storage-submit', disabled: Boolean(gesperrt), on: { click: absenden } }, state.busy ? 'Wird gespeichert …' : plus ? 'Zahlungspflichtig bestellen' : 'Speichern')) + } + const status = [] + if (storage.pendingSetup) status.push(h('p', { class: 'kn-meldung kn-meldung-warn' }, 'Ihr zusätzlicher Speicher wird eingerichtet. Das dauert in der Regel bis zum nächsten Werktag.')) + if (pendingReduction) status.push(h('p', { class: 'kn-hinweis' }, `Geplant: ab ${fmtTagMonat(pendingReduction.effectiveAt)} ${gbOf(Number(pendingReduction.units) || 0)} GB.`)) + ersetze(panel, + h('h3', null, 'E-Mail-Speicher'), + h('p', { class: 'kn-hinweis' }, `${includedGb} GB inklusive. Jedes Zusatzpaket bringt ${gbPer} GB für ${fmtEuro(unitCents / 100)} im Monat (gemäß § 19 UStG ohne Umsatzsteuer), bis zu ${maxUnits} Pakete.`), + notice ? h('p', { class: 'kn-meldung kn-meldung-ok', role: 'status' }, notice) : null, + !canChange ? h('p', { class: 'kn-hinweis', id: 'storage-locked' }, `Änderungen sind derzeit nicht möglich – ${lockedReason}.`) : null, + canChange + ? h('div', { class: 'kn-stepper' }, + h('button', { type: 'button', class: K.knopf, id: 'storage-minus', 'aria-label': 'Ein Paket weniger', disabled: sperre || state.target <= 0, on: { click: () => schritt(-1) } }, '−'), + h('span', { class: 'kn-stepper-zahl', 'aria-live': 'polite' }, h('strong', null, state.target), ` ${paket(state.target)}`), + h('button', { type: 'button', class: K.knopf, id: 'storage-plus', 'aria-label': 'Ein Paket mehr', disabled: sperre || state.target >= maxUnits, on: { click: () => schritt(1) } }, '+')) + : h('p', { class: 'kn-stepper-zahl' }, h('strong', null, state.target), ` ${paket(state.target)}`), + h('p', { class: 'kn-speicher-summe' }, `= ${gbOf(state.target)} GB gesamt · + ${fmtEuro(state.target * unitCents / 100)} im Monat`), + canChange && viewAs ? h('p', { class: K.meldung }, TEXT_ANSICHTSMODUS) : null, + ...status, + summary) + } + + const ladeVorschau = async (token) => { + try { + state.preview = await api('/api/abo/storage/preview', { method: 'POST', body: { units: state.target } }) + } catch (f) { + if (token !== state.token) return + state.previewError = f.message + } + if (token !== state.token) return + state.loading = false + paint() + } + + const schritt = (d) => { + const next = state.target + d + if (viewAs || state.busy || next < 0 || next > maxUnits) return + state.target = next + state.preview = null + state.previewError = '' + fehlerText = '' + state.loading = next !== current + const token = ++state.token + paint() + if (next !== current) ladeVorschau(token) + } + + const absenden = async () => { + if (viewAs) return + const plus = state.target > current + const terms = panel.querySelector('#storage-terms') + haken = Boolean(terms?.checked) + fehlerText = '' + if (plus && !haken) { + fehlerText = 'Bitte stimmen Sie den Regeln zum Zusatzspeicher zu.' + paint() + return + } + const body = { units: state.target } + if (plus) Object.assign(body, { acceptTerms: true, termsVersion: daten.termsVersion }) + state.busy = true + paint() + try { + const res = await api('/api/abo/storage', { method: 'POST', body }) + const ch = res.change || {} + const hinweis = plus + ? (ch.task === false ? 'Gespeichert. Es entstehen keine zusätzlichen Kosten, die Pakete waren schon bezahlt.' : 'Danke, Ihre Bestellung ist eingegangen. Der Speicher wird in der Regel innerhalb eines Werktags eingerichtet.') + : `Gespeichert. Die Verkleinerung wirkt zum ${fmtDatum(ch.effectiveAt)}.` + await nachErfolg(hinweis, res) + } catch (f) { + state.busy = false + fehlerText = f.status === 409 ? `${f.message} Bitte laden Sie die Seite neu, falls sich der Stand geändert hat.` : f.message + paint() + } + } + + paint() + if (start !== current && canChange) ladeVorschau(++state.token) + return panel +} + +/** Reiter-Inhalt E-Mail: Knopf "Postfach anfragen" und der Zusatzspeicher. daten (GET /api/abo) ist optional. */ +export async function zeigeSpeicher(wurzel, { route, me, nav, daten } = {}) { + const nr = (laeufe.get(wurzel) ?? 0) + 1 + laeufe.set(wurzel, nr) + const aktuell = () => laeufe.get(wurzel) === nr + const viewAs = me?.viewAs === true + leeren(wurzel).append(h('p', { class: K.leer }, 'Wird geladen …')) + + let stand = daten ?? await ladeAbo() + if (!aktuell()) return + if (!stand) { leeren(wurzel).append(h('p', { class: K.fehler }, ABO_NICHT_GELADEN)); return } + if (!stand.subscription?.active) { + leeren(wurzel).append(h('p', { class: K.leer }, 'Den E-Mail-Speicher gibt es mit einem laufenden Abo.')) + return + } + + const anfrage = h('button', { type: 'button', class: K.knopf, on: { click: () => { setzeChatVorgabe('Bitte lege ein neues Postfach an: '); navigiere('#chat') } } }, 'Postfach anfragen') + const platz = h('div', { class: 'kn-speicher-platz' }) + const zeichne = (notice = '') => { + leeren(platz).append(baueSpeicherPanel(stand, { + viewAs, + notice, + // Nach einem Erfolg: Abo neu laden (Rückfall: die Antwort der Bestellung) und den Hinweis oben im Panel zeigen + nachErfolg: async (hinweis, antwort) => { + const neu = await ladeAbo() + if (!aktuell()) return + stand = neu ?? (antwort?.subscription ? { ...stand, subscription: antwort.subscription } : stand) + zeichne(hinweis) + }, + })) + } + zeichne() + leeren(wurzel).append(anfrage, platz) +} diff --git a/public/neu/ansicht-stunden.js b/public/neu/ansicht-stunden.js index e347631..334a5eb 100644 --- a/public/neu/ansicht-stunden.js +++ b/public/neu/ansicht-stunden.js @@ -2,7 +2,7 @@ (ab P6 in ledger.vorschussMinutes) wird nie als Mehraufwand gezeigt. */ import { h, leeren } from './dom.js' -import { K, fmtStd, fmtDatum, ladeAbo, ABO_NICHT_GELADEN } from './ansicht-hilfen.js' +import { K, ersetze, fmtStd, fmtDatum, ladeAbo, ABO_NICHT_GELADEN } from './ansicht-hilfen.js' import { aboWarnungen } from './vertraege-abo.js' const laeufe = new WeakMap() // Reiter-Inhalt → Nummer des jüngsten Aufrufs @@ -53,7 +53,7 @@ export async function zeigeStunden(wurzel, { route, me, nav, daten } = {}) { const vorschuss = vorschussZeile(l.vorschussMinutes, gekuendigt) const zeilen = [...(l.usage || []).map(verlaufZeile), ...(l.credits || []).slice().reverse().map(gutschriftZeile)] - leeren(wurzel).append( + ersetze(wurzel, ...aboWarnungen(sub), h('div', { class: 'kn-statistik' }, stat(fmtStd(l.balanceMinutes), 'verfügbare Zeit'), diff --git a/public/neu/ansicht-website.js b/public/neu/ansicht-website.js index 5e53862..f424751 100644 --- a/public/neu/ansicht-website.js +++ b/public/neu/ansicht-website.js @@ -3,7 +3,7 @@ import { api } from './api.js' import { h, leeren } from './dom.js' -import { K, zuAnsicht, meldeTitel, fmtDatumZeit, safeUrl } from './ansicht-hilfen.js' +import { K, ersetze, zuAnsicht, meldeTitel, fmtDatumZeit, safeUrl } from './ansicht-hilfen.js' import { baueAktivitaet, baueProjektDetails, ladeProjektDetails } from './website-details.js' import { ladeOffeneRechnungen, rechnungOffenZeile } from './rechnung-offen.js' import { starteInstallQuelle, installAngebot, baueInstallHinweis, istAusgeblendet, merkeAusgeblendet } from './install-hinweis.js' @@ -128,6 +128,6 @@ export async function zeigeWebsite(wurzel, { route, me, nav } = {}) { ? h('ul', { class: 'kn-projekte' }, projekte.liste.map(projektZeile)) : h('p', { class: K.leer }, 'Noch keine Website-Projekte zugewiesen.') const aktivitaetSektion = aktivitaetAbschnitt(aktivitaet, jetzt) - leeren(wurzel).append(zeile, installPlatz(), liste, aktivitaetSektion) + ersetze(wurzel, zeile, installPlatz(), liste, aktivitaetSektion) if (route?.scroll === 'aktivitaet') aktivitaetSektion.scrollIntoView({ block: 'start' }) } diff --git a/test/knAnsichtHilfen.test.js b/test/knAnsichtHilfen.test.js index f936804..9be67e3 100644 --- a/test/knAnsichtHilfen.test.js +++ b/test/knAnsichtHilfen.test.js @@ -2,7 +2,7 @@ import { test } from 'node:test' import assert from 'node:assert/strict' import { fmtEuro, fmtDatum, fmtDatumZeit, fmtTagMonat, fmtStd, fmtMinuten, fmtBytes, legacyRequest, meldeZaehlerNeu, merkeHinweis, nimmHinweis, - setzeChatVorgabe, nimmChatVorgabe, fmtUhrzeit, berlinTag, wechselFunktionAus, zuAnsicht, meldeTitel, K, ABO_NICHT_GELADEN, navigiere, ladeAbo, safeUrl, + setzeChatVorgabe, nimmChatVorgabe, fmtUhrzeit, berlinTag, ersetze, wechselFunktionAus, zuAnsicht, meldeTitel, K, ABO_NICHT_GELADEN, navigiere, ladeAbo, safeUrl, } from '../public/neu/ansicht-hilfen.js' import { mit, mitDom, abo, aboAntwort, me, projekt, rechnung, nachricht, angebot, ledger, anfrageGutschrift, VORSCHAU, VORSCHAU_SPEICHER, LIMITS, neueWurzel } from './support/knDaten.js' @@ -22,6 +22,12 @@ test('Formate: Uhrzeit und Kalendertag in Berliner Zeit', () => { assert.deepEqual(berlinTag('2026-10-15T22:30:00Z'), { j: 2026, m: 10, t: 16 }); assert.equal(berlinTag(''), null) }) +test('ersetze überspringt null, undefined und false, statt „null“ anzuhängen', () => mitDom((fx) => { + const el = fx.document.createElement('div'); el.append('alt') + ersetze(el, null, 'a', [undefined, fx.document.createElement('b'), false], 'c') + assert.equal(el.textContent, 'ac'); assert.equal(el.childNodes.length, 3) +})) + test('Formate: Stunden, Minuten, Bytes', () => { assert.deepEqual([150, 120, 45, 0].map(fmtStd), ['2,5 Std.', '2 Std.', '0,75 Std.', '0 Std.']) assert.deepEqual([0, 45, 120, 150].map(fmtMinuten), ['', '45 min', '2 h', '2 h 30 min']) @@ -60,11 +66,12 @@ test('Bausteinklassen K tragen die Klassen aus basis.css', () => { assert.match(ABO_NICHT_GELADEN, /nicht geladen werden/) }) -test('ladeAbo liefert das Objekt, navigiere wirft ohne Router nicht', mit({ 'GET /api/abo': aboAntwort() }, async () => { +test('ladeAbo liefert das Objekt, navigiere wirft ohne Router nicht', mit({ 'GET /api/abo': aboAntwort() }, async (fx) => { const antwort = await ladeAbo() assert.equal(antwort.subscription, null) assert.equal(antwort.termsVersion, '2026-09-30-v5') assert.doesNotThrow(() => navigiere('#chat')) + assert.equal(fx.window.location.hash, '#chat') // ohne laufenden Router trägt der Hash die Adresse })) test('ladeAbo bei Fehler: null', mit({ 'GET /api/abo': () => ({ status: 500, body: { error: 'kaputt' } }) }, async () => { diff --git a/test/knAnsichtSpeicher.test.js b/test/knAnsichtSpeicher.test.js new file mode 100644 index 0000000..0157dd7 --- /dev/null +++ b/test/knAnsichtSpeicher.test.js @@ -0,0 +1,50 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { zeigeSpeicher } from '../public/neu/ansicht-speicher.js' +import { nimmChatVorgabe } from '../public/neu/ansicht-hilfen.js' +import { sichtbarerText, ladeAlteApp } from './support/knWortlautOrakel.js' +import { mit, me, abo, aboAntwort, neueWurzel, VORSCHAU_SPEICHER } from './support/knDaten.js' + +const alt = ladeAlteApp() +const speicher = (o = {}, sub = {}) => aboAntwort({ subscription: abo({ storage: { units: 2, pendingSetup: false, pendingReduction: null }, canChangeStorage: true, ...sub }), ...o }) +const zeigeNeu = async (fx, daten, m = me()) => { const w = neueWurzel(fx); await zeigeSpeicher(w, { route: { bereich: 'vertraege', reiter: 'email', query: '' }, me: m, nav: 'tab', daten }); await fx.ruhe(); return w } +for (const [name, daten] of [['zwei Pakete', speicher()], ['gesperrt', speicher({}, { canChangeStorage: false })], ['wird eingerichtet', speicher({}, { storage: { units: 1, pendingSetup: true, pendingReduction: null } })], ['Reduzierung geplant', speicher({}, { storage: { units: 3, pendingSetup: false, pendingReduction: { units: 1, effectiveAt: '2026-11-01T00:00:00.000Z' } } })]]) { + test(`E-Mail-Speicher (${name}): Text wie in app.js`, mit({ 'POST /api/abo/storage/preview': VORSCHAU_SPEICHER }, async (fx) => { + const w = await zeigeNeu(fx, daten) + assert.equal(sichtbarerText(fx.finde(w, '[data-panel="speicher"]')), sichtbarerText(await alt.speicher(daten, VORSCHAU_SPEICHER))) + })) +} +test('Plus: Vorschau, Zustimmung Pflicht, Body mit acceptTerms und termsVersion, Hinweis nach Erfolg', mit({ + 'POST /api/abo/storage/preview': { direction: 'up', units: 3, totalGb: 20, monthlyCents: 300, deltaMonthlyCents: 100, prorationCents: 40, nextChargeAt: '2026-11-16T00:00:00.000Z', effectiveAt: null }, + 'POST /api/abo/storage': { change: { task: true }, subscription: abo() }, 'GET /api/abo': speicher() }, async (fx) => { + const w = await zeigeNeu(fx, speicher()) + await fx.klick(fx.finde(w, '#storage-plus')); await fx.ruhe(); assert.deepEqual(fx.fetchLog.at(-1).body, { units: 3 }) + await fx.klick(fx.finde(w, '#storage-submit')); assert.match(fx.text(w), /Bitte stimmen Sie den Regeln zum Zusatzspeicher zu\./) + await fx.klick(fx.finde(w, '#storage-terms')); await fx.klick(fx.finde(w, '#storage-submit')); await fx.ruhe() + assert.deepEqual(fx.fetchLog.find((a) => a.url === '/api/abo/storage').body, { units: 3, acceptTerms: true, termsVersion: '2026-09-30-v5' }) + assert.match(fx.text(w), /Danke, Ihre Bestellung ist eingegangen\. Der Speicher wird in der Regel innerhalb eines Werktags eingerichtet\./) +})) +test('Minus ohne Haken; 409 hängt den Hinweis zum Neuladen an; Ansichtsmodus sperrt', mit({ + 'POST /api/abo/storage/preview': VORSCHAU_SPEICHER, 'POST /api/abo/storage': () => ({ status: 409, body: { error: 'Stand geändert.' } }) }, async (fx) => { + const w = await zeigeNeu(fx, speicher()); await fx.klick(fx.finde(w, '#storage-minus')); await fx.ruhe(); await fx.klick(fx.finde(w, '#storage-submit')); await fx.ruhe() + assert.deepEqual(fx.fetchLog.find((a) => a.url === '/api/abo/storage').body, { units: 1 }); assert.match(fx.text(w), /Stand geändert\. Bitte laden Sie die Seite neu, falls sich der Stand geändert hat\./) + const gesperrt = await zeigeNeu(fx, speicher(), me({ viewAs: true })); assert.ok(['#storage-minus', '#storage-plus'].every((s) => fx.finde(gesperrt, s).hasAttribute('disabled'))) + assert.match(fx.text(gesperrt), /Im Ansichtsmodus nicht möglich\./) +})) +test('Postfach anfragen setzt die Chat-Vorgabe und geht zum Chat', mit({}, async (fx) => { + fx.window.history.pushState = (_s, _t, url) => { fx.window.location.hash = url } + const w = await zeigeNeu(fx, speicher()); await fx.klick(fx.finde(w, (e) => e.textContent === 'Postfach anfragen')) + assert.equal(nimmChatVorgabe(), 'Bitte lege ein neues Postfach an: '); assert.equal(fx.window.location.hash, '#chat') +})) +test('Speicher: ohne Daten wird das Abo geladen, Ausfall und fehlendes Abo sagen es', mit({ 'GET /api/abo': () => ({ status: 500, body: { error: 'x' } }) }, async (fx) => { + const w = neueWurzel(fx); await zeigeSpeicher(w, { route: { bereich: 'vertraege', query: '' }, me: me(), nav: 'tab' }); await fx.ruhe() + assert.match(fx.text(w), /Deine Verträge konnten gerade nicht geladen werden\./) + const ohne = await zeigeNeu(fx, aboAntwort()); assert.equal(fx.finde(ohne, '[data-panel="speicher"]'), null); assert.match(fx.text(ohne), /Den E-Mail-Speicher gibt es mit einem laufenden Abo\./) +})) +test('Speicher: Ansichtsmodus schickt nichts, ein Vorschau-Fehler sperrt den Knopf', mit({ 'POST /api/abo/storage/preview': () => ({ status: 500, body: { error: 'Vorschau kaputt' } }), 'POST /api/abo/storage': {} }, async (fx) => { + const w = await zeigeNeu(fx, speicher()); await fx.klick(fx.finde(w, '#storage-plus')); await fx.ruhe() + assert.match(fx.text(w), /Vorschau kaputt/); assert.equal(fx.finde(w, '#storage-submit').hasAttribute('disabled'), true) + const vorher = fx.fetchLog.length + const v = await zeigeNeu(fx, speicher(), me({ viewAs: true })); await fx.klick(fx.finde(v, '#storage-plus')); await fx.ruhe() + assert.deepEqual(fx.fetchLog.slice(vorher), []) // im Ansichtsmodus geht nichts raus, auch keine Vorschau +})) diff --git a/test/knAnsichtWebsite.test.js b/test/knAnsichtWebsite.test.js index f6cb988..26b2069 100644 --- a/test/knAnsichtWebsite.test.js +++ b/test/knAnsichtWebsite.test.js @@ -13,6 +13,7 @@ test('projektStatus', () => { assert.deepEqual(s({ previewUrl: '' }), { ton: 'grau', text: 'Noch keine Website' }) assert.equal(vorschauBereit(projekt({ status: 'deployed' })), true); assert.equal(vorschauBereit(projekt({ status: 'building' })), false) }) +test('Website: leere Platzhalter erscheinen nie als Text „null“', mit(HAUPT, async (fx) => { assert.doesNotMatch(fx.text(await zeige(fx)), /null|undefined/) })) test('Website: Ladezustand ist Text und verschwindet', mit(HAUPT, async (fx) => { const w = neueWurzel(fx); const lauf = zeigeWebsite(w, { route: { bereich: 'website', query: '' }, me: me(), nav: 'tab' }) assert.match(fx.text(w), /Wird geladen …/); await lauf; await fx.ruhe(); assert.doesNotMatch(fx.text(w), /Wird geladen …/)