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