Files
Webklar-Kundenbereich/test/analyseInsights.test.js
T
ClaudeandClaude Opus 5.5 0db59d38e9 Analyse: Zeitraum = abgeschlossene Tage, "Heute bisher" getrennt, Feinschliff
Final-Review-Fixwelle der Cockpit-Übersicht:

- F1 Collector: Der Zeitraum sind die range Kalendertage bis gestern (Berliner Tag),
  der Vorzeitraum die range Tage davor; heute steht getrennt in today { day, pv, visitors,
  contacts }. Kein Einbruch mehr am rechten Rand von Diagramm und Mini-Verläufen,
  kein falsches "x % weniger" tagsüber. Portal: weekdayStats bekommt die ganze Reihe
  (completedDays entfällt), "Vergleich ab" = firstDay + 2 x range, Kopfzeile
  "Analyse aktiv · Heute bisher: N Aufrufe · Stand: HH:MM Uhr" (alter Collector ohne today
  bleibt ohne "Heute bisher").
- F2 "direkt kontaktiert" zählt nur Telefon, E-Mail und WhatsApp (auch im Vorzeitraum),
  Links nach außen nicht.
- F3 Mini-Verlauf "Ø Zeit pro Seite": Tage ohne Messung sind Lücken statt 0-Tälern.
- F4 Fehlerfarbe beim Ausschalten rot, --an-faint auf 55 %, Achsenschrift per
  Container-Abfragen auf 11-12 px (y-Rand wächst mit der Stellenzahl, x-Beschriftungen mit
  Mindestabstand), Ranglisten-Anteile an allen Seitenaufrufen, Tippflächen bis 880 px
  mindestens 40 px hoch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:34:43 +00:00

