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>
This commit is contained in:
1 parent
eadb768d1a
commit
0db59d38e9
11 files changed
+376
-135
No files matched your search
+78
-12
@@ -16,6 +16,7 @@ const prev = {
|
||||
}
|
||||
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: rich(days('2026-09-05', 30, (i) => (i === 14 ? 371 : 231))),
|
||||
pages: [{ key: '/', count: 3200 }, { key: '/preise', count: 1310 }, { key: '/kontakt', count: 900 }],
|
||||
@@ -76,7 +77,7 @@ test('summaryHtml: Satz und Hinweise mit Symbol; Leerzustand unter 30 Aufrufen',
|
||||
assert.match(h, /Deine Website wurde in den letzten 30 Tagen <b>6\.930-mal<\/b> aufgerufen/)
|
||||
assert.equal(count(h, /<li>/g), 4); assert.match(h, /<svg class="an-ico"/)
|
||||
assert.equal(count(h, /aria-hidden="true"><\/svg>/g), 0) // alle Symbole der Hinweise sind belegt
|
||||
assert.match(h, /188-mal hat dich jemand direkt kontaktiert, meist per Telefon \(96\)\./)
|
||||
assert.match(h, /178-mal hat dich jemand direkt kontaktiert, meist per Telefon \(96\)\./) // ohne die 10 Links nach außen
|
||||
const few = summaryHtml(stats({ totals: { ...t, pv: 12 } }))
|
||||
assert.match(few, /Sobald ein paar mehr Besuche da sind, steht hier das Wichtigste über deine Website in zwei, drei Sätzen\./)
|
||||
assert.doesNotMatch(few, /<ul/)
|
||||
@@ -92,9 +93,17 @@ test('kpisHtml: Chips, Vorwerte, Mini-Verläufe, Zeit mit Scrolltiefe', () => {
|
||||
assert.match(h, /aria-label="plus 14 Prozent gegenüber dem Zeitraum davor"/)
|
||||
assert.match(h, /Besucher<span class="an-star">\*<\/span>/)
|
||||
assert.equal(count(h, /<svg class="an-spark"/g), 4)
|
||||
// Verlauf der Zeit: fehlende Tageswerte (null) zählen als 0
|
||||
// Verlauf der Zeit: Tage ohne Messung (null) fehlen im Verlauf, sie werden nicht als 0 gezeichnet
|
||||
const noTime = kpisHtml(stats({ series: full.series.map((d, i) => ({ ...d, avgSec: i === 0 ? null : 100 })) }))
|
||||
assert.equal(count(noTime, /<svg class="an-spark"/g), 4)
|
||||
const timeLine = (html) => /data-kpi="time".*?class="an-spark-line" d="([^"]*)"/s.exec(html)[1]
|
||||
assert.match(timeLine(noTime), /^M6\.9 /) // der erste Tag (x = 0) fehlt, die Linie beginnt am zweiten
|
||||
assert.equal(count(timeLine(noTime), /M/g), 1)
|
||||
assert.doesNotMatch(timeLine(noTime), /M0 |L0 /)
|
||||
assert.match(timeLine(kpisHtml(full)), /^M0 /) // vollständige Reihe: Linie ab dem ersten Tag
|
||||
// Zählwerte behalten 0: ein Tag ohne Aufrufe ist ein echter Tiefpunkt
|
||||
const zeroPv = kpisHtml(stats({ series: full.series.map((d, i) => ({ ...d, pv: i === 0 ? 0 : 231 })) }))
|
||||
assert.match(/data-kpi="pv".*?class="an-spark-line" d="([^"]*)"/s.exec(zeroPv)[1], /^M0 36 /)
|
||||
// Reihe ohne avgSec-Feld: kein Verlauf bei der Zeit
|
||||
assert.equal(count(kpisHtml(stats({ series: full.series.map(({ avgSec, ...d }) => d) })), /<svg class="an-spark"/g), 3)
|
||||
// keine Zeitmessung: Strich statt "0:00"
|
||||
@@ -102,12 +111,12 @@ test('kpisHtml: Chips, Vorwerte, Mini-Verläufe, Zeit mit Scrolltiefe', () => {
|
||||
assert.match(none, /<b class="an-v">–<\/b>/); assert.doesNotMatch(none, /gescrollt|vs\. –/)
|
||||
})
|
||||
|
||||
test('kpisHtml: neue Website zeigt "Vergleich ab 18.11."', () => {
|
||||
test('kpisHtml: neue Website zeigt "Vergleich ab 19.11."', () => {
|
||||
const h = kpisHtml(stats({ firstDay: '2026-09-20', previous: { ...prev, complete: false } }))
|
||||
assert.equal(count(h, /Vergleich ab 18\.11\./g), 4)
|
||||
assert.equal(count(h, /Vergleich ab 19\.11\./g), 4)
|
||||
assert.doesNotMatch(h, /an-delta|vs\./)
|
||||
const k7 = kpisHtml(stats({ range: 7, firstDay: '2026-09-28', previous: { ...prev, complete: false } }))
|
||||
assert.match(k7, /Vergleich ab 11\.10\./)
|
||||
assert.match(k7, /Vergleich ab 12\.10\./)
|
||||
// vollständiger Vorzeitraum, aber unter 20 Aufrufen: weder Chip noch Datum
|
||||
const tiny = kpisHtml(stats({ previous: { ...prev, totals: { ...prev.totals, pv: 5 } } }))
|
||||
assert.doesNotMatch(tiny, /an-delta|vs\.|Vergleich ab/)
|
||||
@@ -129,6 +138,22 @@ test('sparkHtml: Fläche und Linie, nichts bei weniger als 2 Werten, kein NaN',
|
||||
assert.doesNotMatch(sparkHtml([3, null, undefined, 'x']), /NaN|undefined|null/)
|
||||
})
|
||||
|
||||
test('sparkHtml: Lücken (null) unterbrechen die Linie statt als 0 zu gelten', () => {
|
||||
const line = (h) => /class="an-spark-line" d="([^"]*)"/.exec(h)[1]
|
||||
const area = (h) => /class="an-spark-area" d="([^"]*)"/.exec(h)[1]
|
||||
// Wert 0 liegt auf der Grundlinie (y = 36); eine Lücke darf dort keinen Punkt erzeugen
|
||||
const gap = sparkHtml([4, 6, null, 6, 8])
|
||||
assert.equal(count(line(gap), /M/g), 2) // zwei Abschnitte: Tage 0-1 und 3-4
|
||||
assert.doesNotMatch(line(gap), / 36( |$)/)
|
||||
assert.equal(count(area(gap), /Z/g), 2) // Fläche je Abschnitt
|
||||
assert.doesNotMatch(line(gap), /(^|[ML])100 /) // der Tag mit Lücke (x = 100) hat keinen Punkt
|
||||
assert.match(line(sparkHtml([null, 5, 7, 9])), /^M66\.7 /) // Lücke am Anfang: Linie beginnt später, nicht bei 0
|
||||
assert.equal(sparkHtml([null, null, null]), ''); assert.equal(sparkHtml([null, 4]), '') // unter 2 Messwerten nichts
|
||||
assert.doesNotMatch(sparkHtml([4, null, 6]), /NaN|undefined|null/)
|
||||
// alle Werte vorhanden: ein Abschnitt wie bisher
|
||||
assert.equal(count(line(sparkHtml([1, 5, 3, 8])), /M/g), 1)
|
||||
})
|
||||
|
||||
test('chartHtml: Linie, Fläche, Vorzeitraum nur mit Vergleich, Legende, Punkte mit Daten', () => {
|
||||
const h = chartHtml(full)
|
||||
assert.match(h, /<title id="an-chart-title">Seitenaufrufe pro Tag vom 05\.09\. bis 04\.10\., insgesamt 7\.\d{3}<\/title>/)
|
||||
@@ -146,6 +171,27 @@ test('chartHtml: Linie, Fläche, Vorzeitraum nur mit Vergleich, Legende, Punkte
|
||||
assert.doesNotMatch(chartHtml(zero), /NaN|Infinity|undefined|null/)
|
||||
})
|
||||
|
||||
test('chartHtml: Achsenbeschriftung hat Platz – y-Rand wächst mit der Stellenzahl, x-Beschriftungen überlappen nicht', () => {
|
||||
const padOf = (html) => Number(/class="an-grid" x1="([\d.]+)"/.exec(html)[1])
|
||||
const series = (pv, n = 30) => days('2026-09-05', n, pv).map((d) => ({ ...d, visitors: 1, contacts: 0, avgSec: 1 }))
|
||||
const none = { previous: { ...prev, complete: false } }
|
||||
// Skala 500 (3 Zeichen), 5.000 (4), 20.000 (5), 50 (2): Platz für die Ziffern der y-Beschriftung
|
||||
assert.equal(padOf(chartHtml(stats({ series: series(300), ...none }))), 49)
|
||||
assert.equal(padOf(chartHtml(stats({ series: series(2400), ...none }))), 64)
|
||||
assert.equal(padOf(chartHtml(stats({ series: series(12000), ...none }))), 78)
|
||||
assert.equal(padOf(chartHtml(stats({ series: series(30), ...none }))), 40)
|
||||
// y-Beschriftung steht rechtsbündig 6 Einheiten vor dem Raster und ist an der Gitterlinie zentriert
|
||||
const h = chartHtml(stats({ series: series(2400), ...none }))
|
||||
assert.match(h, /<text class="an-axis" x="58" y="[\d.]+" dy="0\.35em" text-anchor="end">5000<\/text>/)
|
||||
// x-Beschriftungen: nie näher als 100 Einheiten (Platz für "29.09." in der größten Schrift), letzter Tag bleibt
|
||||
for (const n of [7, 30, 90]) {
|
||||
const xs = [...chartHtml(stats({ series: series(300, n), ...none })).matchAll(/class="an-axis" x="([\d.]+)" y="[\d.]+" text-anchor="middle"/g)].map((m) => Number(m[1]))
|
||||
assert.ok(xs.length >= 3, `range ${n}`)
|
||||
assert.ok(xs.every((x, i) => !i || x - xs[i - 1] >= 100), `range ${n}: ${xs}`)
|
||||
assert.equal(xs[xs.length - 1], 616) // 640 - rechter Rand 24: der letzte Tag
|
||||
}
|
||||
})
|
||||
|
||||
test('chartTipText: Blase mit und ohne Vorwert', () => {
|
||||
assert.equal(chartTipText('2026-09-19', 371, 210), 'Sa., 19.09.: 371 Aufrufe (davor 210)')
|
||||
assert.equal(chartTipText('2026-09-19', '371', '210'), 'Sa., 19.09.: 371 Aufrufe (davor 210)') // Werte aus data-Attributen sind Strings
|
||||
@@ -196,7 +242,7 @@ test('rankCardHtml: Startseite statt /, Anteil in %, 6 Zeilen, expanded bis 20,
|
||||
assert.match(h, /<img src=x>/); assert.doesNotMatch(h, /<img src=x>/)
|
||||
const pages = rankCardHtml('seen', full)
|
||||
assert.match(pages, /Startseite/); assert.doesNotMatch(pages, />\/</)
|
||||
assert.match(pages, /<b>3\.200<\/b><em>59 %<\/em>/) // 3200 von 5410
|
||||
assert.match(pages, /<b>3\.200<\/b><em>46 %<\/em>/) // 3200 von 6930 Aufrufen (die Liste zeigt nur 5410 davon)
|
||||
assert.match(pages, /data-rank="seen" data-rank-active="pages"/)
|
||||
assert.match(pages, /<h3>Was angesehen wird<\/h3>/)
|
||||
assert.match(pages, /data-rank-tab="pages" aria-pressed="true"/); assert.match(pages, /data-rank-tab="refs" aria-pressed="false"/)
|
||||
@@ -228,6 +274,17 @@ test('rankCardHtml: Geräte, Länder mit Flagge, ungültiger Reiter, leere Liste
|
||||
const tiny = rankCardHtml('seen', stats({ pages: [{ key: '/a', count: 10000 }, { key: '/b', count: 1 }] }))
|
||||
assert.match(tiny, /<em>< 1 %<\/em>/)
|
||||
assert.doesNotMatch(rankCardHtml('seen', stats({ pages: [{ key: '/a', count: 0 }] })), /NaN|Infinity/)
|
||||
// Nenner: Seiten, Länder, Geräte relativ zu max(Summe der Liste, Seitenaufrufe); Herkunft nur zur Summe der Liste
|
||||
const part = stats({ totals: { ...t, pv: 1000 }, pages: [{ key: '/a', count: 300 }, { key: '/b', count: 100 }], countries: [{ key: 'DE', count: 250 }], devices: [{ key: 'm', count: 500 }], refs: [{ key: 'google.com', count: 30 }, { key: '', count: 10 }] })
|
||||
assert.match(rankCardHtml('seen', part), /<b>300<\/b><em>30 %<\/em>/) // 300 von 1000, nicht von 400 (75 %)
|
||||
assert.match(rankCardHtml('who', part, { tab: 'countries' }), /<b>250<\/b><em>25 %<\/em>/)
|
||||
assert.match(rankCardHtml('who', part), /<b>500<\/b><em>50 %<\/em>/)
|
||||
assert.match(rankCardHtml('seen', part, { tab: 'refs' }), /<b>30<\/b><em>75 %<\/em>/) // Herkunft: 30 von 40
|
||||
// Liste größer als die Gesamtzahl (Zählungen laufen auseinander): die Summe, nie mehr als 100 %
|
||||
const over = stats({ totals: { ...t, pv: 100 }, pages: [{ key: '/a', count: 300 }, { key: '/b', count: 100 }] })
|
||||
assert.match(rankCardHtml('seen', over), /<b>300<\/b><em>75 %<\/em>/)
|
||||
// ohne totals (alter Collector): Summe der Liste
|
||||
assert.match(rankCardHtml('seen', { pages: [{ key: '/a', count: 3 }, { key: '/b', count: 1 }] }), /<em>75 %<\/em>/)
|
||||
// Länder-Schlüssel maskiert
|
||||
const bad = rankCardHtml('who', stats({ countries: [{ key: '<b>', count: 3 }] }), { tab: 'countries' })
|
||||
assert.match(bad, /Unbekannt/); assert.doesNotMatch(bad, /<b><|<b>&/); assert.doesNotMatch(bad, /an-flag/)
|
||||
@@ -247,26 +304,35 @@ test('basisHtml: Ring, Sätze mit und ohne Entscheidungen', () => {
|
||||
test('headerHtml: Auswahl nur bei mehreren Websites, Zeitraum nur bei showRange, Dev-Hinweis, Name maskiert', () => {
|
||||
const a = project()
|
||||
const b = { projectId: 'p2', projectName: 'Café "Zum <Bären>"' }
|
||||
const one = headerHtml({ project: a, usable: [a], range: 30, showRange: true, devMode: false, stamp: '19:42' })
|
||||
const one = headerHtml({ project: a, usable: [a], range: 30, showRange: true, devMode: false, stamp: '19:42', today: { day: '2026-10-05', pv: 1234, visitors: 80, contacts: 3 } })
|
||||
assert.doesNotMatch(one, /an-project|<select/); assert.match(one, /<b class="an-site-name">Boulderhalle Überhang<\/b>/)
|
||||
assert.match(one, /Analyse aktiv · Stand: heute, 19:42 Uhr/)
|
||||
assert.match(one, /<small class="an-status"><span class="an-st">Analyse aktiv<\/span> · <span class="an-st">Heute bisher: 1\.234 Aufrufe<\/span> · <span class="an-st">Stand: 19:42 Uhr<\/span><\/small>/)
|
||||
assert.match(one, /<button type="button" class="on" data-range="30" aria-pressed="true">30 Tage<\/button>/)
|
||||
assert.equal(count(one, /data-range="/g), 3); assert.match(one, /data-range="7"[^>]*aria-pressed="false"/)
|
||||
const two = headerHtml({ project: b, usable: [a, b], range: 7, showRange: false, devMode: false, stamp: '' })
|
||||
assert.match(two, /<select id="an-project"/); assert.match(two, /<option value="p2" selected>Café "Zum <Bären>"<\/option>/)
|
||||
assert.match(two, /<option value="p1">Boulderhalle Überhang<\/option>/)
|
||||
assert.doesNotMatch(two, /data-range/); assert.doesNotMatch(two, /Zum <Bären>/); assert.doesNotMatch(two, /Stand:/)
|
||||
assert.match(two, /Analyse aktiv/)
|
||||
assert.match(two, /Analyse aktiv/); assert.doesNotMatch(two, /Heute bisher/) // ohne today (alter Collector) entfällt der Teil
|
||||
assert.match(headerHtml({ project: a, usable: [a], range: 365, showRange: true }), /class="on" data-range="30"/) // unbekannter Zeitraum => 30
|
||||
// Dev-Modus
|
||||
const dev = headerHtml({ project: a, usable: [a], range: 30, showRange: true, devMode: true, stamp: '19:42' })
|
||||
assert.match(dev, /Im Dev-Modus wird nicht gemessen\./); assert.doesNotMatch(dev, /Analyse aktiv|Stand:/); assert.match(dev, /data-state="warn"/)
|
||||
assert.match(dev, /Im Dev-Modus wird nicht gemessen\./); assert.doesNotMatch(dev, /Analyse aktiv|Stand:|Heute bisher/); assert.match(dev, /data-state="warn"/)
|
||||
assert.match(one, /data-state="on"/)
|
||||
// Name maskiert
|
||||
const evil = headerHtml({ project: { projectId: 'x', projectName: '<img src=x onerror=alert(1)>' }, usable: [], range: 30, showRange: false })
|
||||
assert.doesNotMatch(evil, /<img/); assert.match(evil, /<img src=x onerror=alert\(1\)>/)
|
||||
const odd = headerHtml({ project: { projectId: 'p"1', projectName: 'A' }, usable: [{ projectId: 'p"1', projectName: 'A' }, { projectId: 'p2', projectName: 'B' }], stamp: '<b>x</b>' })
|
||||
assert.match(odd, /value="p"1" selected/); assert.match(odd, /heute, <b>x<\/b> Uhr/); assert.doesNotMatch(odd, /<b>x/)
|
||||
const odd = headerHtml({ project: { projectId: 'p"1', projectName: 'A' }, usable: [{ projectId: 'p"1', projectName: 'A' }, { projectId: 'p2', projectName: 'B' }], stamp: '<b>x</b>', today: { pv: '<i>5</i>' } })
|
||||
assert.match(odd, /value="p"1" selected/); assert.match(odd, /Stand: <b>x<\/b> Uhr/); assert.doesNotMatch(odd, /<b>x|<i>/)
|
||||
// Heute bisher: Einzahl, 0 und nur mit Zahl; ohne Uhrzeit nur der Zähler
|
||||
const today = (pv, stamp = '') => headerHtml({ project: a, usable: [a], range: 30, showRange: false, stamp, today: { day: '2026-10-05', pv, visitors: 0, contacts: 0 } })
|
||||
const plain = (h) => /<small class="an-status">(.*?)<\/small>/.exec(h)[1].replace(/<[^>]+>/g, '') // Text ohne die Umbruch-Spans
|
||||
assert.equal(plain(today(1, '08:05')), 'Analyse aktiv · Heute bisher: 1 Aufruf · Stand: 08:05 Uhr')
|
||||
assert.match(today(0, '08:05'), /Heute bisher: 0 Aufrufe/)
|
||||
assert.equal(plain(today(42)), 'Analyse aktiv · Heute bisher: 42 Aufrufe')
|
||||
assert.doesNotMatch(headerHtml({ project: a, usable: [a], today: {} }), /Heute bisher|NaN/) // today ohne pv: nichts behaupten
|
||||
// Zustände ohne Messung zeigen kein "Heute bisher"
|
||||
assert.doesNotMatch(headerHtml({ project: project({ analyse: { state: 'disabled' } }), usable: [], today: { pv: 5 } }), /Heute bisher/)
|
||||
// Zustände ohne Messung sagen nicht "aktiv"
|
||||
const off = headerHtml({ project: project({ analyse: { state: 'off', eligible: true } }), usable: [], range: 30, showRange: false })
|
||||
assert.doesNotMatch(off, /Analyse aktiv/); assert.match(off, /data-state="off"/)
|
||||
|
||||
Reference in new issue
Block a user