Terminplaner TP2: Kachel mit Bestellweg, Abo-Felder in der Funktionen-Liste
GET /api/funktionen liefert bei termine.verfuegbar: true zusaetzlich bestellbar
(true | false | null bei Lesefehler im Abo), grund (kein_abo | gekuendigt |
nicht_aktiv | null), preis und einrichtung aus dem Register; nicht verfuegbar
bleibt exakt { verfuegbar: false }. Die Kachel zeigt je Zustand Pille und Knopf:
Aus mit Abo "Terminplaner einrichten · 149 €" bzw. "Fragebogen fortsetzen",
Aus ohne Abo keinen Knopf, sondern den Hinweis zum Abo, Zahlung offen und In
Einrichtung "Status ansehen", Aktiv "Termine oeffnen", Abgeschaltet "Termine
ansehen". Zielprojekt: aktiv, ordered/setup, abgeschaltet, Entwurf, erstes.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8d628ef906
commit
6ae0f67457
5 files changed
+364
-43
No files matched your search
@@ -148,20 +148,93 @@ const termineMit = (mode, pilotEmails = []) => {
|
||||
config.termine.pilotEmails = pilotEmails
|
||||
}
|
||||
after(() => termineMit(MODUS.mode, MODUS.pilotEmails))
|
||||
const verfuegbar = async (c = cookie) => (await get('/api/funktionen', { cookie: c })).termine
|
||||
const termineAntwort = async (c = cookie) => (await get('/api/funktionen', { cookie: c })).termine
|
||||
const verfuegbar = async (c = cookie) => (await termineAntwort(c)).verfuegbar
|
||||
|
||||
test('GET /api/funktionen: termine.verfuegbar je Modus und Pilot-E-Mail', async () => {
|
||||
test('GET /api/funktionen: termine.verfuegbar je Modus und Pilot-E-Mail; false bleibt exakt { verfuegbar: false }', async () => {
|
||||
termineMit('off')
|
||||
assert.deepEqual(await verfuegbar(), { verfuegbar: false })
|
||||
assert.deepEqual(await termineAntwort(), { verfuegbar: false })
|
||||
termineMit('pilot', ['k@example.invalid'])
|
||||
assert.deepEqual(await verfuegbar(), { verfuegbar: true })
|
||||
assert.deepEqual(await verfuegbar(customerCookie('c2')), { verfuegbar: false }, 'Kunde nicht in der Pilotliste')
|
||||
assert.equal(await verfuegbar(), true)
|
||||
assert.deepEqual(await termineAntwort(customerCookie('c2')), { verfuegbar: false }, 'Kunde nicht in der Pilotliste')
|
||||
termineMit('pilot', [])
|
||||
assert.deepEqual(await verfuegbar(), { verfuegbar: false })
|
||||
assert.deepEqual(await termineAntwort(), { verfuegbar: false })
|
||||
termineMit('pilot', ['*'])
|
||||
assert.deepEqual(await verfuegbar(customerCookie('c2')), { verfuegbar: true })
|
||||
assert.equal(await verfuegbar(customerCookie('c2')), true)
|
||||
termineMit('on')
|
||||
assert.deepEqual(await verfuegbar(customerCookie('c2')), { verfuegbar: true })
|
||||
assert.equal(await verfuegbar(customerCookie('c2')), true)
|
||||
})
|
||||
|
||||
// ---- Abo-Felder der Kachel (C10a): bestellbar, grund, preis, einrichtung nur bei verfuegbar: true
|
||||
const wartung = (patch = {}) => aw.seed('subscriptions', subscriptionDoc('sub_h', { ...wartungDoc, ...patch }))
|
||||
const angebot = (bestellbar, grund) => ({
|
||||
verfuegbar: true,
|
||||
bestellbar,
|
||||
grund,
|
||||
preis: config.features.termine.priceLabel,
|
||||
einrichtung: config.features.termine.setupEstimate,
|
||||
})
|
||||
|
||||
test('GET /api/funktionen: Kunde mit Wartungs-Abo => bestellbar true, grund null, Preis und Einrichtung aus dem Register', async (t) => {
|
||||
t.after(() => aw.seed('subscriptions', hostingDoc))
|
||||
termineMit('pilot', ['*'])
|
||||
wartung()
|
||||
const termine = await termineAntwort()
|
||||
assert.deepEqual(termine, angebot(true, null))
|
||||
assert.ok(termine.preis && termine.einrichtung, 'das Register liefert beide Werte')
|
||||
assert.deepEqual(Object.keys(termine), ['verfuegbar', 'bestellbar', 'grund', 'preis', 'einrichtung'])
|
||||
})
|
||||
|
||||
test('GET /api/funktionen: ohne Wartungs-Abo (nur Hosting, gar kein Abo, beendet) => bestellbar false, grund kein_abo', async (t) => {
|
||||
t.after(() => aw.seed('subscriptions', hostingDoc))
|
||||
termineMit('pilot', ['*'])
|
||||
assert.deepEqual(await termineAntwort(), angebot(false, 'kein_abo'), 'c1 hat seit seedFixtures nur Hosting')
|
||||
assert.deepEqual(await termineAntwort(customerCookie('c3')), angebot(false, 'kein_abo'), 'c3 hat gar kein Abo')
|
||||
wartung({ status: 'canceled', endedAt: '2026-09-01T00:00:00.000Z' })
|
||||
assert.deepEqual(await termineAntwort(), angebot(false, 'kein_abo'), 'beendetes Wartungs-Abo')
|
||||
})
|
||||
|
||||
test('GET /api/funktionen: Kuendigung vorgemerkt => grund gekuendigt; past_due und trialing => nicht_aktiv', async (t) => {
|
||||
t.after(() => aw.seed('subscriptions', hostingDoc))
|
||||
termineMit('pilot', ['*'])
|
||||
wartung({ cancelAtPeriodEnd: true })
|
||||
assert.deepEqual(await termineAntwort(), angebot(false, 'gekuendigt'))
|
||||
wartung({ cancelAt: '2026-11-30T00:00:00.000Z' })
|
||||
assert.deepEqual(await termineAntwort(), angebot(false, 'gekuendigt'))
|
||||
wartung({ status: 'past_due' })
|
||||
assert.deepEqual(await termineAntwort(), angebot(false, 'nicht_aktiv'))
|
||||
wartung({ status: 'trialing' })
|
||||
assert.deepEqual(await termineAntwort(), angebot(false, 'nicht_aktiv'))
|
||||
})
|
||||
|
||||
test('GET /api/funktionen: Lesefehler im Abo => bestellbar null, grund null, die Projekte kommen trotzdem; nichts Technisches nach aussen', async (t) => {
|
||||
t.after(() => aw.seed('subscriptions', hostingDoc))
|
||||
const fehler = t.mock.method(console, 'error', () => {})
|
||||
termineMit('pilot', ['*'])
|
||||
wartung()
|
||||
aw.failNext({ collection: 'subscriptions', status: 500, message: 'geheimer Appwrite-Text' })
|
||||
const res = await get('/api/funktionen', { cookie })
|
||||
assert.deepEqual(res.termine, angebot(null, null))
|
||||
for (const id of ['pt1', 'pt2']) assert.ok(res.projects.some((p) => p.projectId === id), `Projekt ${id} ist in der Liste`)
|
||||
assert.equal(featuresOf({ body: res }, 'pt1').termine.state, 'active', 'die Zustaende der Projekte bleiben')
|
||||
assert.ok(!JSON.stringify(res).includes('geheimer'))
|
||||
assert.ok(fehler.mock.calls.every((aufruf) => !String(aufruf.arguments[0]).includes('geheimer')), 'auch nicht im Log')
|
||||
assert.ok(fehler.mock.calls.length >= 1, 'der Fehler steht im Log')
|
||||
// der naechste Aufruf liest wieder, nichts wird gemerkt
|
||||
assert.deepEqual(await termineAntwort(), angebot(true, null))
|
||||
})
|
||||
|
||||
test('GET /api/funktionen: ist der Terminplaner nicht verfuegbar, wird das Abo nicht gelesen', async (t) => {
|
||||
const echt = globalThis.fetch
|
||||
const abfragen = []
|
||||
globalThis.fetch = async (url, optionen) => {
|
||||
if (/\/collections\/subscriptions\/documents/.test(String(url))) abfragen.push(String(url))
|
||||
return echt(url, optionen)
|
||||
}
|
||||
t.after(() => (globalThis.fetch = echt))
|
||||
termineMit('off')
|
||||
assert.deepEqual(await termineAntwort(), { verfuegbar: false })
|
||||
assert.equal(abfragen.length, 0, abfragen.join(' | '))
|
||||
})
|
||||
|
||||
test('GET /api/funktionen: Projekte tragen features.termine und News wie bisher', async () => {
|
||||
|
||||
+137
-11
@@ -4,6 +4,8 @@ import { readFileSync, writeFileSync, mkdtempSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { aboMeldung } from '../server/services/featureOrder.js'
|
||||
import { featureConfig } from '../server/services/featureRegistry.js'
|
||||
|
||||
// public/funktionen.js wirklich laden (die absoluten Importe /analyse-view.js und /geraete.js werden auf die Dateien umgeleitet)
|
||||
const dir = mkdtempSync(join(tmpdir(), 'fn-tiles-'))
|
||||
@@ -144,9 +146,11 @@ test('statisch: News postet weiter consent: true an /api/funktionen/news/checkou
|
||||
})
|
||||
|
||||
// ---- Terminplaner-Kachel: nur mit Schalter (termine.verfuegbar), Zustand je Projekt aus features.termine
|
||||
const withTermine = (id, state) => ({ projectId: id, projectName: `P ${id}`, liveUrl: '', features: { news, termine: { state } } })
|
||||
const withTermine = (id, state, extra = {}) => ({ projectId: id, projectName: `P ${id}`, liveUrl: '', features: { news, termine: { state, ...extra } } })
|
||||
const noAnalyse = { comingSoon: true }
|
||||
async function renderTermine(projects, { verfuegbar, anfragen = {} } = {}) {
|
||||
// Standardantwort bei verfuegbar: true wie GET /api/funktionen (Kunde mit Wartungs-Abo); abo ueberschreibt einzelne Felder
|
||||
const ABO_OK = { bestellbar: true, grund: null, preis: '149 €', einrichtung: 'meist innerhalb von 1–2 Wochen' }
|
||||
async function renderTermine(projects, { verfuegbar, anfragen = {}, abo = {} } = {}) {
|
||||
const calls = []
|
||||
globalThis.fetch = async (url) => {
|
||||
calls.push(String(url))
|
||||
@@ -154,7 +158,8 @@ async function renderTermine(projects, { verfuegbar, anfragen = {} } = {}) {
|
||||
const id = new URL(url, 'http://x').searchParams.get('projectId')
|
||||
return { ok: true, status: 200, json: async () => ({ anfragenOffen: anfragen[id] ?? 0 }) }
|
||||
}
|
||||
return { ok: true, status: 200, json: async () => ({ projects, ...(verfuegbar === undefined ? {} : { termine: { verfuegbar } }) }) }
|
||||
const termine = verfuegbar === undefined ? {} : { termine: verfuegbar ? { verfuegbar, ...ABO_OK, ...abo } : { verfuegbar } }
|
||||
return { ok: true, status: 200, json: async () => ({ projects, ...termine }) }
|
||||
}
|
||||
const count = { textContent: '', hidden: true }
|
||||
const root = { innerHTML: '', querySelector: (sel) => (sel === '[data-termine-count]' ? count : null) }
|
||||
@@ -173,18 +178,106 @@ test('Terminplaner-Kachel: fehlt termine.verfuegbar oder ist false ⇒ „Kommt
|
||||
}
|
||||
})
|
||||
|
||||
test('Terminplaner-Kachel: Aus ⇒ „Wir schalten den Terminplaner für dich frei.“ + Chat', async () => {
|
||||
for (const projects of [[{ ...project('a', noAnalyse), features: { news } }], [withTermine('a', 'off')], [withTermine('a', 'ordered')]]) {
|
||||
const { html } = await renderTermine(projects, { verfuegbar: true })
|
||||
// Die Abo-Hinweise der Kachel kommen aus aboMeldung (server/services/featureOrder.js, einzige Quelle der Abo-Hinweistexte);
|
||||
// die Kachel steht im Browser und kennt den Text nur als Kopie: dieser Test haelt beide gleich.
|
||||
const TERMINE_REGISTER = featureConfig('termine')
|
||||
const KEIN_ABO_HINWEIS = /Im Wartungs-Abo enthalten\. <button type="button" class="kb-link" data-view="vertraege">Zu Verträge<\/button>/
|
||||
const knopfDerKachel = (kachel) => kachel.match(/<button[^>]*fn-tile-cta[^>]*>([^<]*)<\/button>/)
|
||||
|
||||
test('Terminplaner-Kachel, Tabelle: jede Zeile mit Pille, Pillenklasse, Knopftext und Ziel', async () => {
|
||||
const zeilen = [
|
||||
// [Name, Projekte, Abo-Felder, Pillenklasse, Pille, Knopf, data-view]
|
||||
['Aus mit Abo', [withTermine('a', 'off')], {}, 'off', 'Aus', 'Terminplaner einrichten · 149 €', 'termine'],
|
||||
['Aus mit Abo, Entwurf (Dokument und siteKey)', [withTermine('a', 'off', { siteKey: 'abcdefghijklmnop' })], {}, 'off', 'Aus', 'Fragebogen fortsetzen', 'termine'],
|
||||
['Aus, Projekt ohne Funktionsdokument (kein features.termine)', [{ ...project('a', noAnalyse), features: { news } }], {}, 'off', 'Aus', 'Terminplaner einrichten · 149 €', 'termine'],
|
||||
['Zahlung offen', [withTermine('a', 'ordered')], {}, 'setup', 'Zahlung offen', 'Status ansehen', 'termine'],
|
||||
['In Einrichtung', [withTermine('a', 'setup')], {}, 'setup', 'In Einrichtung', 'Status ansehen', 'termine'],
|
||||
['Aktiv', [withTermine('a', 'active')], {}, 'on', 'Aktiv', 'Termine öffnen', 'termine'],
|
||||
['Abgeschaltet', [withTermine('a', 'disabled')], {}, 'off', 'Abgeschaltet', 'Termine ansehen', 'termine'],
|
||||
// das Abo betrifft nur die Bestellung: wer schon bestellt hat oder nutzt, behaelt seinen Weg in die Ansicht
|
||||
['Zahlung offen, Abo gekündigt', [withTermine('a', 'ordered')], { bestellbar: false, grund: 'gekuendigt' }, 'setup', 'Zahlung offen', 'Status ansehen', 'termine'],
|
||||
['In Einrichtung, Abo nicht lesbar', [withTermine('a', 'setup')], { bestellbar: null, grund: null }, 'setup', 'In Einrichtung', 'Status ansehen', 'termine'],
|
||||
['Aktiv, Abo gekündigt', [withTermine('a', 'active')], { bestellbar: false, grund: 'gekuendigt' }, 'on', 'Aktiv', 'Termine öffnen', 'termine'],
|
||||
['Abgeschaltet, Abo nicht aktiv', [withTermine('a', 'disabled')], { bestellbar: false, grund: 'nicht_aktiv' }, 'off', 'Abgeschaltet', 'Termine ansehen', 'termine'],
|
||||
]
|
||||
for (const [name, projects, abo, klasse, pille, knopfText, ziel] of zeilen) {
|
||||
const { html } = await renderTermine(projects, { verfuegbar: true, abo })
|
||||
const kachel = tile(html, 'termine')
|
||||
assert.match(kachel, /Terminplaner/)
|
||||
assert.match(kachel, /fn-pill-off">Aus</)
|
||||
assert.match(kachel, /Wir schalten den Terminplaner für dich frei\./)
|
||||
assert.match(kachel, /data-view="chat"[^>]*>Chat öffnen</)
|
||||
assert.doesNotMatch(kachel, /data-view="termine"/)
|
||||
assert.ok(kachel.includes(`<span class="fn-pill fn-pill-${klasse}">${pille}</span>`), `${name}: Pille ${pille} (${klasse}) in ${kachel}`)
|
||||
const knopf = knopfDerKachel(kachel)
|
||||
assert.ok(knopf, `${name}: Knopf`)
|
||||
assert.equal(knopf[1], knopfText, name)
|
||||
assert.ok(knopf[0].includes(`data-view="${ziel}" data-project="a"`), `${name}: Ziel ${knopf[0]}`)
|
||||
assert.doesNotMatch(kachel, /Wir schalten den Terminplaner für dich frei/, name)
|
||||
assert.doesNotMatch(kachel, /data-view="chat"/, `${name}: kein Chat-Knopf mehr`)
|
||||
}
|
||||
})
|
||||
|
||||
test('Terminplaner-Kachel: Aus ohne Abo ⇒ kein Knopf und kein Weg in die Ansicht, Hinweis je Grund', async () => {
|
||||
const hinweise = [
|
||||
['kein_abo', { bestellbar: false, grund: 'kein_abo' }, KEIN_ABO_HINWEIS],
|
||||
['gekuendigt', { bestellbar: false, grund: 'gekuendigt' }, new RegExp(`${aboMeldung(TERMINE_REGISTER, 'gekuendigt')}`)],
|
||||
['nicht_aktiv', { bestellbar: false, grund: 'nicht_aktiv' }, new RegExp(aboMeldung(TERMINE_REGISTER, 'nicht_aktiv').replace(/[()]/g, '\\$&'))],
|
||||
['null (Lesefehler)', { bestellbar: null, grund: null }, /Das Abo konnte gerade nicht geprüft werden\./],
|
||||
['Feld fehlt (ältere Antwort)', { bestellbar: undefined, grund: undefined }, /Das Abo konnte gerade nicht geprüft werden\./],
|
||||
['unbekannter Grund', { bestellbar: false, grund: 'etwas_neues' }, /Das Abo konnte gerade nicht geprüft werden\./],
|
||||
]
|
||||
// mit und ohne Entwurf: auch ein vorhandener Entwurf oeffnet ohne Abo keinen Weg
|
||||
for (const projekte of [[withTermine('a', 'off')], [withTermine('a', 'off', { siteKey: 'abcdefghijklmnop' })]]) {
|
||||
for (const [name, abo, hinweis] of hinweise) {
|
||||
const { html, calls } = await renderTermine(projekte, { verfuegbar: true, abo })
|
||||
const kachel = tile(html, 'termine')
|
||||
assert.match(kachel, /fn-pill-off">Aus</, name)
|
||||
assert.match(kachel, hinweis, `${name}: ${kachel}`)
|
||||
assert.doesNotMatch(kachel, /data-view="termine"/, name)
|
||||
assert.doesNotMatch(kachel, /fn-tile-cta/, `${name}: kein Knopf`)
|
||||
assert.doesNotMatch(html, /data-view="termine"/, `${name}: nirgends ein Weg in die Ansicht`)
|
||||
assert.deepEqual(calls, ['/api/funktionen'], name)
|
||||
}
|
||||
}
|
||||
// nur kein_abo verweist auf die Vertraege; die uebrigen Hinweise sind reiner Text
|
||||
const gekuendigt = tile((await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { bestellbar: false, grund: 'gekuendigt' } })).html, 'termine')
|
||||
assert.doesNotMatch(gekuendigt, /data-view=/)
|
||||
})
|
||||
|
||||
test('Terminplaner-Kachel: die Abo-Hinweise stimmen wortgleich mit aboMeldung überein (gekündigt, nicht aktiv, nicht lesbar)', async () => {
|
||||
for (const grund of ['gekuendigt', 'nicht_aktiv']) {
|
||||
const { html } = await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { bestellbar: false, grund } })
|
||||
assert.ok(tile(html, 'termine').includes(`<p class="fn-count">${aboMeldung(TERMINE_REGISTER, grund)}</p>`), grund)
|
||||
}
|
||||
const { html } = await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { bestellbar: null, grund: null } })
|
||||
assert.ok(tile(html, 'termine').includes(`<p class="fn-count">${aboMeldung(TERMINE_REGISTER, 'unbekannt')}</p>`))
|
||||
assert.equal(aboMeldung(TERMINE_REGISTER, 'unbekannt'), 'Das Abo konnte gerade nicht geprüft werden.')
|
||||
})
|
||||
|
||||
test('Terminplaner-Kachel: ordered und setup fragen /api/termine nicht nach einer Zahl (nur aktive Websites)', async () => {
|
||||
for (const state of ['ordered', 'setup']) {
|
||||
const { html, calls, count } = await renderTermine([withTermine('a', state)], { verfuegbar: true })
|
||||
assert.deepEqual(calls, ['/api/funktionen'], state)
|
||||
assert.doesNotMatch(tile(html, 'termine'), /data-termine-count/, state)
|
||||
assert.equal(count.textContent, '')
|
||||
}
|
||||
})
|
||||
|
||||
test('Terminplaner-Kachel: Preis kommt aus der Antwort und maskiert; ohne Preis steht der Knopf ohne Zahl', async () => {
|
||||
const mit = await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { preis: '150 €' } })
|
||||
assert.equal(knopfDerKachel(tile(mit.html, 'termine'))[1], 'Terminplaner einrichten · 150 €')
|
||||
const bose = await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { preis: '<img src=x onerror=1>' } })
|
||||
assert.doesNotMatch(bose.html, /<img/)
|
||||
for (const preis of [undefined, '', 149, null]) {
|
||||
const ohne = await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { preis } })
|
||||
assert.equal(knopfDerKachel(tile(ohne.html, 'termine'))[1], 'Terminplaner einrichten', String(preis))
|
||||
}
|
||||
})
|
||||
|
||||
test('Terminplaner-Kachel: ohne Websites kein Absturz und kein Knopf', async () => {
|
||||
const { html } = await renderTermine([], { verfuegbar: true })
|
||||
const kachel = tile(html, 'termine')
|
||||
assert.match(kachel, /fn-pill-off">Aus</)
|
||||
assert.doesNotMatch(kachel, /fn-tile-cta|data-view=/)
|
||||
assert.match(kachel, /Sobald deine Website angelegt ist/)
|
||||
})
|
||||
|
||||
test('Terminplaner-Kachel: Aktiv ⇒ „Termine öffnen“ mit Projekt, Anfragen aus anfragenOffen', async () => {
|
||||
const { html, count, calls } = await renderTermine([withTermine('a', 'off'), withTermine('b', 'active')], { verfuegbar: true, anfragen: { b: 3 } })
|
||||
const kachel = tile(html, 'termine')
|
||||
@@ -234,10 +327,43 @@ test('Terminplaner-Kachel: mehrere Websites, Hinweis „aktiv für n von m“, A
|
||||
assert.match(kachel, /data-project="c"/)
|
||||
})
|
||||
|
||||
test('Terminplaner-Kachel: Zielprojekt = aktiv, dann ordered/setup, dann abgeschaltet, dann Entwurf, dann das erste', async () => {
|
||||
const entwurf = { siteKey: 'abcdefghijklmnop' }
|
||||
const faelle = [
|
||||
['aktiv vor allem', [withTermine('a', 'off', entwurf), withTermine('b', 'disabled'), withTermine('c', 'setup'), withTermine('d', 'ordered'), withTermine('e', 'active')], 'e', 'Aktiv', 'Termine öffnen'],
|
||||
['ordered/setup vor abgeschaltet und Entwurf (setup zuerst in der Liste)', [withTermine('a', 'off', entwurf), withTermine('b', 'disabled'), withTermine('c', 'setup'), withTermine('d', 'ordered')], 'c', 'In Einrichtung', 'Status ansehen'],
|
||||
['ordered/setup vor abgeschaltet und Entwurf (ordered zuerst in der Liste)', [withTermine('a', 'off', entwurf), withTermine('b', 'disabled'), withTermine('c', 'ordered'), withTermine('d', 'setup')], 'c', 'Zahlung offen', 'Status ansehen'],
|
||||
['abgeschaltet vor Entwurf', [withTermine('a', 'off', entwurf), withTermine('b', 'off'), withTermine('c', 'disabled')], 'c', 'Abgeschaltet', 'Termine ansehen'],
|
||||
['Entwurf vor dem ersten', [withTermine('a', 'off'), withTermine('b', 'off'), withTermine('c', 'off', entwurf)], 'c', 'Aus', 'Fragebogen fortsetzen'],
|
||||
['sonst das erste', [withTermine('a', 'off'), withTermine('b', 'off')], 'a', 'Aus', 'Terminplaner einrichten · 149 €'],
|
||||
]
|
||||
for (const [name, projekte, ziel, pille, knopf] of faelle) {
|
||||
const { html } = await renderTermine(projekte, { verfuegbar: true })
|
||||
const kachel = tile(html, 'termine')
|
||||
assert.ok(kachel.includes(`data-view="termine" data-project="${ziel}"`), `${name}: Ziel ${ziel} in ${kachel}`)
|
||||
assert.match(kachel, new RegExp(`<span class="fn-pill fn-pill-[a-z]+">${pille}</span>`), name)
|
||||
assert.equal(knopfDerKachel(kachel)[1], knopf, name)
|
||||
}
|
||||
})
|
||||
|
||||
test('Terminplaner-Kachel: mehrere Websites ohne Abo ⇒ die Pille bleibt „Aus“, nirgends ein Knopf (auch mit Entwurf)', async () => {
|
||||
const { html } = await renderTermine([withTermine('a', 'off'), withTermine('b', 'off', { siteKey: 'abcdefghijklmnop' })], { verfuegbar: true, abo: { bestellbar: false, grund: 'kein_abo' } })
|
||||
const kachel = tile(html, 'termine')
|
||||
assert.match(kachel, /fn-pill-off">Aus</)
|
||||
assert.doesNotMatch(kachel, /fn-tile-cta|data-view="termine"/)
|
||||
assert.match(kachel, KEIN_ABO_HINWEIS)
|
||||
})
|
||||
|
||||
test('Terminplaner-Kachel: Projektname und Zustand kommen nur maskiert in die Kachel', async () => {
|
||||
const { html } = await renderTermine([{ ...withTermine('a<b>', 'active'), projectName: '<img src=x onerror=1>' }], { verfuegbar: true })
|
||||
assert.doesNotMatch(html, /<img/)
|
||||
assert.match(tile(html, 'termine'), /data-project="a<b>"/)
|
||||
// auch im Zustand Aus mit Entwurf und in Einrichtung
|
||||
for (const state of ['off', 'ordered', 'setup', 'disabled']) {
|
||||
const bose = await renderTermine([{ ...withTermine('a<b>', state, { siteKey: 'abcdefghijklmnop' }), projectName: '<img src=x onerror=1>' }], { verfuegbar: true })
|
||||
assert.doesNotMatch(bose.html, /<img/, state)
|
||||
assert.match(tile(bose.html, 'termine'), /data-project="a<b>"/, state)
|
||||
}
|
||||
})
|
||||
|
||||
// ---- Analyse-Einschalten und Ausschalten (ohne DOM: Attrappen, die innerHTML und Klick-Handler festhalten)
|
||||
|
||||
@@ -72,6 +72,8 @@ function portalHilfe(p, json) {
|
||||
function neueFixture() {
|
||||
const fx = {
|
||||
verfuegbar: true,
|
||||
bestellbar: true, // Abo des Kunden (GET /api/funktionen, termine.bestellbar / termine.grund): true | false | null
|
||||
aboGrund: null,
|
||||
projekte: [{ projectId: 'p1', projectName: 'Salon Müller', liveUrl: 'https://salon-mueller.example', features: { news: { state: 'off' }, termine: { state: 'active' } } }],
|
||||
einstellung: srv.einstellung(),
|
||||
vorlagen: [
|
||||
@@ -128,7 +130,12 @@ async function fixtureAntwort(fx, route, req, url, json) {
|
||||
const p = url.pathname
|
||||
const q = url.searchParams
|
||||
const m = req.method()
|
||||
if (p === '/api/funktionen') return json({ projects: fx.projekte, termine: { verfuegbar: fx.verfuegbar } })
|
||||
if (p === '/api/funktionen') {
|
||||
const termine = fx.verfuegbar
|
||||
? { verfuegbar: true, bestellbar: fx.bestellbar, grund: fx.aboGrund, preis: '149 €', einrichtung: 'meist innerhalb von 1–2 Wochen' }
|
||||
: { verfuegbar: false }
|
||||
return json({ projects: fx.projekte, termine })
|
||||
}
|
||||
if (p === '/api/termine/einstellungen' && m === 'GET') {
|
||||
const projekt = q.get('projectId')
|
||||
if (fx.verzoegerung?.[projekt]) await new Promise((fertig) => { setTimeout(fertig, fx.verzoegerung[projekt]) })
|
||||
@@ -298,15 +305,67 @@ async function pruefungen() {
|
||||
|
||||
await check('fx_kachel_aus_aktiv_abgeschaltet', async (page) => {
|
||||
const fx = page.fx
|
||||
// Aus
|
||||
// Aus mit Abo: Weg in die Bestellung
|
||||
fx.projekte[0].features.termine = { state: 'off' }
|
||||
await oeffne(page, '#funktionen')
|
||||
let kachel = page.locator('[data-fn-tile="termine"]')
|
||||
await kachel.waitFor()
|
||||
let t = await textVon(kachel)
|
||||
assert(t.includes('Aus') && t.includes('Wir schalten den Terminplaner für dich frei.'), `Aus: ${t}`)
|
||||
await knopf(kachel, 'Chat öffnen').waitFor()
|
||||
eq(await kachel.locator('[data-view="termine"]').count(), 0, 'Aus: kein Weg in die Ansicht')
|
||||
assert(t.includes('Aus') && t.includes('Terminplaner einrichten · 149 €'), `Aus: ${t}`)
|
||||
assert(!t.includes('Wir schalten den Terminplaner für dich frei.'), 'der alte Text ist weg')
|
||||
await knopf(kachel, 'Terminplaner einrichten · 149 €').waitFor()
|
||||
eq(await kachel.locator('[data-view="termine"]').getAttribute('data-project'), 'p1', 'Aus: Knopf fuehrt in die Ansicht')
|
||||
eq(await kachel.locator('[data-view="chat"]').count(), 0, 'Aus: kein Chat-Knopf')
|
||||
await shot(page, 'kachel-aus')
|
||||
// Entwurf vorhanden (Funktionsdokument mit siteKey)
|
||||
fx.projekte[0].features.termine = { state: 'off', siteKey: 'abcdefghijklmnop' }
|
||||
await page.reload()
|
||||
kachel = page.locator('[data-fn-tile="termine"]')
|
||||
await knopf(kachel, 'Fragebogen fortsetzen').waitFor()
|
||||
// Aus ohne Abo: kein Knopf, Hinweis; die Hinweise der uebrigen Gruende
|
||||
const faelle = [
|
||||
['kein_abo', 'Im Wartungs-Abo enthalten.'],
|
||||
['gekuendigt', 'Dein Wartungs-Abo ist gekündigt.'],
|
||||
['nicht_aktiv', 'Dein Wartungs-Abo ist gerade nicht aktiv'],
|
||||
]
|
||||
for (const [grund, text] of faelle) {
|
||||
fx.bestellbar = false
|
||||
fx.aboGrund = grund
|
||||
await page.reload()
|
||||
kachel = page.locator('[data-fn-tile="termine"]')
|
||||
await kachel.waitFor()
|
||||
t = await textVon(kachel)
|
||||
assert(t.includes('Aus') && t.includes(text), `Aus ohne Abo (${grund}): ${t}`)
|
||||
eq(await kachel.locator('.fn-tile-cta').count(), 0, `${grund}: kein Knopf`)
|
||||
eq(await kachel.locator('[data-view="termine"]').count(), 0, `${grund}: kein Weg in die Ansicht`)
|
||||
}
|
||||
eq(await kachel.locator('[data-view="vertraege"]').count(), 0, 'nur kein_abo verweist auf die Vertraege')
|
||||
fx.aboGrund = 'kein_abo'
|
||||
await page.reload()
|
||||
kachel = page.locator('[data-fn-tile="termine"]')
|
||||
await knopf(kachel, 'Zu Verträge').waitFor()
|
||||
await shot(page, 'kachel-ohne-abo')
|
||||
fx.bestellbar = null
|
||||
fx.aboGrund = null
|
||||
await page.reload()
|
||||
kachel = page.locator('[data-fn-tile="termine"]')
|
||||
await kachel.getByText('Das Abo konnte gerade nicht geprüft werden.').waitFor()
|
||||
eq(await kachel.locator('.fn-tile-cta').count(), 0, 'nicht lesbar: kein Knopf')
|
||||
// bestellt: Zahlung offen und in Einrichtung (das Abo spielt dann keine Rolle mehr)
|
||||
fx.bestellbar = false
|
||||
fx.aboGrund = 'gekuendigt'
|
||||
for (const [state, pille, text] of [['ordered', 'Zahlung offen', 'Status ansehen'], ['setup', 'In Einrichtung', 'Status ansehen']]) {
|
||||
fx.projekte[0].features.termine = { state }
|
||||
await page.reload()
|
||||
kachel = page.locator('[data-fn-tile="termine"]')
|
||||
await kachel.waitFor()
|
||||
t = await textVon(kachel)
|
||||
assert(t.includes(pille) && t.includes(text), `${state}: ${t}`)
|
||||
eq(await kachel.locator('[data-view="termine"]').getAttribute('data-project'), 'p1', `${state}: Knopf fuehrt in die Ansicht`)
|
||||
}
|
||||
await shot(page, 'kachel-in-einrichtung')
|
||||
fx.bestellbar = true
|
||||
fx.aboGrund = null
|
||||
// Aktiv mit zwei Anfragen
|
||||
fx.projekte[0].features.termine = { state: 'active' }
|
||||
fx.buchungen = alleStatus().map((b) => (b.id === 'bkunft' ? { ...b, status: 'angefragt' } : b))
|
||||
|
||||
Reference in new issue
Block a user