V3: In der Fahrstuhl-Zeile läuft unter dem aktiven Wort eine dünne Linie statt des schwarzen Blocks; das Wort wird dunkel und fett. Die weiße Overlay-Kopie bleibt im Baum, wird aber nicht mehr gezeigt. V5: Terminplaner: Tag/Woche/Monat als hellgrauer Umschalter mit weißer Auswahl, Filter als leise Chips. „Einschalten“ in der Liste Entdecken ist ein heller Knopf; schwarz bleibt der Knopf der Bestätigungsseite. V7: Verträge nur noch mit Abo und Rechnungen. Das Stundenkonto steht beim Wartungs-Abo als Abschnitt unter dem Abo (ohne doppelte Warnungen), #stunden springt dorthin. Der E-Mail-Speicher ist die Unterseite #speicher (Link „ändern“ in der Abo-Zeile), ohne Widerrufsstreifen wie die anderen Bestellseiten. Alte Links bleiben gültig. V8: Konto mit vier Zeilen (Passwort, Verträge kündigen, Vertrag widerrufen, Abmelden), Datenschutz und Impressum klein darunter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
9.3 KiB
JavaScript
104 lines
9.3 KiB
JavaScript
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { zeigeVertraege, sichtbareReiter, waehleReiter, AUF_ABO_WARTEN } from '../public/neu/ansicht-vertraege.js'
|
|
import { merkeHinweis } from '../public/neu/ansicht-hilfen.js'
|
|
import { routeAusHash } from '../public/neu/routen.js'
|
|
import { mit, me, abo, aboAntwort, ledger, rechnung, neueWurzel, LIMITS } from './support/knDaten.js'
|
|
|
|
const ZWEI = ['abo', 'rechnungen']
|
|
test('sichtbareReiter: immer Abo und Rechnungen (Navigation leichter V7), unabhängig vom Abo', () => {
|
|
const faelle = [null, aboAntwort(), aboAntwort({ subscription: abo({ active: false }) }), aboAntwort({ subscription: abo({ produkt: 'hosting' }) }), aboAntwort({ subscription: abo() })]
|
|
for (const d of faelle) assert.deepEqual(sichtbareReiter(d), ZWEI)
|
|
})
|
|
test('Vorbedingung (VF8): der Bereichs-Hash hat keinen Reiter, die Reiter-Hashes haben einen', () => {
|
|
assert.equal(routeAusHash('#vertraege').reiter, undefined)
|
|
for (const [hash, reiter] of [['#abo', 'abo'], ['#wartung', 'abo'], ['#rechnungen', 'rechnungen'], ['#stunden', 'stunden']]) assert.equal(routeAusHash(hash).reiter, reiter, hash)
|
|
assert.deepEqual(routeAusHash('#speicher'), { bereich: 'vertraege', etage: 'speicher', query: '' }) // der Speicher ist eine Unterseite
|
|
})
|
|
test('waehleReiter: nur der Bereichs-Einstieg springt, jede ausdrückliche Wahl bleibt (Review Focus 2)', () => {
|
|
const w = (o) => waehleReiter({ routeReiter: undefined, sichtbar: ZWEI, offeneRechnungen: 1, ...o })
|
|
assert.equal(w({}), 'rechnungen'); assert.equal(w({ offeneRechnungen: 0 }), 'abo')
|
|
assert.equal(w({ routeReiter: 'abo' }), 'abo') // #abo, #wartung und der Klick auf „Abo“ trotz offener Rechnung
|
|
assert.equal(w({ routeReiter: 'stunden' }), 'abo'); assert.equal(w({ routeReiter: 'rechnungen', offeneRechnungen: 0 }), 'rechnungen') // #stunden öffnet das Abo
|
|
})
|
|
const SERVER = { 'GET /api/abo': aboAntwort(), 'GET /api/invoices': { invoices: [rechnung({ balance: 50 })], incomplete: false }, 'GET /api/auftraege': { aktiv: false }, 'GET /api/abo/takeover-credits': { requests: [], limits: LIMITS } }
|
|
const zeige = async (fx, route = {}) => { const w = neueWurzel(fx); await zeigeVertraege(w, { route: { bereich: 'vertraege', query: '', ...route }, me: me(), nav: 'tab' }); await fx.ruhe(); return w }
|
|
const aktiv = (fx, w) => fx.finde(w, '[role="tab"][aria-selected="true"]').textContent
|
|
const WOHIN = (fx) => { fx.window.history.replaceState = (_s, _t, url) => { fx.window.location.hash = `#${String(url).split('#')[1] ?? ''}` }; fx.window.history.pushState = fx.window.history.replaceState }
|
|
test('Verträge: Reiter in fester Reihenfolge, Kündigen-Link unter jedem Reiter, ausdrückliche Reiterwahl bleibt trotz offener Rechnung', mit(SERVER, async (fx) => {
|
|
for (const [route, gewaehlt] of [[{ reiter: 'abo' }, 'Abo'], [{ reiter: 'rechnungen' }, 'Rechnungen'], [{}, 'Rechnungen']]) { // {} = Bereichs-Einstieg
|
|
const w = await zeige(fx, route)
|
|
assert.deepEqual(fx.alle(w, '[role="tab"]').map((t) => t.textContent), ['Abo', 'Rechnungen'])
|
|
assert.equal(aktiv(fx, w), gewaehlt)
|
|
assert.equal(fx.finde(w, 'a[href="/kuendigen.html"]').textContent, 'Verträge hier kündigen')
|
|
}
|
|
}))
|
|
test('Verträge: die Route entscheidet, die Adressleiste nicht (keine zweite Wahrheit)', mit(SERVER, async (fx) => {
|
|
fx.window.location.hash = '#vertraege'; assert.equal(aktiv(fx, await zeige(fx, { reiter: 'abo' })), 'Abo') // widerspricht der Route absichtlich
|
|
fx.window.location.hash = '#abo'; assert.equal(aktiv(fx, await zeige(fx, {})), 'Rechnungen')
|
|
}))
|
|
test('Reiterklick navigiert ausdrücklich und ersetzt: #abo, #rechnungen; auch mit Wartungs-Abo nur zwei Reiter', mit({ ...SERVER, 'GET /api/abo': aboAntwort({ subscription: abo() }) }, async (fx) => {
|
|
WOHIN(fx); const w = await zeige(fx, { reiter: 'rechnungen' })
|
|
assert.deepEqual(fx.alle(w, '[role="tab"]').map((t) => t.textContent), ['Abo', 'Rechnungen'])
|
|
for (const [label, hash] of [['Abo', '#abo'], ['Rechnungen', '#rechnungen']]) {
|
|
await fx.klick(fx.finde(w, (e) => e.getAttribute?.('role') === 'tab' && e.textContent === label)); assert.equal(fx.window.location.hash, hash, label)
|
|
}
|
|
}))
|
|
test('Verträge: /api/abo fällt aus, Reiter, Rechnungen und Kündigen bleiben', mit({ ...SERVER, 'GET /api/abo': () => ({ status: 500, body: { error: 'x' } }) }, async (fx) => {
|
|
const w = await zeige(fx, { reiter: 'rechnungen' })
|
|
assert.match(fx.text(w), /Deine Verträge konnten gerade nicht geladen werden\./); assert.ok(fx.finde(w, 'a[href="/kuendigen.html"]')); assert.ok(fx.finde(w, 'tbody tr'))
|
|
}))
|
|
test('route.reiter stunden ohne Wartung: Abo-Reiter mit Hinweis, kein Stundenkonto', mit({ ...SERVER, 'GET /api/abo': aboAntwort({ subscription: abo({ produkt: 'hosting' }) }) }, async (fx) => {
|
|
const w = await zeige(fx, { reiter: 'stunden' })
|
|
assert.equal(aktiv(fx, w), 'Abo'); assert.match(fx.text(w), /Ein Stundenkonto gibt es im Wartungs-Abo\./)
|
|
assert.equal(fx.finde(w, '#kn-stundenkonto'), null)
|
|
}))
|
|
const LEDGER = ledger({ balanceMinutes: 90, usedMinutes: 120, usage: [], credits: [] })
|
|
test('Navigation leichter V7: Wartungs-Abo zeigt das Stundenkonto unter dem Abo, Warnungen und Bedingungen-Link nur einmal', mit({ ...SERVER, 'GET /api/abo': aboAntwort({ subscription: abo({ paymentProblem: true }), ledger: LEDGER }) }, async (fx) => {
|
|
for (const route of [{ reiter: 'abo' }, { reiter: 'stunden' }]) {
|
|
const w = await zeige(fx, route)
|
|
assert.equal(aktiv(fx, w), 'Abo')
|
|
const konto = fx.finde(w, '#kn-stundenkonto')
|
|
assert.ok(konto, 'Abschnitt Stundenkonto'); assert.equal(fx.finde(konto, 'h2').textContent, 'Stundenkonto')
|
|
assert.match(fx.text(konto), /verfügbare Zeit/); assert.match(fx.text(konto), /Verlauf/)
|
|
assert.equal(fx.text(w).split('Die letzte Abbuchung ist fehlgeschlagen').length - 1, 1, 'Zahlungswarnung genau einmal')
|
|
assert.doesNotMatch(fx.text(konto), /Leistungen & Bedingungen/)
|
|
assert.doesNotMatch(fx.text(w), /Ein Stundenkonto gibt es im Wartungs-Abo/)
|
|
}
|
|
}))
|
|
let speicherAbo = null
|
|
test('Navigation leichter V7: „ändern“ am E-Mail-Speicher führt auf #speicher, mit Hosting- und Wartungs-Abo', mit({ ...SERVER, 'GET /api/abo': () => ({ body: speicherAbo }) }, async (fx) => {
|
|
for (const produkt of ['hosting', 'wartung']) {
|
|
speicherAbo = aboAntwort({ subscription: abo({ produkt }), ledger: LEDGER })
|
|
WOHIN(fx); const w = await zeige(fx, { reiter: 'abo' })
|
|
const link = fx.finde(w, 'a[data-aktion="speicher"]')
|
|
assert.deepEqual([link.textContent, link.getAttribute('href')], ['ändern', '#speicher'], produkt)
|
|
await fx.klick(link); assert.equal(fx.window.location.hash, '#speicher', produkt)
|
|
}
|
|
}))
|
|
test('Hinweis aus einer Aktion steht genau einmal über den Reitern', mit(SERVER, async (fx) => {
|
|
merkeHinweis('Dein Wechsel ist abgeschlossen.'); assert.match(fx.text(await zeige(fx, { reiter: 'abo' })), /Dein Wechsel ist abgeschlossen\./)
|
|
assert.doesNotMatch(fx.text(await zeige(fx, { reiter: 'abo' })), /Dein Wechsel ist abgeschlossen\./)
|
|
}))
|
|
test('Rückkehr von Stripe wartet auf das Abo und entfernt ?abo= aus der Adresse, der Verlaufsstempel bleibt', mit({ ...SERVER, 'GET /api/abo': (() => { let n = 0; return () => ({ body: n++ < 2 ? aboAntwort() : aboAntwort({ subscription: abo() }) }) })() }, async (fx) => {
|
|
Object.assign(AUF_ABO_WARTEN, { versuche: 5, pauseMs: 0 }); fx.window.location.search = '?abo=success'; fx.window.location.hash = '#abo'; fx.window.history.state = { kn: { tiefe: 0 } }
|
|
let ersetzt = null; fx.window.history.replaceState = (s, _t, url) => { ersetzt = { s, url } }
|
|
const w = await zeige(fx, { reiter: 'abo' })
|
|
assert.match(fx.text(w), /Mein Vertrag/); assert.deepEqual(ersetzt, { s: { kn: { tiefe: 0 } }, url: '/dashboard.html#abo' }); assert.ok(fx.fetchLog.filter((a) => a.url === '/api/abo').length >= 3)
|
|
}))
|
|
let letztes = null
|
|
test('Rückkehr von Stripe: mit zurückgekommener wechselFunktion geht es mit ersetzen zu #funktionen, ohne sie bleibt es in Verträge (E14, C5)', mit({ ...SERVER, 'GET /api/abo': () => ({ body: letztes }) }, async (fx) => {
|
|
Object.assign(AUF_ABO_WARTEN, { versuche: 5, pauseMs: 0 }); fx.window.location.search = '?abo=success'; WOHIN(fx)
|
|
letztes = aboAntwort({ subscription: abo({ wechselFunktion: { key: 'termine', projectId: 'p1' } }) }); fx.window.location.hash = '#vertraege'
|
|
await zeige(fx, {}); assert.equal(fx.window.location.hash, '#funktionen')
|
|
letztes = aboAntwort({ subscription: abo({ wechselFunktion: null }) }); fx.window.location.hash = '#vertraege'
|
|
await zeige(fx, {}); assert.equal(fx.window.location.hash, '#vertraege')
|
|
}))
|
|
test('Verträge: ?abo=cancel räumt die Adresse ohne zu warten; Ladezustand ist Text; Rechnungen fallen aus, Abo bleibt', mit({ ...SERVER, 'GET /api/invoices': () => ({ status: 500, body: { error: 'Rechnungen kaputt' } }) }, async (fx) => {
|
|
fx.window.location.search = '?abo=cancel'; let ersetzt = 0; fx.window.history.replaceState = () => { ersetzt += 1 }
|
|
const w = neueWurzel(fx); const lauf = zeigeVertraege(w, { route: { bereich: 'vertraege', reiter: 'rechnungen', query: '' }, me: me(), nav: 'tab' })
|
|
assert.match(fx.text(w), /Wird geladen …/); await lauf; await fx.ruhe()
|
|
assert.equal(ersetzt, 1); assert.ok(!fx.fetchLog.some((a) => a.url === '/api/abo' && a.method !== 'GET')); assert.equal(fx.fetchLog.filter((a) => a.url === '/api/abo').length, 1)
|
|
assert.match(fx.text(w), /Rechnungen konnten nicht geladen werden: Rechnungen kaputt/)
|
|
}))
|