Files
Webklar-Kundenbereich/test/analyseInsights.test.js
T

285 lines
21 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',
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: '18.11.' }) // range 30: +59 Tage
assert.deepEqual(compareFrom(stats({ range: 7, firstDay: '2026-09-28', previous: { ...prev, complete: false } })), { ok: false, from: '11.10.' }) // range 7: +13 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, '188-mal hat dich jemand direkt kontaktiert, meist per Telefon (96). Das sind 45 mehr als zuvor.')
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, '188-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', () => {
const html = (contactsNow, contactsBefore, kinds) => summarize(stats({
totals: { ...t, contacts: contactsNow },
contacts: kinds,
previous: { ...prev, totals: { ...prev.totals, contacts: contactsBefore } },
})).items[0].html
assert.equal(html(188, 200, [{ key: 'tel', count: 96 }]), '188-mal hat dich jemand direkt kontaktiert, meist per Telefon (96). Das sind 12 weniger als zuvor.')
assert.equal(html(188, 188, [{ key: 'mail', count: 100 }]), '188-mal hat dich jemand direkt kontaktiert, meist per E-Mail (100). Genauso oft wie zuvor.')
assert.equal(html(5, 0, [{ key: 'wa', count: 3 }]), '5-mal hat dich jemand direkt kontaktiert, meist per WhatsApp (3).') // Vorwert 0: kein Vergleichssatz
assert.equal(html(5, 2, [{ key: 'out', count: 3 }]), '5-mal hat dich jemand direkt kontaktiert, meist über einen Link nach außen (3). Das sind 3 mehr als zuvor.')
})
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: der heutige, unvollständige Tag zählt beim Wochentag-Hinweis nicht mit', () => {
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, heute = Samstag 03.10. mit erst 5 Aufrufen; Samstage sonst 280, alle anderen Tage 200
const today = '2026-10-03'
const sat = (i, day) => (day === today ? 5 : isSat(day) ? 280 : 200)
const series = days('2026-09-04', 30, sat)
const base = { from: '2026-09-04', to: today, series }
// zählte der Tag mit, läge der Samstag-Schnitt (225) unter 1,2 × Gesamtschnitt (204) → kein Hinweis
assert.equal(weekdayStats(series, '').best, null)
// ohne den heutigen Tag: Samstag Ø 280 ≥ 1,2 × 211, stärkster Tag; Spitzentag nein (280 < 1,8 × 211)
assert.equal(dayHtml(summarize(stats(base))), 'Samstag ist im Schnitt dein stärkster Tag.')
// Spitzentag: Mi., 16.09. mit 365 (zählte heute mit, wäre 365 ≥ 1,8 × 199 ein Spitzentag; ohne heute 365 < 1,8 × 205,7)
const wed = days('2026-09-04', 30, (i, day) => (day === today ? 5 : day === '2026-09-16' ? 365 : 200))
assert.ok(weekdayStats(wed, '').peak)
assert.equal(dayHtml(summarize(stats({ ...base, series: wed }))), undefined)
// fehlt `to`, entfällt der letzte Tag trotzdem
assert.equal(dayHtml(summarize(stats({ ...base, to: undefined }))), 'Samstag ist im Schnitt dein stärkster Tag.')
// endet die Reihe nicht auf `to`, ist der letzte Tag ein abgeschlossener Tag und zählt mit
assert.equal(dayHtml(summarize(stats({ ...base, to: '2026-10-04' }))), undefined)
// 7-Tage-Ansicht: nur 6 abgeschlossene Tage → kein Wochentag-Hinweis, auch bei deutlichem Muster
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 }))), undefined)
assert.ok(weekdayStats(week, '')) // mit allen 7 Tagen wäre es ein Ergebnis gewesen
})
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. ]+$/)
})