Funktionen neu: Ansicht mit Meine und Entdecken, Website-Auswahl, Hinweise je Abo-Lage, Rechnungszeile
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
57112cc51f
commit
f30e72ebbb
2 files changed
+370
-5
No files matched your search
@@ -2,8 +2,12 @@
|
|||||||
Der erste Teil ist rein (Modell aus der Antwort von GET /api/funktionen im Abo-Modell, V5); die Ansicht darunter zeichnet es.
|
Der erste Teil ist rein (Modell aus der Antwort von GET /api/funktionen im Abo-Modell, V5); die Ansicht darunter zeichnet es.
|
||||||
Keine Preise: Der Katalog trägt keinen. Fremdtext (Funktions- und Websitenamen) wird nur als Text gesetzt. */
|
Keine Preise: Der Katalog trägt keinen. Fremdtext (Funktions- und Websitenamen) wird nur als Text gesetzt. */
|
||||||
|
|
||||||
import { h } from './dom.js'
|
import { api as apiStandard } from './api.js'
|
||||||
import { ETAGE_FUER, anfrageStatusVon } from './funktion-einschalten.js'
|
import { h, leeren } from './dom.js'
|
||||||
|
import { baueReiter } from './reiter.js'
|
||||||
|
import { geheZu as geheZuStandard, oeffneOverlay as oeffneOverlayStandard, beendeOverlay as beendeOverlayStandard } from './router.js'
|
||||||
|
import { ladeOffeneRechnungen, rechnungOffenZeile } from './rechnung-offen.js'
|
||||||
|
import { ETAGE_FUER, TEXTE, anfrageStatusVon, einschaltWeg, navigiere, nimmMeldung, renderEinschalten } from './funktion-einschalten.js'
|
||||||
|
|
||||||
const ANFRAGE_SICHTBAR = ['offen', 'angenommen', 'abgelehnt'] // zurückgezogen zählt nicht
|
const ANFRAGE_SICHTBAR = ['offen', 'angenommen', 'abgelehnt'] // zurückgezogen zählt nicht
|
||||||
|
|
||||||
@@ -118,7 +122,168 @@ export function zeilenWeg(zeile) {
|
|||||||
return { art: 'etage', etage: ETAGE_FUER[zeile.key] }
|
return { art: 'etage', etage: ETAGE_FUER[zeile.key] }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Platzhalter bis Task 5 (zeichnet die Ansicht)
|
/* ------------------------------------------------------------------------------------------------ Ansicht --- */
|
||||||
export async function zeigeFunktionen(wurzel, { route, me, nav }) {
|
|
||||||
wurzel.replaceChildren(h('p', { class: 'kn-leer' }, 'Funktionen (folgt)'))
|
const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs: eine ältere Antwort zeichnet nichts mehr
|
||||||
|
let reiterGewaehlt = false // Tipp auf „Meine“: der nächste Aufruf zeigt den Leerzustand statt zu Entdecken zu springen
|
||||||
|
let vorgabe = null // nach einer eigenen Navigation (Reiter, Sprung zu Entdecken) nimmt der nächste Aufruf die geladenen Antworten
|
||||||
|
|
||||||
|
/** Setzt die Merker des Moduls zurück (nur für Tests). */
|
||||||
|
export function vergissMerker() {
|
||||||
|
reiterGewaehlt = false
|
||||||
|
vorgabe = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const BILD = /^\/neu\/bilder\/[a-z0-9-]+\.webp$/
|
||||||
|
|
||||||
|
/** Stück für die Pille einer Zeile. */
|
||||||
|
const pille = (p) => h('span', { class: `kn-fn-pille kn-fn-pille--${p.ton}` }, p.text)
|
||||||
|
|
||||||
|
export async function zeigeFunktionen(wurzel, { route, me, nav } = {}, abh = {}) {
|
||||||
|
const A = {
|
||||||
|
api: abh.api ?? apiStandard,
|
||||||
|
geheZu: abh.geheZu ?? geheZuStandard,
|
||||||
|
oeffneOverlay: abh.oeffneOverlay ?? oeffneOverlayStandard,
|
||||||
|
beendeOverlay: abh.beendeOverlay ?? beendeOverlayStandard,
|
||||||
|
speicher: abh.speicher,
|
||||||
|
jetzt: abh.jetzt ?? (() => new Date()),
|
||||||
|
}
|
||||||
|
const nr = (laeufe.get(wurzel) ?? 0) + 1
|
||||||
|
laeufe.set(wurzel, nr)
|
||||||
|
const aktuell = () => laeufe.get(wurzel) === nr
|
||||||
|
const nochmal = () => zeigeFunktionen(wurzel, { route, me, nav }, abh)
|
||||||
|
const reiterWahl = route?.reiter === 'entdecken' ? 'entdecken' : 'meine'
|
||||||
|
const ausdruecklich = reiterGewaehlt
|
||||||
|
reiterGewaehlt = false
|
||||||
|
const mitgebracht = vorgabe && vorgabe.wurzel === wurzel && vorgabe.bis > Date.now() ? vorgabe : null
|
||||||
|
vorgabe = null
|
||||||
|
|
||||||
|
let daten
|
||||||
|
let offen
|
||||||
|
if (mitgebracht) {
|
||||||
|
({ daten, offen } = mitgebracht)
|
||||||
|
} else {
|
||||||
|
leeren(wurzel).append(h('p', { class: 'kn-leer' }, 'Wird geladen …'))
|
||||||
|
try {
|
||||||
|
[daten, offen] = await Promise.all([A.api('/api/funktionen'), ladeOffeneRechnungen(A.api)])
|
||||||
|
} catch {
|
||||||
|
if (!aktuell()) return
|
||||||
|
leeren(wurzel).append(
|
||||||
|
h('p', { class: 'kn-fehler', role: 'alert' }, 'Die Funktionen konnten gerade nicht geladen werden.'),
|
||||||
|
h('button', { type: 'button', class: 'kn-fn-knopf kn-fn-knopf--zweit', 'data-aktion': 'nochmal', on: { click: () => nochmal() } }, 'Nochmal versuchen'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!aktuell()) return
|
||||||
|
}
|
||||||
|
if (daten?.modell !== 'abo') {
|
||||||
|
leeren(wurzel).append(h('p', { class: 'kn-fehler', role: 'alert' }, 'Die Funktionen sind gerade nicht verfügbar. Lade die Seite neu.'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const kundenart = me?.customer?.kundenartWirksam ?? daten.kundenart
|
||||||
|
const viewAs = me?.viewAs === true
|
||||||
|
const zeilen = meineZeilen(daten)
|
||||||
|
if (reiterWahl === 'meine' && zeilen.length === 0 && !ausdruecklich) {
|
||||||
|
vorgabe = { wurzel, daten, offen, bis: Date.now() + 5000 }
|
||||||
|
A.geheZu('#entdecken', { ersetzen: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const projektVon = (projectId) => daten.projects.find((p) => p?.projectId === projectId)
|
||||||
|
const weiter = (weg, ziel) => navigiere(weg, ziel, { geheZu: A.geheZu, speicher: A.speicher })
|
||||||
|
|
||||||
|
/** Die Bestätigungsseite für diese Funktion und Website. */
|
||||||
|
function oeffneSeite(key, projectId) {
|
||||||
|
renderEinschalten(wurzel, { key, projectId, daten, kundenartWirksam: kundenart, viewAs, jetzt: A.jetzt, api: A.api, geheZu: A.geheZu, speicher: A.speicher })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** „Einschalten“ für eine Website: die Vorprüfung wählt den Ort; eine Meldung steht im Eintrag. */
|
||||||
|
function einschaltTipp(eintrag, kandidat, meldungEl) {
|
||||||
|
const weg = einschaltWeg({ eintrag, projekt: projektVon(kandidat.projectId), abo: daten.abo, kundenart })
|
||||||
|
if (weg.art === 'meldung') { meldungEl.textContent = weg.text; return }
|
||||||
|
meldungEl.textContent = ''
|
||||||
|
if (weg.art === 'seite') oeffneSeite(eintrag.key, kandidat.projectId)
|
||||||
|
else weiter(weg, { key: eintrag.key, projectId: kandidat.projectId })
|
||||||
|
}
|
||||||
|
|
||||||
|
function meineListe() {
|
||||||
|
if (zeilen.length === 0) {
|
||||||
|
return [
|
||||||
|
h('p', { class: 'kn-leer' }, 'Du hast noch keine Funktion eingeschaltet.'),
|
||||||
|
h('button', { type: 'button', class: 'kn-fn-knopf', 'data-aktion': 'entdecken', on: { click: () => wechsleReiter('entdecken') } }, 'Entdecken'),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return h('ul', { class: 'kn-fn-liste' }, zeilen.map((z) => h('li', { class: 'kn-fn-zeile', 'data-fn': z.key, 'data-projekt': z.projectId },
|
||||||
|
h('button', {
|
||||||
|
type: 'button',
|
||||||
|
class: 'kn-fn-zeile-knopf',
|
||||||
|
on: {
|
||||||
|
click: () => {
|
||||||
|
const weg = zeilenWeg(z)
|
||||||
|
if (weg.art === 'seite') oeffneSeite(z.key, z.projectId)
|
||||||
|
else weiter(weg, { key: z.key, projectId: z.projectId })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
h('span', { class: 'kn-fn-text' }, h('span', { class: 'kn-fn-name' }, z.name), h('span', { class: 'kn-fn-zeile2' }, z.zeile2)),
|
||||||
|
pille(z.pille)))))
|
||||||
|
}
|
||||||
|
|
||||||
|
function eintragKarte({ eintrag, kandidaten }) {
|
||||||
|
const nurBetriebHinweis = eintrag.nurBetrieb === true && kundenart !== 'unternehmen'
|
||||||
|
const meldungEl = h('p', { class: 'kn-fn-meldung', role: 'alert' })
|
||||||
|
let wahl = null
|
||||||
|
const knopf = h('button', {
|
||||||
|
type: 'button',
|
||||||
|
class: 'kn-fn-knopf',
|
||||||
|
'data-aktion': 'einschalten',
|
||||||
|
on: {
|
||||||
|
click: () => {
|
||||||
|
if (kandidaten.length === 1) { einschaltTipp(eintrag, kandidaten[0], meldungEl); return }
|
||||||
|
if (wahl) { wahl.remove(); wahl = null; return }
|
||||||
|
wahl = h('div', { class: 'kn-fn-wahl', role: 'group', 'aria-label': 'Für welche Website?' },
|
||||||
|
kandidaten.map((k) => h('button', { type: 'button', class: 'kn-fn-knopf kn-fn-knopf--zweit', on: { click: () => einschaltTipp(eintrag, k, meldungEl) } }, k.projektName)))
|
||||||
|
knopf.parentNode.insertBefore(wahl, meldungEl)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, 'Einschalten')
|
||||||
|
const bild = typeof eintrag.bild === 'string' && BILD.test(eintrag.bild)
|
||||||
|
? h('img', { class: 'kn-fn-bild', src: eintrag.bild, alt: `Beispiel: ${eintrag.name}`, width: 960, height: 600, loading: 'lazy', on: { error: (ev) => ev.target.remove() } })
|
||||||
|
: null
|
||||||
|
return h('section', { class: 'kn-fn-eintrag', 'data-fn': eintrag.key },
|
||||||
|
bild,
|
||||||
|
h('div', { class: 'kn-fn-eintrag-text' },
|
||||||
|
h('h3', null, String(eintrag.name ?? '')),
|
||||||
|
h('p', { class: 'kn-fn-beschreibung' }, String(eintrag.beschreibung ?? '')),
|
||||||
|
h('p', { class: 'kn-fn-enthalten' }, eintrag.enthaltenIn === 'hosting' ? 'Im Hosting-Abo enthalten' : 'Im Wartungs-Abo enthalten'),
|
||||||
|
nurBetriebHinweis ? h('p', { class: 'kn-fn-hinweis' }, TEXTE.nurBetrieb(String(eintrag.name ?? ''))) : (kandidaten.length ? knopf : null),
|
||||||
|
meldungEl))
|
||||||
|
}
|
||||||
|
|
||||||
|
function entdeckenListe() {
|
||||||
|
const eintraege = entdeckenEintraege(daten)
|
||||||
|
if (eintraege.length === 0) return h('p', { class: 'kn-leer' }, 'Du hast schon alles eingeschaltet, was es gibt.')
|
||||||
|
const ohneWebsite = daten.projects.length === 0
|
||||||
|
return [
|
||||||
|
ohneWebsite ? h('p', { class: 'kn-fn-info' }, 'Sobald deine Website angelegt ist, kannst du Funktionen einschalten.') : null,
|
||||||
|
...eintraege.map(eintragKarte),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reiterwechsel: Meine oder Entdecken; die geladenen Antworten bleiben für den folgenden Aufruf des Routers. */
|
||||||
|
function wechsleReiter(key) {
|
||||||
|
if (key === 'meine') reiterGewaehlt = true
|
||||||
|
vorgabe = { wurzel, daten, offen, bis: Date.now() + 5000 }
|
||||||
|
A.geheZu(key === 'meine' ? '#funktionen' : '#entdecken', { ersetzen: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const meldung = nimmMeldung()
|
||||||
|
const reiter = baueReiter({ eintraege: [{ key: 'meine', label: 'Meine' }, { key: 'entdecken', label: 'Entdecken' }], aktiv: reiterWahl, art: 'unterstrich', ariaLabel: 'Funktionen', bei: wechsleReiter })
|
||||||
|
leeren(wurzel).append(
|
||||||
|
...[
|
||||||
|
meldung ? h('p', { class: 'kn-meldung kn-meldung-ok', role: 'status' }, meldung) : null,
|
||||||
|
rechnungOffenZeile(offen),
|
||||||
|
reiter,
|
||||||
|
h('div', { class: 'kn-reiter-inhalt' }, reiterWahl === 'meine' ? meineListe() : entdeckenListe()),
|
||||||
|
].filter(Boolean))
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import { test } from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { installFakeDom } from './support/fakeDom.js'
|
||||||
|
import { zeigeFunktionen, vergissMerker } from '../public/neu/ansicht-funktionen.js'
|
||||||
|
import { daten, projekt, feature } from './support/knFunktionenDaten.js'
|
||||||
|
|
||||||
|
const JETZT = () => new Date('2026-10-20T10:00:00Z')
|
||||||
|
const MEINE = { bereich: 'funktionen', reiter: 'meine', query: '' }
|
||||||
|
const ENTDECKEN = { bereich: 'funktionen', reiter: 'entdecken', query: '' }
|
||||||
|
const mit = (fn) => async () => { vergissMerker(); const fx = installFakeDom(); try { await fn(fx) } finally { fx.restore() } }
|
||||||
|
function umgebung(fx, { d = daten(), abo, api, post = { feature: { state: 'setup' }, ticketWoid: '100301' }, me = {} } = {}) {
|
||||||
|
const wurzel = fx.document.createElement('div'); fx.document.body.append(wurzel)
|
||||||
|
const u = { wurzel, d, rechnungen: [], rechnungenFehler: false, gegangen: [], aufrufe: [], gespeichert: {}, overlay: null }
|
||||||
|
const basis = async (pfad, o = {}) => {
|
||||||
|
u.aufrufe.push([o.method ?? 'GET', pfad])
|
||||||
|
if (o.method === 'POST') return post
|
||||||
|
if (pfad === '/api/funktionen') return u.d
|
||||||
|
if (pfad === '/api/invoices') { if (u.rechnungenFehler) throw Object.assign(new Error('x'), { status: 500, body: {} }); return { invoices: u.rechnungen, incomplete: false } }
|
||||||
|
if (pfad === '/api/abo') { if (abo instanceof Error) throw abo; return abo ?? { subscription: null } }
|
||||||
|
throw new Error(`unerwartet ${pfad}`)
|
||||||
|
}
|
||||||
|
const abh = {
|
||||||
|
api: api ?? basis, jetzt: JETZT, speicher: { setItem: (k, v) => { u.gespeichert[k] = v } },
|
||||||
|
geheZu: (hash, o) => { u.gegangen.push(o ? [hash, o] : [hash]) },
|
||||||
|
oeffneOverlay: ({ schliesse }) => { u.overlay = schliesse },
|
||||||
|
beendeOverlay: () => { const s = u.overlay; u.overlay = null; s?.() },
|
||||||
|
}
|
||||||
|
u.zeige = (route, ctx = {}) => zeigeFunktionen(wurzel, { route, me: { customer: { kundenartWirksam: 'unternehmen' }, viewAs: false, ...me }, nav: 'tab', ...ctx }, abh)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
const tab = (fx, u, text) => fx.alle(u.wurzel, '[role="tab"]').find((t) => t.textContent === text)
|
||||||
|
|
||||||
|
test('Meine: Liste mit Name, zweiter Zeile und Pille; Tipp öffnet die Etage mit der Website der Zeile, eine angenommene Anfrage die Bestätigung', mit(async (fx) => {
|
||||||
|
const u = umgebung(fx, { d: daten({ projekte: [projekt('p1', 'Bäckerei', { termine: feature('active'), shop: feature('off', { anfrageStatus: 'angenommen' }) }), projekt('p2', 'Café', { termine: feature('active') })] }) })
|
||||||
|
await u.zeige(MEINE)
|
||||||
|
const zeilen = fx.alle(u.wurzel, 'li[data-fn]')
|
||||||
|
assert.deepEqual(zeilen.map((z) => [z.getAttribute('data-fn'), z.getAttribute('data-projekt')]), [['termine', 'p1'], ['termine', 'p2'], ['shop', 'p1']])
|
||||||
|
assert.match(fx.text(zeilen[1]), /Terminplaner.*Café.*Aktiv/)
|
||||||
|
await fx.klick(fx.finde(zeilen[0], 'button'))
|
||||||
|
await fx.klick(fx.finde(zeilen[1], 'button'))
|
||||||
|
assert.deepEqual(u.gegangen, [['#termine?projekt=p1'], ['#termine?projekt=p2']])
|
||||||
|
await fx.klick(fx.finde(zeilen[2], 'button'))
|
||||||
|
assert.ok(fx.finde(u.wurzel, 'section.kn-fn-einschalten[data-key="shop"][data-projekt="p1"]'))
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('Meine leer: der erste Aufruf springt ersetzend zu Entdecken, ein ausdrücklich gewähltes Meine zeigt den Leerzustand', mit(async (fx) => {
|
||||||
|
const u = umgebung(fx)
|
||||||
|
await u.zeige(MEINE)
|
||||||
|
assert.deepEqual(u.gegangen, [['#entdecken', { ersetzen: true }]])
|
||||||
|
await u.zeige(ENTDECKEN)
|
||||||
|
u.gegangen.length = 0
|
||||||
|
await fx.klick(tab(fx, u, 'Meine'))
|
||||||
|
assert.deepEqual(u.gegangen, [['#funktionen', { ersetzen: true }]])
|
||||||
|
await u.zeige(MEINE) // so ruft der Router danach auf
|
||||||
|
assert.equal(u.gegangen.length, 1)
|
||||||
|
assert.match(fx.text(u.wurzel), /Du hast noch keine Funktion eingeschaltet\./)
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('Rechnungszeile oben: die Ansicht lädt GET /api/invoices selbst (ohne Zähler), nur balance > 0, in Meine und Entdecken; fällt der Abruf aus, bleibt die Liste', mit(async (fx) => {
|
||||||
|
const u = umgebung(fx, { d: daten({ projekte: [projekt('p1', 'B', { news: feature('active') })] }) })
|
||||||
|
await u.zeige(MEINE)
|
||||||
|
assert.equal(fx.finde(u.wurzel, '.kn-rechnung-offen'), null)
|
||||||
|
assert.ok(u.aufrufe.some(([m, p]) => m === 'GET' && p === '/api/invoices'))
|
||||||
|
u.rechnungen = [{ id: 'r0', number: 'RE-2026-0040', balance: 0, portalUrl: null }, { id: 'r1', number: 'RE-2026-0042', balance: 50, portalUrl: 'https://invoice.stripe.com/i/x' }]
|
||||||
|
await u.zeige(MEINE)
|
||||||
|
assert.match(fx.text(fx.finde(u.wurzel, '.kn-rechnung-offen')), /^Rechnung offen · RE-2026-0042 · 50,00\s€ · Zahlen$/)
|
||||||
|
assert.ok(fx.finde(u.wurzel, 'li[data-fn]'))
|
||||||
|
await u.zeige(ENTDECKEN)
|
||||||
|
assert.ok(fx.finde(u.wurzel, '.kn-rechnung-offen'))
|
||||||
|
u.rechnungenFehler = true
|
||||||
|
await u.zeige(MEINE)
|
||||||
|
assert.equal(fx.finde(u.wurzel, '.kn-rechnung-offen'), null)
|
||||||
|
assert.ok(fx.finde(u.wurzel, 'li[data-fn]'))
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('Entdecken: Bild, Name, Beschreibung, "Im … Abo enthalten", Knopf; Katalogpreise erscheinen nie', mit(async (fx) => {
|
||||||
|
const u = umgebung(fx, { d: daten({ katalogEdit: { termine: { priceLabel: '149 €' } } }) })
|
||||||
|
await u.zeige(ENTDECKEN)
|
||||||
|
const e = fx.alle(u.wurzel, 'section[data-fn]')
|
||||||
|
assert.deepEqual(e.map((x) => x.getAttribute('data-fn')), ['news', 'termine', 'shop', 'unterkunft', 'zeiten', 'analyse'])
|
||||||
|
assert.equal(fx.finde(e[1], 'img').getAttribute('src'), '/neu/bilder/termine.webp')
|
||||||
|
assert.deepEqual(['alt', 'width', 'height', 'loading'].map((a) => fx.finde(e[1], 'img').getAttribute(a)), ['Beispiel: Terminplaner', '960', '600', 'lazy'])
|
||||||
|
assert.match(fx.text(e[1]), /Terminplaner.*Beschreibung Terminplaner\..*Im Wartungs-Abo enthalten/)
|
||||||
|
assert.match(fx.text(e[5]), /Im Hosting-Abo enthalten/)
|
||||||
|
assert.equal(fx.finde(e[1], '[data-aktion="einschalten"]').textContent, 'Einschalten')
|
||||||
|
assert.doesNotMatch(fx.text(u.wurzel), /€|Euro/)
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('Einschalten-Tap (eine Website) geht je Abo-Lage an den richtigen Ort', mit(async (fx) => {
|
||||||
|
const faelle = [ // [Abo, Funktion, Sprung ('seite' = Bestätigung ohne Sprung)]
|
||||||
|
[{ produkt: 'wartung' }, 'news', 'seite'],
|
||||||
|
[{ produkt: 'wartung' }, 'termine', ['#termine?projekt=p1']],
|
||||||
|
[{ produkt: 'wartung' }, 'shop', ['#webshop?projekt=p1']],
|
||||||
|
[{ produkt: 'hosting' }, 'analyse', ['#analyse?projekt=p1']],
|
||||||
|
[{ produkt: 'hosting' }, 'zeiten', ['#wechsel']],
|
||||||
|
[{ produkt: null, laeuft: false }, 'zeiten', ['#bestellen-wartung']],
|
||||||
|
[{ produkt: null, laeuft: false }, 'analyse', ['#bestellen-hosting']],
|
||||||
|
]
|
||||||
|
for (const [abo, key, sprung] of faelle) {
|
||||||
|
const u = umgebung(fx, { d: daten({ abo }) })
|
||||||
|
await u.zeige(ENTDECKEN)
|
||||||
|
await fx.klick(fx.finde(u.wurzel, `section[data-fn="${key}"] [data-aktion="einschalten"]`))
|
||||||
|
if (sprung === 'seite') { assert.deepEqual(u.gegangen, []); assert.ok(fx.finde(u.wurzel, `section.kn-fn-einschalten[data-key="${key}"]`)); continue }
|
||||||
|
assert.deepEqual(u.gegangen, [sprung], `${abo.produkt} ${key}`)
|
||||||
|
if (sprung[0] === '#wechsel') assert.deepEqual(JSON.parse(u.gespeichert['kn-wechsel-funktion']), { key, projectId: 'p1' })
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('Einschalten-Tap mit Meldung: gekündigtes Abo zeigt den Text im Eintrag und geht nirgends hin', mit(async (fx) => {
|
||||||
|
const u = umgebung(fx, { d: daten({ abo: { gekuendigt: true } }) })
|
||||||
|
await u.zeige(ENTDECKEN)
|
||||||
|
const news = fx.finde(u.wurzel, 'section[data-fn="news"]')
|
||||||
|
await fx.klick(fx.finde(news, '[data-aktion="einschalten"]'))
|
||||||
|
assert.match(fx.text(fx.finde(news, '[role="alert"]')), /^Dein Abo ist gekündigt\./)
|
||||||
|
assert.deepEqual(u.gegangen, [])
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('Privatkunde: Webshop zeigt den Hinweis statt des Knopfes; ohne Websites steht ein Hinweis statt der Knöpfe', mit(async (fx) => {
|
||||||
|
const u = umgebung(fx, { d: daten({ kundenart: 'privat' }), me: { customer: { kundenartWirksam: 'privat' } } })
|
||||||
|
await u.zeige(ENTDECKEN)
|
||||||
|
const shop = fx.finde(u.wurzel, 'section[data-fn="shop"]')
|
||||||
|
assert.match(fx.text(shop), /Webshop gibt es für Betriebe\. Stimmt das nicht\? Schreib uns im Chat\./)
|
||||||
|
assert.equal(fx.finde(shop, '[data-aktion="einschalten"]'), null)
|
||||||
|
const leer = umgebung(fx, { d: daten({ projekte: [] }) })
|
||||||
|
await leer.zeige(ENTDECKEN)
|
||||||
|
assert.match(fx.text(leer.wurzel), /Sobald deine Website angelegt ist, kannst du Funktionen einschalten\./)
|
||||||
|
assert.equal(fx.alle(leer.wurzel, '[data-aktion="einschalten"]').length, 0)
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('Zwei Websites: Einschalten fragt "Für welche Website?" und bietet nur Websites ohne die Funktion', mit(async (fx) => {
|
||||||
|
const u = umgebung(fx, { d: daten({ projekte: [projekt('p1', 'Bäckerei', { news: feature('active') }), projekt('p2', 'Café'), projekt('p3', 'Kiosk')] }) })
|
||||||
|
await u.zeige(ENTDECKEN)
|
||||||
|
const news = fx.finde(u.wurzel, 'section[data-fn="news"]')
|
||||||
|
await fx.klick(fx.finde(news, '[data-aktion="einschalten"]'))
|
||||||
|
const wahl = fx.finde(news, '[role="group"]')
|
||||||
|
assert.equal(wahl.getAttribute('aria-label'), 'Für welche Website?')
|
||||||
|
assert.deepEqual(fx.alle(wahl, 'button').map((b) => b.textContent), ['Café', 'Kiosk'])
|
||||||
|
await fx.klick(fx.alle(wahl, 'button')[1])
|
||||||
|
assert.ok(fx.finde(u.wurzel, 'section.kn-fn-einschalten[data-key="news"][data-projekt="p3"]'))
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('Zwei Websites: ein zweiter Tipp schließt die Auswahl wieder', mit(async (fx) => {
|
||||||
|
const u = umgebung(fx, { d: daten({ projekte: [projekt('p1', 'Bäckerei'), projekt('p2', 'Café')] }) })
|
||||||
|
await u.zeige(ENTDECKEN)
|
||||||
|
const news = fx.finde(u.wurzel, 'section[data-fn="news"]')
|
||||||
|
await fx.klick(fx.finde(news, '[data-aktion="einschalten"]'))
|
||||||
|
assert.ok(fx.finde(news, '[role="group"]'))
|
||||||
|
await fx.klick(fx.finde(news, '[data-aktion="einschalten"]'))
|
||||||
|
assert.equal(fx.finde(news, '[role="group"]'), null)
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('Laden und Fehler: erst "Wird geladen …", dann Text und "Nochmal versuchen"; Antwort ohne Abo-Modell', mit(async (fx) => {
|
||||||
|
let los; const warten = new Promise((r) => { los = r }); let n = 0
|
||||||
|
const u = umgebung(fx, { api: async (pfad) => { if (pfad === '/api/invoices') return { invoices: [], incomplete: false }; if (++n === 1) { await warten; throw Object.assign(new Error('x'), { status: 500, body: {} }) } return n === 2 ? daten() : { ...daten(), modell: undefined } } })
|
||||||
|
const lauf = u.zeige(ENTDECKEN)
|
||||||
|
assert.match(fx.text(u.wurzel), /Wird geladen …/)
|
||||||
|
los(); await lauf
|
||||||
|
assert.match(fx.text(u.wurzel), /Die Funktionen konnten gerade nicht geladen werden\./)
|
||||||
|
await fx.klick(fx.finde(u.wurzel, '[data-aktion="nochmal"]'))
|
||||||
|
assert.equal(fx.alle(u.wurzel, 'section[data-fn]').length, 6)
|
||||||
|
await u.zeige(ENTDECKEN)
|
||||||
|
assert.match(fx.text(u.wurzel), /Die Funktionen sind gerade nicht verfügbar\. Lade die Seite neu\./)
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('veraltete Antwort überschreibt die neuere nicht', mit(async (fx) => {
|
||||||
|
let langsam; const erster = new Promise((r) => { langsam = r }); let n = 0
|
||||||
|
const mit1 = (name) => daten({ katalogEdit: { news: { name } }, projekte: [projekt('p1', 'B', { news: feature('active') })] })
|
||||||
|
const u = umgebung(fx, { api: async (pfad) => (pfad === '/api/invoices' ? { invoices: [], incomplete: false } : ++n === 1 ? (await erster, mit1('Alt-Pinnwand')) : mit1('Neu-Pinnwand')) })
|
||||||
|
const a = u.zeige(MEINE); const b = u.zeige(MEINE)
|
||||||
|
await b; langsam(); await a
|
||||||
|
assert.match(fx.text(u.wurzel), /Neu-Pinnwand/)
|
||||||
|
assert.doesNotMatch(fx.text(u.wurzel), /Alt-Pinnwand/)
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('Fremdtext und fehlendes Bild: Namen nur als Text, ohne Bild bleibt der Eintrag lesbar', mit(async (fx) => {
|
||||||
|
const u = umgebung(fx, { d: daten({ katalogEdit: { news: { name: '<b>News</b>', beschreibung: '<i>x</i>' } }, projekte: [projekt('p1', '<u>Café</u>')] }) })
|
||||||
|
await u.zeige(ENTDECKEN)
|
||||||
|
assert.equal(fx.alle(u.wurzel, 'b, i, u').length, 0)
|
||||||
|
const news = fx.finde(u.wurzel, 'section[data-fn="news"]')
|
||||||
|
await fx.sende(fx.finde(news, 'img'), 'error')
|
||||||
|
assert.equal(fx.finde(news, 'img'), null)
|
||||||
|
assert.match(fx.text(news), /<b>News<\/b>/)
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('Bildadresse: nur /neu/bilder/<name>.webp; alles andere erscheint ohne Bild', mit(async (fx) => {
|
||||||
|
for (const bild of ['javascript:alert(1)', 'https://example.org/x.webp', '//example.org/x.webp', '/neu/bilder/../x.webp', '', null]) {
|
||||||
|
const u = umgebung(fx, { d: daten({ katalogEdit: { news: { bild } } }) })
|
||||||
|
await u.zeige(ENTDECKEN)
|
||||||
|
assert.equal(fx.finde(fx.finde(u.wurzel, 'section[data-fn="news"]'), 'img'), null, String(bild))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('Meine mit zwei Websites zeigt den Websitenamen in der zweiten Zeile, mit einer den Status', mit(async (fx) => {
|
||||||
|
const einzel = umgebung(fx, { d: daten({ projekte: [projekt('p1', 'Bäckerei', { news: feature('active') })] }) })
|
||||||
|
await einzel.zeige(MEINE)
|
||||||
|
assert.match(fx.text(fx.finde(einzel.wurzel, 'li[data-fn]')), /News-Pinnwand.*Läuft auf deiner Website.*Aktiv/)
|
||||||
|
const zwei = umgebung(fx, { d: daten({ projekte: [projekt('p1', 'Bäckerei', { news: feature('active') }), projekt('p2', 'Café')] }) })
|
||||||
|
await zwei.zeige(MEINE)
|
||||||
|
assert.match(fx.text(fx.finde(zwei.wurzel, 'li[data-fn]')), /News-Pinnwand.*Bäckerei.*Aktiv/)
|
||||||
|
}))
|
||||||
Reference in new issue
Block a user