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>
352 lines
22 KiB
JavaScript
352 lines
22 KiB
JavaScript
import { test } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import { readFileSync, writeFileSync, mkdtempSync } from 'node:fs'
|
||
import { tmpdir } from 'node:os'
|
||
import { join } from 'node:path'
|
||
import { pathToFileURL } from 'node:url'
|
||
|
||
// Kachel „Öffnungszeiten live“ in public/funktionen.js (Spec 3.1, Ruling R20). Die Kachel steht inline in funktionen.js (keine neuen Importe,
|
||
// test/termineView.test.js und test/termineFragebogenView.test.js zählen sie) und macht keine Abfrage über /api/funktionen hinaus.
|
||
// Eigener Loader: ALLE Importe von funktionen.js (absolut /x.js oder relativ ./x.js) zeigen auf die Dateien in public/, damit ein künftiger
|
||
// Import die Tests nicht stillschweigend bricht.
|
||
const dir = mkdtempSync(join(tmpdir(), 'fn-zeiten-tile-'))
|
||
const fileUrl = (name) => pathToFileURL(new URL(`../public/${name}`, import.meta.url).pathname).href
|
||
const quelle = readFileSync(new URL('../public/funktionen.js', import.meta.url), 'utf8')
|
||
const src = quelle.replace(/(\bfrom\s+)'(?:\/|\.\/)([^']+)'/g, (_, von, name) => `${von}'${fileUrl(name)}'`)
|
||
const file = join(dir, 'funktionen.mjs')
|
||
writeFileSync(file, src)
|
||
const { initFunktionen } = await import(pathToFileURL(file).href)
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------- Hilfen
|
||
const zeitenFeature = (state, extra = {}) => ({
|
||
state, siteKey: null, ticketWoid: null, orderedAt: null, activatedAt: null, invoiceUrl: null, disabledReason: null, disabledAt: null, loeschungAm: null,
|
||
unlockedBy: state === 'off' ? null : 'abo', aboEndeAt: null, einbau: null, ...extra,
|
||
})
|
||
const projekt = (id, state = 'off', feature = {}, extra = {}) => ({
|
||
projectId: id,
|
||
projectName: `Website ${id}`,
|
||
liveUrl: '',
|
||
features: { news: { state: 'off' }, zeiten: zeitenFeature(state, feature) },
|
||
analyse: { comingSoon: true },
|
||
zeitenStatus: null,
|
||
zeitenLeer: true,
|
||
...extra,
|
||
})
|
||
const ANGEBOT = (inklusive = true, grund = null) => ({
|
||
verfuegbar: true,
|
||
inklusive,
|
||
grund,
|
||
preis: '30 €',
|
||
einschalten: { consent: { version: 'v', knopf: 'Einschalten', items: [{ id: 'leistung', text: 'Haken', haken: true }] }, leistungsbeschreibung: { version: 'v', url: '/vertragsunterlagen/zeiten-leistungsbeschreibung.html' } },
|
||
})
|
||
|
||
async function render(projects, extra = {}) {
|
||
const aufrufe = []
|
||
globalThis.fetch = async (url) => {
|
||
aufrufe.push(String(url))
|
||
return { ok: true, status: 200, json: async () => ({ projects, ...extra }) }
|
||
}
|
||
const root = { innerHTML: '', querySelector: () => null }
|
||
await initFunktionen(root)
|
||
return { html: root.innerHTML, aufrufe }
|
||
}
|
||
const kachel = (html) => {
|
||
const von = html.indexOf('data-fn-tile="zeiten"')
|
||
assert.ok(von >= 0, 'die Kachel fehlt')
|
||
return html.slice(von, html.indexOf('</article>', von))
|
||
}
|
||
const nurText = (html) => html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
|
||
const knoepfe = (html) => [...html.matchAll(/<button\b[^>]*>([^<]*)<\/button>/g)].map((m) => m[1].trim())
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------- Tests
|
||
test('ohne verfuegbar keine Kachel', async () => {
|
||
const P = [projekt('a')]
|
||
const ohne = (await render(P)).html
|
||
assert.doesNotMatch(ohne, /data-fn-tile="zeiten"/)
|
||
assert.doesNotMatch(ohne, /Öffnungszeiten/)
|
||
const aus = (await render(P, { zeiten: { verfuegbar: false } })).html
|
||
assert.equal(aus, ohne, 'verfuegbar: false sieht aus wie ein Server ohne Zeiten')
|
||
assert.equal(ohne.match(/Kommt bald/g).length, 2, 'Terminplaner und Analyse, wie bisher')
|
||
// auch ein beschädigtes Angebot macht keine Kachel
|
||
for (const kaputt of [null, 'ja', { verfuegbar: 'ja' }, { inklusive: true }]) assert.doesNotMatch((await render(P, { zeiten: kaputt })).html, /data-fn-tile="zeiten"/, JSON.stringify(kaputt))
|
||
})
|
||
|
||
test('Kachel steht zwischen Terminplaner und Analyse, mit Angebot genau eine zusätzliche Kachel', async () => {
|
||
const { html } = await render([projekt('a')], { zeiten: ANGEBOT(true) })
|
||
const stellen = ['data-fn-tile="news"', 'Terminplaner', 'data-fn-tile="zeiten"', 'Analyse'].map((s) => html.indexOf(s))
|
||
assert.ok(stellen.every((s) => s >= 0), stellen.join())
|
||
assert.deepEqual(stellen, [...stellen].sort((x, y) => x - y))
|
||
assert.equal(html.match(/class="fn-tile[ "]/g).length, 4, 'News, Terminplaner (Kommt bald), Zeiten, Analyse (Kommt bald)')
|
||
assert.equal(html.match(/Kommt bald/g).length, 2)
|
||
assert.match(kachel(html), /<h3>Öffnungszeiten live<\/h3>/)
|
||
assert.match(kachel(html), /<svg class="fn-ico"/)
|
||
})
|
||
|
||
test('Abo: „Im Wartungs-Abo enthalten“ und Knopf Einschalten', async () => {
|
||
const k = kachel((await render([projekt('p1')], { zeiten: ANGEBOT(true) })).html)
|
||
assert.match(nurText(k), /Im Wartungs-Abo enthalten/)
|
||
assert.match(k, /fn-pill fn-pill-off">Aus</)
|
||
assert.deepEqual(knoepfe(k), ['Einschalten'])
|
||
assert.match(k, /<button type="button" class="kb-btn-primary fn-tile-cta" data-view="zeiten" data-project="p1">Einschalten<\/button>/)
|
||
assert.doesNotMatch(k, /30 €|Zahlungspflichtig|einmalig/)
|
||
})
|
||
|
||
test('Abo nicht aktiv: „sobald dein Abo aktiv ist“, kein Bezahlen', async () => {
|
||
const k = kachel((await render([projekt('p1')], { zeiten: ANGEBOT(false, 'nicht_aktiv') })).html)
|
||
assert.match(nurText(k), /Im Wartungs-Abo enthalten – einschaltbar, sobald dein Abo aktiv ist/)
|
||
assert.deepEqual(knoepfe(k), [])
|
||
assert.doesNotMatch(k, /30 €|Zahlungspflichtig|einmalig|<button/)
|
||
})
|
||
|
||
test('ohne Abo: „30 € einmalig“', async () => {
|
||
for (const grund of ['kein_abo', 'gekuendigt', null]) {
|
||
const k = kachel((await render([projekt('p1')], { zeiten: ANGEBOT(false, grund) })).html)
|
||
assert.match(nurText(k), /30 € einmalig/, String(grund))
|
||
assert.deepEqual(knoepfe(k), ['Öffnungszeiten einrichten · 30 €'], String(grund))
|
||
assert.match(k, /data-view="zeiten" data-project="p1"/)
|
||
assert.doesNotMatch(k, /Im Wartungs-Abo enthalten/, 'nichts verspricht, was nicht gilt')
|
||
}
|
||
// der Preis kommt aus dem Register, maskiert
|
||
const anderer = kachel((await render([projekt('p1')], { zeiten: { ...ANGEBOT(false, 'kein_abo'), preis: '31 €' } })).html)
|
||
assert.match(nurText(anderer), /31 € einmalig/)
|
||
assert.deepEqual(knoepfe(anderer), ['Öffnungszeiten einrichten · 31 €'])
|
||
assert.doesNotMatch(anderer, /30 €/)
|
||
const bose = kachel((await render([projekt('p1')], { zeiten: { ...ANGEBOT(false, 'kein_abo'), preis: '<b>31 €</b>' } })).html)
|
||
assert.doesNotMatch(bose, /<b>/)
|
||
// ohne Preis im Angebot gilt der Rückfall 30 €
|
||
assert.match(nurText(kachel((await render([projekt('p1')], { zeiten: { ...ANGEBOT(false, 'kein_abo'), preis: '' } })).html)), /30 € einmalig/)
|
||
})
|
||
|
||
test('Abo-Lesefehler (inklusive null): „Gerade nicht prüfbar – bitte später erneut“', async () => {
|
||
const k = kachel((await render([projekt('p1')], { zeiten: ANGEBOT(null, null) })).html)
|
||
assert.match(nurText(k), /Gerade nicht prüfbar – bitte später erneut/)
|
||
assert.deepEqual(knoepfe(k), [])
|
||
assert.doesNotMatch(k, /30 €|Im Wartungs-Abo enthalten/)
|
||
})
|
||
|
||
test('setup: Text je nach einbau, Hinweis „Trag jetzt deine Zeiten ein“ bei zeitenLeer', async () => {
|
||
const zeigen = async (feature, extra) => kachel((await render([projekt('p1', 'setup', feature, extra)], { zeiten: ANGEBOT(true) })).html)
|
||
|
||
const angestossen = await zeigen({ einbau: 'angestossen' }, { zeitenLeer: false })
|
||
assert.match(angestossen, /fn-pill fn-pill-setup">In Einrichtung</)
|
||
assert.match(nurText(angestossen), /Hinweis- und Kontakt-Leiste erscheinen nach dem nächsten Veröffentlichen \(meist in wenigen Minuten\)/)
|
||
assert.doesNotMatch(nurText(angestossen), /Trag jetzt deine Zeiten ein/)
|
||
assert.deepEqual(knoepfe(angestossen), ['Zeiten bearbeiten'])
|
||
assert.match(angestossen, /data-view="zeiten" data-project="p1"/)
|
||
|
||
const manuell = await zeigen({ einbau: 'manuell', ticketWoid: '4711' }, { zeitenLeer: false })
|
||
assert.match(nurText(manuell), /Unser Team baut die Funktion ein \(Ticket #4711\)/)
|
||
assert.doesNotMatch(nurText(manuell), /nach dem nächsten Veröffentlichen/)
|
||
|
||
const ohneTicket = await zeigen({ einbau: 'manuell' }, { zeitenLeer: false })
|
||
assert.match(nurText(ohneTicket), /Unser Team baut die Funktion ein\./)
|
||
assert.doesNotMatch(ohneTicket, /Ticket #/)
|
||
|
||
const leer = await zeigen({ einbau: 'angestossen' }, { zeitenLeer: true })
|
||
assert.match(nurText(leer), /Trag jetzt deine Zeiten ein – erst dann kann das Team die Tabelle einbauen/)
|
||
const leerManuell = await zeigen({ einbau: 'manuell', ticketWoid: '1' }, { zeitenLeer: true })
|
||
assert.match(nurText(leerManuell), /Trag jetzt deine Zeiten ein/)
|
||
|
||
// Kauf: der Neubau steht noch aus (einbau leer): der Text nennt weder Veröffentlichen noch ein Ticket, das es nicht gibt
|
||
const kauf = await zeigen({ unlockedBy: 'kauf', einbau: null }, { zeitenLeer: false })
|
||
assert.match(nurText(kauf), /Unser Team baut die Funktion ein\./)
|
||
})
|
||
|
||
test('active: Kurzstatus aus zeitenStatus, keine Zusatzabfrage', async () => {
|
||
const status = { offen: true, kurz: 'Geöffnet', text: 'Jetzt geöffnet · schließt um 18 Uhr' }
|
||
const { html, aufrufe } = await render([projekt('p1', 'active', { einbau: 'angestossen' }, { zeitenStatus: status, zeitenLeer: false })], { zeiten: ANGEBOT(true) })
|
||
assert.deepEqual(aufrufe, ['/api/funktionen'], 'der Status kommt in der Liste, die Kachel fragt nichts nach')
|
||
const k = kachel(html)
|
||
assert.match(k, /fn-pill fn-pill-on">Aktiv</)
|
||
assert.match(nurText(k), /Jetzt geöffnet · schließt um 18 Uhr/)
|
||
assert.deepEqual(knoepfe(k), ['Zeiten bearbeiten'])
|
||
// ohne gespeicherte Zeiten kein Status; stattdessen der Hinweis
|
||
const ohne = kachel((await render([projekt('p1', 'active', {}, { zeitenStatus: null, zeitenLeer: true })], { zeiten: ANGEBOT(true) })).html)
|
||
assert.doesNotMatch(ohne, /Jetzt geöffnet/)
|
||
assert.match(nurText(ohne), /Trag jetzt deine Zeiten ein/)
|
||
// Fremdtext nur maskiert
|
||
const bose = kachel((await render([projekt('p1', 'active', {}, { zeitenStatus: { offen: true, kurz: 'Geöffnet', text: '<img src=x onerror=1>' }, zeitenLeer: false })], { zeiten: ANGEBOT(true) })).html)
|
||
assert.doesNotMatch(bose, /<img/)
|
||
assert.match(bose, /<img/)
|
||
})
|
||
|
||
test('mehrere Websites: Zielprojekt ist die aktive, dann die in Einrichtung, dann die abgeschaltete, sonst die erste; „aktiv für 1 von 2 Websites“', async () => {
|
||
const wahl = async (liste, angebot = ANGEBOT(true)) => kachel((await render(liste, { zeiten: angebot })).html).match(/data-project="([^"]+)"/)?.[1]
|
||
assert.equal(await wahl([projekt('a'), projekt('b', 'active', {}, { zeitenLeer: false })]), 'b')
|
||
assert.equal(await wahl([projekt('a'), projekt('b', 'setup'), projekt('c', 'disabled')]), 'b')
|
||
assert.equal(await wahl([projekt('a'), projekt('b', 'disabled', { disabledReason: 'abo_ende', loeschungAm: '2999-01-01' }, { zeitenLeer: false })], ANGEBOT(false, 'kein_abo')), 'b')
|
||
assert.equal(await wahl([projekt('a'), projekt('b')]), 'a')
|
||
assert.match(nurText(kachel((await render([projekt('a'), projekt('b', 'active', {}, { zeitenLeer: false })], { zeiten: ANGEBOT(true) })).html)), /aktiv für 1 von 2 Websites/)
|
||
// ohne Website: ein Satz statt eines Knopfs
|
||
const keine = kachel((await render([], { zeiten: ANGEBOT(true) })).html)
|
||
assert.deepEqual(knoepfe(keine), [])
|
||
assert.match(nurText(keine), /Sobald deine Website angelegt ist/)
|
||
})
|
||
|
||
test('aboEndeAt: Behalten-Hinweis mit Datum', async () => {
|
||
const zeigen = async (feature, angebot = ANGEBOT(false, 'kein_abo'), state = 'active') => kachel((await render([projekt('p1', state, feature, { zeitenLeer: false })], { zeiten: angebot })).html)
|
||
|
||
const k = await zeigen({ aboEndeAt: '2026-10-01T10:00:00.000Z', einbau: 'angestossen' })
|
||
assert.match(nurText(k), /Dein Wartungs-Abo ist beendet – behalte die Funktion bis 31\.10\.2026 für einmalig 30 €/)
|
||
assert.deepEqual(knoepfe(k), ['Zeiten bearbeiten', 'Für 30 € behalten'])
|
||
assert.match(k, /<button type="button" class="kb-btn-outline" data-view="zeiten" data-project="p1" data-zeiten-weg="behalten">Für 30 € behalten<\/button>/)
|
||
|
||
// Berliner Kalendertag, nicht UTC: 01.10. 23:30 UTC ist schon der 02.10. in Berlin ⇒ Frist 01.11.
|
||
assert.match(nurText(await zeigen({ aboEndeAt: '2026-10-01T23:30:00.000Z' })), /bis 01\.11\.2026/)
|
||
// Appwrite liefert +00:00
|
||
assert.match(nurText(await zeigen({ aboEndeAt: '2026-10-01T10:00:00.000+00:00' })), /bis 31\.10\.2026/)
|
||
// auch in der Einrichtung
|
||
assert.match(nurText(await zeigen({ aboEndeAt: '2026-10-01T10:00:00.000Z' }, ANGEBOT(false, 'kein_abo'), 'setup')), /behalte die Funktion bis 31\.10\.2026/)
|
||
// läuft das Abo wieder (inklusive true), gibt es nichts zu bezahlen: der Abgleich nimmt das Ende gleich zurück
|
||
const wieder = await zeigen({ aboEndeAt: '2026-10-01T10:00:00.000Z' }, ANGEBOT(true))
|
||
assert.doesNotMatch(wieder, /behalten|Für 30 €/)
|
||
assert.deepEqual(knoepfe(wieder), ['Zeiten bearbeiten'])
|
||
// Abo nicht lesbar: kein Bezahlknopf auf Verdacht
|
||
const unklar = await zeigen({ aboEndeAt: '2026-10-01T10:00:00.000Z' }, ANGEBOT(null, null))
|
||
assert.deepEqual(knoepfe(unklar), ['Zeiten bearbeiten'])
|
||
// ohne Datum kein Hinweis
|
||
assert.doesNotMatch(await zeigen({ aboEndeAt: null }), /Wartungs-Abo ist beendet/)
|
||
// ein unlesbares Datum erfindet kein Datum
|
||
const kaputt = await zeigen({ aboEndeAt: 'kein-datum' })
|
||
assert.doesNotMatch(nurText(kaputt), /bis NaN|Invalid|undefined/)
|
||
})
|
||
|
||
// Fix Runde 1: Behalten (30 €) gibt es nur ohne Abo (inklusive false UND grund kein_abo). nicht_aktiv und gekuendigt heissen, dass das Abo läuft (R20/R25).
|
||
test('aboEndeAt bei laufendem Abo (nicht_aktiv, gekuendigt): „Abo läuft wieder“, nie ein Bezahlknopf', async () => {
|
||
for (const grund of ['nicht_aktiv', 'gekuendigt']) {
|
||
for (const state of ['active', 'setup']) {
|
||
const k = kachel((await render([projekt('p1', state, { aboEndeAt: '2026-10-01T10:00:00.000Z', einbau: 'angestossen' }, { zeitenLeer: false })], { zeiten: ANGEBOT(false, grund) })).html)
|
||
const was = `${grund}/${state}`
|
||
assert.match(nurText(k), /Dein Wartungs-Abo läuft wieder – Öffnungszeiten live ist darin enthalten\./, was)
|
||
assert.deepEqual(knoepfe(k), ['Zeiten bearbeiten'], was)
|
||
assert.doesNotMatch(nurText(k), /ist beendet|behalte die Funktion|für einmalig|Für 30 €|30 €/, was)
|
||
assert.doesNotMatch(k, /data-zeiten-weg/, was)
|
||
}
|
||
}
|
||
// unbekannter Grund oder kein Grund (false, null): nicht auf Verdacht bezahlen lassen
|
||
for (const grund of [null, 'unbekannt']) {
|
||
const k = kachel((await render([projekt('p1', 'active', { aboEndeAt: '2026-10-01T10:00:00.000Z' }, { zeitenLeer: false })], { zeiten: ANGEBOT(false, grund) })).html)
|
||
assert.deepEqual(knoepfe(k), ['Zeiten bearbeiten'], String(grund))
|
||
assert.doesNotMatch(nurText(k), /Für 30 €|behalte die Funktion/, String(grund))
|
||
}
|
||
// Gegenprobe: ohne Abo (kein_abo) bleibt es beim Angebot
|
||
const ohne = kachel((await render([projekt('p1', 'active', { aboEndeAt: '2026-10-01T10:00:00.000Z' }, { zeitenLeer: false })], { zeiten: ANGEBOT(false, 'kein_abo') })).html)
|
||
assert.deepEqual(knoepfe(ohne), ['Zeiten bearbeiten', 'Für 30 € behalten'])
|
||
})
|
||
|
||
test('disabled abo_ende mit Daten bei laufendem Abo (nicht_aktiv, gekuendigt): kein „Für 30 € wieder einschalten“, stattdessen „Abo läuft wieder“', async () => {
|
||
const feature = { disabledReason: 'abo_ende', disabledAt: '2026-10-04T10:00:00.000Z', loeschungAm: '2999-01-01' }
|
||
for (const grund of ['nicht_aktiv', 'gekuendigt']) {
|
||
const k = kachel((await render([projekt('p1', 'disabled', feature, { zeitenLeer: false })], { zeiten: ANGEBOT(false, grund) })).html)
|
||
assert.doesNotMatch(k, /Für 30 €|data-zeiten-weg|<button/, grund)
|
||
assert.match(nurText(k), /Dein Wartungs-Abo läuft wieder – wir schalten Öffnungszeiten live automatisch wieder ein/, grund)
|
||
assert.match(nurText(k), /Deine Eingaben bleiben bis 01\.01\.2999 gespeichert/, grund)
|
||
}
|
||
for (const grund of [null, 'unbekannt']) {
|
||
const k = kachel((await render([projekt('p1', 'disabled', feature, { zeitenLeer: false })], { zeiten: ANGEBOT(false, grund) })).html)
|
||
assert.doesNotMatch(k, /Für 30 €|<button/, String(grund))
|
||
assert.match(nurText(k), /Gerade nicht prüfbar/, String(grund))
|
||
}
|
||
// Gegenprobe: kein_abo bietet es an
|
||
const ohne = kachel((await render([projekt('p1', 'disabled', feature, { zeitenLeer: false })], { zeiten: ANGEBOT(false, 'kein_abo') })).html)
|
||
assert.deepEqual(knoepfe(ohne), ['Für 30 € wieder einschalten'])
|
||
})
|
||
|
||
test('ordered: „Zahlung wird bestätigt …“', async () => {
|
||
const k = kachel((await render([projekt('p1', 'ordered', { unlockedBy: null })], { zeiten: ANGEBOT(false, 'kein_abo') })).html)
|
||
assert.match(k, /fn-pill fn-pill-setup">Zahlung offen</)
|
||
assert.match(nurText(k), /Zahlung wird bestätigt …/)
|
||
assert.deepEqual(knoepfe(k), ['Status ansehen'])
|
||
assert.match(k, /data-view="zeiten" data-project="p1"/)
|
||
})
|
||
|
||
test('disabled abo_ende mit Daten: Knopf „Für 30 € wieder einschalten“', async () => {
|
||
const zeigen = async (feature, extra, angebot = ANGEBOT(false, 'kein_abo')) => kachel((await render([projekt('p1', 'disabled', feature, extra)], { zeiten: angebot })).html)
|
||
|
||
const k = await zeigen({ disabledReason: 'abo_ende', disabledAt: '2026-10-04T10:00:00.000Z', loeschungAm: '2999-01-01' }, { zeitenLeer: false })
|
||
assert.match(k, /fn-pill fn-pill-off">Abgeschaltet</)
|
||
assert.match(nurText(k), /Öffnungszeiten live ist abgeschaltet, weil dein Wartungs-Abo beendet ist/)
|
||
assert.match(nurText(k), /Deine Eingaben bleiben bis 01\.01\.2999 gespeichert/)
|
||
assert.deepEqual(knoepfe(k), ['Für 30 € wieder einschalten'])
|
||
assert.match(k, /data-view="zeiten" data-project="p1" data-zeiten-weg="behalten"/)
|
||
|
||
// keine Daten mehr (gelöscht oder nie eingetragen): nichts zu behalten
|
||
const leer = await zeigen({ disabledReason: 'abo_ende', loeschungAm: '2999-01-01' }, { zeitenLeer: true })
|
||
assert.doesNotMatch(leer, /Für 30 €/)
|
||
assert.deepEqual(knoepfe(leer), ['Chat öffnen'])
|
||
// die Frist der Löschung ist vorbei
|
||
const zuSpaet = await zeigen({ disabledReason: 'abo_ende', loeschungAm: '2000-01-01' }, { zeitenLeer: false })
|
||
assert.doesNotMatch(zuSpaet, /Für 30 €/)
|
||
// Abschaltung durch das Team: Grundtext aus der Abschaltung, kein Behalten
|
||
const team = await zeigen({ disabledReason: 'team' }, { zeitenLeer: false })
|
||
assert.doesNotMatch(team, /Für 30 €/)
|
||
assert.match(nurText(team), /Öffnungszeiten live wurde für diese Website abgeschaltet\. Schreib uns im Chat, wenn du die Funktion wieder möchtest\./)
|
||
assert.deepEqual(knoepfe(team), ['Chat öffnen'])
|
||
// Altbestand ohne Grund
|
||
assert.deepEqual(knoepfe(await zeigen({}, { zeitenLeer: false })), ['Chat öffnen'])
|
||
// das Abo läuft wieder: der Abgleich schaltet selbst wieder ein, 30 € wären doppelt bezahlt
|
||
const wieder = await zeigen({ disabledReason: 'abo_ende', loeschungAm: '2999-01-01' }, { zeitenLeer: false }, ANGEBOT(true))
|
||
assert.doesNotMatch(wieder, /Für 30 €/)
|
||
assert.match(nurText(wieder), /Dein Wartungs-Abo läuft wieder/)
|
||
})
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------- Klick
|
||
test('Knopf: das Projekt (und beim Behalten der Weg) geht an #zeiten-body; nur Knöpfe der Zeiten-Kachel', async () => {
|
||
const gesetzt = []
|
||
const gehoert = {}
|
||
const artikel = { addEventListener: (typ, fn) => { gehoert[typ] = fn } }
|
||
const body = { dataset: {} }
|
||
const vorher = globalThis.document
|
||
globalThis.document = { getElementById: (id) => { gesetzt.push(id); return id === 'zeiten-body' ? body : null } }
|
||
try {
|
||
globalThis.fetch = async () => ({ ok: true, status: 200, json: async () => ({ projects: [projekt('p1', 'active', { aboEndeAt: '2026-10-01T10:00:00.000Z' }, { zeitenLeer: false })], zeiten: ANGEBOT(false, 'kein_abo') }) })
|
||
const root = { innerHTML: '', querySelector: (sel) => (sel === '[data-fn-tile="zeiten"]' ? artikel : null) }
|
||
await initFunktionen(root)
|
||
assert.equal(typeof gehoert.click, 'function', 'ein Klick-Handler an der Kachel')
|
||
const knopf = (dataset) => ({ closest: (sel) => (sel === 'button[data-view="zeiten"]' ? { dataset } : null) })
|
||
|
||
gehoert.click({ target: knopf({ view: 'zeiten', project: 'p1' }) })
|
||
assert.equal(body.dataset.wunschProjekt, 'p1')
|
||
assert.equal(body.dataset.wunschWeg, undefined, 'ohne Weg keiner')
|
||
|
||
gehoert.click({ target: knopf({ view: 'zeiten', project: 'p1', zeitenWeg: 'behalten' }) })
|
||
assert.equal(body.dataset.wunschWeg, 'behalten')
|
||
// der nächste Klick ohne Weg nimmt den alten zurück
|
||
gehoert.click({ target: knopf({ view: 'zeiten', project: 'p1' }) })
|
||
assert.equal(body.dataset.wunschWeg, undefined)
|
||
gehoert.click({ target: knopf({ view: 'zeiten', project: 'p1', zeitenWeg: 'behalten' }) })
|
||
assert.equal(body.dataset.wunschWeg, 'behalten')
|
||
|
||
// ein Klick auf etwas anderes in der Kachel (Text) legt nichts ab
|
||
body.dataset = {}
|
||
gehoert.click({ target: { closest: () => null } })
|
||
gehoert.click({ target: {} })
|
||
assert.deepEqual(body.dataset, {})
|
||
// ein unbekannter Weg wird nicht übernommen
|
||
gehoert.click({ target: knopf({ view: 'zeiten', project: 'p2', zeitenWeg: 'alles-frei' }) })
|
||
assert.equal(body.dataset.wunschProjekt, 'p2')
|
||
assert.equal(body.dataset.wunschWeg, undefined)
|
||
} finally {
|
||
if (vorher === undefined) delete globalThis.document
|
||
else globalThis.document = vorher
|
||
}
|
||
})
|
||
|
||
test('Projekt-IDs und Namen kommen nur maskiert in die Kachel', async () => {
|
||
const k = kachel((await render([projekt('"><script>x</script>', 'off')], { zeiten: ANGEBOT(true) })).html)
|
||
assert.doesNotMatch(k, /<script>/)
|
||
assert.match(k, /data-project=""><script>x<\/script>"/)
|
||
})
|
||
|
||
// ---------------------------------------------------------------------------------------------------------------- Quelltext
|
||
test('funktionen.js: keine neuen Importe für die Zeiten (die Kachel steht inline), keine „24 Uhr“, Du-Form', () => {
|
||
assert.deepEqual(quelle.match(/from '\/[^']+'/g), ["from '/fn-util.js'", "from '/analyse-view.js'"])
|
||
assert.deepEqual(quelle.match(/from '\.[^']+'/g), ["from './vertraege-link.js'"])
|
||
assert.doesNotMatch(quelle, /import\s*\(/)
|
||
assert.doesNotMatch(quelle, /from '[^']*zeiten/, 'kein Zeiten-Modul in funktionen.js')
|
||
const von = quelle.indexOf('Öffnungszeiten live')
|
||
assert.ok(von > 0)
|
||
assert.doesNotMatch(quelle.slice(von), /24 Uhr/)
|
||
assert.doesNotMatch(quelle, /\bSie\b/)
|
||
})
|