127 lines
9.3 KiB
JavaScript
127 lines
9.3 KiB
JavaScript
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\}/)
|
||
})
|