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

129 lines
10 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 { readFileSync, readdirSync } from 'node:fs'
// Die React-Teile der Hustler-Seite sind unter Node nicht ladbar (JSX, Canvas, Router): geprüft wird der Quelltext
// (Muster adminRoute.test.mjs). Pfade relativ zu src/components/workers/.
const read = (p) => readFileSync(new URL('../../src/components/workers/' + p, import.meta.url), 'utf8')
test('WorkerCard: Links nur über safeLink, extern mit rel noopener; deutsche Beschriftungen (Review Focus 5)', () => {
const src = read('WorkerCard.jsx')
assert.match(src, /safeLink\(/); assert.match(src, /rel="noopener noreferrer"/); assert.match(src, /target="_blank"/)
for (const t of ['Macht gerade', 'Tokens heute', 'Kosten heute', 'Letzte Aktionen']) assert.ok(src.includes(t), t)
assert.match(src, /MODEL_IDS/); assert.match(src, /wk-chip-/)
// jeder href und jedes to kommt aus dem safeLink-Ergebnis, nie aus dem Ereignis selbst
assert.match(src, /href=\{safe\.href\}/); assert.match(src, /to=\{safe\.href\}/)
assert.doesNotMatch(src, /\b(?:href|to)=\{(?!safe\.href\})/)
})
test('kein innerHTML in Komponenten und Engine', () => {
for (const dir of ['', 'engine/']) for (const f of readdirSync(new URL('../../src/components/workers/' + dir, import.meta.url))) { if (!/\.(jsx|js)$/.test(f)) continue; const s = read(dir + f); assert.doesNotMatch(s, /dangerouslySetInnerHTML|innerHTML/, dir + f) }
})
test('WorkersKpis nutzt Stat und kpiTiles; WorkerList: Knopf je Worker mit data-wk-worker, aria-pressed und Statustext', () => {
assert.match(read('WorkersKpis.jsx'), /kpiTiles\(/); assert.match(read('WorkersKpis.jsx'), /<Stat /)
const l = read('WorkerList.jsx'); assert.match(l, /data-wk-worker=/); assert.match(l, /aria-pressed=/); assert.match(l, /aria-label=/); assert.match(l, /FLOOR_TITLES/)
// die Liste enthält keine Links: ein Klick wählt nur aus
assert.doesNotMatch(l, /<a[\s>]|<Link[\s>]|href=/)
})
test('Einstellungs-Dialog: Dialog-Baustein, Formularlogik, kein paused, Admin-Beschriftungen, Notbremsen-Block', () => {
const s = read('WorkersSettingsDialog.jsx')
assert.match(s, /from '\.\.\/ui\/Dialog'/); assert.match(s, /formToPatch\(/); assert.match(s, /validateForm\(/); assert.match(s, /api\.saveSettings\(/)
assert.doesNotMatch(s, /paused/)
for (const t of ['Hustler-Einstellungen', 'Tagesgrenze (USD)', 'Monatsgrenze (USD)', 'Unverlangte Mails pro Tag', 'Angebotstext', 'Regionen', 'Speichern', 'Abbrechen']) assert.ok(s.includes(t), t)
assert.match(s, /disabled=\{/)
// H4/A21: Block „Versand gestoppt“ mit Freigabe, außerhalb des Formular-Patches
assert.match(s, /sendingHalted/); assert.ok(s.includes('Versand gestoppt')); assert.ok(s.includes('Versand wieder freigeben'))
assert.match(s, /api\.saveSettings\(\{ sendingHalted: false \}\)/); assert.match(s, /sendingHaltedReason/)
assert.doesNotMatch(s, /formToPatch\([^)]*sendingHalted/)
})
test('WorkersStage: Aufräumen, Sichtbarkeit, Bewegungsreduktion', () => {
const s = read('WorkersStage.jsx')
assert.match(s, /\.destroy\(\)/); assert.match(s, /visibilitychange/); assert.match(s, /removeEventListener\('visibilitychange'/); assert.match(s, /prefers-reduced-motion: reduce/)
assert.match(s, /ResizeObserver/); assert.match(s, /disconnect\(\)/); assert.match(s, /fontsReady/); assert.match(s, /forwardRef/)
// Controller-Beschluss: Sichtbarkeit steuert die Szene (setPaused), beim Zurückkehren wird die Regie beruhigt (flush)
assert.match(s, /setPaused\(/); assert.match(s, /\.flush\(\)/); assert.match(s, /mountTower\(/); assert.match(s, /engineKey/)
assert.match(s, /className="wk-stage"/); assert.match(s, /className="wk-canvas"/); assert.match(s, /className="wk-zoom"/)
assert.ok(s.includes('Pixel-Hochhaus: Etagen und Räume der Hustler. Auswahl auch über die Liste.'))
for (const t of ['+', '−', 'Ganz']) assert.ok(s.includes(`>${t}<`), t)
// jeder Knopf der Bühne ist ein type="button" (WOMS-Button hat keinen Standard-type, PF-08)
assert.equal((s.match(/<Button\b/g) || []).length, (s.match(/<Button\b[^>]*type="button"/g) || []).length)
})
test('WorkersBoard: Zustände und Rechte (Review Focus 1)', () => {
const s = read('WorkersBoard.jsx')
assert.match(s, /Der Hustler-Dienst ist noch nicht erreichbar/); assert.match(s, /Verbindung gestört/); assert.match(s, /reduceEvents\(/); assert.match(s, /pauseLabel\(/)
assert.match(s, /isAdmin &&/); assert.match(s, /stageRef\s*(\?\?|\|\|)/); assert.match(s, /useRef\(/); assert.doesNotMatch(s, /useAuth|appwrite/)
// H4/A21: Notbremsen-Zeile für alle, Grund als Textknoten
assert.match(s, /state\.sendingHalted/); assert.match(s, /state\.sendingHaltedReason/); assert.match(s, /className="msg err"/); assert.match(s, /role="alert"/)
assert.ok(s.includes('Versand gestoppt: ')); assert.ok(s.includes('Ein Admin kann ihn in den Einstellungen freigeben.')); assert.ok(s.includes('Grund nicht angegeben'))
assert.match(s, /onSaved=\{/); assert.doesNotMatch(s, /dangerouslySetInnerHTML|innerHTML/)
// Controller-Beschluss T19: Tabelle nach workerId an Karte und Liste, Karte bekommt Eintrag und Spur, Zustand kommt aus indexWorkers
assert.match(s, /indexWorkers\(/); assert.match(s, /useWorkersFeed\(/); assert.match(s, /useWorkerTrace\(/); assert.match(s, /api\.pause\(/)
assert.match(s, /<WorkerCard\b[^>]*\bworker=\{/); assert.match(s, /<WorkerCard\b[^>]*\btrace=\{/); assert.match(s, /<WorkerList\b[^>]*\bworkers=\{workers\}/); assert.match(s, /<WorkersKpis\b/)
// Links entstehen nur in der Karte über safeLink, das Board hat keine
assert.doesNotMatch(s, /href=|<a[\s>]|<Link[\s>]/)
assert.match(s, /className="wk-toolbar"/); assert.match(s, /className="wk-main"/); assert.match(s, /className="loading"/)
assert.ok(s.includes('Angehalten: Es wird nichts gesendet und nichts gebaut.')); assert.ok(s.includes('Neu laden')); assert.ok(s.includes('Grund nicht angegeben'))
// PF-08: jeder Knopf ein type="button" (WOMS-Button hat keinen Standard-type)
assert.equal((s.match(/<Button\b/g) || []).length, (s.match(/<Button\b[^>]*type="button"/g) || []).length)
})
test('Karte und Board: Auswahl setzt den Fokus auf den Namen und holt die Karte ins Bild, Schließen gibt den Fokus zurück (Endprüfung P2, Handy/Tablet)', () => {
const card = read('WorkerCard.jsx')
// Namen fokussierbar machen (tabIndex -1), Fokus ohne eigenes Springen, danach gezielt an den oberen Rand holen, wenn der Kartenanfang nicht im Fenster liegt
assert.match(card, /<h2 className="wk-name" ref=\{headingRef\} tabIndex=\{-1\}>/)
assert.match(card, /ref=\{sectionRef\}/)
assert.match(card, /heading\.focus\(\{ preventScroll: true \}\)/)
assert.match(card, /section\.scrollIntoView\(\{ block: 'start' \}\)/)
assert.match(card, /scrollMarginTop/)
assert.match(card, /\}, \[workerId\]\)/)
// Hooks stehen vor der frühen Rückgabe ohne Auswahl (Regeln der Hooks)
assert.ok(card.indexOf('useEffect(() => {\n if (!workerId) return') > 0)
assert.ok(card.indexOf('useEffect(() => {\n if (!workerId) return') < card.indexOf(' if (!workerId) {\n return ('))
const board = read('WorkersBoard.jsx')
assert.match(board, /const sideRef = useRef\(null\)/); assert.match(board, /<div className="wk-side" ref=\{sideRef\}>/)
assert.match(board, /onClose=\{closeCard\}/); assert.match(board, /data-wk-worker/); assert.match(board, /button\.focus\(\)/)
assert.doesNotMatch(board, /onClose=\{\(\) => setSelectedId\(null\)\}/)
})
// ---- P3-T21: Abmelden setzt das JWT zurück, „Alles anhalten“ fragt vorher nach ----
const readSrc = (p) => readFileSync(new URL('../../src/' + p, import.meta.url), 'utf8')
test('Abmelden: AuthContext ruft resetWorkersJwt() auf, workersClient setzt JWT-Cache und Sperre zurück (P3-T21)', () => {
const auth = readSrc('context/AuthContext.jsx')
assert.match(auth, /import \{ resetWorkersJwt \} from '\.\.\/lib\/workersClient'/)
const start = auth.indexOf('async function logout()')
assert.ok(start > 0)
const logout = auth.slice(start, auth.indexOf('const isAdmin', start))
assert.match(logout, /resetWorkersJwt\(\)/)
// im finally: auch ein fehlgeschlagenes deleteSessions lässt das JWT des alten Nutzers nicht im Cache
assert.ok(logout.indexOf('finally') > 0 && logout.indexOf('resetWorkersJwt()') > logout.indexOf('finally'))
const client = readSrc('lib/workersClient.js')
assert.match(client, /export function resetWorkersJwt\(\)/)
const fn = client.slice(client.indexOf('export function resetWorkersJwt()'))
assert.match(fn, /jwtCache\.invalidate\(\)/); assert.match(fn, /workersApi\.resetAuth\(\)/)
})
test('WorkersBoard: „Alles anhalten“ öffnet erst einen Bestätigungsdialog, „Fortsetzen“ geht sofort (P3-T21)', () => {
const s = read('WorkersBoard.jsx')
assert.match(s, /import Dialog from '\.\.\/ui\/Dialog'/)
assert.match(s, /<Dialog\b[^>]*title="Alles anhalten\?"/)
assert.ok(s.includes('Die Worker hören sofort auf, bis jemand fortsetzt.'))
// beide Knöpfe des Dialogs: type="button", Beschriftung „Anhalten“ und „Abbrechen“
assert.match(s, /<Button type="button" onClick=\{[^}]*\}[^>]*>Abbrechen<\/Button>/)
assert.match(s, /<Button variant="primary" type="button" onClick=\{confirmHalt\}[^>]*>Anhalten<\/Button>/)
// Der Knopf der Leiste fragt nur beim Anhalten nach; Fortsetzen ruft den Dienst ohne Dialog
assert.match(s, /<Button type="button" onClick=\{requestPause\}/)
assert.match(s, /const requestPause = \(\) => \{\s*if \(paused\) setPause\(false\)\s*else setConfirmOpen\(true\)\s*\}/)
// Bestätigen schließt den Dialog und hält an; der Zielwert steht fest (nicht !paused: sonst setzte ein veralteter Dialog die Arbeit wieder fort)
assert.match(s, /const confirmHalt = \(\) => \{\s*setConfirmOpen\(false\)\s*setPause\(true\)\s*\}/)
assert.match(s, /api\.pause\(target\)/); assert.doesNotMatch(s, /api\.pause\(!paused\)/)
// der Dialog steht nur offen, solange nicht schon angehalten ist (hat jemand anderes angehalten, entfällt die Frage)
assert.match(s, /\{confirmOpen && !paused && \(/)
// und die Frage taucht nach einem späteren Fortsetzen nicht von selbst wieder auf
assert.match(s, /useEffect\(\(\) => \{\s*if \(haltedByState\) setConfirmOpen\(false\)\s*\}, \[haltedByState\]\)/)
// Hooks vor der frühen Rückgabe
assert.ok(s.indexOf('useState(false)') < s.indexOf('if (!loaded || !state)') && s.indexOf('const [confirmOpen') < s.indexOf('if (!loaded || !state)'))
// PF-08 gilt weiter: jeder Knopf ein type="button"
assert.equal((s.match(/<Button\b/g) || []).length, (s.match(/<Button\b[^>]*type="button"/g) || []).length)
assert.doesNotMatch(s, /window\.confirm|confirm\(/)
})