Files
tickte-system/tests/unit/botleadsView.test.mjs
T
ClaudeandClaude Sonnet 5.5 ed5dfa9c0e workers: Bot-Leads behält den Tastaturfokus beim Blättern und nach „Alle anzeigen“, aria-current nur in der Einzelansicht
Endprüfung P6 (C5, W1): Beim Blättern hängt die Seitenzeile beim Laden aus, und „Alle anzeigen“
wird durch den Raumfilter ersetzt; der Fokus fiel jeweils auf <body>.
- Blättern: gemerkt wird der gedrückte Knopf; nach dem Laden bekommt er den Fokus zurück, ist er
  gesperrt (erste oder letzte Seite), der andere. Raum oder Suche bedient der Nutzer inzwischen,
  geschieht nichts; scheitert die Anfrage, gibt es keine Seitenzeile und nichts geschieht.
- „Alle anzeigen“: der Raumfilter bekommt den Fokus, sobald die Seite bot aus der Adresse
  genommen hat. Nicht beim Einhängen (am Handy öffnete das die Auswahl) und nicht bei einem
  anderen Weg zur Liste (Zurück-Taste).
- aria-current nur mit bot (single) und passender Kennung; Zeilen ohne Kennung teilen sich keinen key mehr.
- Tests: botleadList.test.mjs (Fokus-Attrappe über __fakeRef, Mutationsprobe für die Reset-Zeilen),
  botleadsView.test.mjs (Quelltext-Probe); der Prüfstand prüft zusätzlich den Fokus nach „Alle anzeigen“.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 15:19:52 +00:00

105 lines
7.9 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' })
})
test('BotLeadList.jsx: Links nur aus der Zeilen-Ableitung, extern mit rel noopener, kein HTML-Einschub, Knöpfe mit type="button"', () => {
const src = read('src/components/workers/BotLeadList.jsx')
assert.match(src, /botleadRow\(/)
assert.match(src, /rel="noopener noreferrer"/); assert.match(src, /target="_blank"/)
// jeder href kommt aus row.sourceHref (botleadRow → safeHref), nie aus dem Eintrag selbst
assert.match(src, /href=\{row\.sourceHref\}/)
assert.doesNotMatch(src, /\bhref=\{(?!row\.sourceHref\})/)
assert.doesNotMatch(src, /\bhref="/)
assert.doesNotMatch(src, /dangerouslySetInnerHTML|innerHTML/)
// kein Anruf-Link, kein Mailversand
assert.doesNotMatch(src, /tel:|mailto:/)
// WOMS-Button hat keinen Standard-type: jeder Knopf nennt ihn
assert.equal((src.match(/<(?:button|Button)\b/g) || []).length > 0, true)
assert.equal((src.match(/<(?:button|Button)\b/g) || []).length, (src.match(/<(?:button|Button)\b[^>]*type="button"/g) || []).length)
// kein Polling
assert.doesNotMatch(src, /setInterval/)
})
test('BotLeadList.jsx: Zeilen ohne Kennung teilen sich keinen key (Ausweichwert aus der Position), aria-current nur in der Einzelansicht', () => {
const src = read('src/components/workers/BotLeadList.jsx')
assert.match(src, /key=\{row\.id \|\| /)
assert.match(src, /aria-current=\{single && row\.id === botId \?/)
})