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'), /]|]|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(/]*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, /]*\bworker=\{/); assert.match(s, /]*\btrace=\{/); assert.match(s, /]*\bworkers=\{workers\}/); assert.match(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(/]*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, /

/) 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, /
/) 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, /]*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, /