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>
105 lines
7.9 KiB
JavaScript
105 lines
7.9 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' })
|
||
})
|
||
|
||
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 \?/)
|
||
})
|