Files

140 lines
6.0 KiB
JavaScript
Raw Permalink 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 { loadModule } from './load.mjs'
// Anzeige-Logik der Hustler-Seite: Zahlenformate, Status, Kacheln, Zähler, sichere Links, Spur-Zeilen
const V = await loadModule('src/lib/workersView.js')
const NB = String.fromCharCode(160) // geschütztes Leerzeichen vor $ und €
// Tagesgrenze 6 $, Monatsgrenze 150 $ (Kenso, 17.10.: max. 150 € im Monat)
const K = {
newAbosMonth: 3,
mrrNewEur: 60,
dealsMonth: 3,
openThreads: 7,
pultOpen: 2,
botLeads: 112,
apiTodayUsd: 5.1,
apiMonthUsd: 50,
dailyBudgetUsd: 6,
monthlyBudgetUsd: 150,
}
test('Zahlenformate', () => {
assert.equal(V.formatUsd(7.84), `7,84${NB}$`)
assert.equal(V.formatUsd(1234.5), `1.234,50${NB}$`)
assert.equal(V.formatUsd(0), `0,00${NB}$`)
for (const bad of [null, undefined, NaN, 'x']) assert.equal(V.formatUsd(bad), '–')
assert.equal(V.formatEur(120), `120${NB}€`)
assert.equal(V.formatEur(19.5), `19,50${NB}€`)
assert.equal(V.formatCount(1234), '1.234')
assert.equal(V.formatCount(null), '–')
})
test('formatTime: Berlin-Zeit, heute nur Uhrzeit', () => {
const now = Date.parse('2026-10-17T10:00:00Z')
assert.equal(V.formatTime('2026-10-17T20:30:00Z', now), '22:30')
assert.equal(V.formatTime('2026-10-16T21:30:00Z', now), '16.10. 23:30')
assert.equal(V.formatTime('kaputt', now), '–')
assert.equal(V.formatTime(null, now), '–')
})
test('Status: Texte und Töne', () => {
const s = ['arbeitet', 'wartet', 'leerlauf', 'grau', 'fehler', 'x']
assert.deepEqual(s.map(V.workerStatusLabel), ['Arbeitet', 'Wartet auf Auftrag', 'Leerlauf', 'Noch nicht angeschlossen', 'Fehler', 'Unbekannt'])
assert.deepEqual(s.map(V.statusTone), ['green', 'orange', 'plain', 'plain', 'red', 'plain'])
})
test('kpiTiles: sechs Kacheln, Töne nach 80-%-Regel', () => {
const t = V.kpiTiles(K)
assert.deepEqual(t.map((x) => x.key), ['abos', 'deals', 'threads', 'pult', 'botleads', 'api'])
assert.deepEqual(t.map((x) => x.label), ['Neue Abos (Monat)', 'Deals (Monat)', 'Gespräche offen', 'Wartet auf dich', 'Bot-Leads gesamt', 'API heute'])
assert.deepEqual(t.map((x) => x.value), ['3', '3', '7', '2', '112', `5,10${NB}$`])
assert.equal(t[0].sub, `60${NB}€ neu pro Monat`)
assert.equal(t[5].sub, `von 6,00${NB}$`)
assert.deepEqual(t.map((x) => x.tone), ['', '', '', 'warn', '', 'warn'])
assert.equal(V.kpiTiles({ ...K, apiTodayUsd: 6 })[5].tone, 'neg')
assert.equal(V.kpiTiles({ ...K, apiTodayUsd: 4.7 })[5].tone, '')
assert.equal(V.kpiTiles({ ...K, dailyBudgetUsd: 0 })[5].tone, '')
assert.equal(V.kpiTiles({ ...K, pultOpen: 0 })[3].tone, '')
})
test('kpiTiles: genau 80 % der Tagesgrenze warnt (keine Gleitkomma-Lücke)', () => {
assert.equal(V.kpiTiles({ ...K, apiTodayUsd: 4.8 })[5].tone, 'warn')
assert.equal(V.kpiTiles({ ...K, apiTodayUsd: 4.79 })[5].tone, '')
})
test('kpiTiles ohne Daten: Striche, kein NaN (Review Focus 1)', () => {
for (const k of [null, undefined, {}]) {
const tiles = V.kpiTiles(k)
assert.equal(tiles.length, 6)
for (const x of tiles) {
assert.equal(x.value, '–')
assert.equal(typeof x.sub, 'string')
assert.ok(!/NaN|undefined/.test(x.sub))
assert.equal(x.tone, '')
}
}
})
test('counterLines: bekannte Zähler zuerst, Rest alphabetisch, unbekannter Schlüssel als Beschriftung', () => {
assert.deepEqual(V.counterLines({ zeta: 1, gesendet: 3, gelesen: 41 }), [
{ key: 'gelesen', label: 'gelesen', value: '41' },
{ key: 'gesendet', label: 'gesendet', value: '3' },
{ key: 'zeta', label: 'zeta', value: '1' },
])
assert.equal(V.counterLines({ entwuerfe: 2 })[0].label, 'Entwürfe')
assert.equal(V.counterLines({ geprueft: 2 })[0].label, 'geprüft')
assert.deepEqual(V.counterLines(null), [])
assert.equal(V.tokenTotal(1200), 1200)
assert.equal(V.tokenTotal({ in: 1000, out: 234 }), 1234)
assert.equal(V.tokenTotal(undefined), 0)
})
test('safeLink: nur http(s) und interne Pfade (Review Focus 5)', () => {
assert.deepEqual(V.safeLink('https://x.de/a'), { href: 'https://x.de/a', external: true })
assert.deepEqual(V.safeLink('/customers/1'), { href: '/customers/1', external: false })
const bad = ['javascript:alert(1)', ' JavaScript:alert(1)', 'data:text/html,x', '//evil.example', 'mailto:a@b.de', '', null, 42]
// Browser lesen "/\host" und "/<Tab>/host" wie "//host": auch das bleibt Text
bad.push('/\\evil.example', '/\t/evil.example', '/\n/evil.example')
for (const b of bad) assert.equal(V.safeLink(b), null, String(b))
})
test('traceRows: neueste zuerst, höchstens 50, unsicherer Link bleibt Text', () => {
const evs = Array.from({ length: 60 }, (_, i) => ({
id: `e${i}`,
ts: new Date(Date.UTC(2026, 9, 17, 8, 0, i)).toISOString(),
text: `T${i}`,
link: i === 59 ? 'javascript:x' : null,
}))
const rows = V.traceRows(evs, Date.parse('2026-10-17T12:00:00Z'))
assert.equal(rows.length, 50)
assert.equal(rows[0].id, 'e59')
assert.equal(rows[0].link, null)
assert.equal(rows[0].text, 'T59')
assert.equal(rows[0].time, '10:00')
assert.equal(rows[49].id, 'e10')
})
test('traceRows: gültiger Link wird übernommen, ungültige Zeiten zuletzt, Nicht-Listen ergeben []', () => {
const now = Date.parse('2026-10-17T12:00:00Z')
const rows = V.traceRows([
{ id: 'a', ts: 'kaputt', text: 'ohne Zeit', link: null },
{ id: 'b', ts: '2026-10-17T08:00:00Z', text: 'mit Link', link: '/customers/1' },
], now)
assert.deepEqual(rows.map((r) => r.id), ['b', 'a'])
assert.deepEqual(rows[0].link, { href: '/customers/1', external: false })
assert.equal(rows[1].time, '–')
assert.deepEqual(V.traceRows(null, now), [])
assert.deepEqual(V.traceRows(undefined, now), [])
})
test('pauseLabel und pickTab', () => {
assert.equal(V.pauseLabel(false), 'Alles anhalten')
assert.equal(V.pauseLabel(true), 'Fortsetzen')
const tabs = [{ id: 'hochhaus' }, { id: 'pult' }]
assert.equal(V.pickTab(tabs, 'pult').id, 'pult')
assert.equal(V.pickTab(tabs, 'nope').id, 'hochhaus')
assert.equal(V.pickTab(tabs, null).id, 'hochhaus')
})