290 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 { delta, fmtDuration, fmtNum, dayLabel, periodLabel, compareFrom, contactRate, refName, weekdayStats, summarize, chartModel } from '../public/analyse-view.js'
// Testdaten: eine vollstaendige Statistik-Antwort mit 30 Tagen (Form wie analytics/src/stats.js)
const isoDay = (from, n) => new Date(Date.UTC(+from.slice(0, 4), +from.slice(5, 7) - 1, +from.slice(8, 10) + n)).toISOString().slice(0, 10)
const days = (from, n, pv) => Array.from({ length: n }, (_, i) => ({ day: isoDay(from, i), pv: typeof pv === 'function' ? pv(i, isoDay(from, i)) : pv }))
const t = { pv: 6930, visitors: 2410, contacts: 188, consentRate: 0.61, engaged: 1800, avgSec: 108, avgScroll: 64 }
const prev = {
from: '2026-08-06', to: '2026-09-04', complete: true,
totals: { pv: 6080, visitors: 2200, contacts: 143, consentRate: 0.6, engaged: 1500, avgSec: 114, avgScroll: 60 },
series: days('2026-08-06', 30, 203),
contacts: [{ key: 'tel', count: 80 }, { key: 'mail', count: 40 }, { key: 'wa', count: 23 }],
}
const stats = (over = {}) => ({
range: 30, from: '2026-09-05', to: '2026-10-04', firstDay: '2026-07-01',
today: { day: '2026-10-05', pv: 42, visitors: 20, contacts: 1 },
totals: t,
series: days('2026-09-05', 30, 231),
pages: [{ key: '/', count: 3200 }, { key: '/preise', count: 1310 }, { key: '/kontakt', count: 900 }],
refs: [{ key: 'google.com', count: 880 }, { key: '', count: 700 }, { key: 'instagram.com', count: 400 }, { key: 'l.facebook.com', count: 290 }],
countries: [{ key: 'DE', count: 6000 }],
devices: [{ key: 'm', count: 4590 }, { key: 'd', count: 2200 }, { key: 't', count: 140 }],
contacts: [{ key: 'tel', count: 96 }, { key: 'mail', count: 52 }, { key: 'wa', count: 30 }, { key: 'out', count: 10 }],
banner: { shown: 3950, accept: 2410, reject: 1540 },
previous: prev,
...over,
})
test('delta: Prozent ab 100, sonst absolut, flat, Sekunden, null', () => {
assert.deepEqual(delta(6930, 6080), { dir: 'up', text: '▲ 14 %', aria: 'plus 14 Prozent gegenüber dem Zeitraum davor' })
assert.equal(delta(188, 143).text, '▲ 31 %') // beide ≥ 100 → Prozent
assert.equal(delta(45, 33).text, '▲ 12')
assert.equal(delta(30, 33).text, '▼ 3')
assert.deepEqual(delta(7, 7), { dir: 'flat', text: '± 0', aria: 'unverändert gegenüber dem Zeitraum davor' })
assert.equal(delta(1004, 1000).text, '± 0 %') // |0,4 %| < 1
assert.equal(delta(1004, 1000).dir, 'flat')
assert.equal(delta(108, 114, { kind: 'sec' }).text, '▼ 6 s')
assert.equal(delta(200, 135, { kind: 'sec' }).text, '▲ 1:05')
assert.equal(delta(null, 3), null)
assert.equal(delta(3, undefined), null)
assert.equal(delta('3', 4), null)
})
test('delta: Richtung und aria in Wörtern', () => {
assert.deepEqual(delta(5000, 6000), { dir: 'down', text: '▼ 17 %', aria: 'minus 17 Prozent gegenüber dem Zeitraum davor' })
assert.equal(delta(45, 33).aria, 'plus 12 gegenüber dem Zeitraum davor')
assert.equal(delta(30, 33).aria, 'minus 3 gegenüber dem Zeitraum davor')
assert.equal(delta(108, 114, { kind: 'sec' }).aria, 'minus 6 Sekunden gegenüber dem Zeitraum davor')
assert.equal(delta(200, 135, { kind: 'sec' }).aria, 'plus 1 Minute 5 Sekunden gegenüber dem Zeitraum davor')
assert.equal(delta(240, 120, { kind: 'sec' }).aria, 'plus 2 Minuten gegenüber dem Zeitraum davor')
})
test('fmtDuration, dayLabel, periodLabel, fmtNum', () => {
assert.equal(fmtDuration(108), '1:48'); assert.equal(fmtDuration(7), '0:07'); assert.equal(fmtDuration(null), '–')
assert.equal(fmtDuration(NaN), '–'); assert.equal(fmtDuration(-3), '–'); assert.equal(fmtDuration(undefined), '–')
assert.equal(dayLabel('2026-09-19'), 'Sa., 19.09.')
assert.equal(dayLabel('2026-09-20'), 'So., 20.09.')
assert.equal(periodLabel('2026-09-05', '2026-10-04'), '5. Sep. – 4. Okt.')
assert.equal(periodLabel('2026-03-01', '2026-06-30'), '1. März – 30. Juni')
assert.equal(periodLabel('', ''), '')
assert.equal(periodLabel('2026-09-05', ''), '')
assert.equal(fmtNum(6930), '6.930')
assert.equal(fmtNum(0), '0')
assert.equal(fmtNum(1234567), '1.234.567')
})
test('compareFrom: ok, Datum bei neuer Website, nichts ohne previous', () => {
assert.deepEqual(compareFrom(stats()), { ok: true, from: '' })
assert.deepEqual(compareFrom(stats({ firstDay: '2026-09-20', previous: { ...prev, complete: false } })), { ok: false, from: '19.11.' }) // range 30: +60 Tage (2 × range, der erste Tag mit vollem Vorzeitraum)
assert.deepEqual(compareFrom(stats({ range: 7, firstDay: '2026-09-28', previous: { ...prev, complete: false } })), { ok: false, from: '12.10.' }) // range 7: +14 Tage
assert.deepEqual(compareFrom(stats({ firstDay: '', previous: { ...prev, complete: false } })), { ok: false, from: '' })
assert.deepEqual(compareFrom({ totals: { pv: 50 }, series: [] }), { ok: false, from: '' })
assert.equal(compareFrom(stats({ previous: { ...prev, totals: { ...prev.totals, pv: 19 } } })).ok, false)
assert.equal(compareFrom(stats({ previous: { ...prev, totals: { ...prev.totals, pv: 20 } } })).ok, true)
})
test('contactRate und refName', () => {
assert.equal(contactRate(188, 2410), 7.8); assert.equal(contactRate(5, 29), null)
assert.equal(contactRate(0, 30), 0); assert.equal(contactRate(null, 100), null)
assert.equal(refName('www.google.de'), 'Google'); assert.equal(refName('l.facebook.com'), 'Facebook'); assert.equal(refName(''), 'Direkt oder unbekannt'); assert.equal(refName('blog.example'), 'blog.example')
assert.equal(refName('google.com'), 'Google'); assert.equal(refName('google.co.uk'), 'Google')
assert.equal(refName('www.bing.com'), 'Bing'); assert.equal(refName('l.instagram.com'), 'Instagram')
assert.equal(refName('m.facebook.com'), 'Facebook'); assert.equal(refName('facebook.com'), 'Facebook')
assert.equal(refName('duckduckgo.com'), 'DuckDuckGo'); assert.equal(refName('www.ecosia.org'), 'Ecosia')
assert.equal(refName('notgoogle.com'), 'notgoogle.com'); assert.equal(refName('google.evil.de'), 'google.evil.de')
assert.equal(refName('Google.DE'), 'Google')
})
test('weekdayStats: stärkster Wochentag, Spitzentag, < 7 Tage null', () => {
// 05.09.2026 ist ein Samstag: 5 Samstage mit 20, alle anderen 10 → Schnitt 11,67; Samstag 20 ≥ 1,2 × 11,67, aber < 1,8 × 11,67 (kein Spitzentag)
const sat = (i, day) => (new Date(day + 'T00:00:00Z').getUTCDay() === 6 ? 20 : 10)
const w = weekdayStats(days('2026-09-05', 30, sat), '')
assert.equal(w.best.weekday, 6); assert.equal(w.best.name, 'Samstag'); assert.equal(w.best.avg, 20)
assert.ok(Math.abs(w.avg - 350 / 30) < 1e-9)
assert.equal(w.peak, null)
// ein Tag mit dem 3-Fachen des Schnitts → Spitzentag
const spike = (i, day) => (day === '2026-09-19' ? 60 : 10)
const p = weekdayStats(days('2026-09-05', 30, spike), '')
assert.deepEqual(p.peak, { day: '2026-09-19', pv: 60 })
assert.equal(p.best.name, 'Samstag')
// gleichmäßig: weder stärkster Tag noch Spitzentag
assert.deepEqual(weekdayStats(days('2026-09-05', 30, 10), ''), { best: null, avg: 10, peak: null })
// weniger als 7 Tage → null; firstDay schneidet Tage davor ab
assert.equal(weekdayStats(days('2026-09-05', 6, 10), ''), null)
assert.notEqual(weekdayStats(days('2026-09-05', 30, 10), '2026-09-28'), null) // 28.09.–04.10. = genau 7 Tage
})
test('weekdayStats: firstDay zählt nur Tage ab firstDay', () => {
assert.equal(weekdayStats(days('2026-09-05', 30, 10), '2026-09-29'), null) // nur 6 Tage (29.09.–04.10.)
const w = weekdayStats(days('2026-09-05', 30, (i, day) => (day < '2026-09-20' ? 1000 : 10)), '2026-09-20')
assert.equal(w.avg, 10) // die Tage davor (1000) bleiben außen vor
assert.equal(weekdayStats(null, ''), null)
})
test('summarize: Wachstum mit Prozent', () => {
const s = summarize(stats())
assert.equal(s.lead, 'Deine Website wurde in den letzten 30 Tagen <b>6.930-mal</b> aufgerufen – <b>14 % mehr</b> als in den 30 Tagen davor.')
assert.equal(s.items[0].icon, 'contact')
assert.equal(s.items[0].html, '178-mal hat dich jemand direkt kontaktiert, meist per Telefon (96). Das sind 35 mehr als zuvor.') // Telefon + E-Mail + WhatsApp: 96 + 52 + 30 gegen 80 + 40 + 23
assert.ok(s.items.length <= 4)
})
test('summarize: Rückgang, etwa gleich, kleine Zahlen absolut', () => {
const lead = (totalsPv, prevPv) => summarize(stats({ totals: { ...t, pv: totalsPv }, previous: { ...prev, totals: { ...prev.totals, pv: prevPv } } })).lead
assert.equal(lead(6930, 7615), 'Deine Website wurde in den letzten 30 Tagen <b>6.930-mal</b> aufgerufen – <b>9 % weniger</b> als in den 30 Tagen davor.')
assert.equal(lead(6930, 6800), 'Deine Website wurde in den letzten 30 Tagen <b>6.930-mal</b> aufgerufen – etwa so oft wie in den 30 Tagen davor.')
assert.equal(lead(80, 68), 'Deine Website wurde in den letzten 30 Tagen <b>80-mal</b> aufgerufen – <b>12 Aufrufe mehr</b> als in den 30 Tagen davor.')
assert.equal(lead(60, 75), 'Deine Website wurde in den letzten 30 Tagen <b>60-mal</b> aufgerufen – <b>15 Aufrufe weniger</b> als in den 30 Tagen davor.')
assert.equal(lead(84, 83), 'Deine Website wurde in den letzten 30 Tagen <b>84-mal</b> aufgerufen – etwa so oft wie in den 30 Tagen davor.') // unter 5 % gilt auch bei kleinen Zahlen
assert.match(lead(32, 31), /aufgerufen – etwa so oft wie in den 30 Tagen davor\.$/)
assert.equal(lead(40, 30), 'Deine Website wurde in den letzten 30 Tagen <b>40-mal</b> aufgerufen – <b>10 Aufrufe mehr</b> als in den 30 Tagen davor.')
assert.equal(lead(31, 20), 'Deine Website wurde in den letzten 30 Tagen <b>31-mal</b> aufgerufen – <b>11 Aufrufe mehr</b> als in den 30 Tagen davor.')
assert.equal(lead(80, 80), 'Deine Website wurde in den letzten 30 Tagen <b>80-mal</b> aufgerufen – etwa so oft wie in den 30 Tagen davor.')
assert.equal(lead(7200, 6857), 'Deine Website wurde in den letzten 30 Tagen <b>7.200-mal</b> aufgerufen – <b>5 % mehr</b> als in den 30 Tagen davor.') // genau 5 % gilt noch als Veränderung
})
test('summarize: ohne Vergleich und neu gestartet', () => {
// previous fehlt: '…in den letzten 30 Tagen <b>X-mal</b> aufgerufen.'
const { previous, ...ohne } = stats()
assert.equal(summarize(ohne).lead, 'Deine Website wurde in den letzten 30 Tagen <b>6.930-mal</b> aufgerufen.')
// Vorzeitraum unvollständig, firstDay vor dem Zeitraum: kein Vergleich, aber kein „Seit dem Start“
assert.equal(summarize(stats({ firstDay: '2026-08-20', previous: { ...prev, complete: false } })).lead, 'Deine Website wurde in den letzten 30 Tagen <b>6.930-mal</b> aufgerufen.')
// Vorzeitraum zu klein: kein Vergleich
assert.equal(summarize(stats({ previous: { ...prev, totals: { ...prev.totals, pv: 12 } } })).lead, 'Deine Website wurde in den letzten 30 Tagen <b>6.930-mal</b> aufgerufen.')
// firstDay im Zeitraum (complete false): 'Seit dem Start der Analyse am 20.09. wurde deine Website <b>X-mal</b> aufgerufen.'
const neu = summarize(stats({ firstDay: '2026-09-20', previous: { ...prev, complete: false } }))
assert.equal(neu.lead, 'Seit dem Start der Analyse am 20.09. wurde deine Website <b>6.930-mal</b> aufgerufen.')
// auch das Startdatum wird maskiert, falls der Collector je Unsinn liefert
assert.ok(!summarize(stats({ firstDay: '<i>x</i>', previous: { ...prev, complete: false } })).lead.includes('<i>'))
// ohne Vergleich kein „Das sind … mehr als zuvor“
assert.equal(neu.items[0].html, '178-mal hat dich jemand direkt kontaktiert, meist per Telefon (96).')
// 7 Tage: „7 Tagen“
assert.match(summarize(stats({ range: 7 })).lead, /in den letzten 7 Tagen .* als in den 7 Tagen davor\.$/)
})
test('summarize: zu wenig Daten → null; keine Kontakte → Hinweis nur ab 30 Besuchern', () => {
assert.equal(summarize(stats({ totals: { ...t, pv: 29 } })), null)
assert.equal(summarize(null), null)
assert.notEqual(summarize(stats({ totals: { ...t, pv: 30 } })), null)
// contacts 0, visitors 40: 'Von den Besuchern mit Zustimmung hat noch niemand über die Website angerufen oder geschrieben.'
const keine = summarize(stats({ totals: { ...t, contacts: 0, visitors: 40 }, contacts: [] }))
assert.deepEqual(keine.items[0], { icon: 'contact', html: 'Von den Besuchern mit Zustimmung hat noch niemand über die Website angerufen oder geschrieben.' })
const wenig = summarize(stats({ totals: { ...t, contacts: 0, visitors: 29 }, contacts: [] }))
assert.notEqual(wenig.items[0].icon, 'contact')
})
test('summarize: Kontakte – Vergleichssatz und häufigste Art, Links nach außen zählen nicht mit', () => {
const tel = (n) => ({ key: 'tel', count: n })
const html = (kinds, before) => summarize(stats({
totals: { ...t, contacts: kinds.reduce((a, k) => a + k.count, 0) },
contacts: kinds,
previous: { ...prev, contacts: before, totals: { ...prev.totals, contacts: before.reduce((a, k) => a + k.count, 0) } },
})).items[0].html
assert.equal(html([tel(96)], [tel(108)]), '96-mal hat dich jemand direkt kontaktiert, meist per Telefon (96). Das sind 12 weniger als zuvor.')
assert.equal(html([{ key: 'mail', count: 100 }], [{ key: 'mail', count: 100 }]), '100-mal hat dich jemand direkt kontaktiert, meist per E-Mail (100). Genauso oft wie zuvor.')
assert.equal(html([{ key: 'wa', count: 5 }], []), '5-mal hat dich jemand direkt kontaktiert, meist per WhatsApp (5).') // Vorwert 0: kein Vergleichssatz
assert.equal(html([tel(5)], [tel(2)]), '5-mal hat dich jemand direkt kontaktiert, meist per Telefon (5). Das sind 3 mehr als zuvor.')
// Links nach außen sind Klicks auf andere Seiten, kein Kontakt: weder in der Zahl noch als häufigste Art, auch nicht im Vorzeitraum
assert.equal(html([{ key: 'out', count: 300 }, tel(40)], [{ key: 'out', count: 900 }, tel(40)]), '40-mal hat dich jemand direkt kontaktiert, meist per Telefon (40). Genauso oft wie zuvor.')
assert.equal(html([{ key: 'out', count: 300 }, tel(40)], [{ key: 'out', count: 900 }]), '40-mal hat dich jemand direkt kontaktiert, meist per Telefon (40).')
// nur Links nach außen (Besucher ≥ 30): wie „keine Kontakte“
const nurOut = summarize(stats({ totals: { ...t, contacts: 300, visitors: 400 }, contacts: [{ key: 'out', count: 300 }] }))
assert.deepEqual(nurOut.items[0], { icon: 'contact', html: 'Von den Besuchern mit Zustimmung hat noch niemand über die Website angerufen oder geschrieben.' })
// Collector ohne Kontaktliste: kein Satz, der etwas Falsches behauptet
const { contacts, ...ohneListe } = stats({ totals: { ...t, contacts: 0, visitors: 400 } })
assert.notEqual(summarize(ohneListe).items[0].icon, 'contact')
})
test('summarize: Herkunft, Gerät, Seite und Reihenfolge, höchstens 4', () => {
// refs google.com 880 von 2270 → 'Die meisten, die über einen Link kamen, kommen von Google (880).'
const s = summarize(stats())
const byIcon = Object.fromEntries(s.items.map((i) => [i.icon, i.html]))
assert.equal(byIcon.source, 'Die meisten, die über einen Link kamen, kommen von Google (880).')
// devices m 4590/6930 → '66 % der Aufrufe kommen vom Handy.'
assert.equal(byIcon.device, '66 % der Aufrufe kommen vom Handy.')
// pages / dann /preise → 'Am meisten angesehen: die Startseite, danach /preise (1.310).'
assert.equal(byIcon.page, 'Am meisten angesehen: die Startseite, danach /preise (1.310).')
assert.deepEqual(s.items.map((i) => i.icon), ['contact', 'source', 'device', 'page'])
// Computer, Seite ohne Startseite
const pc = summarize(stats({ devices: [{ key: 'd', count: 600 }, { key: 'm', count: 300 }, { key: 't', count: 100 }], pages: [{ key: '/preise', count: 1310 }, { key: '/', count: 900 }] }))
assert.equal(pc.items.find((i) => i.icon === 'device').html, '60 % der Aufrufe kommen vom Computer.')
assert.equal(pc.items.find((i) => i.icon === 'page').html, 'Am meisten angesehen: /preise (1.310 Aufrufe).')
// nur Startseite
assert.equal(summarize(stats({ pages: [{ key: '/', count: 50 }] })).items.find((i) => i.icon === 'page').html, 'Am meisten angesehen: die Startseite (50 Aufrufe).')
// Gerät unter 55 %, Herkunft unter 20 %: beide Hinweise entfallen
const flach = summarize(stats({ devices: [{ key: 'm', count: 50 }, { key: 'd', count: 50 }], refs: [{ key: 'google.com', count: 10 }, { key: '', count: 90 }] }))
assert.ok(!flach.items.some((i) => i.icon === 'device' || i.icon === 'source'))
// Direkt (leerer Host) zählt nie als Quelle, auch wenn er vorne steht
const direkt = summarize(stats({ refs: [{ key: '', count: 900 }, { key: 'google.com', count: 300 }] }))
assert.equal(direkt.items.find((i) => i.icon === 'source').html, 'Die meisten, die über einen Link kamen, kommen von Google (300).')
// Samstage 2× → Wochentag-Hinweis; alle 5 Bedingungen wahr → Seite fällt weg
const sat = (i, day) => (new Date(day + 'T00:00:00Z').getUTCDay() === 6 ? 20 : 10)
const voll = summarize(stats({ series: days('2026-09-05', 30, sat) }))
assert.deepEqual(voll.items.map((i) => i.icon), ['contact', 'day', 'source', 'device'])
assert.equal(voll.items[1].html, 'Samstag ist im Schnitt dein stärkster Tag.')
})
test('summarize: Wochentag und Spitzentag in einem Hinweis', () => {
// Samstage 20, Spitzentag Sa., 19.09. mit 371, sonst 10: beides trifft zu
const both = (i, day) => (day === '2026-09-19' ? 371 : new Date(day + 'T00:00:00Z').getUTCDay() === 6 ? 40 : 10)
const s = summarize(stats({ series: days('2026-09-05', 30, both) }))
assert.equal(s.items.find((i) => i.icon === 'day').html, 'Samstag ist im Schnitt dein stärkster Tag. Spitzentag war Sa., 19.09. mit 371 Aufrufen.')
// nur Spitzentag am Mittwoch 23.09.: die anderen Mittwoche (0) gleichen den Wochentagsschnitt aus (alle Wochentage Ø 25)
const spike = (i, day) => (day === '2026-09-23' ? 100 : new Date(day + 'T00:00:00Z').getUTCDay() === 3 ? 0 : 25)
const p = summarize(stats({ series: days('2026-09-05', 30, spike) }))
assert.equal(p.items.find((i) => i.icon === 'day').html, 'Spitzentag war Mi., 23.09. mit 100 Aufrufen.')
// weniger als 70 Aufrufe oder weniger als 7 Tage: kein Wochentag-Hinweis
assert.ok(!summarize(stats({ totals: { ...t, pv: 69 }, series: days('2026-09-05', 30, spike) })).items.some((i) => i.icon === 'day'))
assert.ok(!summarize(stats({ firstDay: '2026-09-30', series: days('2026-09-05', 30, spike) })).items.some((i) => i.icon === 'day'))
})
test('summarize: Wochentag-Hinweis zählt alle Tage der Reihe (alle sind abgeschlossen), auch in der 7-Tage-Ansicht', () => {
const isSat = (day) => new Date(day + 'T00:00:00Z').getUTCDay() === 6
const dayHtml = (s) => s.items.find((i) => i.icon === 'day')?.html
// Reihe 04.09.–03.10.2026 (Ende = gestern, heute steht getrennt in `today`); der letzte Tag, Samstag 03.10., zählt mit
const last = '2026-10-03'
const base = { from: '2026-09-04', to: last, today: { day: '2026-10-04', pv: 5, visitors: 3, contacts: 0 } }
assert.equal(dayHtml(summarize(stats({ ...base, series: days('2026-09-04', 30, (i, day) => (isSat(day) ? 280 : 200)) }))), 'Samstag ist im Schnitt dein stärkster Tag.')
// Spitzentag am letzten Tag der Reihe wird erkannt
const spike = days('2026-09-04', 30, (i, day) => (day === last ? 400 : 100))
assert.equal(dayHtml(summarize(stats({ ...base, series: spike }))), 'Samstag ist im Schnitt dein stärkster Tag. Spitzentag war Sa., 03.10. mit 400 Aufrufen.')
// 7-Tage-Ansicht: genau 7 abgeschlossene Tage reichen für den Hinweis (28.09.–04.10., Sa. 03.10.)
const week = days('2026-09-28', 7, (i, day) => (isSat(day) ? 500 : 100))
assert.equal(dayHtml(summarize(stats({ range: 7, from: '2026-09-28', to: '2026-10-04', series: week }))), 'Samstag ist im Schnitt dein stärkster Tag. Spitzentag war Sa., 03.10. mit 500 Aufrufen.')
// weniger als 7 Tage (neue Website): weiter kein Hinweis
assert.equal(dayHtml(summarize(stats({ range: 7, from: '2026-09-28', to: '2026-10-04', firstDay: '2026-09-29', series: week }))), undefined)
// alter Collector ohne `today`: derselbe Hinweis, kein Absturz
const { today, ...alt } = stats({ ...base, series: days('2026-09-04', 30, (i, day) => (isSat(day) ? 280 : 200)) })
assert.equal(dayHtml(summarize(alt)), 'Samstag ist im Schnitt dein stärkster Tag.')
})
test('summarize: Pfade und Hosts werden maskiert', () => {
const s = summarize(stats({ pages: [{ key: '/<img src=x>', count: 900 }], refs: [{ key: 'a"b.de', count: 900 }] }))
assert.ok(!JSON.stringify(s).includes('<img'))
assert.ok(JSON.stringify(s).includes('&lt;img'))
assert.equal(s.items.find((i) => i.icon === 'source').html, 'Die meisten, die über einen Link kamen, kommen von a&quot;b.de (900).')
assert.equal(s.items.find((i) => i.icon === 'page').html, 'Am meisten angesehen: /&lt;img src=x&gt; (900 Aufrufe).')
// auch an zweiter Stelle hinter der Startseite
const z = summarize(stats({ pages: [{ key: '/', count: 900 }, { key: '/<script>', count: 100 }] }))
assert.ok(!JSON.stringify(z).includes('<script>'))
})
test('chartModel mit compare: gemeinsame Skala, prevLine', () => {
const cur = days('2026-09-05', 5, 10)
const before = days('2026-08-06', 5, (i) => (i === 2 ? 400 : 20))
const m = chartModel(cur, { compare: before })
assert.equal(m.top, 500) // über beide Reihen (Maximum 400)
assert.equal(m.points.length, 5)
assert.deepEqual(m.points.map((p) => p.prev), [20, 20, 400, 20, 20])
assert.match(m.prevLine, /^M[\d. ]+( L[\d. ]+){4}$/)
// nach Tagesindex ausgerichtet: gleiche x-Werte wie die aktuelle Linie, y am Vorwert
const first = m.prevLine.split(' L')[0].slice(1).split(' ').map(Number)
assert.equal(first[0], m.points[0].x)
const mid = m.prevLine.split(' L')[2].split(' ').map(Number)
assert.equal(mid[0], m.points[2].x)
assert.ok(mid[1] < first[1]) // 400 liegt höher als 20
// ohne compare: wie bisher, prevLine leer
const o = chartModel(cur)
assert.equal(o.top, 10)
assert.equal(o.prevLine, '')
assert.ok(o.points.every((p) => p.prev === null))
assert.equal(o.line, chartModel(cur, { compare: [] }).line)
assert.equal(chartModel(cur, { compare: [] }).prevLine, '')
// kürzerer Vergleich: nur die vorhandenen Tage, Rest null
const k = chartModel(cur, { compare: before.slice(0, 2) })
assert.deepEqual(k.points.map((p) => p.prev), [20, 20, null, null, null])
assert.match(k.prevLine, /^M[\d. ]+ L[\d. ]+$/)
})