Files
Webklar-Kundenbereich/test/funktionenZeitenTile.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

352 lines
22 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 } 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, /&lt;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="&quot;&gt;&lt;script&gt;x&lt;\/script&gt;"/)
})
// ---------------------------------------------------------------------------------------------------------------- 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/)
})