Files
tickte-system/tests/unit/botleadsView.test.mjs
T

127 lines
9.3 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 fs from 'node:fs'
import { loadModule } from './load.mjs'
const { safeHref, phoneLabel, botleadRow, ROOM_FILTERS, pageInfo } = await loadModule('src/lib/botleadsView.js')
// Die React-Teile sind unter Node nicht ladbar (JSX): der Quelltext wird geprüft (Muster workersComponents.test.mjs); das Verhalten prüft botleadList.test.mjs
const read = (p) => fs.readFileSync(new URL('../../' + p, import.meta.url), 'utf8')
test('safeHref: nur http und https, keine Zugangsdaten in der Adresse', () => {
assert.equal(safeHref('https://beispiel-elektro.de/impressum'), 'https://beispiel-elektro.de/impressum')
assert.equal(safeHref(' http://beispiel-elektro.de/ '), 'http://beispiel-elektro.de/')
for (const bose of ['javascript:alert(1)', '//evil.example', 'data:text/html,x', '', ' ', 'ftp://x.example/', 'mailto:a@b.de', '/pfad', 'https://', null, undefined, 42, {}]) assert.equal(safeHref(bose), '', String(bose))
assert.equal(safeHref('https://user:pw@beispiel-elektro.de/'), '') // Zugangsdaten
assert.equal(safeHref('https://beispiel-elektro.de@evil.example/'), '') // sähe nach dem Betrieb aus, führt aber woandershin
})
test('phoneLabel: Nummer nur mit Quelle, Heilberuf nie', () => {
assert.equal(phoneLabel({ phone: '0631 111', phoneSource: 'https://x.de/i', isHealth: false }), '0631 111')
assert.equal(phoneLabel({ phone: '0631 111', phoneSource: '', isHealth: false }), 'nicht verfügbar')
assert.equal(phoneLabel({ phone: '', phoneSource: '', isHealth: true }), 'bewusst nicht gespeichert')
assert.equal(phoneLabel({ phone: '0631 111', phoneSource: 'https://x.de/i', isHealth: true }), 'bewusst nicht gespeichert')
assert.equal(phoneLabel({ phone: '', phoneSource: 'https://x.de/i', isHealth: false }), 'nicht verfügbar')
assert.equal(phoneLabel({}), 'nicht verfügbar')
assert.equal(phoneLabel(null), 'nicht verfügbar')
})
test('botleadRow: alle zehn Felder', () => {
const item = { id: 'a', room: 'handwerk', roomLabel: 'Handwerk', name: 'Elektro Beispiel GmbH', postalCode: '67655', city: 'Kaiserslautern', score: 82, reason: 'keine veröffentlichte Mail', sourceUrl: 'javascript:x', phone: '', phoneSource: '', isHealth: false, foundAt: '2026-10-19T20:00:00Z', gekuerzt: false }
const r = botleadRow(item)
assert.deepEqual(Object.keys(r), ['id', 'title', 'place', 'roomLabel', 'scoreLabel', 'reason', 'phone', 'sourceHref', 'found', 'gekuerzt'])
assert.deepEqual(r, { id: 'a', title: 'Elektro Beispiel GmbH', place: '67655 Kaiserslautern', roomLabel: 'Handwerk', scoreLabel: '82 Punkte', reason: 'keine veröffentlichte Mail', phone: 'nicht verfügbar', sourceHref: '', found: '19.10.2026', gekuerzt: false })
// sichere Quelle, Nummer mit Quelle, gekürzt
const b = botleadRow({ ...item, sourceUrl: 'https://beispiel-elektro.de/impressum', phone: '0631 111', phoneSource: 'https://beispiel-elektro.de/impressum', gekuerzt: true })
assert.deepEqual([b.sourceHref, b.phone, b.gekuerzt], ['https://beispiel-elektro.de/impressum', '0631 111', true])
// Heilberuf: nie die Nummer, auch wenn der Dienst eine mitschickt
assert.equal(botleadRow({ ...item, isHealth: true, phone: '0631 111', phoneSource: 'https://x.de/i' }).phone, 'bewusst nicht gespeichert')
})
test('botleadRow: fehlende Werte ergeben Platzhalter, nie undefined oder NaN', () => {
const r = botleadRow({ id: 'b' })
assert.deepEqual(r, { id: 'b', title: 'nicht verfügbar', place: '', roomLabel: '–', scoreLabel: '–', reason: '–', phone: 'nicht verfügbar', sourceHref: '', found: '–', gekuerzt: false })
assert.equal(botleadRow({ id: 'c', name: ' ', postalCode: '', city: 'Mainz' }).title, 'nicht verfügbar')
assert.equal(botleadRow({ id: 'c', city: 'Mainz' }).place, 'Mainz')
assert.equal(botleadRow({ id: 'c', postalCode: '55116' }).place, '55116')
assert.equal(botleadRow({ id: 'c', score: 0 }).scoreLabel, '0 Punkte')
assert.equal(botleadRow({ id: 'c', score: 'x' }).scoreLabel, '–')
assert.equal(botleadRow({ id: 'c', foundAt: 'kein Datum' }).found, '–')
assert.equal(botleadRow(null).id, '')
})
test('botleadRow: das Funddatum zählt in Berlin (Mitternacht und Zeitumstellung)', () => {
assert.equal(botleadRow({ id: 'a', foundAt: '2026-10-19T22:30:00Z' }).found, '20.10.2026') // 00:30 Uhr am nächsten Tag
assert.equal(botleadRow({ id: 'a', foundAt: '2026-12-31T23:30:00Z' }).found, '01.01.2027')
assert.equal(botleadRow({ id: 'a', foundAt: '2026-10-25T00:30:00Z' }).found, '25.10.2026') // Nacht der Zeitumstellung
})
test('ROOM_FILTERS: Reihenfolge, Werte und Beschriftungen', () => {
assert.deepEqual(ROOM_FILTERS, [
{ value: '', label: 'Alle Räume' },
{ value: 'aerzte', label: 'Ärzte & Praxen' },
{ value: 'handwerk', label: 'Handwerk' },
{ value: 'selbst', label: 'Selbstständige' },
{ value: 'laden', label: 'Läden & Gastro' },
])
})
test('pageInfo: Seiten, Blättern und Beschriftung', () => {
assert.deepEqual(pageInfo({ total: 120, page: 2, pageSize: 50 }), { pages: 3, hasPrev: true, hasNext: true, label: 'Seite 2 von 3 · 120 Einträge' })
assert.deepEqual(pageInfo({ total: 120, page: 1, pageSize: 50 }), { pages: 3, hasPrev: false, hasNext: true, label: 'Seite 1 von 3 · 120 Einträge' })
assert.deepEqual(pageInfo({ total: 120, page: 3, pageSize: 50 }), { pages: 3, hasPrev: true, hasNext: false, label: 'Seite 3 von 3 · 120 Einträge' })
assert.equal(pageInfo({ total: 0, page: 1, pageSize: 50 }).label, 'Keine Einträge')
assert.deepEqual(pageInfo({ total: 0, page: 1, pageSize: 50 }), { pages: 1, hasPrev: false, hasNext: false, label: 'Keine Einträge' })
assert.equal(pageInfo({ total: 1, page: 1, pageSize: 50 }).label, 'Seite 1 von 1 · 1 Eintrag') // Einzahl
assert.equal(pageInfo({ total: 50, page: 1, pageSize: 50 }).pages, 1)
assert.equal(pageInfo({ total: 51, page: 1, pageSize: 50 }).pages, 2)
assert.equal(pageInfo({ total: 1234, page: 1, pageSize: 50 }).label, 'Seite 1 von 25 · 1.234 Einträge') // deutsche Tausenderpunkte
// der Dienst schickt pageSize mit; fehlt es oder ist es unbrauchbar, gilt 50
assert.equal(pageInfo({ total: 120, page: 1 }).pages, 3)
for (const bad of [0, -5, NaN, 'x', null]) assert.equal(pageInfo({ total: 120, page: 1, pageSize: bad }).pages, 3, String(bad))
assert.equal(pageInfo({ total: 120, page: 1, pageSize: 25 }).pages, 5)
// unbrauchbare Zahlen ergeben nie NaN
assert.deepEqual(pageInfo({ total: 'x', page: 'y', pageSize: 50 }), { pages: 1, hasPrev: false, hasNext: false, label: 'Keine Einträge' })
assert.deepEqual(pageInfo({}), { pages: 1, hasPrev: false, hasNext: false, label: 'Keine Einträge' })
})
// Quelltext des Kellers (BotLeadList.jsx und keller/*, Hustler-Pixelwelt T10): die Zeilen-Ableitung steckt in src/lib/hustlerKeller.js (eintragModell → botleadRow),
// Links gibt es nur in der Akte. UMGESTELLT gegenüber der alten Tabelle: die Prüfungen laufen über alle Dateien des Kellers statt über BotLeadList.jsx allein.
const KELLER_DATEIEN = [
'src/components/workers/BotLeadList.jsx',
'src/components/workers/keller/KellerSzene.jsx',
'src/components/workers/keller/RegisterBuch.jsx',
'src/components/workers/keller/Akte.jsx',
]
// Quelltext ohne Kommentare (die Kommentare nennen auch, was es nicht gibt, etwa „kein innerHTML“)
const ohneKommentare = (src) => src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|\s)\/\/.*$/gm, '$1')
test('Keller: Links nur aus der Zeilen-Ableitung, extern mit rel noopener, kein HTML-Einschub, Knöpfe mit type="button"', () => {
const quellen = KELLER_DATEIEN.map((p) => ({ p, src: ohneKommentare(read(p)) }))
const alle = quellen.map((q) => q.src).join('\n')
// die Ableitung läuft über botleadRow (safeHref für die Quelle, Telefonregel, Datum)
assert.match(ohneKommentare(read('src/components/workers/BotLeadList.jsx')), /eintragModell\(/)
assert.match(read('src/lib/hustlerKeller.js'), /botleadRow\(/)
// genau ein Link im ganzen Keller: die Quelle in der Akte, extern in neuem Tab ohne Rückbezug
const akte = ohneKommentare(read('src/components/workers/keller/Akte.jsx'))
assert.match(akte, /rel="noopener noreferrer"/); assert.match(akte, /target="_blank"/)
// jeder href kommt aus e.sourceHref (eintragModell → botleadRow → safeHref), nie aus dem Eintrag selbst
assert.match(akte, /href=\{e\.sourceHref\}/)
assert.equal((alle.match(/\bhref=/g) || []).length, 1)
assert.doesNotMatch(alle, /\bhref=\{(?!e\.sourceHref\})/)
assert.doesNotMatch(alle, /\bhref="/)
assert.equal((alle.match(/<a\b/g) || []).length, 1)
assert.doesNotMatch(alle, /dangerouslySetInnerHTML|innerHTML/)
// kein Anruf-Link, kein Mailversand
assert.doesNotMatch(alle, /tel:|mailto:/)
// WOMS-Button hat keinen Standard-type: jeder Knopf nennt ihn
for (const { p, src } of quellen) {
assert.equal((src.match(/<(?:button|Button)\b/g) || []).length, (src.match(/<(?:button|Button)\b[^>]*type="button"/g) || []).length, p)
}
assert.ok((alle.match(/<button\b/g) || []).length > 0)
// kein Polling
assert.doesNotMatch(alle, /setInterval/)
})
test('Keller: Zeilen ohne Kennung teilen sich keinen key (Ausweichwert aus der Position), aria-current nur in der Einzelansicht', () => {
const src = ohneKommentare(read('src/components/workers/keller/RegisterBuch.jsx'))
assert.match(src, /key=\{z\.id \|\| /)
assert.match(src, /aktuell=\{einzeln && z\.id === botId\}/)
assert.match(src, /aria-current=\{aktuell \? 'true' : undefined\}/)
})