Files
tickte-system/tests/unit/workersComponents.test.mjs
T
ClaudeandClaude Sonnet 5.5 84fb37a98e Workers P2: Auswahl aus der Liste holt die Karte ins Bild und setzt den Fokus (Handy und Tablet)
Endprüfung P2. Unter 1100 px steht die Karte über der Liste mit 19 Einträgen, aus der man wählt (die Liste ist dort die
Alternative zur Leinwand). Ein Klick in der Liste änderte nichts Sichtbares: die Karte öffnete sich oberhalb des
Fensters, Screenreader hörten nichts, und „Karte schließen“ ließ den Fokus ins Leere fallen.

Bei neuer Auswahl nimmt jetzt der Name in der Karte den Fokus (tabIndex -1), und liegt der Kartenanfang außerhalb des
Fensters, wird die Karte an den oberen Rand geholt (scroll-margin-top: unter der festen Leiste der Handy-Ansicht 64 px).
„Karte schließen“ gibt den Fokus an den Listeneintrag des Workers zurück.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 05:41:03 +00:00

85 lines
7.3 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\)\}/)
})