Öffnungszeiten: Website-Skript mit Status und Tabelle
public/embed/zeiten.js ruft die öffentliche Zeiten-API einmal je Seite ab und füllt die Platzhalter [data-webklar-zeiten] (tabelle, status, badge; data-plan, Hauptplan ohne Angabe). Reine Funktionen (waehleWechsel, naechsterWechsel, waehleHinweis, jetztMitServer, tabelleDaten) stehen zwischen den Marken @rein-anfang/@rein-ende. Timer auf den nächsten Wechsel (jede Wartezeit auf 10 Minuten begrenzt), Neuladen höchstens alle 10 Minuten, Daten älter als 24 Stunden, Fehler oder aktiv:false lassen das Ersatz-HTML stehen bzw. stellen es wieder her. Testseite zeiten-demo.html. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a2d11ad6c4
commit
a6a4eae294
3 files changed
+984
No files matched your search
@@ -0,0 +1,403 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import vm from 'node:vm'
|
||||
|
||||
// Statische Pruefung und Funktionstests des Website-Skripts public/embed/zeiten.js (Oeffnungszeiten live, Aufgabe 25;
|
||||
// Spezifikation 4.2 und 4.3 Punkt 4, Rulings R6 und Skript-Regeln der Gesamtvorgaben):
|
||||
// - Das Skript rechnet nie mit Datum oder Zeitzone des Browsers: "jetzt" ist Date.now() (bei grosser Uhrabweichung die Serverzeit aus
|
||||
// dem Date-Kopf), Wochentage und Kalendertage kommen als Zeichenketten vom Server und werden rein rechnerisch ausgewertet.
|
||||
// - Kein Browser-Speicher, keine Cookies, keine Fremdadressen im Code; Kundentext nur ueber textContent.
|
||||
// - Die reinen Funktionen (Marken @rein-anfang / @rein-ende) laufen hier einzeln in einem leeren vm-Kontext.
|
||||
// Task 26 haengt Hinweis-Leiste, Kontakt-Leiste und JSON-LD an dieselbe Datei und ergaenzt diese Datei um deren Pruefungen.
|
||||
|
||||
const lies = (rel) => readFileSync(new URL(`../${rel}`, import.meta.url), 'utf8')
|
||||
const QUELLE = lies('public/embed/zeiten.js')
|
||||
|
||||
/** Quelltext ohne Kommentare (die Kopfzeile darf Beispiele nennen, der Code nicht). */
|
||||
const OHNE_KOMMENTARE = QUELLE.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|\s)\/\/.*$/gm, '$1')
|
||||
|
||||
/** Ergebnisse aus dem vm-Kontext haben andere Prototypen (deepStrictEqual wuerde scheitern): ueber JSON angleichen. */
|
||||
const norm = (x) => JSON.parse(JSON.stringify(x))
|
||||
|
||||
test('Quelltext ist gueltiges JavaScript (kompiliert, wird nicht ausgefuehrt)', () => {
|
||||
assert.doesNotThrow(() => new vm.Script(QUELLE, { filename: 'zeiten.js' }))
|
||||
})
|
||||
|
||||
test('Quelltext: keine Cookies, kein Storage, kein fremder Host', () => {
|
||||
for (const verboten of ['localStorage', 'sessionStorage', 'document.cookie', 'indexedDB', 'document.referrer', 'sendBeacon', 'navigator.userAgent', 'resolvedOptions']) {
|
||||
assert.ok(!OHNE_KOMMENTARE.includes(verboten), `${verboten} darf im Skript nicht vorkommen`)
|
||||
}
|
||||
// auch nicht in Kommentaren oder Texten: nirgends ein https://-Literal (die Adresse kommt aus data-api oder der Herkunft des Skripts)
|
||||
assert.doesNotMatch(QUELLE, /https?:\/\//, 'Adressen gehoeren in data-api, nicht in die Datei')
|
||||
assert.doesNotMatch(OHNE_KOMMENTARE, /\bimport\s*\(|\bimportScripts\b|\bXMLHttpRequest\b|\bWebSocket\b/)
|
||||
})
|
||||
|
||||
test('Quelltext: Date nur als Date.now( und Date.parse(, keine Zeitzonen- und Kalenderrechnung des Browsers', () => {
|
||||
// erlaubt: Date.now(, Date.parse( und der Kopfname 'Date' (headers.get('Date'))
|
||||
const rest = OHNE_KOMMENTARE.replace(/\bDate\.now\(/g, '').replace(/\bDate\.parse\(/g, '').replace(/headers\.get\('Date'\)/g, '')
|
||||
assert.ok(!/\bDate\b/.test(rest), 'Date darf nur als Date.now( oder Date.parse( vorkommen')
|
||||
assert.ok(OHNE_KOMMENTARE.includes('Date.now('), 'Date.now( fehlt')
|
||||
assert.ok(OHNE_KOMMENTARE.includes('Date.parse('), 'Date.parse( fehlt')
|
||||
for (const verboten of ['getHours', 'getMinutes', 'getDay', 'getDate', 'getFullYear', 'getMonth', 'getTime(', 'toLocale', 'Intl.', 'getTimezoneOffset', 'toISOString', 'toUTCString', 'new Date']) {
|
||||
assert.ok(!OHNE_KOMMENTARE.includes(verboten), `${verboten} darf im Skript nicht vorkommen`)
|
||||
}
|
||||
})
|
||||
|
||||
test('Quelltext: Kundentexte nie per innerHTML (kein innerHTML, outerHTML, insertAdjacentHTML, document.write, eval, srcdoc)', () => {
|
||||
for (const verboten of ['innerHTML', 'outerHTML', 'insertAdjacentHTML', 'document.write', 'eval(', 'new Function', 'createContextualFragment', 'srcdoc', 'DOMParser']) {
|
||||
assert.ok(!OHNE_KOMMENTARE.includes(verboten), `${verboten} darf im Skript nicht vorkommen`)
|
||||
}
|
||||
assert.ok(OHNE_KOMMENTARE.includes('textContent'), 'Texte kommen ueber textContent')
|
||||
})
|
||||
|
||||
test('Quelltext: setTimeout nur mit Funktionsargument, in genau einer Stelle, jede Wartezeit auf 0 bis 600000 ms begrenzt (R6)', () => {
|
||||
const stellen = [...OHNE_KOMMENTARE.matchAll(/\bsetTimeout\(/g)]
|
||||
assert.equal(stellen.length, 1, 'genau eine Stelle ruft setTimeout auf (Hilfsfunktion timer)')
|
||||
assert.match(OHNE_KOMMENTARE, /setTimeout\(function \(\) \{/, 'setTimeout bekommt eine Funktion, nie eine Zeichenkette')
|
||||
assert.doesNotMatch(OHNE_KOMMENTARE, /\bsetInterval\(/, 'kein setInterval')
|
||||
assert.match(OHNE_KOMMENTARE, /var TIMER_MAX_MS = 600000\b/, 'Obergrenze 10 Minuten (setTimeout laeuft ab 2^31-1 ms ueber)')
|
||||
const zeile = OHNE_KOMMENTARE.split('\n').find((z) => z.includes('setTimeout('))
|
||||
const hilfe = OHNE_KOMMENTARE.slice(OHNE_KOMMENTARE.indexOf('function timer ('), OHNE_KOMMENTARE.indexOf(zeile) + zeile.length + 80)
|
||||
assert.match(hilfe, /Math\.max\(0, Math\.min\(TIMER_MAX_MS,/, 'die Wartezeit wird auf [0, TIMER_MAX_MS] begrenzt')
|
||||
assert.match(OHNE_KOMMENTARE, /var RELOAD_MS = 600000\b/, 'Neuladen hoechstens alle 10 Minuten')
|
||||
assert.ok(OHNE_KOMMENTARE.includes('visibilitychange'), 'Neuladen auch beim Zurueckkehren in den Tab')
|
||||
})
|
||||
|
||||
test('Doppel-Laden-Schutz __wkZeiten', () => {
|
||||
assert.match(OHNE_KOMMENTARE, /if \(window\.__wkZeiten\) return/)
|
||||
assert.match(OHNE_KOMMENTARE, /window\.__wkZeiten = true/)
|
||||
assert.ok(OHNE_KOMMENTARE.indexOf('window.__wkZeiten = true') < OHNE_KOMMENTARE.indexOf("var STYLE_ID"), 'der Schutz steht vor allem anderen')
|
||||
})
|
||||
|
||||
test('Abruf: genau ein fetch, ohne Cookies, nur an die Zeiten-Schnittstelle; letzter erfolgreicher Abruf im Speicher der Seite', () => {
|
||||
assert.equal([...OHNE_KOMMENTARE.matchAll(/\bfetch\(/g)].length, 1, 'genau ein fetch (ein Abruf je Seite fuer alle Teile)')
|
||||
assert.ok(OHNE_KOMMENTARE.includes("credentials: 'omit'"), "fetch braucht credentials: 'omit'")
|
||||
assert.ok(OHNE_KOMMENTARE.includes('/api/public/zeiten/v1/'))
|
||||
assert.match(OHNE_KOMMENTARE, /var ALTER_MAX_MS = 24 \* 60 \* 60 \* 1000\b/, 'Daten aelter als 24 Stunden gelten nicht mehr')
|
||||
assert.match(OHNE_KOMMENTARE, /geladenMs = /, 'Zeitpunkt des letzten erfolgreichen Abrufs wird gemerkt (nicht body.stand)')
|
||||
assert.ok(!OHNE_KOMMENTARE.includes('.stand'), 'body.stand zaehlt nicht fuer das Alter der Daten')
|
||||
assert.ok(OHNE_KOMMENTARE.includes("headers.get('Date')"), 'Serverzeit aus dem Date-Kopf')
|
||||
})
|
||||
|
||||
test('Pflichtbausteine der Spezifikation (Platzhalter, Stile, Tabelle, Plaene, Texte)', () => {
|
||||
for (const noetig of [
|
||||
'data-webklar-zeiten', 'data-site', 'data-api', 'data-modus', 'data-plan', 'currentScript',
|
||||
"'wkz-style'", '@layer wkz', 'wkz-', '--wkz-',
|
||||
"'caption'", "'scope'", "'row'", "'th'", 'aria-current', "'date'",
|
||||
'tabelle', 'status', 'badge', 'haupt', 'console.warn',
|
||||
'Abweichungen in den nächsten 14 Tagen', 'rund um die Uhr', 'nach Vereinbarung', 'geschlossen', 'Montag', 'Sonntag',
|
||||
'DOMContentLoaded', 'removeChild',
|
||||
]) {
|
||||
assert.ok(QUELLE.includes(noetig), `${noetig} fehlt im Skript`)
|
||||
}
|
||||
assert.doesNotMatch(OHNE_KOMMENTARE, /aria-live/, 'der Status meldet sich nicht von selbst (kein aria-live)')
|
||||
})
|
||||
|
||||
test('Stile stehen einmal in <style id="wkz-style">, alle Klassen beginnen mit wkz-, alle Variablen mit --wkz-', () => {
|
||||
const css = /var CSS = \[([\s\S]*?)\]\.join/.exec(QUELLE)
|
||||
assert.ok(css, 'CSS-Block fehlt')
|
||||
const klassen = [...css[1].matchAll(/\.([a-zA-Z][\w-]*)/g)].map((m) => m[1])
|
||||
assert.ok(klassen.length > 15)
|
||||
for (const k of klassen) assert.ok(k.startsWith('wkz-'), `Klasse ${k} ohne Praefix wkz-`)
|
||||
assert.ok(css[1].includes('@layer wkz{'), 'Stile in @layer wkz')
|
||||
for (const v of css[1].matchAll(/(?<![\w-])--([\w-]+)/g)) assert.ok(v[1].startsWith('wkz-'), `Variable --${v[1]} ohne Praefix --wkz-`)
|
||||
assert.equal([...QUELLE.matchAll(/getElementById\(STYLE_ID\)/g)].length, 1)
|
||||
// Kontakt- und Hinweisleisten (Task 26) leben im Shadow DOM: hier steht kein Seitenstil dafuer
|
||||
assert.ok(!/position:\s*fixed|position:fixed/.test(css[1]), 'Platzhalter-Stile setzen nichts fest an den Rand')
|
||||
})
|
||||
|
||||
test('Ersatz-HTML wird nie verbraucht (nur Kopien zurueck auf die Seite) und ohne Daten oder bei Fehlern nicht angefasst', () => {
|
||||
assert.ok(!/appendChild\(p\.ausweich\)/.test(OHNE_KOMMENTARE), 'das gemerkte Ersatz-HTML darf nur als cloneNode(true) zurueckgesetzt werden')
|
||||
assert.ok(OHNE_KOMMENTARE.includes('ausweich.cloneNode(true)'))
|
||||
assert.ok(OHNE_KOMMENTARE.includes('child.cloneNode(true)') || OHNE_KOMMENTARE.includes('.cloneNode(true)'), 'beim Start wird eine Kopie des Ersatz-HTML gemerkt, das Original bleibt stehen')
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Telefonnummer (gleiche Regel wie public/embed/termine.js)
|
||||
|
||||
test('telefonLink: Nummern werden gewaehlt, freier Text nie, "(0)" hinter der Landesvorwahl entfaellt', () => {
|
||||
const m = /\/\/ @telefon-anfang[^\n]*\n([\s\S]*?)\/\/ @telefon-ende/.exec(QUELLE)
|
||||
assert.ok(m, 'Marken @telefon-anfang/@telefon-ende fehlen')
|
||||
const telefonLink = vm.runInNewContext(`${m[1]}; telefonLink`, {})
|
||||
const faelle = [
|
||||
['06301 1234', 'tel:063011234'],
|
||||
['+49 6301 1234', 'tel:+4963011234'],
|
||||
['+49 (0) 6301-1234', 'tel:+4963011234'],
|
||||
['+49(0)6301 1234', 'tel:+4963011234'],
|
||||
['0049 (0) 6301 1234', 'tel:004963011234'],
|
||||
['(06301) 1234', 'tel:063011234'],
|
||||
['06301/1234-0', 'tel:0630112340'],
|
||||
[' +49 160 1234567 ', 'tel:+491601234567'],
|
||||
['06301 1234 (ab 9 Uhr)', ''],
|
||||
['Tel. 06301 1234', ''],
|
||||
['06301 1234 oder 0171 555', ''],
|
||||
['0171 1, 0171 2', ''],
|
||||
['ruf an', ''],
|
||||
['', ''], [' ', ''], ['12', ''], ['+', ''], [undefined, ''], [null, ''], [1234567, ''], [{}, ''],
|
||||
]
|
||||
for (const [eingabe, soll] of faelle) assert.equal(telefonLink(eingabe), soll, JSON.stringify(eingabe))
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- reine Funktionen
|
||||
|
||||
/** Die reinen Funktionen des Skripts (zwischen den Marken @rein-anfang und @rein-ende) einzeln ausfuehren. */
|
||||
function rein() {
|
||||
const m = /\/\/ @rein-anfang[^\n]*\n([\s\S]*?)\/\/ @rein-ende/.exec(QUELLE)
|
||||
assert.ok(m, 'Marken @rein-anfang/@rein-ende fehlen')
|
||||
return vm.runInNewContext(
|
||||
`${m[1]}; ({ waehleWechsel, naechsterWechsel, waehleHinweis, jetztMitServer, tabelleDaten, wochentagVon, zeitenText, gruppiereAbweichungen, abweichungsZeile })`,
|
||||
{},
|
||||
)
|
||||
}
|
||||
|
||||
const MIN = 60 * 1000
|
||||
const ENDLOS = 8.64e15
|
||||
|
||||
test('Marken: die reinen Funktionen stehen zwischen @rein-anfang und @rein-ende und greifen auf nichts ausserhalb zu', () => {
|
||||
const f = rein()
|
||||
for (const name of ['waehleWechsel', 'naechsterWechsel', 'waehleHinweis', 'jetztMitServer', 'tabelleDaten']) {
|
||||
assert.equal(typeof f[name], 'function', name)
|
||||
}
|
||||
})
|
||||
|
||||
test('waehleWechsel nimmt den letzten Eintrag mit ab ≤ jetzt', () => {
|
||||
const { waehleWechsel } = rein()
|
||||
const liste = [
|
||||
{ ab: 1000, offen: true, text: 'A', kurz: 'a', tag: '2026-10-05' },
|
||||
{ ab: 2000, offen: false, text: 'B', kurz: 'b', tag: '2026-10-05' },
|
||||
{ ab: 3000, offen: null, text: 'C', kurz: 'c', tag: '2026-10-06' },
|
||||
]
|
||||
assert.equal(waehleWechsel(liste, 2500).text, 'B')
|
||||
assert.equal(waehleWechsel(liste, 2000).text, 'B', 'ab == jetzt zaehlt schon')
|
||||
assert.equal(waehleWechsel(liste, 1999).text, 'A')
|
||||
assert.equal(waehleWechsel(liste, 1000).text, 'A')
|
||||
assert.equal(waehleWechsel(liste, 999), null, 'vor dem ersten Eintrag: keiner')
|
||||
assert.equal(waehleWechsel(liste, 9e12).text, 'C', 'nach dem letzten: der letzte')
|
||||
assert.equal(waehleWechsel([], 5000), null)
|
||||
assert.equal(waehleWechsel(undefined, 5000), null)
|
||||
assert.equal(waehleWechsel(null, 5000), null)
|
||||
// unsortierte Eingabe und unbrauchbare Eintraege stoeren nicht
|
||||
assert.equal(waehleWechsel([liste[2], null, { ab: 'x' }, liste[0], liste[1]], 2500).text, 'B')
|
||||
})
|
||||
|
||||
test('naechsterWechsel berücksichtigt Hinweis-Beginn und -Ende, nie Vergangenes, nie „ohne Ende“', () => {
|
||||
const { naechsterWechsel } = rein()
|
||||
const wechsel = [{ ab: 1000 }, { ab: 5000 }, { ab: 9000 }]
|
||||
assert.equal(naechsterWechsel(wechsel, [], 1000), 5000, 'ab == jetzt ist schon da, gesucht ist der naechste')
|
||||
assert.equal(naechsterWechsel(wechsel, [], 4999), 5000)
|
||||
assert.equal(naechsterWechsel(wechsel, [], 9000), null, 'nichts mehr')
|
||||
assert.equal(naechsterWechsel([], [], 10), null)
|
||||
// Hinweis-Beginn
|
||||
assert.equal(naechsterWechsel(wechsel, [{ ab: 3000, bis: ENDLOS }], 1000), 3000)
|
||||
// Hinweis-Ende
|
||||
assert.equal(naechsterWechsel(wechsel, [{ ab: 0, bis: 2500 }], 1000), 2500)
|
||||
// Beginn und Ende, nimmt das fruehere
|
||||
assert.equal(naechsterWechsel(wechsel, [{ ab: 4000, bis: 4500 }, { ab: 0, bis: 4800 }], 3999), 4000)
|
||||
assert.equal(naechsterWechsel(wechsel, [{ ab: 4000, bis: 4500 }, { ab: 0, bis: 4800 }], 4000), 4500)
|
||||
// unbegrenzt (ab 0, bis 8.64e15): kein Wechsel
|
||||
assert.equal(naechsterWechsel([], [{ ab: 0, bis: ENDLOS }], 1000), null)
|
||||
assert.equal(naechsterWechsel(wechsel, [{ ab: 0, bis: ENDLOS }], 9000), null)
|
||||
// Muell
|
||||
assert.equal(naechsterWechsel(wechsel, [null, { ab: 'x', bis: NaN }, {}], 4000), 5000)
|
||||
assert.equal(naechsterWechsel(undefined, undefined, 4000), null)
|
||||
})
|
||||
|
||||
test('waehleHinweis: höchster Rang, bei Gleichstand späterer Beginn; nur gültige (ab ≤ jetzt < bis)', () => {
|
||||
const { waehleHinweis } = rein()
|
||||
const info = { art: 'info', text: 'I', rang: 1, ab: 0, bis: ENDLOS }
|
||||
const aktion = { art: 'aktion', text: 'A', rang: 2, ab: 1000, bis: 9000 }
|
||||
const urlaub = { art: 'urlaub', text: 'U', rang: 3, ab: 500, bis: 4000 }
|
||||
const wichtig = { art: 'wichtig', text: 'W', rang: 4, ab: 2000, bis: 3000 }
|
||||
const liste = [info, aktion, urlaub, wichtig]
|
||||
assert.equal(waehleHinweis(liste, 100).text, 'I', 'nur der Info-Hinweis gilt schon')
|
||||
assert.equal(waehleHinweis(liste, 1500).text, 'U', 'Urlaub (3) vor Aktion (2) und Info (1)')
|
||||
assert.equal(waehleHinweis(liste, 2500).text, 'W', 'Wichtig (4) schlaegt alles')
|
||||
assert.equal(waehleHinweis(liste, 3000).text, 'U', 'bis ist exklusiv: ab 3000 gilt Wichtig nicht mehr')
|
||||
assert.equal(waehleHinweis(liste, 4000).text, 'A', 'Urlaub endet bei 4000')
|
||||
assert.equal(waehleHinweis(liste, 9000).text, 'I')
|
||||
assert.equal(waehleHinweis([aktion], 999), null, 'noch nicht begonnen')
|
||||
assert.equal(waehleHinweis([aktion], 9000), null, 'schon vorbei')
|
||||
// Gleichstand beim Rang: der spaetere Beginn gewinnt, unabhaengig von der Reihenfolge
|
||||
const frueh = { art: 'aktion', text: 'frueh', rang: 2, ab: 1000, bis: 9000 }
|
||||
const spaet = { art: 'aktion', text: 'spaet', rang: 2, ab: 3000, bis: 9000 }
|
||||
assert.equal(waehleHinweis([frueh, spaet], 5000).text, 'spaet')
|
||||
assert.equal(waehleHinweis([spaet, frueh], 5000).text, 'spaet')
|
||||
assert.equal(waehleHinweis([frueh, spaet], 2000).text, 'frueh', 'der spaetere hat noch nicht begonnen')
|
||||
assert.equal(waehleHinweis([], 1), null)
|
||||
assert.equal(waehleHinweis(undefined, 1), null)
|
||||
assert.equal(waehleHinweis([null, { rang: 'x' }, {}], 1), null)
|
||||
})
|
||||
|
||||
test('jetztMitServer: 4 min Abweichung ⇒ lokal, 6 min ⇒ Server', () => {
|
||||
const { jetztMitServer } = rein()
|
||||
const lokal = Date.parse('2026-10-05T10:00:00Z')
|
||||
const kopf = (ms) => new Date(ms).toUTCString() // Date-Kopf wie ihn der Server sendet (sekundengenau)
|
||||
assert.equal(jetztMitServer(kopf(lokal), lokal), lokal, 'identisch')
|
||||
assert.equal(jetztMitServer(kopf(lokal + 4 * MIN), lokal), lokal, 'Server 4 min voraus: lokale Uhr bleibt')
|
||||
assert.equal(jetztMitServer(kopf(lokal - 4 * MIN), lokal), lokal, 'Server 4 min zurueck: lokale Uhr bleibt')
|
||||
assert.equal(jetztMitServer(kopf(lokal + 5 * MIN), lokal), lokal, 'genau 5 min: noch lokal')
|
||||
assert.equal(jetztMitServer(kopf(lokal + 6 * MIN), lokal), lokal + 6 * MIN, 'Uhr geht 6 min nach: Serverzeit')
|
||||
assert.equal(jetztMitServer(kopf(lokal - 6 * MIN), lokal), lokal - 6 * MIN, 'Uhr geht 6 min vor: Serverzeit')
|
||||
assert.equal(jetztMitServer(kopf(lokal + 36 * 60 * MIN), lokal), lokal + 36 * 60 * MIN)
|
||||
// fehlender oder kaputter Kopf: lokale Uhr
|
||||
for (const kaputt of [null, undefined, '', 'gestern', 12345, {}]) assert.equal(jetztMitServer(kaputt, lokal), lokal, JSON.stringify(kaputt))
|
||||
})
|
||||
|
||||
test('wochentagVon rechnet den ISO-Wochentag aus der Zeichenkette (1 = Montag) und stimmt mit dem Kalender ueberein', () => {
|
||||
const { wochentagVon } = rein()
|
||||
assert.equal(wochentagVon('2026-10-05'), 1, 'Montag')
|
||||
assert.equal(wochentagVon('2026-10-11'), 7, 'Sonntag')
|
||||
assert.equal(wochentagVon('1970-01-01'), 4, 'Donnerstag')
|
||||
assert.equal(wochentagVon('2024-02-29'), 4, 'Schalttag')
|
||||
// Gegenprobe ueber sieben Jahre, jeden Tag
|
||||
const ende = Date.UTC(2031, 0, 1)
|
||||
for (let ms = Date.UTC(2024, 0, 1); ms < ende; ms += 86400000) {
|
||||
const d = new Date(ms)
|
||||
const tag = d.toISOString().slice(0, 10)
|
||||
const iso = d.getUTCDay() === 0 ? 7 : d.getUTCDay()
|
||||
assert.equal(wochentagVon(tag), iso, tag)
|
||||
}
|
||||
})
|
||||
|
||||
test('zeitenText: Zeiten, rund um die Uhr, nach Vereinbarung, geschlossen – nie 24:00', () => {
|
||||
const { zeitenText } = rein()
|
||||
assert.equal(zeitenText('zeiten', [['09:00', '18:00']]), '09:00–18:00')
|
||||
assert.equal(zeitenText('zeiten', [['09:00', '12:30'], ['13:30', '18:00']]), '09:00–12:30, 13:30–18:00')
|
||||
assert.equal(zeitenText('zeiten', [['18:00', '24:00']]), '18:00–00:00', '24:00 wird als 00:00 gezeigt')
|
||||
assert.equal(zeitenText('zeiten', [['22:00', '02:00']]), '22:00–02:00', 'ueber Mitternacht')
|
||||
assert.equal(zeitenText('zeiten', [['00:00', '24:00']]), 'rund um die Uhr', 'ganztags = rund um die Uhr')
|
||||
assert.equal(zeitenText('rund', []), 'rund um die Uhr')
|
||||
assert.equal(zeitenText('vereinbarung', []), 'nach Vereinbarung')
|
||||
assert.equal(zeitenText('zu', []), 'geschlossen')
|
||||
assert.equal(zeitenText('zeiten', []), 'geschlossen', 'Zeiten ohne Fenster = geschlossen')
|
||||
assert.equal(zeitenText('irgendwas', [['09:00', '10:00']]), 'geschlossen', 'unbekannter Modus = geschlossen')
|
||||
assert.equal(zeitenText('zeiten', undefined), 'geschlossen')
|
||||
for (const m of ['zeiten', 'rund', 'zu', 'vereinbarung']) assert.ok(!zeitenText(m, [['08:00', '24:00']]).includes('24:00'))
|
||||
})
|
||||
|
||||
/** Plan wie ihn die oeffentliche API liefert: Montag bis Freitag 09:00–18:00, Samstag 09:00–13:00, Sonntag zu. */
|
||||
function plan(tage = []) {
|
||||
const woche = []
|
||||
for (let tag = 1; tag <= 7; tag += 1) {
|
||||
if (tag <= 5) woche.push({ tag, modus: 'zeiten', fenster: [['09:00', '18:00']] })
|
||||
else if (tag === 6) woche.push({ tag, modus: 'zeiten', fenster: [['09:00', '13:00']] })
|
||||
else woche.push({ tag, modus: 'zu', fenster: [] })
|
||||
}
|
||||
return { kurz: 'haupt', name: 'Öffnungszeiten', haupt: true, woche, tage, wechsel: [] }
|
||||
}
|
||||
|
||||
/** n aufeinanderfolgende Tage ab `von`; `abweichend` ersetzt einzelne Tage (datum → Eintrag ohne datum). */
|
||||
function tageListe(von, n, abweichend = {}, wochenplan = plan().woche) {
|
||||
const liste = []
|
||||
for (let i = 0; i < n; i += 1) {
|
||||
const d = new Date(Date.parse(`${von}T12:00:00Z`) + i * 86400000)
|
||||
const datum = d.toISOString().slice(0, 10)
|
||||
const iso = d.getUTCDay() === 0 ? 7 : d.getUTCDay()
|
||||
const regulaer = { art: 'regulaer', name: null, modus: wochenplan[iso - 1].modus, fenster: wochenplan[iso - 1].fenster }
|
||||
liste.push({ datum, ...(abweichend[datum] ?? regulaer) })
|
||||
}
|
||||
return liste
|
||||
}
|
||||
|
||||
test('tabelleDaten: heutiger Tag markiert, Abweichungen der nächsten 14 Tage', () => {
|
||||
const { tabelleDaten } = rein()
|
||||
const tage = tageListe('2026-10-05', 60, {
|
||||
'2026-10-09': { art: 'sondertag', name: 'Inventur', modus: 'zu', fenster: [] },
|
||||
'2026-10-12': { art: 'urlaub', name: 'Betriebsurlaub', modus: 'zu', fenster: [] },
|
||||
'2026-10-13': { art: 'urlaub', name: 'Betriebsurlaub', modus: 'zu', fenster: [] },
|
||||
'2026-10-17': { art: 'sondertag', name: 'Kurz', modus: 'zeiten', fenster: [['09:00', '11:00']] }, // heute + 12
|
||||
'2026-10-18': { art: 'feiertag', name: 'Testfeiertag', modus: 'zu', fenster: [] }, // heute + 13: noch drin
|
||||
'2026-10-19': { art: 'feiertag', name: 'Zu spaet', modus: 'zu', fenster: [] }, // heute + 14: schon draussen
|
||||
'2026-11-01': { art: 'feiertag', name: 'Allerheiligen', modus: 'zu', fenster: [] },
|
||||
})
|
||||
const t = norm(tabelleDaten(plan(tage), '2026-10-05'))
|
||||
assert.equal(t.woche.length, 7)
|
||||
assert.deepEqual(t.woche.map((z) => z.name), ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'])
|
||||
assert.deepEqual(t.woche.map((z) => z.tag), [1, 2, 3, 4, 5, 6, 7])
|
||||
assert.deepEqual(t.woche.map((z) => z.text), ['09:00–18:00', '09:00–18:00', '09:00–18:00', '09:00–18:00', '09:00–18:00', '09:00–13:00', 'geschlossen'])
|
||||
assert.deepEqual(t.woche.map((z) => z.heute), [true, false, false, false, false, false, false], '2026-10-05 ist ein Montag')
|
||||
assert.deepEqual(t.abweichungen14.map((a) => a.datum), ['2026-10-09', '2026-10-12', '2026-10-13', '2026-10-17', '2026-10-18'])
|
||||
assert.deepEqual(t.abweichungen14[0], { datum: '2026-10-09', art: 'sondertag', name: 'Inventur', modus: 'zu', fenster: [], text: 'geschlossen' })
|
||||
assert.equal(t.abweichungen14[3].text, '09:00–11:00')
|
||||
assert.equal(t.abweichungen14[4].name, 'Testfeiertag')
|
||||
|
||||
// ein anderer „heute“: Sonntag, 2026-10-11 → Fenster 11.10. bis 24.10.
|
||||
const sonntag = norm(tabelleDaten(plan(tage), '2026-10-11'))
|
||||
assert.deepEqual(sonntag.woche.map((z) => z.heute), [false, false, false, false, false, false, true])
|
||||
assert.deepEqual(sonntag.abweichungen14.map((a) => a.datum), ['2026-10-12', '2026-10-13', '2026-10-17', '2026-10-18', '2026-10-19'], 'Vergangenes (09.10.) raus, 19.10. rein, 01.11. noch nicht')
|
||||
})
|
||||
|
||||
test('tabelleDaten: Sonderfaelle (rund, vereinbarung, 24:00, regulaere Tage ohne Abweichung, kaputte Eingaben)', () => {
|
||||
const { tabelleDaten } = rein()
|
||||
const woche = plan().woche
|
||||
woche[0] = { tag: 1, modus: 'rund', fenster: [] }
|
||||
woche[1] = { tag: 2, modus: 'vereinbarung', fenster: [] }
|
||||
woche[2] = { tag: 3, modus: 'zeiten', fenster: [['18:00', '24:00']] }
|
||||
woche[3] = { tag: 4, modus: 'zeiten', fenster: [['09:00', '12:00'], ['14:00', '18:00']] }
|
||||
woche[4] = { tag: 5, modus: 'zu', fenster: [] }
|
||||
const p = { ...plan(tageListe('2026-10-05', 20, {}, woche)), woche: woche.filter((z) => z.tag !== 5) } // Freitag fehlt in den Daten: gilt als geschlossen
|
||||
const t = norm(tabelleDaten(p, '2026-10-06'))
|
||||
assert.deepEqual(t.woche.map((z) => z.text), ['rund um die Uhr', 'nach Vereinbarung', '18:00–00:00', '09:00–12:00, 14:00–18:00', 'geschlossen', '09:00–13:00', 'geschlossen'])
|
||||
assert.ok(t.woche.every((z) => !z.text.includes('24:00')))
|
||||
assert.deepEqual(t.woche.map((z) => z.heute), [false, true, false, false, false, false, false], 'Dienstag')
|
||||
assert.deepEqual(t.abweichungen14, [], 'regulaere Tage ohne Abweichung werden nicht aufgelistet')
|
||||
|
||||
// ein „regulaerer“ Tag, dessen Zeiten vom Wochenplan abweichen, zaehlt trotzdem als Abweichung
|
||||
const q = plan(tageListe('2026-10-05', 20, { '2026-10-08': { art: 'regulaer', name: null, modus: 'zeiten', fenster: [['10:00', '15:00']] } }))
|
||||
assert.deepEqual(norm(tabelleDaten(q, '2026-10-05')).abweichungen14.map((a) => a.datum), ['2026-10-08'])
|
||||
|
||||
// Feiertag mit den ueblichen Zeiten ist trotzdem eine Zeile (die Art ist die Abweichung)
|
||||
const r = plan(tageListe('2026-10-05', 20, { '2026-10-07': { art: 'feiertag', name: 'Fest', modus: 'zeiten', fenster: [['09:00', '18:00']] } }))
|
||||
assert.deepEqual(norm(tabelleDaten(r, '2026-10-05')).abweichungen14.map((a) => a.datum), ['2026-10-07'])
|
||||
|
||||
// kaputte Eingaben: sieben Zeilen „geschlossen“ ohne Markierung, keine Abweichungen, kein Fehler
|
||||
for (const kaputt of [undefined, null, {}, { woche: 'x', tage: 5 }]) {
|
||||
const k = norm(tabelleDaten(kaputt, '2026-10-05'))
|
||||
assert.equal(k.woche.length, 7)
|
||||
assert.ok(k.woche.every((z) => z.text === 'geschlossen'))
|
||||
assert.deepEqual(k.abweichungen14, [])
|
||||
}
|
||||
// ohne verwertbaren heutigen Tag: keine Markierung und keine Abweichungen
|
||||
for (const tag of [undefined, null, '', 'heute', '5.10.2026']) {
|
||||
const k = norm(tabelleDaten(p, tag))
|
||||
assert.ok(k.woche.every((z) => z.heute === false), String(tag))
|
||||
assert.deepEqual(k.abweichungen14, [])
|
||||
}
|
||||
})
|
||||
|
||||
test('Abweichungen: aufeinanderfolgende gleiche Tage in einer Zeile, Beschriftung mit Wochentag und Datum', () => {
|
||||
const { tabelleDaten, gruppiereAbweichungen, abweichungsZeile } = rein()
|
||||
const urlaub = { art: 'urlaub', name: 'Betriebsurlaub', modus: 'zu', fenster: [] }
|
||||
const tage = tageListe('2026-10-05', 20, {
|
||||
'2026-10-06': urlaub, '2026-10-07': urlaub, '2026-10-08': urlaub,
|
||||
'2026-10-10': { art: 'sondertag', name: 'Hoffest', modus: 'zeiten', fenster: [['10:00', '16:00']] },
|
||||
'2026-10-12': { art: 'feiertag', name: 'Feiertag A', modus: 'zu', fenster: [] },
|
||||
'2026-10-13': { art: 'feiertag', name: 'Feiertag B', modus: 'zu', fenster: [] }, // anderer Name: eigene Zeile
|
||||
'2026-10-15': { art: 'urlaub', name: null, modus: 'zu', fenster: [] },
|
||||
})
|
||||
const gruppen = norm(gruppiereAbweichungen(tabelleDaten(plan(tage), '2026-10-05').abweichungen14))
|
||||
assert.deepEqual(gruppen.map((g) => [g.von, g.bis]), [
|
||||
['2026-10-06', '2026-10-08'], ['2026-10-10', '2026-10-10'], ['2026-10-12', '2026-10-12'], ['2026-10-13', '2026-10-13'], ['2026-10-15', '2026-10-15'],
|
||||
])
|
||||
assert.deepEqual(norm(abweichungsZeile(gruppen[0])), { datum: 'Dienstag, 06.10. – Donnerstag, 08.10.', text: 'Betriebsurlaub: geschlossen' })
|
||||
assert.deepEqual(norm(abweichungsZeile(gruppen[1])), { datum: 'Samstag, 10.10.', text: 'Hoffest: 10:00–16:00' })
|
||||
assert.deepEqual(norm(abweichungsZeile(gruppen[4])), { datum: 'Donnerstag, 15.10.', text: 'Betriebsurlaub: geschlossen' }, 'ohne Namen die Art als Beschriftung')
|
||||
assert.deepEqual(norm(abweichungsZeile({ von: '2026-11-01', bis: '2026-11-01', art: 'feiertag', name: null, text: 'geschlossen' })), { datum: 'Sonntag, 01.11.', text: 'Feiertag: geschlossen' })
|
||||
assert.deepEqual(norm(abweichungsZeile({ von: '2026-11-02', bis: '2026-11-02', art: 'sondertag', name: null, text: 'geschlossen' })).text, 'Sondertag: geschlossen')
|
||||
assert.deepEqual(norm(gruppiereAbweichungen([])), [])
|
||||
assert.deepEqual(norm(gruppiereAbweichungen(undefined)), [])
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Demo-Seite
|
||||
|
||||
test('Demo-Seite: noindex, Skript per defer mit data-site, alle drei Platzhalter-Modi mit Ersatz-HTML', () => {
|
||||
const demo = lies('public/embed/zeiten-demo.html')
|
||||
assert.match(demo, /<meta name="robots" content="noindex"/)
|
||||
assert.match(demo, /<script src="\/embed\/zeiten\.js" data-site="[a-z2-7]{16}" defer><\/script>/)
|
||||
for (const modus of ['tabelle', 'status', 'badge']) {
|
||||
assert.match(demo, new RegExp(`<[a-z]+ data-webklar-zeiten data-modus="${modus}"`), `Platzhalter ${modus} fehlt`)
|
||||
}
|
||||
assert.match(demo, /data-plan="/, 'ein Platzhalter mit data-plan')
|
||||
assert.match(demo, /\d\d:\d\d\s*(?:–|–)\s*\d\d:\d\d/, 'Ersatz-HTML mit Zeiten fehlt')
|
||||
assert.match(demo, /<table>[\s\S]*<caption>[\s\S]*Montag[\s\S]*Sonntag/, 'Ersatz-Tabelle fehlt')
|
||||
})
|
||||
Reference in new issue
Block a user