Terminplaner TP2 C10a: Nachbesserung 1 (Abo-Hinweis vom Server, R111)
GET /api/funktionen liefert bei termine.verfuegbar ein fuenftes Feld hinweis (aboMeldung mit dem Register des Terminplaners, null bei bestellbar true). Die Kachel zeigt diesen Text maskiert an; die Browser-Kopie der Abo-Texte und der Gleichheitstest entfallen. Der Link "Zu Verträge" ist eine gemeinsame Konstante fuer Analyse und Terminplaner und erscheint bei kein_abo. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6ae0f67457
commit
52456dac76
5 files changed
+141
-63
No files matched your search
@@ -4,8 +4,6 @@ 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-'))
|
||||
@@ -149,7 +147,7 @@ test('statisch: News postet weiter consent: true an /api/funktionen/news/checkou
|
||||
const withTermine = (id, state, extra = {}) => ({ projectId: id, projectName: `P ${id}`, liveUrl: '', features: { news, termine: { state, ...extra } } })
|
||||
const noAnalyse = { comingSoon: true }
|
||||
// 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' }
|
||||
const ABO_OK = { bestellbar: true, grund: null, preis: '149 €', einrichtung: 'meist innerhalb von 1–2 Wochen', hinweis: null }
|
||||
async function renderTermine(projects, { verfuegbar, anfragen = {}, abo = {} } = {}) {
|
||||
const calls = []
|
||||
globalThis.fetch = async (url) => {
|
||||
@@ -178,10 +176,10 @@ test('Terminplaner-Kachel: fehlt termine.verfuegbar oder ist false ⇒ „Kommt
|
||||
}
|
||||
})
|
||||
|
||||
// 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>/
|
||||
// Die Abo-Hinweise der Kachel kommen vom Server (termine.hinweis = aboMeldung in server/services/featureOrder.js, einzige Quelle der
|
||||
// Abo-Hinweistexte, R111); der Browser hat keine Kopie und zeigt den Text nur maskiert an. Die Tests nehmen eigene Texte, damit
|
||||
// eine Kopie im Browser auffaellt. Der Link zu den Vertraegen steht nur bei kein_abo.
|
||||
const ZU_VERTRAEGE = '<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 () => {
|
||||
@@ -195,10 +193,10 @@ test('Terminplaner-Kachel, Tabelle: jede Zeile mit Pille, Pillenklasse, Knopftex
|
||||
['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'],
|
||||
['Zahlung offen, Abo gekündigt', [withTermine('a', 'ordered')], { bestellbar: false, grund: 'gekuendigt', hinweis: 'Serverhinweis gekündigt.' }, 'setup', 'Zahlung offen', 'Status ansehen', 'termine'],
|
||||
['In Einrichtung, Abo nicht lesbar', [withTermine('a', 'setup')], { bestellbar: null, grund: null, hinweis: 'Serverhinweis nicht prüfbar.' }, 'setup', 'In Einrichtung', 'Status ansehen', 'termine'],
|
||||
['Aktiv, Abo gekündigt', [withTermine('a', 'active')], { bestellbar: false, grund: 'gekuendigt', hinweis: 'Serverhinweis gekündigt.' }, 'on', 'Aktiv', 'Termine öffnen', 'termine'],
|
||||
['Abgeschaltet, Abo nicht aktiv', [withTermine('a', 'disabled')], { bestellbar: false, grund: 'nicht_aktiv', hinweis: 'Serverhinweis 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 })
|
||||
@@ -210,44 +208,75 @@ test('Terminplaner-Kachel, Tabelle: jede Zeile mit Pille, Pillenklasse, Knopftex
|
||||
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`)
|
||||
assert.doesNotMatch(kachel, /Serverhinweis/, `${name}: der Abo-Hinweis gilt nur im Zustand Aus`)
|
||||
}
|
||||
})
|
||||
|
||||
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\./],
|
||||
test('Terminplaner-Kachel: Aus ohne Abo => kein Knopf und kein Weg in die Ansicht, der Hinweis kommt vom Server (Link zu Verträge nur bei kein_abo)', async () => {
|
||||
const faelle = [
|
||||
['kein_abo', { bestellbar: false, grund: 'kein_abo', hinweis: 'Serverhinweis ohne Abo.' }, `Serverhinweis ohne Abo. ${ZU_VERTRAEGE}`],
|
||||
['gekuendigt', { bestellbar: false, grund: 'gekuendigt', hinweis: 'Serverhinweis gekündigt & beendet.' }, 'Serverhinweis gekündigt & beendet.'],
|
||||
['nicht_aktiv', { bestellbar: false, grund: 'nicht_aktiv', hinweis: 'Serverhinweis „nicht aktiv“ (offen).' }, 'Serverhinweis „nicht aktiv“ (offen).'],
|
||||
['unbekannt (Lesefehler: bestellbar null)', { bestellbar: null, grund: null, hinweis: 'Serverhinweis nicht prüfbar.' }, 'Serverhinweis nicht prüfbar.'],
|
||||
['unbekannter Grund', { bestellbar: false, grund: 'etwas_neues', hinweis: 'Serverhinweis zu etwas Neuem.' }, 'Serverhinweis zu etwas Neuem.'],
|
||||
]
|
||||
// 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) {
|
||||
for (const [name, abo, erwartet] of faelle) {
|
||||
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.ok(kachel.includes(`<p class="fn-count">${erwartet}</p>`), `${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
|
||||
assert.equal(kachel.split('data-view=').length - 1, abo.grund === 'kein_abo' ? 1 : 0, `${name}: Links`)
|
||||
}
|
||||
}
|
||||
// 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)
|
||||
test('Terminplaner-Kachel: der Hinweis des Servers kommt maskiert in die Kachel (auch bei kein_abo neben dem Link)', async () => {
|
||||
const bose = '<img src=x onerror=1>"\'&'
|
||||
const maskiert = '<img src=x onerror=1>"'&'
|
||||
for (const grund of ['gekuendigt', 'nicht_aktiv', 'kein_abo']) {
|
||||
const { html } = await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { bestellbar: false, grund, hinweis: bose } })
|
||||
const kachel = tile(html, 'termine')
|
||||
assert.doesNotMatch(html, /<img/, grund)
|
||||
assert.ok(kachel.includes(`<p class="fn-count">${maskiert}${grund === 'kein_abo' ? ` ${ZU_VERTRAEGE}` : ''}</p>`), `${grund}: ${kachel}`)
|
||||
}
|
||||
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: fehlt der Hinweis (leer, null, kein Text) gibt es keine Zeile und keinen Knopf; kein_abo behält den Link', async () => {
|
||||
for (const hinweis of [undefined, null, '', 5, { text: 'x' }]) {
|
||||
const ohne = tile((await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { bestellbar: false, grund: 'gekuendigt', hinweis } })).html, 'termine')
|
||||
assert.match(ohne, /fn-pill-off">Aus</)
|
||||
assert.doesNotMatch(ohne, /fn-tile-cta|data-view=|fn-count|\[object/, String(hinweis))
|
||||
const keinAbo = tile((await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { bestellbar: false, grund: 'kein_abo', hinweis } })).html, 'termine')
|
||||
assert.ok(keinAbo.includes(`<p class="fn-count">${ZU_VERTRAEGE}</p>`), String(hinweis))
|
||||
assert.doesNotMatch(keinAbo, /fn-tile-cta|data-view="termine"/)
|
||||
}
|
||||
})
|
||||
|
||||
test('Terminplaner-Kachel: mit Abo (bestellbar true) steht der Knopf da und ein Hinweis erscheint nicht', async () => {
|
||||
const kachel = tile((await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { hinweis: 'Serverhinweis darf nicht erscheinen.' } })).html, 'termine')
|
||||
assert.equal(knopfDerKachel(kachel)[1], 'Terminplaner einrichten · 149 €')
|
||||
assert.doesNotMatch(kachel, /Serverhinweis|fn-count/)
|
||||
})
|
||||
|
||||
test('Der Browser hat keine Kopie der Abo-Hinweise; der Link zu Verträge steht einmal im Quelltext (Analyse und Terminplaner teilen ihn)', async () => {
|
||||
assert.doesNotMatch(src, /TERMINE_ABO_TEXTE|termineAboHinweis|KEIN_ABO_HINWEIS|ist gekündigt\. Für|gerade nicht aktiv|nicht geprüft werden/)
|
||||
assert.equal(src.split('data-view="vertraege"').length - 1, 1)
|
||||
// Analyse-Kachel und -Seite (unveraendert) und Terminplaner-Kachel zeigen genau diesen Link
|
||||
const analyse = tile(await render([project('a', { eligible: false, reason: 'kein_abo', reasonText: 'Kein Abo' })]), 'analyse')
|
||||
assert.ok(analyse.includes(`<p class="fn-count">Im Wartungs-Abo enthalten. ${ZU_VERTRAEGE}</p>`), analyse)
|
||||
const andere = tile(await render([project('a', { eligible: false, reason: 'gekuendigt', reasonText: 'Gekündigt' })]), 'analyse')
|
||||
assert.ok(andere.includes('<p class="fn-count">Gekündigt</p>') && !andere.includes('data-view='), andere)
|
||||
const stage = await renderActivation({ state: undefined, eligible: false, reason: 'kein_abo', reasonText: 'Kein Abo' })
|
||||
assert.ok(stage.innerHTML.includes(`<p>Kein Abo ${ZU_VERTRAEGE}</p>`), stage.innerHTML)
|
||||
const termine = tile((await renderTermine([withTermine('a', 'off')], { verfuegbar: true, abo: { bestellbar: false, grund: 'kein_abo', hinweis: 'T' } })).html, 'termine')
|
||||
assert.ok(termine.includes(ZU_VERTRAEGE), termine)
|
||||
})
|
||||
|
||||
test('Terminplaner-Kachel: ordered und setup fragen /api/termine nicht nach einer Zahl (nur aktive Websites)', async () => {
|
||||
@@ -347,11 +376,11 @@ test('Terminplaner-Kachel: Zielprojekt = aktiv, dann ordered/setup, dann abgesch
|
||||
})
|
||||
|
||||
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 { html } = await renderTermine([withTermine('a', 'off'), withTermine('b', 'off', { siteKey: 'abcdefghijklmnop' })], { verfuegbar: true, abo: { bestellbar: false, grund: 'kein_abo', hinweis: 'Serverhinweis ohne 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)
|
||||
assert.ok(kachel.includes(`<p class="fn-count">Serverhinweis ohne Abo. ${ZU_VERTRAEGE}</p>`), kachel)
|
||||
})
|
||||
|
||||
test('Terminplaner-Kachel: Projektname und Zustand kommen nur maskiert in die Kachel', async () => {
|
||||
|
||||
Reference in new issue
Block a user