Files
Webklar-Kundenbereich/test/zeitenRegister.test.js
T
ClaudeandClaude Opus 5.5 3e66dfa91c Tests nach dem Zusammenführen mit main: Reihenfolge der Funktionsschlüssel und Importe von funktionen.js
Die Öffnungszeiten stehen jetzt hinter dem Webshop (news, termine, shop, zeiten): Register, FEATURE_KEYS und
ticketFreischaltungKeys in zeitenRegister, die drei Listen in app.js (VIEW_HASHES, APP_PARENT, FEATURE_LABELS)
in zeitenBestellen und shopWiring (APP_PARENT endet mit webshop, zeiten). funktionenZeitenTile prüft die Importe
von funktionen.js gegen den Stand von main (fn-util.js und analyse-view.js); die Kachel der Öffnungszeiten
bringt weiterhin keinen neuen Import mit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:15:58 +00:00

213 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, after } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { useTestEnv } from './support/testEnv.js'
// Register-Eintrag config.features.zeiten ("Öffnungszeiten live"), freiweg() und der Stripe-Katalog zeitenSetup.
// Dienste (siteFeatures, stripeBilling) brauchen die Test-Umgebung vor dem ersten Import von server/config.js.
const aw = await startAppwriteMock()
const st = await startFakeStripe()
after(() => Promise.all([aw.close(), st.close()]))
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const reg = await import('../server/services/featureRegistry.js')
const sf = await import('../server/services/siteFeatures.js')
const { ensurePrice } = await import('../server/services/stripeBilling.js')
const feature = () => reg.featureConfig('zeiten')
test('zeiten steht im Register mit 30 € und Abo-Inklusive', () => {
const f = feature()
assert.equal(f, config.features.zeiten)
assert.equal(f.key, 'zeiten')
assert.equal(f.product, 'zeitenSetup')
assert.deepEqual(f.label, { name: 'Öffnungszeiten live', kurz: 'Zeiten', keine: 'keine Öffnungszeiten live' })
assert.equal(f.priceLabel, '30 €')
assert.equal(f.setupEstimate, 'meist innerhalb weniger Tage')
assert.equal(f.requiresWartungsAbo, false)
assert.equal(f.aboInklusive, true)
assert.equal(f.ticketFreischaltung, true)
assert.deepEqual(f.teilweiseLive, ['setup', 'active'])
assert.equal(f.returnHash, 'zeiten')
assert.equal(f.returnProjekt, true)
assert.deepEqual(f.leistungsbeschreibung, { version: '2026-10-04', url: '/vertragsunterlagen/zeiten-leistungsbeschreibung.html' })
assert.equal(f.avv, null)
assert.equal(f.widerruf, null)
assert.equal(f.disabledMessage, 'Öffnungszeiten live wurde für diese Website abgeschaltet. Schreib uns im Chat, wenn du die Funktion wieder möchtest.')
assert.equal(f.orderedConflictMessage, 'Öffnungszeiten live ist für dieses Projekt schon bestellt oder eingeschaltet.')
assert.equal(f.unavailableMessage, 'Öffnungszeiten live ist noch nicht verfügbar.')
// Funktionsschlüssel und Katalog stehen in den abgeleiteten Listen
assert.deepEqual(reg.featureKeys(), ['news', 'termine', 'shop', 'zeiten'])
assert.deepEqual(sf.FEATURE_KEYS, ['news', 'termine', 'shop', 'zeiten'])
assert.deepEqual(sf.FEATURE_LABELS.zeiten, { name: 'Öffnungszeiten live', keine: 'keine Öffnungszeiten live' })
assert.deepEqual(reg.ticketFreischaltungKeys(), ['news', 'termine', 'shop', 'zeiten'])
})
test('Zustimmung bezahlt: zwei Haken, Knopf „Zahlungspflichtig bestellen · 30 €“, § 356 Abs. 5 Nr. 2', () => {
const { consent } = feature()
assert.equal(consent.version, '2026-10-05', 'Fassung nach dem Wertersatz-Satz (§ 357a Abs. 2 BGB)')
assert.equal(consent.knopf, 'Zahlungspflichtig bestellen · 30 €')
assert.deepEqual(consent.items.map((i) => [i.id, i.haken]), [['leistung', true], ['beginn', true]])
assert.equal(
consent.items[0].text,
'Ich habe die Leistungsbeschreibung „Öffnungszeiten live“ gelesen und bestelle die Einrichtung.'
)
assert.equal(
consent.items[1].text,
'Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit vollständiger Erbringung der Leistung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).'
)
assert.match(consent.items[1].text, /§ 356 Abs\. 5 Nr\. 2/)
})
test('Zustimmung bezahlt: der Wertersatz-Satz ist wörtlich der von News und Terminplaner (einheitliche Fassungen; Wortlaut bleibt Sache der Rechtsrunde, ZQ3)', () => {
const satz = (text) => text.match(/Widerrufe ich vor der vollständigen Erbringung[^.]*\.[^.]*\)\./)?.[0]
const zeiten = satz(config.features.zeiten.consent.items.find((i) => i.id === 'beginn').text)
assert.ok(zeiten, 'Wertersatz-Satz vorhanden')
for (const key of ['news', 'termine']) {
assert.equal(satz(config.features[key].consent.items.find((i) => i.id === 'beginn').text), zeiten, `${key} hat denselben Satz`)
}
})
test('Zustimmung gratis: ein Haken, Knopf „Einschalten“, kein „zahlungspflichtig“', () => {
const f = feature()
assert.equal(f.consentFrei.version, '2026-10-04')
assert.equal(f.consentFrei.knopf, 'Einschalten')
assert.deepEqual(f.consentFrei.items, [
{
id: 'leistung',
haken: true,
text: 'Ich habe die Leistungsbeschreibung „Öffnungszeiten live“ gelesen und schalte die Funktion ein. Sie ist in meinem Wartungs-Abo ohne Aufpreis enthalten.',
},
])
assert.doesNotMatch(JSON.stringify(f.consentFrei), /zahlungspflichtig/i)
})
test('freiweg ersetzt nur consent', () => {
const f = feature()
const frei = reg.freiweg(f)
assert.equal(frei.consent, f.consentFrei)
assert.notEqual(frei, f, 'eine Kopie, das Register bleibt unberührt')
assert.equal(f.consent.knopf, 'Zahlungspflichtig bestellen · 30 €')
assert.equal(frei.product, f.product)
assert.equal(frei.key, f.key)
assert.equal(frei.mails, f.mails)
assert.deepEqual(Object.keys(frei).sort(), Object.keys(f).sort())
for (const key of Object.keys(f)) if (key !== 'consent') assert.equal(frei[key], f[key], key)
})
test('verfuegbar folgt ZEITEN_ENABLED', () => {
const f = feature()
const vorher = { ...config.zeiten }
try {
config.zeiten.mode = 'off'
assert.equal(reg.featureSperre('zeiten', 'a@b.de'), f.unavailableMessage)
config.zeiten.mode = 'on'
assert.equal(reg.featureSperre('zeiten', 'a@b.de'), null)
config.zeiten.mode = 'pilot'
config.zeiten.pilotEmails = ['pilot@b.de']
assert.equal(reg.featureSperre('zeiten', 'a@b.de'), f.unavailableMessage)
assert.equal(reg.featureSperre('zeiten', 'pilot@b.de'), null)
} finally {
Object.assign(config.zeiten, vorher)
}
})
test('Standard-Haken: verfuegbar, unterlagenBereit, belehrung, freischaltbar, nachFreischaltung (keine Bestellprüfung)', async () => {
const haken = reg.featureHaken('zeiten')
assert.deepEqual(Object.keys(haken).sort(), ['belehrung', 'freischaltbar', 'nachFreischaltung', 'unterlagenBereit', 'verfuegbar'])
const termine = reg.featureHaken('termine')
assert.equal(await haken.unterlagenBereit(), true)
assert.equal(await haken.belehrung(), await termine.belehrung(), 'wie beim Terminplaner')
// kein Platzhalter mehr (der alte Haken lieferte immer true): Verhalten und Nacharbeit prüft test/zeitenFreischalten.test.js
for (const name of ['freischaltbar', 'nachFreischaltung']) assert.equal(typeof haken[name], 'function', name)
})
test('oeffentlicheAnsicht(zeiten) hat dieselben Schlüssel wie news', () => {
const zeiten = reg.oeffentlicheAnsicht('zeiten')
const news = reg.oeffentlicheAnsicht('news')
assert.deepEqual(Object.keys(zeiten).sort(), Object.keys(news).sort())
assert.deepEqual(Object.keys(zeiten.consent).sort(), Object.keys(news.consent).sort())
assert.equal(zeiten.aboInklusive, undefined)
assert.equal(zeiten.consentFrei, undefined)
assert.equal(zeiten.teilweiseLive, undefined)
assert.doesNotMatch(JSON.stringify(zeiten), /aboInklusive|consentFrei|Einschalten/)
assert.deepEqual(JSON.parse(JSON.stringify(zeiten)), zeiten)
assert.equal(zeiten.requiresWartungsAbo, false)
assert.equal(zeiten.name, 'Öffnungszeiten live')
assert.equal(zeiten.priceLabel, '30 €')
})
test('Mails: Werte aus dem Register, Preis aus zeitenSetup', () => {
const f = feature()
assert.deepEqual(f.mails, {
funktion: 'Öffnungszeiten live',
formen: { dein: 'Deine Öffnungszeiten live', nom: 'Öffnungszeiten live', akk: 'Öffnungszeiten live', gen: 'von Öffnungszeiten live', pron: 'sie' },
knopf: 'Zeiten bearbeiten',
danach: 'Danach kannst Du Deine Zeiten im Portal eintragen.',
bereit: 'Deine Öffnungszeiten stehen ab sofort live auf Deiner Seite.',
preisHinweis: 'einmalig, ohne Umsatzsteuer nach § 19 UStG; die Nutzung ist kostenlos, es läuft kein Abo',
anleitung: false,
})
const k15 = sf.bestellMail(f, { kundeName: 'Kunde Eins', projekt: 'Salon', invoiceUrl: '' })
assert.equal(k15.values.preis, '30,00 €')
assert.equal(k15.values.portal_url, `${config.publicBaseUrl}/dashboard.html#zeiten`)
})
test('Ticket: Betreff mit Projektname, Details beginnen mit den Notfall-Details (Einbau-Text: test/zeitenTicket.test.js)', async () => {
const f = feature()
const project = { projectName: 'Salon Haarlinie', subdomain: 'salon' }
assert.equal(f.ticket.topic(project), 'Öffnungszeiten live einbauen – Salon Haarlinie')
assert.equal(f.ticket.topic({ subdomain: 'salon' }), 'Öffnungszeiten live einbauen – salon')
const ctx = { feature: f, doc: { customerId: 'c1', projectId: 'p1', siteKey: 'abcdefghijklmnop' }, project, customer: { name: 'Kunde Eins' } }
const details = await f.ticket.details(ctx)
assert.ok(details.startsWith(reg.minimaleTicketDetails(ctx)))
assert.match(details, /^Funktion: zeiten$/m)
assert.match(details, /data-wk-zeiten src="https:\/\/project\.webklar\.com\/embed\/zeiten\.js" data-site="abcdefghijklmnop"/)
const texte = await reg.ticketTexte(ctx)
assert.equal(texte.topic, 'Öffnungszeiten live einbauen – Salon Haarlinie')
assert.equal(texte.details, details)
})
test('Stripe-Katalog: zeitenSetup mit eigener Metadaten-Marke', async () => {
assert.deepEqual(config.zeitenSetup, {
key: 'zeitenSetup',
name: 'Öffnungszeiten live – Einrichtung',
amountCents: 3000,
lookupKey: 'webklar_zeiten_einrichtung_3000',
oneTime: true,
priceNickname: 'Öffnungszeiten-Einrichtung 30 €',
})
const before = st.calls.length
const id = await ensurePrice('zeitenSetup')
const calls = st.calls.slice(before)
const preise = calls.filter((c) => c.method === 'POST' && c.path === '/v1/prices')
assert.equal(preise.length, 1)
assert.equal(preise[0].body['unit_amount'], '3000')
assert.equal(preise[0].body['lookup_key'], 'webklar_zeiten_einrichtung_3000')
assert.equal(preise[0].body['nickname'], 'Öffnungszeiten-Einrichtung 30 €')
assert.ok(!Object.keys(preise[0].body).some((k) => k.startsWith('recurring')), 'kein recurring')
assert.equal(st.prices.get(id).recurring, null)
const produkte = calls.filter((c) => c.method === 'POST' && c.path === '/v1/products')
assert.equal(produkte.length, 1)
assert.equal(produkte[0].body['name'], 'Öffnungszeiten live – Einrichtung')
assert.equal(produkte[0].body['metadata[webklar]'], 'zeiten-einrichtung')
// zweiter Aufruf: kein zweiter Preis
const again = st.calls.length
assert.equal(await ensurePrice('zeitenSetup'), id)
assert.equal(st.calls.slice(again).filter((c) => c.method === 'POST').length, 0)
})
test('Skript stripe-products kennt zeitenSetup', async () => {
const realExit = process.exit
process.exit = () => {}
let script
try {
script = await import('../scripts/stripe-products.mjs')
} finally {
process.exit = realExit
}
assert.ok(script.KEYS.includes('zeitenSetup'))
assert.equal(script.catalogEntry('zeitenSetup'), config.zeitenSetup)
})