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"/)
|
||||
|
||||
@@ -14,6 +14,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: days('2026-09-05', 30, 231),
|
||||
pages: [{ key: '/', count: 3200 }, { key: '/preise', count: 1310 }, { key: '/kontakt', count: 900 }],
|
||||
@@ -66,8 +67,8 @@ test('fmtDuration, dayLabel, periodLabel, fmtNum', () => {
|
||||
|
||||
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: '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)
|
||||
@@ -116,7 +117,7 @@ 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.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)
|
||||
})
|
||||
|
||||
@@ -148,7 +149,7 @@ test('summarize: ohne Vergleich und neu gestartet', () => {
|
||||
// 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).')
|
||||
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\.$/)
|
||||
})
|
||||
@@ -164,16 +165,26 @@ test('summarize: zu wenig Daten → null; keine Kontakte → Hinweis nur ab 30 B
|
||||
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 },
|
||||
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, totals: { ...prev.totals, contacts: contactsBefore } },
|
||||
previous: { ...prev, contacts: before, totals: { ...prev.totals, contacts: before.reduce((a, k) => a + k.count, 0) } },
|
||||
})).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.')
|
||||
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', () => {
|
||||
@@ -219,30 +230,24 @@ test('summarize: Wochentag und Spitzentag in einem Hinweis', () => {
|
||||
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', () => {
|
||||
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, 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
|
||||
// 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 }))), undefined)
|
||||
assert.ok(weekdayStats(week, '')) // mit allen 7 Tagen wäre es ein Ergebnis gewesen
|
||||
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', () => {
|
||||
|
||||
@@ -37,6 +37,13 @@ test('Diagramm-Skalierung', () => {
|
||||
assert.ok(m.xLabels.length <= 7 && m.xLabels.length >= 2)
|
||||
assert.equal(chartModel([]).points.length, 0)
|
||||
assert.equal(chartModel([{ day: '2026-10-01', pv: 3 }]).points.length, 1)
|
||||
// labelGap: die vorletzte Beschriftung rückt weg, wenn sie der letzten zu nahe kommt (30 Tage: 29.09. ist 79 Einheiten vor 03.10.)
|
||||
const labels = (opts) => chartModel(series, { width: 640, ...opts }).xLabels.map((l) => l.label)
|
||||
assert.equal(labels({}).length, 7)
|
||||
const wide = chartModel(series, { labelGap: 90 }).xLabels
|
||||
assert.equal(wide.length, 6)
|
||||
assert.ok(wide.every((l, i, a) => !i || l.x - a[i - 1].x >= 90))
|
||||
assert.equal(wide[wide.length - 1].label, '30.09.') // letzter Tag bleibt
|
||||
})
|
||||
|
||||
test('Balkenzeilen', () => {
|
||||
|
||||
@@ -4,6 +4,8 @@ import assert from 'node:assert/strict'
|
||||
import { createAnalyticsClient } from '../server/services/analyticsClient.js'
|
||||
import { createAdminServer, createPublicServer } from '../analytics/src/server.js'
|
||||
import { openDb } from '../analytics/src/db.js'
|
||||
import { computeStats } from '../analytics/src/stats.js'
|
||||
import { compareFrom, shortDay } from '../public/analyse-view.js'
|
||||
|
||||
const SITE = 'abcdef0123456789abcd'
|
||||
const DOMAIN = 'kunde.example.com'
|
||||
@@ -39,9 +41,18 @@ test('Vertrag: putSite -> Ereignisse -> getStats -> heatToken -> deleteSite', as
|
||||
await post({ t: 'ct', path: '/', kind: 'tel' })
|
||||
await post({ t: 'bn', a: 'accept' })
|
||||
|
||||
// Frisch gemessen: heute steht nur in `today`, der Zeitraum sind die abgeschlossenen Tage bis gestern
|
||||
const jetzt = await client.getStats(SITE, '7')
|
||||
assert.deepEqual(jetzt.today, { day: jetzt.today.day, pv: 3, visitors: 1, contacts: 1 })
|
||||
assert.equal(jetzt.totals.pv, 0)
|
||||
assert.ok(jetzt.today.day > jetzt.to && jetzt.series.every((d) => d.day <= jetzt.to))
|
||||
// Um 36 Stunden zurückdatieren: liegt immer auf einem abgeschlossenen Tag des 7-Tage-Zeitraums
|
||||
db.prepare('UPDATE events_raw SET ts = ts - 36 * 3600 * 1000 WHERE site = ?').run(SITE)
|
||||
|
||||
const stats = await client.getStats(SITE, '7')
|
||||
assert.equal(stats.totals.pv, 3)
|
||||
assert.equal(stats.totals.contacts, 1)
|
||||
assert.equal(stats.today.pv, 0)
|
||||
assert.equal(stats.series.length, 7)
|
||||
assert.equal(stats.series.reduce((n, d) => n + d.pv, 0), 3)
|
||||
assert.equal(stats.range, 7)
|
||||
@@ -76,3 +87,22 @@ test('Vertrag: stopSite setzt active=0 und offSince, unbekannt zaehlt als erledi
|
||||
assert.equal(db.prepare('SELECT offSince FROM sites WHERE siteKey = ?').get(KEY2).offSince, row.offSince)
|
||||
assert.deepEqual(await client.stopSite('00000000000000000000'), { ok: true, missing: true })
|
||||
})
|
||||
|
||||
test('Vertrag: "Vergleich ab" des Portals ist der erste Tag, an dem der Collector den Vorzeitraum als vollständig meldet', () => {
|
||||
const firstDay = '2026-09-20'
|
||||
const evDb = openDb(':memory:')
|
||||
evDb.prepare('INSERT INTO sites (siteKey, projectId, domains, active) VALUES (?,?,?,1)').run(SITE, 'p1', '["a.de"]')
|
||||
evDb.prepare("INSERT INTO events_raw (site, ts, type, path, vp, country, device) VALUES (?,?,'pv','/','d','DE','d')").run(SITE, Date.UTC(2026, 8, 20, 10))
|
||||
for (const range of [7, 30]) {
|
||||
const addDays = (day, n) => new Date(Date.UTC(+day.slice(0, 4), +day.slice(5, 7) - 1, +day.slice(8, 10) + n)).toISOString().slice(0, 10)
|
||||
const at = (day) => computeStats(evDb, SITE, range, Date.parse(`${day}T10:00:00Z`))
|
||||
const first = addDays(firstDay, 2 * range) // "Vergleich ab" laut Portal
|
||||
const dayBefore = at(addDays(first, -1))
|
||||
assert.equal(dayBefore.previous.complete, false, `range ${range}: am Tag davor noch nicht vollständig`)
|
||||
assert.deepEqual(compareFrom(dayBefore), { ok: false, from: shortDay(first) })
|
||||
const onDay = at(first)
|
||||
assert.equal(onDay.previous.complete, true, `range ${range}: am Tag selbst vollständig`)
|
||||
assert.equal(onDay.previous.from, firstDay)
|
||||
assert.equal(compareFrom(onDay).from, '') // kein Datum mehr (der Vorzeitraum ist voll; ok bleibt false, denn es gibt nur 1 Aufruf)
|
||||
}
|
||||
})
|
||||
+47
-3
@@ -47,11 +47,13 @@ const BEFORE = {
|
||||
}
|
||||
const SCALE = { 7: 0.25, 30: 1, 90: 3 } // Anteil der 30-Tage-Zahlen je Zeitraum
|
||||
const PAGES = [['/', 2480], ['/preise', 1310], ['/kurse', 1150], ['/routen', 960], ['/oeffnungszeiten', 640], ['/team', 410], ['/kontakt', 300], ['/impressum', 120], ['/datenschutz', 90]]
|
||||
// Der Zeitraum endet gestern (abgeschlossene Tage); heute steht getrennt in `today`
|
||||
function makeStats(range, over = {}) {
|
||||
const from = addDays(TODAY, -(range - 1))
|
||||
const from = addDays(TODAY, -range)
|
||||
const pfrom = addDays(from, -range)
|
||||
return {
|
||||
range, from, to: TODAY, firstDay: addDays(pfrom, -40), totals: TOTALS[range], series: daysOf(from, range, 231, Math.min(14, range - 1)),
|
||||
range, from, to: addDays(TODAY, -1), firstDay: addDays(pfrom, -40), totals: TOTALS[range], series: daysOf(from, range, 231, Math.min(14, range - 1)),
|
||||
today: { day: TODAY, pv: 42, visitors: 17, contacts: 2 },
|
||||
pages: PAGES.map(([key, count]) => ({ key, count: Math.round(count * SCALE[range]) })),
|
||||
refs: [{ key: 'google.com', count: 880 }, { key: '', count: 700 }, { key: 'instagram.com', count: 400 }, { key: 'l.facebook.com', count: 290 }].map((r) => ({ ...r, count: Math.round(r.count * SCALE[range]) })),
|
||||
countries: [{ key: 'DE', count: 6000 }, { key: 'AT', count: 600 }, { key: 'CH', count: 330 }],
|
||||
@@ -79,6 +81,8 @@ const SCENARIOS = {
|
||||
wenig: { project: project(), stats: (r) => makeStats(r, { totals: { ...TOTALS[r], pv: 12 } }) },
|
||||
dev: { project: project({ devMode: true }), stats: (r) => makeStats(r) },
|
||||
alt: { project: project(), stats: (r) => OLD(r) },
|
||||
stark: { project: project(), stats: (r) => makeStats(r, { series: daysOf(addDays(TODAY, -r), r, 2400, Math.min(14, r - 1)) }) },
|
||||
sehrStark: { project: project(), stats: (r) => makeStats(r, { series: daysOf(addDays(TODAY, -r), r, 12000, Math.min(14, r - 1)) }) },
|
||||
langerPfad: { project: project(), stats: (r) => makeStats(r, { pages: [{ key: longPath, count: 900 }, { key: '/kurz', count: 500 }], refs: [{ key: `${'h'.repeat(90)}.example.com`, count: 300 }] }) },
|
||||
fehler: { project: project(), stats: null },
|
||||
zwei: { projects: [project(), { ...baseProject, projectId: 'p2', projectName: 'Zweite Website mit einem wirklich sehr langen Namen, der nicht in eine Zeile passt', analyse: analyse({}) }], stats: (r) => makeStats(r) },
|
||||
@@ -168,7 +172,7 @@ try {
|
||||
await open('full')
|
||||
check(L('aktiv: Block "Das Wichtigste" mit "14 % mehr"'), (await text('.an-sum')).includes('Das Wichtigste') && (await text('.an-sum')).includes('14 % mehr'))
|
||||
check(L('aktiv: 4 Kacheln, 3 Chips ▲, 1 Chip ▼'), (await count('.an-kpis .an-kpi')) === 4 && (await count('.an-kpis .an-delta.up')) === 3 && (await count('.an-kpis .an-delta.down')) === 1 && (await text('.an-kpis')).includes('▲') && (await text('.an-kpis')).includes('▼'))
|
||||
check(L('aktiv: Kopfleiste mit Stand-Uhrzeit, Zeitraum 30 Tage gewählt, Reiter Übersicht gewählt'), /Analyse aktiv · Stand: heute, \d\d:\d\d Uhr/.test(await text('.an-status')) && (await page.getAttribute('.an-seg .on', 'data-range')) === '30' && (await page.getAttribute('.an-tabbtn[aria-selected="true"]', 'data-tab')) === 'uebersicht')
|
||||
check(L('aktiv: Kopfleiste mit "Heute bisher: 42 Aufrufe" und Stand-Uhrzeit, Zeitraum 30 Tage gewählt, Reiter Übersicht gewählt'), /^Analyse aktiv · Heute bisher: 42 Aufrufe · Stand: \d\d:\d\d Uhr$/.test(await text('.an-status')) && (await page.getAttribute('.an-seg .on', 'data-range')) === '30' && (await page.getAttribute('.an-tabbtn[aria-selected="true"]', 'data-tab')) === 'uebersicht')
|
||||
check(L('kein_horizontaler_scroll aktiv'), await noScroll())
|
||||
// Raster: Desktop 4 Kacheln nebeneinander, Handy 2 x 2; Diagramm und Kontaktkarte gleich hoch (Desktop)
|
||||
const tiles = await page.$$eval('.an-kpis .an-kpi', (els) => els.map((e) => { const r = e.getBoundingClientRect(); return { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width) } }))
|
||||
@@ -208,6 +212,28 @@ try {
|
||||
}
|
||||
await page.waitForFunction(() => document.querySelector('.an-tip')?.hidden === true)
|
||||
check(L('Diagramm-Blase: verschwindet wieder, kein Punkt bleibt markiert'), (await count('.an-dot.on')) === 0)
|
||||
// Achsenbeschriftung: gerendert ca. 11-12 px (viewBox 640 skaliert mit der Breite), nichts ragt aus der Karte, x-Beschriftungen überlappen nicht
|
||||
const axis = () => page.evaluate(() => {
|
||||
const svg = document.querySelector('.an-chart')
|
||||
const scale = svg.getBoundingClientRect().width / 640
|
||||
const box = document.querySelector('.an-chartbox').getBoundingClientRect()
|
||||
const texts = [...svg.querySelectorAll('.an-axis')].map((t) => ({ r: t.getBoundingClientRect(), px: parseFloat(getComputedStyle(t).fontSize) * scale, anchor: t.getAttribute('text-anchor'), s: t.textContent }))
|
||||
const xs = texts.filter((t) => t.anchor === 'middle').sort((a, b) => a.r.left - b.r.left)
|
||||
return {
|
||||
px: Math.min(...texts.map((t) => t.px)), pxMax: Math.max(...texts.map((t) => t.px)),
|
||||
inside: texts.every((t) => t.r.left >= box.left + 1 && t.r.right <= box.right - 1),
|
||||
gap: xs.every((t, i) => !i || t.r.left >= xs[i - 1].r.right + 4),
|
||||
y: texts.filter((t) => t.anchor === 'end').map((t) => t.s).join(','),
|
||||
}
|
||||
})
|
||||
const ax = await axis()
|
||||
check(L(`Achsenbeschriftung ca. 11-12 px (${ax.px.toFixed(1)} px), liegt in der Karte, x-Werte überlappen nicht`), ax.px >= 10.5 && ax.pxMax <= 13 && ax.inside && ax.gap)
|
||||
const faint = await page.evaluate(() => getComputedStyle(document.querySelector('.an-credit')).color)
|
||||
check(L(`Hilfstext "--an-faint" kontrastreich (${faint})`), Number(/rgba?\([^)]*?,\s*([\d.]+)\)$/.exec(faint)?.[1] ?? 1) >= 0.55)
|
||||
if (touch) {
|
||||
const heights = await page.evaluate(() => Object.fromEntries(['.an-tabbtn', '.an-seg button', '.an-ltab', '.an-more', '.an-teaser .an-btn'].map((sel) => [sel, Math.min(...[...document.querySelectorAll(sel)].map((e) => e.getBoundingClientRect().height))])))
|
||||
check(L(`Tippflächen mindestens 40 px: ${Object.entries(heights).map(([k, v]) => `${k} ${v.toFixed(0)}`).join(', ')}`), Object.values(heights).every((h) => h >= 39.5))
|
||||
}
|
||||
// Ranglisten: Umschalter und "Alle anzeigen" ändern nur ihre Karte
|
||||
check(L('Rangliste: 6 Zeilen und "Alle anzeigen"'), (await count('[data-rank="seen"] .an-lr')) === 6 && (await count('[data-rank="seen"] [data-rank-more]')) === 1)
|
||||
await page.click('[data-rank="seen"] [data-rank-more]')
|
||||
@@ -255,6 +281,16 @@ try {
|
||||
const settings = await text('#analyse-body')
|
||||
check(L('Reiter Einstellungen: "Analyse ausschalten" sichtbar, "aktiv seit"'), (await page.isVisible('#an-off-ask')) && settings.includes('Analyse ausschalten') && /aktiv seit 12\.08\.2026/.test(settings) && settings.includes('Fassung 2026-10-02') && (await count('.an-docs a')) === 2 && (await count('.an-swatch')) === 1 && (await count('.an-top .an-seg')) === 0)
|
||||
check(L('kein_horizontaler_scroll einstellungen'), await noScroll())
|
||||
const errColor = await page.evaluate(() => {
|
||||
const p = document.createElement('p')
|
||||
p.className = 'error'
|
||||
p.textContent = 'x'
|
||||
document.querySelector('#an-off').appendChild(p)
|
||||
const c = getComputedStyle(p).color
|
||||
p.remove()
|
||||
return c
|
||||
})
|
||||
check(L(`Fehlertext beim Ausschalten rot (${errColor})`), errColor === 'rgb(252, 165, 165)')
|
||||
await shot('einstellungen')
|
||||
await page.click('#an-off-ask')
|
||||
await page.click('#an-off-yes')
|
||||
@@ -311,6 +347,7 @@ try {
|
||||
await shot('dev')
|
||||
|
||||
await open('alt')
|
||||
check(L('alt: kein "Heute bisher" (alter Collector), Stand bleibt'), /^Analyse aktiv · Stand: \d\d:\d\d Uhr$/.test(await text('.an-status')))
|
||||
check(L('alt: keine Chips, keine pageerror'), (await count('.an-delta')) === 0 && (await count('.an-prev')) === 0 && errors.length === 0 && (await text('.an-kpis')).includes('612'))
|
||||
await shot('alt')
|
||||
|
||||
@@ -323,6 +360,13 @@ try {
|
||||
check(L('fehler: "Neu laden" fragt erneut an'), statsRequests.length === requestsBefore + 1)
|
||||
await shot('fehler')
|
||||
|
||||
for (const [sc, label5] of [['stark', 'vierstellige'], ['sehrStark', 'fünfstellige']]) {
|
||||
await open(sc)
|
||||
const big = await axis()
|
||||
check(L(`${label5} y-Werte (${big.y}) nicht abgeschnitten, x-Werte überlappen nicht, Schrift ${big.px.toFixed(1)} px`), big.inside && big.gap && big.px >= 10.5 && big.pxMax <= 13)
|
||||
await shot(sc === 'stark' ? 'skala-4-stellig' : 'skala-5-stellig')
|
||||
}
|
||||
|
||||
await open('langerPfad')
|
||||
const name = await page.evaluate(() => { const n = document.querySelector('[data-rank="seen"] .an-name'); return { sw: n.scrollWidth, cw: n.clientWidth, title: n.title } })
|
||||
check(L(`langer Pfad${touch ? ', 402 px' : ''}: document.scrollingElement.scrollWidth <= innerWidth`), (await noScroll()) && name.sw > name.cw && name.title.length > 100)
|
||||
|
||||
Reference in new issue
Block a user