Merge kn/meine-pilot (Pilot-Feedback 17.10.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
d7724afc02
8 files changed
+630
-41
No files matched your search
@@ -31,12 +31,23 @@ function umgebung(fx, { d = daten(), abo, api, post = { feature: { state: 'setup
|
||||
}
|
||||
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') })] }) })
|
||||
test('Meine: Liste mit Name, Detailzeile und Pfeil, Pille nur ohne aktiven Zustand; 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', { kurzinfo: '3 Termine heute · nächster 10:30' }), 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/)
|
||||
assert.deepEqual(zeilen.map((z) => [fx.finde(z, '.kn-fn-name').textContent, fx.finde(z, '.kn-fn-zeile2').textContent]), [
|
||||
['Terminplaner', 'Bäckerei · 3 Termine heute · nächster 10:30'],
|
||||
['Terminplaner', 'Café · Aktiv'],
|
||||
['Webshop', 'Bäckerei · Jetzt einschalten'],
|
||||
])
|
||||
assert.deepEqual(zeilen.map((z) => fx.finde(z, '.kn-fn-pille')?.textContent ?? null), [null, null, 'Angenommen'])
|
||||
assert.doesNotMatch(fx.text(u.wurzel), /Aktiv seit/)
|
||||
for (const z of zeilen) {
|
||||
const pfeil = fx.finde(z, '.kn-fn-pfeil')
|
||||
assert.deepEqual([pfeil.textContent, pfeil.getAttribute('aria-hidden')], ['›', 'true'])
|
||||
assert.equal(fx.finde(z, 'button').lastElementChild.lastElementChild, pfeil, 'der Pfeil steht ganz rechts')
|
||||
}
|
||||
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']])
|
||||
@@ -191,13 +202,24 @@ test('Bildadresse: nur /neu/bilder/<name>.webp; alles andere erscheint ohne 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') })] }) })
|
||||
test('Meine: bei einer Website steht die Detailzeile allein, bei mehreren der Websitename davor; ohne Kurzinfo "Aktiv"', mit(async (fx) => {
|
||||
const einzel = umgebung(fx, { d: daten({ projekte: [projekt('p1', 'Bäckerei', { news: feature('active', { kurzinfo: '4 Beiträge sichtbar' }), zeiten: feature('active', { kurzinfo: null }) })] }) })
|
||||
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é')] }) })
|
||||
assert.deepEqual(fx.alle(einzel.wurzel, 'li[data-fn] .kn-fn-zeile2').map((e) => e.textContent), ['4 Beiträge sichtbar', 'Aktiv'])
|
||||
const zwei = umgebung(fx, { d: daten({ projekte: [projekt('p1', 'Bäckerei', { news: feature('active', { kurzinfo: '4 Beiträge sichtbar' }) }), projekt('p2', 'Café')] }) })
|
||||
await zwei.zeige(MEINE)
|
||||
assert.match(fx.text(fx.finde(zwei.wurzel, 'li[data-fn]')), /News-Pinnwand.*Bäckerei.*Aktiv/)
|
||||
assert.deepEqual(fx.alle(zwei.wurzel, 'li[data-fn] .kn-fn-zeile2').map((e) => e.textContent), ['Bäckerei · 4 Beiträge sichtbar'])
|
||||
}))
|
||||
|
||||
test('Meine: Pille und Pfeil liegen rechts nebeneinander (Pille zuerst); Namen und Kurzinfo werden nur als Text gesetzt', mit(async (fx) => {
|
||||
const u = umgebung(fx, { d: daten({ projekte: [projekt('p1', '<u>Café</u>', { news: feature('active', { kurzinfo: '<b>4</b> Beiträge' }), termine: feature('setup', { ticketWoid: '100301' }) })] }) })
|
||||
await u.zeige(MEINE)
|
||||
assert.equal(fx.alle(u.wurzel, 'b, u').length, 0)
|
||||
const [news, termine] = fx.alle(u.wurzel, 'li[data-fn]')
|
||||
assert.equal(fx.finde(news, '.kn-fn-zeile2').textContent, '<b>4</b> Beiträge')
|
||||
const rechts = fx.finde(termine, '.kn-fn-rechts')
|
||||
assert.deepEqual([...rechts.children].map((k) => k.className), ['kn-fn-pille kn-fn-pille--blau', 'kn-fn-pfeil'])
|
||||
assert.equal(fx.finde(termine, '.kn-fn-zeile2').textContent, 'Wir richten sie gerade ein')
|
||||
}))
|
||||
|
||||
/* ---------------------------------------------------------------- Teil 2: Overlay, Erfolgsmeldung, Wechsel --- */
|
||||
|
||||
@@ -3,41 +3,68 @@ import assert from 'node:assert/strict'
|
||||
import { meineZeilen, entdeckenEintraege, zeilenWeg, datumDe } from '../public/neu/ansicht-funktionen.js'
|
||||
import { daten, projekt, feature } from './support/knFunktionenDaten.js'
|
||||
|
||||
test('Meine: alles außer off, dazu Entwurf und Anfrage; zurückgezogen zählt nicht', () => {
|
||||
test('Meine: alles außer off, dazu Entwurf und Anfrage; zurückgezogen zählt nicht; aktiv ohne Pille, mit Kurzinfo', () => {
|
||||
const d = daten({ projekte: [projekt('p1', 'Bäckerei', {
|
||||
news: feature('active', { activatedAt: '2026-10-05T08:00:00.000Z' }),
|
||||
news: feature('active', { activatedAt: '2026-10-05T08:00:00.000Z', kurzinfo: '4 Beiträge sichtbar' }),
|
||||
termine: feature('off', { siteKey: 'abcdefghijklmnop' }),
|
||||
shop: feature('off', { anfrageStatus: 'offen' }),
|
||||
unterkunft: feature('off', { anfrageStatus: 'zurueckgezogen' }),
|
||||
zeiten: feature('disabled', { disabledReason: 'abo_ende', loeschungAm: '2026-11-20' }),
|
||||
})] })
|
||||
const z = meineZeilen(d)
|
||||
assert.deepEqual(z.map((r) => [r.key, r.pille.text, r.pille.ton]), [['news', 'Aktiv', 'gruen'], ['termine', 'Entwurf', 'grau'], ['shop', 'Angefragt', 'violett'], ['zeiten', 'Abgeschaltet', 'grau']])
|
||||
assert.deepEqual(z.map((r) => r.status), ['Aktiv seit 05.10.2026', 'Fragebogen fortsetzen', 'Wir prüfen deine Anfrage', 'Dein Abo ist beendet. Deine Daten bleiben bis 20.11.2026 gespeichert.'])
|
||||
assert.deepEqual(z.map((r) => [r.key, r.pille ? [r.pille.text, r.pille.ton] : null]), [['news', null], ['termine', ['Entwurf', 'grau']], ['shop', ['Angefragt', 'violett']], ['zeiten', ['Abgeschaltet', 'grau']]])
|
||||
assert.deepEqual(z.map((r) => r.status), ['4 Beiträge sichtbar', 'Fragebogen fortsetzen', 'Wir prüfen deine Anfrage', 'Dein Abo ist beendet. Deine Daten bleiben bis 20.11.2026 gespeichert.'])
|
||||
assert.equal(z[0].zeile2, z[0].status)
|
||||
assert.doesNotMatch(JSON.stringify(z), /Aktiv seit|05\.10\.2026/)
|
||||
})
|
||||
|
||||
test('Status je Zustand: Pille, Ton, Zeile', () => {
|
||||
test('Status je Zustand: Pille nur wenn nicht aktiv, Ton, Zeile', () => {
|
||||
const zeile = (key, f) => meineZeilen(daten({ projekte: [projekt('p1', 'B', { [key]: f })] })).find((r) => r.key === key)
|
||||
const faelle = [
|
||||
['news', feature('setup', { ticketWoid: '100201' }), 'In Einrichtung', 'blau', 'Einrichtung läuft · Ticket #100201'],
|
||||
['news', feature('setup'), 'In Einrichtung', 'blau', 'Einrichtung läuft'],
|
||||
['news', feature('setup', { ticketWoid: '100201' }), 'In Einrichtung', 'blau', 'Wir richten sie gerade ein'],
|
||||
['news', feature('setup'), 'In Einrichtung', 'blau', 'Wir richten sie gerade ein'],
|
||||
['news', feature('ordered'), 'Zahlung offen', 'orange', 'Wir warten auf deine Zahlung'],
|
||||
['news', feature('active'), 'Aktiv', 'gruen', 'Läuft auf deiner Website'],
|
||||
['news', feature('disabled'), 'Abgeschaltet', 'grau', 'Schreib uns im Chat, wenn du die Funktion wieder möchtest.'],
|
||||
['news', feature('active'), null, null, 'Aktiv'],
|
||||
['news', feature('active', { activatedAt: '2026-10-05T08:00:00.000Z' }), null, null, 'Aktiv'],
|
||||
['termine', feature('active', { kurzinfo: '3 Termine heute · nächster 10:30' }), null, null, '3 Termine heute · nächster 10:30'],
|
||||
['news', feature('disabled'), 'Abgeschaltet', 'grau', 'Im Chat wieder anfragen'],
|
||||
['news', feature('disabled', { disabledReason: 'abo_ende' }), 'Abgeschaltet', 'grau', 'Dein Abo ist beendet.'],
|
||||
['shop', feature('off', { anfrageStatus: 'angenommen' }), 'Angenommen', 'blau', 'Jetzt einschalten'],
|
||||
['shop', feature('off', { anfrageStatus: 'abgelehnt' }), 'Abgelehnt', 'rot', 'Schreib uns im Chat'],
|
||||
['shop', feature('off', { anfrageStatus: 'abgelehnt' }), 'Abgelehnt', 'rot', 'Anfrage abgelehnt · Im Chat nachfragen'],
|
||||
['shop', feature('off', { anfrageStatus: 'offen' }), 'Angefragt', 'violett', 'Wir prüfen deine Anfrage'],
|
||||
]
|
||||
for (const [key, f, pille, ton, status] of faelle) {
|
||||
const r = zeile(key, f)
|
||||
assert.deepEqual([r.pille.text, r.pille.ton, r.status], [pille, ton, status], `${key} ${f.state}`)
|
||||
assert.deepEqual([r.pille?.text ?? null, r.pille?.ton ?? null, r.status], [pille, ton, status], `${key} ${f.state}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('mehrere Websites: eine Zeile je Funktion und Website, zweite Zeile ist der Websitename', () => {
|
||||
const d = daten({ projekte: [projekt('p1', 'Bäckerei', { termine: feature('active') }), projekt('p2', 'Café', { termine: feature('setup'), news: feature('active') })] })
|
||||
assert.deepEqual(meineZeilen(d).map((r) => [r.key, r.projectId, r.zeile2]), [['news', 'p2', 'Café'], ['termine', 'p1', 'Bäckerei'], ['termine', 'p2', 'Café']])
|
||||
test('Kurzinfo: nur bei aktiver Funktion, nur als kurzer nicht leerer Text; sonst "Aktiv"', () => {
|
||||
const status = (f) => meineZeilen(daten({ projekte: [projekt('p1', 'B', { news: f })] })).find((r) => r.key === 'news').status
|
||||
assert.equal(status(feature('active', { kurzinfo: ' 4 Beiträge sichtbar ' })), '4 Beiträge sichtbar')
|
||||
for (const k of [null, undefined, '', ' ', 5, true, {}, ['x'], 'x'.repeat(81)]) assert.equal(status(feature('active', { kurzinfo: k })), 'Aktiv', String(JSON.stringify(k)))
|
||||
assert.equal(status(feature('active', { kurzinfo: 'x'.repeat(80) })), 'x'.repeat(80))
|
||||
// ein Text an einer Funktion, die nicht läuft, wird nicht gezeigt (der Server schickt dort null)
|
||||
assert.equal(status(feature('setup', { kurzinfo: '4 Beiträge sichtbar' })), 'Wir richten sie gerade ein')
|
||||
assert.equal(status(feature('disabled', { kurzinfo: '4 Beiträge sichtbar' })), 'Im Chat wieder anfragen')
|
||||
})
|
||||
|
||||
test('Kurzinfo der Analyse: aktiv mit Text oder "Aktiv", gesperrt bleibt mit Pille', () => {
|
||||
const zeile = (analyse) => meineZeilen(daten({ projekte: [projekt('p1', 'B', {}, analyse)] })).find((r) => r.key === 'analyse')
|
||||
assert.deepEqual([zeile({ state: 'active' }).pille, zeile({ state: 'active' }).status], [null, 'Aktiv'])
|
||||
assert.equal(zeile({ state: 'active', kurzinfo: '120 Besuche in 7 Tagen' }).status, '120 Besuche in 7 Tagen')
|
||||
assert.deepEqual([zeile({ state: 'disabled' }).pille.text, zeile({ state: 'disabled' }).status], ['Gesperrt', 'Schreib uns im Chat'])
|
||||
})
|
||||
|
||||
test('mehrere Websites: eine Zeile je Funktion und Website, die zweite Zeile beginnt mit dem Websitenamen', () => {
|
||||
const d = daten({ projekte: [projekt('p1', 'Bäckerei', { termine: feature('active', { kurzinfo: '3 Termine heute · nächster 10:30' }) }), projekt('p2', 'Café', { termine: feature('setup'), news: feature('active') })] })
|
||||
assert.deepEqual(meineZeilen(d).map((r) => [r.key, r.projectId, r.zeile2]), [
|
||||
['news', 'p2', 'Café · Aktiv'],
|
||||
['termine', 'p1', 'Bäckerei · 3 Termine heute · nächster 10:30'],
|
||||
['termine', 'p2', 'Café · Wir richten sie gerade ein'],
|
||||
])
|
||||
// der Websitename allein steht nie in der Zeile, der Status bleibt eigenes Feld
|
||||
assert.deepEqual(meineZeilen(d).map((r) => r.status), ['Aktiv', '3 Termine heute · nächster 10:30', 'Wir richten sie gerade ein'])
|
||||
})
|
||||
|
||||
test('Entdecken: Kandidaten sind die Websites ohne die Funktion; eine einzelne Website mit aktiver Funktion blendet sie aus', () => {
|
||||
@@ -56,8 +83,8 @@ test('C9: verfuegbar:false fehlt in Meine und Entdecken; ohne Websites zeigt Ent
|
||||
})
|
||||
|
||||
test('Analyse: Zeile nur bei active/disabled; unknown und comingSoon sind kein Kandidat', () => {
|
||||
const zeilen = (analyse) => meineZeilen(daten({ projekte: [projekt('p1', 'B', {}, analyse)] })).filter((r) => r.key === 'analyse').map((r) => r.pille.text)
|
||||
assert.deepEqual([zeilen({ state: 'active' }), zeilen({ state: 'disabled' }), zeilen({ state: 'off' }), zeilen({ state: 'unknown' }), zeilen({ comingSoon: true })], [['Aktiv'], ['Gesperrt'], [], [], []])
|
||||
const zeilen = (analyse) => meineZeilen(daten({ projekte: [projekt('p1', 'B', {}, analyse)] })).filter((r) => r.key === 'analyse').map((r) => r.pille?.text ?? 'ohne Pille')
|
||||
assert.deepEqual([zeilen({ state: 'active' }), zeilen({ state: 'disabled' }), zeilen({ state: 'off' }), zeilen({ state: 'unknown' }), zeilen({ comingSoon: true })], [['ohne Pille'], ['Gesperrt'], [], [], []])
|
||||
const kand = (analyse) => entdeckenEintraege(daten({ projekte: [projekt('p1', 'B', {}, analyse)] })).find((e) => e.eintrag.key === 'analyse')?.kandidaten.length ?? -1
|
||||
assert.deepEqual([kand({ state: 'off' }), kand({ state: 'unknown' }), kand({ comingSoon: true }), kand({ state: 'active' })], [1, 0, 0, -1])
|
||||
})
|
||||
@@ -69,7 +96,7 @@ test('zeilenWeg: angenommene Anfrage geht zur Bestätigung, alles andere in die
|
||||
|
||||
test('Analyse-Zeilen: Texte, Etage; Abo-Ende mit Löschtag; fehlende Felder brechen nichts', () => {
|
||||
const zeile = (analyse) => meineZeilen(daten({ projekte: [projekt('p1', 'B', {}, analyse)] })).find((r) => r.key === 'analyse')
|
||||
assert.deepEqual([zeile({ state: 'active' }).status, zeile({ state: 'disabled' }).status], ['Läuft auf deiner Website', 'Schreib uns im Chat'])
|
||||
assert.deepEqual([zeile({ state: 'active' }).status, zeile({ state: 'disabled' }).status], ['Aktiv', 'Schreib uns im Chat'])
|
||||
assert.equal(zeile({ state: 'disabled', disabledReason: 'abo_ende', loeschungAm: '2026-12-01' }).status, 'Dein Abo ist beendet. Deine Daten bleiben bis 01.12.2026 gespeichert.')
|
||||
assert.deepEqual(zeilenWeg(zeile({ state: 'active' })), { art: 'etage', etage: 'analyse' })
|
||||
const kaputt = { modell: 'abo', katalog: [], projects: [{ projectId: 'p1' }] }
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
import { test, after, beforeEach, afterEach, mock } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||||
import { startFakeStripe } from './support/fakeStripe.js'
|
||||
import { mailSink } from './support/mailSink.js'
|
||||
import { schemaKeys } from './support/schema.js'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
import { ZEITEN_DATETIMES } from '../server/services/zeiten/schema.js'
|
||||
import { TERMINE_DATETIMES } from '../server/services/termine/schema.js'
|
||||
import { SHOP_DATETIMES } from '../server/services/shop/schema.js'
|
||||
import { UNTERKUNFT_DATETIMES } from '../server/services/unterkunft/schema.js'
|
||||
|
||||
// GET /api/funktionen in der neuen Ansicht: je Projekt und Funktion `kurzinfo` (Live-Detail der Zeile in "Meine"). Nur aktive Funktionen
|
||||
// bekommen einen Text, jede Einzelabfrage ist begrenzt und fehlertolerant (Fehler oder Zeitueberschreitung ergibt null), Berliner Zeit.
|
||||
// Ohne neue Ansicht bleibt die Antwort wie sie war und nichts davon wird gelesen.
|
||||
const aw = await startAppwriteMock({
|
||||
schema: {
|
||||
portalFeatures: schemaKeys('portalFeatures'),
|
||||
subscriptions: schemaKeys('subscriptions'),
|
||||
contractEvents: schemaKeys('contractEvents'),
|
||||
featureConsents: schemaKeys('featureConsents'),
|
||||
shopConfig: schemaKeys('shopConfig'),
|
||||
ukEinstellungen: schemaKeys('ukEinstellungen'),
|
||||
oeffnungszeiten: schemaKeys('oeffnungszeiten'),
|
||||
zeitenErinnerungen: schemaKeys('zeitenErinnerungen'),
|
||||
termineBookings: schemaKeys('termineBookings'),
|
||||
shopOrders: schemaKeys('shopOrders'),
|
||||
newsPosts: schemaKeys('newsPosts'),
|
||||
ukAnfragen: schemaKeys('ukAnfragen'),
|
||||
},
|
||||
datetimes: {
|
||||
...ZEITEN_DATETIMES,
|
||||
termineBookings: TERMINE_DATETIMES.termineBookings,
|
||||
shopOrders: SHOP_DATETIMES.shopOrders,
|
||||
ukAnfragen: UNTERKUNFT_DATETIMES.ukAnfragen,
|
||||
newsPosts: ['visibleFrom', 'visibleUntil', 'takenDownAt', 'createdAt', 'updatedAt'],
|
||||
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'einschaltMailAt'],
|
||||
featureConsents: ['acceptedAt'],
|
||||
},
|
||||
defaultLimit: 25,
|
||||
})
|
||||
const st = await startFakeStripe()
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
const { config } = await import('../server/config.js')
|
||||
const { startTestApp, get } = await import('./support/testApp.js')
|
||||
const { customerCookie, seedFixtures, subscriptionDoc, wartungDoc } = await import('./support/fixtures.js')
|
||||
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
||||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||
const { default: funktionenRoutes } = await import('../server/routes/funktionen.js')
|
||||
const { resetZeitenCacheForTests } = await import('../server/services/zeiten/cache.js')
|
||||
const { einschaltenDeps } = await import('../server/services/zeiten/einschalten.js')
|
||||
const { kurzinfoDeps } = await import('../server/services/kundenansicht/kurzinfo.js')
|
||||
|
||||
const app = await startTestApp({ '/api/funktionen': funktionenRoutes })
|
||||
after(() => Promise.all([app.close(), aw.close(), st.close()]))
|
||||
after(() => setMailTransportForTests(null))
|
||||
|
||||
const JETZT = new Date('2026-10-17T08:00:00.000Z') // Samstag 10:00 Uhr in Berlin (Sommerzeit)
|
||||
const c1 = customerCookie('c1')
|
||||
const vorher = {
|
||||
neu: { mode: config.kundenansichtNeu.mode, pilotEmails: [...config.kundenansichtNeu.pilotEmails] },
|
||||
termine: config.termine.mode,
|
||||
shop: { mode: config.shop.mode, pilotEmails: [...config.shop.pilotEmails] },
|
||||
unterkunft: { mode: config.unterkunft.mode, pilotEmails: [...config.unterkunft.pilotEmails] },
|
||||
zeiten: { mode: config.zeiten.mode, pilotEmails: [...config.zeiten.pilotEmails] },
|
||||
analyse: config.analyse.enabled,
|
||||
}
|
||||
after(() => {
|
||||
Object.assign(config.kundenansichtNeu, vorher.neu)
|
||||
config.termine.mode = vorher.termine
|
||||
Object.assign(config.shop, vorher.shop)
|
||||
Object.assign(config.unterkunft, vorher.unterkunft)
|
||||
Object.assign(config.zeiten, vorher.zeiten)
|
||||
config.analyse.enabled = vorher.analyse
|
||||
})
|
||||
|
||||
const SAMMLUNGEN = ['subscriptions', 'portalFeatures', 'featureConsents', 'oeffnungszeiten', 'workorders', 'websiteProjects', 'contractEvents', 'shopConfig', 'ukEinstellungen', 'termineBookings', 'shopOrders', 'newsPosts', 'ukAnfragen']
|
||||
const leere = async (...collections) => {
|
||||
for (const col of collections) for (const id of aw.docs(col).keys()) await deleteDocument(col, id)
|
||||
}
|
||||
const projekt = (id, over = {}) => aw.seed('websiteProjects', { $id: id, customerId: 'c1', projectName: `Projekt ${id}`, subdomain: id, repoFullName: `JUSN/${id}`, previewMode: 'auto', previewDetected: 'static', ...over })
|
||||
const fid = (p, key) => `f_${p}_${key}`
|
||||
const funktion = (p, key, state = 'active', over = {}) =>
|
||||
aw.seed('portalFeatures', { $id: fid(p, key), customerId: 'c1', projectId: p, featureKey: key, siteKey: `${key}${p}`.padEnd(16, 'x').slice(0, 16), state, enabled: state === 'active', ...over })
|
||||
const NAMEN = ['news', 'termine', 'shop', 'unterkunft', 'zeiten']
|
||||
const alleAktiv = (p) => NAMEN.forEach((k) => funktion(p, k))
|
||||
const inStd = (h) => new Date(JETZT.getTime() + h * 3600000).toISOString()
|
||||
|
||||
let zaehler = 0
|
||||
const nr = () => `d${(zaehler += 1)}`
|
||||
const buchung = (p, over) => aw.seed('termineBookings', { $id: nr(), featureId: fid(p, 'termine'), customerId: 'c1', projectId: p, status: 'bestaetigt', quelle: 'online', endsAt: inStd(1), blockEndsAt: inStd(1), ...over })
|
||||
const bestellung = (p, status, over = {}) => aw.seed('shopOrders', { $id: nr(), shopId: fid(p, 'shop'), customerId: 'c1', status, bestellnummer: nr(), summeCent: 1000, erstelltAt: inStd(-5), ...over })
|
||||
const zettel = (p, over = {}) => aw.seed('newsPosts', { $id: nr(), customerId: 'c1', projectId: p, type: 'note', title: 'Hallo', text: 'Text', paper: 'sun', duration: 'open', visibleFrom: inStd(-48), visibleUntil: null, takenDownAt: null, createdAt: inStd(-48), updatedAt: inStd(-48), ...over })
|
||||
const anfrage = (p, status, von, bis, over = {}) => aw.seed('ukAnfragen', { $id: nr(), featureId: fid(p, 'unterkunft'), customerId: 'c1', projectId: p, unterkunftId: 'u1', nummer: nr(), von, bis, status, personen: 2, ...over })
|
||||
function plan(woche) {
|
||||
const tage = {}
|
||||
for (let d = 1; d <= 7; d += 1) tage[d] = woche[d] ? { modus: 'zeiten', fenster: [woche[d]] } : { modus: 'zu', fenster: [] }
|
||||
return { id: 'p1', kurz: 'standard', name: 'Standard', haupt: true, tage }
|
||||
}
|
||||
const zeitenDaten = (woche) => ({ land: 'RP', plaene: [plan(woche)], feiertage: {}, ausnahmen: [], hinweise: [], kontakt: { an: false, telefon: '', adresse: '', whatsapp: null } })
|
||||
const zeitenDok = (p, daten) => aw.seed('oeffnungszeiten', { $id: fid(p, 'zeiten'), customerId: 'c1', projectId: p, featureId: fid(p, 'zeiten'), version: 1, teamVersion: 0, daten: JSON.stringify(daten) })
|
||||
|
||||
const holen = async (cookie = c1) => (await get('/api/funktionen', { cookie }))
|
||||
const info = (b, p, key) => b.projects.find((x) => x.projectId === p).features[key].kurzinfo
|
||||
const abfragen = (sammlung) => aw.listCalls(sammlung)
|
||||
|
||||
beforeEach(async () => {
|
||||
await leere(...SAMMLUNGEN)
|
||||
resetZeitenCacheForTests()
|
||||
kurzinfoDeps.limitMs = 1500
|
||||
zaehler = 0
|
||||
seedFixtures(aw)
|
||||
aw.seed('subscriptions', subscriptionDoc('sub_h', { ...wartungDoc }))
|
||||
projekt('p1')
|
||||
config.kundenansichtNeu.mode = 'pilot'
|
||||
config.kundenansichtNeu.pilotEmails = ['k@example.invalid']
|
||||
config.termine.mode = 'on'
|
||||
config.shop.mode = 'on'
|
||||
config.unterkunft.mode = 'on'
|
||||
config.zeiten.mode = 'pilot'
|
||||
config.zeiten.pilotEmails = ['*']
|
||||
config.analyse.enabled = false
|
||||
einschaltenDeps.jetzt = () => JETZT
|
||||
setMailTransportForTests(mailSink())
|
||||
mock.method(console, 'error', () => {})
|
||||
mock.method(console, 'warn', () => {})
|
||||
mock.method(console, 'log', () => {})
|
||||
aw.resetListCalls()
|
||||
})
|
||||
afterEach(() => {
|
||||
mock.restoreAll()
|
||||
einschaltenDeps.jetzt = () => new Date()
|
||||
})
|
||||
|
||||
/* ------------------------------------------------------------------------------------------ Terminplaner --- */
|
||||
test('Terminplaner: Termine von heute (Berliner Tag), nächster nach jetzt; Sperren, Abgesagte, andere Tage und andere Funktionen zählen nicht', async () => {
|
||||
funktion('p1', 'termine'); projekt('p2'); funktion('p2', 'termine')
|
||||
buchung('p1', { status: 'erledigt', startsAt: '2026-10-17T06:00:00.000Z' }) // 08:00 Berlin, schon vorbei, zählt
|
||||
buchung('p1', { startsAt: '2026-10-17T08:30:00.000Z' }) // 10:30, der nächste
|
||||
buchung('p1', { startsAt: '2026-10-17T12:00:00.000Z' }) // 14:00
|
||||
buchung('p1', { status: 'abgesagt', startsAt: '2026-10-17T09:00:00.000Z' })
|
||||
buchung('p1', { quelle: 'sperre', startsAt: '2026-10-17T09:30:00.000Z' })
|
||||
buchung('p1', { status: 'angefragt', startsAt: '2026-10-17T10:00:00.000Z' })
|
||||
buchung('p1', { startsAt: '2026-10-18T08:00:00.000Z' }) // morgen
|
||||
buchung('p1', { startsAt: '2026-10-16T21:30:00.000Z' }) // 23:30 Berlin, gestern
|
||||
buchung('p2', { startsAt: '2026-10-17T07:00:00.000Z' }) // andere Website
|
||||
const b = await holen()
|
||||
assert.equal(info(b, 'p1', 'termine'), '3 Termine heute · nächster 10:30')
|
||||
assert.equal(info(b, 'p2', 'termine'), '1 Termin heute')
|
||||
})
|
||||
|
||||
test('Terminplaner: ein Termin, alle vorbei, keiner', async () => {
|
||||
funktion('p1', 'termine')
|
||||
assert.equal(info(await holen(), 'p1', 'termine'), 'Heute keine Termine')
|
||||
buchung('p1', { startsAt: '2026-10-17T11:00:00.000Z' })
|
||||
assert.equal(info(await holen(), 'p1', 'termine'), '1 Termin heute · nächster 13:00')
|
||||
await leere('termineBookings')
|
||||
buchung('p1', { status: 'erledigt', startsAt: '2026-10-17T05:00:00.000Z' })
|
||||
buchung('p1', { status: 'erledigt', startsAt: '2026-10-17T06:00:00.000Z' })
|
||||
assert.equal(info(await holen(), 'p1', 'termine'), '2 Termine heute')
|
||||
})
|
||||
|
||||
test('Terminplaner: der Tag ist der Berliner Tag, auch kurz nach Mitternacht (UTC liegt noch beim Vortag)', async () => {
|
||||
funktion('p1', 'termine')
|
||||
einschaltenDeps.jetzt = () => new Date('2026-10-17T22:30:00.000Z') // 00:30 Berlin am 18.10.
|
||||
buchung('p1', { startsAt: '2026-10-17T21:59:00.000Z' }) // 23:59 am 17.10., nicht heute
|
||||
buchung('p1', { startsAt: '2026-10-17T22:15:00.000Z' }) // 00:15 am 18.10., zählt, vorbei
|
||||
buchung('p1', { startsAt: '2026-10-18T06:00:00.000Z' }) // 08:00, nächster
|
||||
buchung('p1', { startsAt: '2026-10-18T22:00:00.000Z' }) // 00:00 am 19.10., nicht heute
|
||||
assert.equal(info(await holen(), 'p1', 'termine'), '2 Termine heute · nächster 08:00')
|
||||
})
|
||||
|
||||
/* ------------------------------------------------------------------------------------------ Webshop --- */
|
||||
test('Webshop: neue (bezahlte, noch nicht bearbeitete) Bestellungen dieses Shops', async () => {
|
||||
funktion('p1', 'shop'); projekt('p2'); funktion('p2', 'shop')
|
||||
assert.equal(info(await holen(), 'p1', 'shop'), 'Keine offenen Bestellungen')
|
||||
bestellung('p1', 'bezahlt'); bestellung('p1', 'bezahlt')
|
||||
bestellung('p1', 'versendet'); bestellung('p1', 'offen'); bestellung('p1', 'abgeschlossen'); bestellung('p1', 'storniert')
|
||||
bestellung('p2', 'bezahlt')
|
||||
let b = await holen()
|
||||
assert.equal(info(b, 'p1', 'shop'), '2 neue Bestellungen')
|
||||
assert.equal(info(b, 'p2', 'shop'), '1 neue Bestellung')
|
||||
})
|
||||
|
||||
/* ------------------------------------------------------------------------------------------ News --- */
|
||||
test('News-Pinnwand: sichtbare Beiträge (nicht abgenommen, begonnen, nicht abgelaufen) dieser Website', async () => {
|
||||
funktion('p1', 'news'); projekt('p2'); funktion('p2', 'news')
|
||||
assert.equal(info(await holen(), 'p1', 'news'), 'Keine Beiträge sichtbar')
|
||||
zettel('p1') // ohne Ende
|
||||
zettel('p1', { visibleFrom: inStd(-24), visibleUntil: inStd(72) })
|
||||
zettel('p1', { visibleFrom: inStd(-240), visibleUntil: inStd(1) })
|
||||
zettel('p1', { visibleFrom: inStd(-120), visibleUntil: inStd(480) })
|
||||
zettel('p1', { visibleFrom: inStd(-240), visibleUntil: inStd(-1) }) // abgelaufen
|
||||
zettel('p1', { takenDownAt: inStd(-2) }) // abgenommen
|
||||
zettel('p1', { visibleFrom: inStd(24) }) // beginnt erst morgen
|
||||
zettel('p2')
|
||||
const b = await holen()
|
||||
assert.equal(info(b, 'p1', 'news'), '4 Beiträge sichtbar')
|
||||
assert.equal(info(b, 'p2', 'news'), '1 Beitrag sichtbar')
|
||||
})
|
||||
|
||||
/* ------------------------------------------------------------------------------------------ Buchungskalender --- */
|
||||
test('Buchungskalender: offene Anfragen, sonst die nächste Anreise, sonst keine offenen Anfragen', async () => {
|
||||
funktion('p1', 'unterkunft'); projekt('p2'); funktion('p2', 'unterkunft')
|
||||
assert.equal(info(await holen(), 'p1', 'unterkunft'), 'Keine offenen Anfragen')
|
||||
anfrage('p1', 'gebucht', '2026-11-02', '2026-11-05')
|
||||
anfrage('p1', 'bezahlt', '2026-10-24', '2026-10-27')
|
||||
anfrage('p1', 'gebucht', '2026-10-01', '2026-10-05') // vorbei
|
||||
anfrage('p1', 'gebucht', '2026-10-15', '2026-10-19') // läuft schon: keine Anreise mehr
|
||||
anfrage('p1', 'freigegeben', '2026-10-20', '2026-10-22') // noch nicht gebucht
|
||||
anfrage('p1', 'abgelehnt', '2026-10-18', '2026-10-20')
|
||||
assert.equal(info(await holen(), 'p1', 'unterkunft'), 'Nächste Anreise 24.10.')
|
||||
anfrage('p1', 'neu', '2026-12-01', '2026-12-03')
|
||||
anfrage('p2', 'neu', '2026-12-01', '2026-12-03')
|
||||
anfrage('p2', 'neu', '2026-12-05', '2026-12-07')
|
||||
const b = await holen()
|
||||
assert.equal(info(b, 'p1', 'unterkunft'), '1 offene Anfrage')
|
||||
assert.equal(info(b, 'p2', 'unterkunft'), '2 offene Anfragen')
|
||||
})
|
||||
|
||||
test('Buchungskalender: Anreise heute und morgen werden ausgeschrieben', async () => {
|
||||
funktion('p1', 'unterkunft')
|
||||
anfrage('p1', 'gebucht', '2026-10-18', '2026-10-20')
|
||||
assert.equal(info(await holen(), 'p1', 'unterkunft'), 'Anreise morgen')
|
||||
anfrage('p1', 'bezahlt', '2026-10-17', '2026-10-19')
|
||||
assert.equal(info(await holen(), 'p1', 'unterkunft'), 'Anreise heute')
|
||||
})
|
||||
|
||||
/* ------------------------------------------------------------------------------------------ Öffnungszeiten --- */
|
||||
test('Öffnungszeiten live: der Status der Website von jetzt (ohne Zusatzabfrage), ohne Zeiten ein Hinweis', async () => {
|
||||
funktion('p1', 'zeiten')
|
||||
assert.equal(info(await holen(), 'p1', 'zeiten'), 'Noch keine Öffnungszeiten eingetragen')
|
||||
zeitenDok('p1', zeitenDaten({ 6: ['09:00', '18:00'], 1: ['09:00', '18:00'] }))
|
||||
resetZeitenCacheForTests()
|
||||
aw.resetListCalls()
|
||||
assert.equal(info(await holen(), 'p1', 'zeiten'), 'Jetzt geöffnet · schließt um 18 Uhr')
|
||||
einschaltenDeps.jetzt = () => new Date('2026-10-17T18:00:00.000Z') // Samstag 20:00
|
||||
assert.equal(info(await holen(), 'p1', 'zeiten'), 'Geschlossen · öffnet Montag um 9 Uhr')
|
||||
})
|
||||
|
||||
/* ------------------------------------------------------------------------------------------ Regeln --- */
|
||||
test('nur aktive Funktionen bekommen einen Text und eine Abfrage; alle anderen null', async () => {
|
||||
for (const [key, state] of [['news', 'setup'], ['termine', 'ordered'], ['shop', 'disabled'], ['unterkunft', 'off'], ['zeiten', 'setup']]) funktion('p1', key, state)
|
||||
buchung('p1', { startsAt: '2026-10-17T08:30:00.000Z' })
|
||||
bestellung('p1', 'bezahlt')
|
||||
zettel('p1')
|
||||
anfrage('p1', 'neu', '2026-12-01', '2026-12-03')
|
||||
zeitenDok('p1', zeitenDaten({ 6: ['09:00', '18:00'] }))
|
||||
aw.resetListCalls()
|
||||
const b = await holen()
|
||||
for (const key of NAMEN) assert.equal(info(b, 'p1', key), null, key)
|
||||
for (const key of NAMEN) assert.ok(Object.hasOwn(b.projects[0].features[key], 'kurzinfo'), `${key}: Feld da`)
|
||||
for (const sammlung of ['termineBookings', 'shopOrders', 'newsPosts', 'ukAnfragen']) assert.equal(abfragen(sammlung).length, 0, sammlung)
|
||||
})
|
||||
|
||||
test('eine kleine feste Zahl schmaler Abfragen je aktiver Funktion: limit gesetzt, nur nötige Felder, nur Filter dieser Funktion', async () => {
|
||||
alleAktiv('p1')
|
||||
const gesamt = () => ['termineBookings', 'shopOrders', 'newsPosts', 'ukAnfragen'].map((s) => [s, abfragen(s)])
|
||||
aw.resetListCalls()
|
||||
await holen()
|
||||
const [termine, shop, news, uk] = gesamt().map(([, a]) => a)
|
||||
assert.equal(termine.length, 1)
|
||||
assert.equal(shop.length, 1)
|
||||
assert.equal(news.length, 1)
|
||||
assert.equal(uk.length, 2)
|
||||
const hat = (aufruf, methode) => aufruf.queries.find((q) => q.method === methode)
|
||||
for (const aufruf of [...termine, ...news, ...uk]) {
|
||||
const limit = hat(aufruf, 'limit')
|
||||
assert.ok(limit && limit.values[0] <= 200, `limit ${JSON.stringify(limit)}`)
|
||||
}
|
||||
assert.deepEqual(hat(shop[0], 'limit').values, [1]) // nur die Anzahl (total)
|
||||
assert.ok(hat(termine[0], 'select') && hat(news[0], 'select'))
|
||||
assert.deepEqual(hat(termine[0], 'equal').values, ['f_p1_termine'])
|
||||
assert.deepEqual(hat(shop[0], 'equal').values, ['f_p1_shop'])
|
||||
assert.deepEqual(hat(news[0], 'equal').values, ['p1'])
|
||||
for (const aufruf of uk) assert.deepEqual(hat(aufruf, 'equal').values, ['f_p1_unterkunft'])
|
||||
assert.equal(aw.listCalls('oeffnungszeiten').length <= 1, true) // Zeiten: höchstens das Lesen für zeitenStatus, nichts zusätzlich
|
||||
})
|
||||
|
||||
test('mehrere Websites: jede bekommt ihre eigene Zeile, die Abfragen laufen für alle gleichzeitig', async () => {
|
||||
alleAktiv('p1'); projekt('p2'); alleAktiv('p2'); projekt('p3'); funktion('p3', 'termine')
|
||||
buchung('p1', { startsAt: '2026-10-17T08:30:00.000Z' })
|
||||
buchung('p3', { startsAt: '2026-10-17T07:00:00.000Z' })
|
||||
buchung('p3', { startsAt: '2026-10-17T13:00:00.000Z' })
|
||||
const b = await holen()
|
||||
assert.deepEqual(['p1', 'p2', 'p3'].map((p) => info(b, p, 'termine')).sort(), ['1 Termin heute · nächster 10:30', '2 Termine heute · nächster 15:00', 'Heute keine Termine'].sort())
|
||||
assert.equal(info(b, 'p3', 'shop'), null)
|
||||
})
|
||||
|
||||
test('Fehler einer Einzelabfrage ergibt null für diese Funktion; der Rest der Liste bleibt', async () => {
|
||||
alleAktiv('p1')
|
||||
buchung('p1', { startsAt: '2026-10-17T08:30:00.000Z' })
|
||||
bestellung('p1', 'bezahlt')
|
||||
zettel('p1')
|
||||
aw.failNext({ collection: 'termineBookings', method: 'GET', status: 500, message: 'geheimer Gästename im Fehlertext' })
|
||||
const b = await holen()
|
||||
assert.equal(info(b, 'p1', 'termine'), null)
|
||||
assert.equal(info(b, 'p1', 'shop'), '1 neue Bestellung')
|
||||
assert.equal(info(b, 'p1', 'news'), '1 Beitrag sichtbar')
|
||||
assert.equal(b.modell, 'abo')
|
||||
assert.ok(Array.isArray(b.katalog))
|
||||
const log = console.error.mock.calls.map((c) => c.arguments.join(' ')).join('\n')
|
||||
assert.match(log, /Kurzinfo \(termine\)/)
|
||||
assert.doesNotMatch(log, /geheimer/)
|
||||
// ein Teilfehler im Buchungskalender (eine der zwei Abfragen) ergibt null, nie eine halbe Aussage
|
||||
anfrage('p1', 'neu', '2026-12-01', '2026-12-03')
|
||||
aw.failNext({ collection: 'ukAnfragen', method: 'GET', status: 500 })
|
||||
assert.equal(info(await holen(), 'p1', 'unterkunft'), null)
|
||||
})
|
||||
|
||||
test('Zeitüberschreitung ergibt null, ohne die Liste aufzuhalten', async () => {
|
||||
alleAktiv('p1')
|
||||
bestellung('p1', 'bezahlt'); zettel('p1')
|
||||
kurzinfoDeps.limitMs = 60
|
||||
const echt = globalThis.fetch
|
||||
mock.method(globalThis, 'fetch', async (url, ...rest) => {
|
||||
if (String(url).includes('/shopOrders/')) await new Promise((r) => setTimeout(r, 600))
|
||||
return echt(url, ...rest)
|
||||
})
|
||||
const start = Date.now()
|
||||
const b = await holen()
|
||||
assert.ok(Date.now() - start < 550, `Antwort dauerte ${Date.now() - start} ms`)
|
||||
assert.equal(info(b, 'p1', 'shop'), null)
|
||||
assert.equal(info(b, 'p1', 'news'), '1 Beitrag sichtbar')
|
||||
await new Promise((r) => setTimeout(r, 700)) // die späte Antwort richtet nichts mehr an
|
||||
})
|
||||
|
||||
test('ohne neue Ansicht: keine Kurzinfo in der Antwort, keine einzige Abfrage dafür; sonst ist die neue Antwort die alte plus ergänzte Felder', async () => {
|
||||
alleAktiv('p1')
|
||||
buchung('p1', { startsAt: '2026-10-17T08:30:00.000Z' }); bestellung('p1', 'bezahlt'); zettel('p1'); anfrage('p1', 'neu', '2026-12-01', '2026-12-03')
|
||||
config.kundenansichtNeu.mode = 'off'
|
||||
aw.resetListCalls()
|
||||
const alt = await holen()
|
||||
assert.doesNotMatch(JSON.stringify(alt), /kurzinfo/)
|
||||
for (const sammlung of ['termineBookings', 'shopOrders', 'newsPosts', 'ukAnfragen']) assert.equal(abfragen(sammlung).length, 0, sammlung)
|
||||
config.kundenansichtNeu.mode = 'pilot'
|
||||
config.kundenansichtNeu.pilotEmails = ['k2@example.invalid'] // Kunde nicht in der Liste
|
||||
assert.deepEqual(await holen(), alt)
|
||||
config.kundenansichtNeu.pilotEmails = ['k@example.invalid']
|
||||
const neu = await holen()
|
||||
for (const key of ['modell', 'kundenart', 'abo', 'katalog']) delete neu[key]
|
||||
for (const p of neu.projects) for (const f of Object.values(p.features)) { delete f.kurzinfo; delete f.anfrageStatus }
|
||||
assert.deepEqual(neu, alt)
|
||||
})
|
||||
|
||||
test('die Kurzinfo ist ein kurzer Text ohne Preis, Mail-Adresse oder Namen von Gästen', async () => {
|
||||
alleAktiv('p1')
|
||||
buchung('p1', { startsAt: '2026-10-17T08:30:00.000Z', gastName: 'Erika Beispiel', gastEmail: 'erika@example.invalid', preisCent: 4500 })
|
||||
bestellung('p1', 'bezahlt', { kaeuferEmail: 'kauf@example.invalid', summeCent: 9900 })
|
||||
zettel('p1', { title: 'Geheimer Titel' })
|
||||
anfrage('p1', 'neu', '2026-12-01', '2026-12-03', { gast: JSON.stringify({ name: 'Max Gast' }) })
|
||||
const b = await holen()
|
||||
const texte = NAMEN.map((k) => info(b, 'p1', k))
|
||||
for (const t of texte) {
|
||||
assert.equal(typeof t, 'string')
|
||||
assert.ok(t.length > 0 && t.length <= 60, t)
|
||||
assert.doesNotMatch(t, /€|Euro|@|Erika|Max|Geheimer|\d+,\d\d/)
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user