Hustler-Pixelwelt T11: alte helle Oberfläche und Kanvas-Engine entfernt, workers.css gekürzt, Zusicherungen in die neuen Tests übertragen
- gelöscht: components/workers/engine/* (10 Dateien), WorkersStage, WorkersBoard, WorkerCard, WorkerList, WorkersKpis, die neun Engine-Tests samt workersSmoke (211 Tests) und die Helfer engineVm.mjs und fakeCtx.mjs (nur die Engine-Tests brauchten sie) - workers.css (310 -> 60 Zeilen) behält nur Einstellungen-Dialog und Interessenten-Panel; @font-face für Pixelify Sans steht jetzt in hustler/schrift.css - workersComponents: kein innerHTML und kein Kanvas rekursiv unter components/workers/**, jeder Knopf mit type, Links nur aus geprüften Feldern (externe mit noopener), keine Anmeldung in den Bildschirmen, alte Dateien weg und nirgends mehr importiert - workersCss: jede wk-Klasse im Code hat eine Regel, keine Regel ohne Nutzer; workersFont: Pixelify Sans genau einmal in schrift.css - hustlerAssets: Pixelify Sans in schrift.css erlaubt; hustlerDialog: Link-Ziele nur aus safeLink, Knöpfe mit type; hustlerSeite: Laden/Fehler/bereit am selben Einhängepunkt Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4be9750f36
commit
ba007a325a
35 files changed
+277
-6495
No files matched your search
@@ -1,29 +1,141 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync, readdirSync } from 'node:fs'
|
||||
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
// 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')
|
||||
// Regeln für alle React-Teile der Hustler-Seite, geprüft am Quelltext (Muster adminRoute.test.mjs): src/components/workers/** mit hochhaus/, pult/ und keller/.
|
||||
// Das Verhalten der einzelnen Bildschirme prüfen die Komponententests (hustlerKopf, hustlerDialog, hustlerHochhausWelt, hustlerSeite, hustlerPult, hustlerKeller ...).
|
||||
// Seit T11 gibt es die alte helle Oberfläche und die Kanvas-Engine nicht mehr; ihre Zusicherungen sind in diese Tests gewandert (Tabelle im Übergabebericht von T11).
|
||||
const COMPONENTS = fileURLToPath(new URL('../../src/components/workers/', import.meta.url))
|
||||
const SRC = fileURLToPath(new URL('../../src/', import.meta.url))
|
||||
const read = (p) => readFileSync(path.join(COMPONENTS, p), 'utf8')
|
||||
const readSrc = (p) => readFileSync(path.join(SRC, p), 'utf8')
|
||||
const walk = (dir) => readdirSync(dir, { withFileTypes: true }).flatMap((e) => (e.isDirectory() ? walk(path.join(dir, e.name)) : [path.join(dir, e.name)]))
|
||||
const komponenten = () => walk(COMPONENTS).filter((f) => /\.(jsx?|mjs)$/.test(f))
|
||||
const rel = (f) => path.relative(COMPONENTS, f)
|
||||
// Kommentare weg: Block- und Zeilenkommentare (// nur am Zeilenanfang oder nach Leerraum, damit https:// in Zeichenketten bleibt)
|
||||
const ohneKommentare = (code) => code.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|\s)\/\/.*$/gm, '$1')
|
||||
|
||||
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\})/)
|
||||
// Öffnende Tags `<name …>` samt Attributen. Klammern {…} und Zeichenketten werden übersprungen, damit das `=>` eines onClick das Tag nicht beendet.
|
||||
function oeffnendeTags(src, name) {
|
||||
const tags = []
|
||||
const re = new RegExp(`<${name}(?=[\\s/>])`, 'g')
|
||||
for (let m; (m = re.exec(src)) !== null;) {
|
||||
let tiefe = 0
|
||||
let quote = ''
|
||||
let i = m.index + m[0].length
|
||||
for (; i < src.length; i++) {
|
||||
const c = src[i]
|
||||
if (quote) {
|
||||
if (c === '\\') i += 1
|
||||
else if (c === quote) quote = ''
|
||||
continue
|
||||
}
|
||||
if (c === '"' || c === "'" || c === '`') quote = c
|
||||
else if (c === '{') tiefe += 1
|
||||
else if (c === '}') tiefe -= 1
|
||||
else if (c === '>' && tiefe === 0) break
|
||||
}
|
||||
tags.push(src.slice(m.index, i + 1))
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
// Wert eines Attributs im Tag: Zeichenkette (mit Anführungszeichen) oder der Ausdruck in {…} ohne Klammern; null, wenn es das Attribut nicht gibt
|
||||
function attribut(tag, name) {
|
||||
const m = new RegExp(`(?<![\\w-])${name}=`).exec(tag)
|
||||
if (!m) return null
|
||||
const start = m.index + m[0].length
|
||||
if (tag[start] === '"') return tag.slice(start, tag.indexOf('"', start + 1) + 1)
|
||||
if (tag[start] !== '{') return null
|
||||
let tiefe = 0
|
||||
for (let i = start; i < tag.length; i++) {
|
||||
if (tag[i] === '{') tiefe += 1
|
||||
else if (tag[i] === '}') {
|
||||
tiefe -= 1
|
||||
if (tiefe === 0) return tag.slice(start + 1, i).trim()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
test('kein innerHTML und keine Kanvas-Zeichnung in den Komponenten unter src/components/workers/** (rekursiv, Kommentare ignoriert)', () => {
|
||||
const dateien = komponenten()
|
||||
for (const dir of ['hochhaus', 'pult', 'keller']) assert.ok(dateien.some((f) => rel(f).startsWith(dir + path.sep)), `${dir}/ wird mitgelesen`)
|
||||
assert.ok(dateien.length >= 15, `nur ${dateien.length} Dateien gelesen`)
|
||||
for (const f of dateien) {
|
||||
const code = ohneKommentare(readFileSync(f, 'utf8'))
|
||||
assert.doesNotMatch(code, /dangerouslySetInnerHTML|innerHTML|outerHTML|insertAdjacentHTML|document\.write/, rel(f))
|
||||
assert.doesNotMatch(code, /<canvas\b|getContext\s*\(/, `${rel(f)}: die Pixelwelt ist DOM + CSS, kein Kanvas`)
|
||||
}
|
||||
})
|
||||
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) }
|
||||
|
||||
// Frühere Prüfung des Boards: Rechte und Nutzer kommen als Props von der Seite (isAdmin, me), die Bildschirme greifen nie selbst auf Anmeldung oder Appwrite zu.
|
||||
test('Komponenten ohne Anmeldungsbezug: kein useAuth und kein appwrite unter src/components/workers/**', () => {
|
||||
for (const f of komponenten()) assert.doesNotMatch(ohneKommentare(readFileSync(f, 'utf8')), /useAuth|appwrite/i, rel(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('die alte Oberfläche und die Kanvas-Engine sind weg, und nichts unter src/ verweist noch darauf (T11)', () => {
|
||||
for (const alt of ['engine', 'WorkersStage.jsx', 'WorkersBoard.jsx', 'WorkerCard.jsx', 'WorkerList.jsx', 'WorkersKpis.jsx']) assert.equal(existsSync(path.join(COMPONENTS, alt)), false, alt)
|
||||
const verweis = /['"][^'"\n]*(?:\/engine\/[\w.-]+|\/(?:WorkersStage|WorkersBoard|WorkerCard|WorkerList|WorkersKpis))(?:\.jsx?)?['"]/
|
||||
let gelesen = 0
|
||||
for (const f of walk(SRC).filter((x) => /\.jsx?$/.test(x))) {
|
||||
gelesen += 1
|
||||
assert.doesNotMatch(ohneKommentare(readFileSync(f, 'utf8')), verweis, path.relative(SRC, f))
|
||||
}
|
||||
assert.ok(gelesen > 100, `nur ${gelesen} Dateien gelesen`)
|
||||
})
|
||||
|
||||
// Frühere Prüfung der Bühne und des Boards (PF-08): der WOMS-Button hat keinen Standard-type, ein <button> ohne type schickt in einem Formular ab.
|
||||
test('jeder Knopf (<button>, <Button>) hat ein ausdrückliches type="button"; „submit“ gibt es nur im Formular der Einstellungen', () => {
|
||||
let anzahl = 0
|
||||
for (const f of komponenten().filter((x) => x.endsWith('.jsx'))) {
|
||||
const code = ohneKommentare(readFileSync(f, 'utf8'))
|
||||
for (const name of ['button', 'Button']) {
|
||||
const tags = oeffnendeTags(code, name)
|
||||
assert.equal(tags.length, (code.match(new RegExp(`<${name}(?=[\\s/>])`, 'g')) || []).length, `${rel(f)}: ${name}-Tags vollständig gelesen`)
|
||||
for (const tag of tags) {
|
||||
anzahl += 1
|
||||
const erlaubt = rel(f) === 'WorkersSettingsDialog.jsx' ? /\btype="(?:button|submit)"/ : /\btype="button"/
|
||||
assert.match(tag, erlaubt, `${rel(f)}: ${tag.replace(/\s+/g, ' ').slice(0, 90)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.ok(anzahl >= 35, `nur ${anzahl} Knöpfe gelesen`)
|
||||
// „submit“ kommt genau einmal vor: „Speichern“ im Formular
|
||||
assert.equal((ohneKommentare(read('WorkersSettingsDialog.jsx')).match(/type="submit"/g) || []).length, 1)
|
||||
})
|
||||
|
||||
// Frühere Prüfung der Worker-Karte (Review Focus 5): Ziele von Links kommen nur aus geprüften Feldern, externe öffnen mit noopener.
|
||||
test('Links: jedes Ziel (href, to) kommt aus einem geprüften Feld oder ist fest; externe Ziele öffnen im neuen Tab mit rel="noopener noreferrer"', () => {
|
||||
// safeLink (Spur im Dialog) · sourceHref/previewHref (workersPult, botleadsView, workersProspect über safeExternalUrl/safeHref/previewUrl) · mailtoHref · feste Pfade der Etagen
|
||||
const ERLAUBT = new Set(['safe.href', 'row.sourceHref', 'row.previewHref', 'view.sourceHref', 'e.sourceHref', 'mail', '`/hustler?tab=${link.tab}`'])
|
||||
let links = 0
|
||||
for (const f of komponenten().filter((x) => x.endsWith('.jsx'))) {
|
||||
const code = ohneKommentare(readFileSync(f, 'utf8'))
|
||||
for (const [name, ziel] of [['a', 'href'], ['Link', 'to']]) {
|
||||
for (const tag of oeffnendeTags(code, name)) {
|
||||
links += 1
|
||||
const wert = attribut(tag, ziel)
|
||||
assert.ok(wert !== null, `${rel(f)}: ${name} ohne ${ziel}: ${tag.slice(0, 80)}`)
|
||||
assert.ok(wert.startsWith('"') || ERLAUBT.has(wert), `${rel(f)}: ${ziel}={${wert}} ist kein geprüftes Ziel`)
|
||||
if (attribut(tag, 'target') === '"_blank"') assert.equal(attribut(tag, 'rel'), '"noopener noreferrer"', `${rel(f)}: ${tag.slice(0, 80)}`)
|
||||
if (wert.startsWith('"')) assert.doesNotMatch(wert, /javascript:|data:/i)
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.ok(links >= 8, `nur ${links} Links gelesen`)
|
||||
// die Felder selbst entstehen in den Ansichten nur über die Prüfer
|
||||
assert.match(readSrc('lib/workersPult.js'), /sourceHref: freigabe \? null : safeExternalUrl\(item\.sourceUrl\)/)
|
||||
assert.match(readSrc('lib/workersPult.js'), /previewHref: freigabe \? previewUrl\(item\.sourceUrl\) : null/)
|
||||
assert.match(readSrc('lib/workersProspect.js'), /sourceHref: safeExternalUrl\(prospect\.sourceUrl\)/)
|
||||
assert.match(readSrc('lib/botleadsView.js'), /sourceHref: safeHref\(it\.sourceUrl\)/)
|
||||
assert.match(readSrc('lib/workersPult.js'), /return `mailto:\$\{to\}/)
|
||||
// die Etagen-Links des Hochhauses sind feste Pfade
|
||||
assert.match(read('hochhaus/HochhausWelt.jsx'), /tab: 'freigabe'[\s\S]*tab: 'botleads'/)
|
||||
})
|
||||
|
||||
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\(/)
|
||||
@@ -35,57 +147,8 @@ test('Einstellungs-Dialog: Dialog-Baustein, Formularlogik, kein paused, Admin-Be
|
||||
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')
|
||||
// ---- P3-T21: Abmelden setzt das JWT zurück ----
|
||||
|
||||
test('Abmelden: AuthContext ruft resetWorkersJwt() auf, workersClient setzt JWT-Cache und Sperre zurück (P3-T21)', () => {
|
||||
const auth = readSrc('context/AuthContext.jsx')
|
||||
@@ -101,50 +164,3 @@ test('Abmelden: AuthContext ruft resetWorkersJwt() auf, workersClient setzt JWT-
|
||||
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\(/)
|
||||
})
|
||||
|
||||
// Endprüfung P3 (W3): Nach „Anhalten“ schließt der Dialog im selben Takt, in dem pausing beginnt und den Knopf der Leiste sperrt;
|
||||
// useDialogFocus kann den Fokus dann nicht zurückgeben (focus() auf einem gesperrten Knopf tut nichts) und er bliebe auf <body>.
|
||||
// Das Board gibt ihn deshalb selbst zurück, sobald pausing endet. Das echte Verhalten prüft der Prüfstand (shot.cjs, Szenario I).
|
||||
test('WorkersBoard: der Fokus kehrt nach dem Umschalten auf den Knopf der Leiste zurück, sobald er wieder bedienbar ist (W3)', () => {
|
||||
const s = read('WorkersBoard.jsx')
|
||||
assert.match(s, /const toolbarRef = useRef\(null\)/); assert.match(s, /const refocusPauseRef = useRef\(false\)/)
|
||||
assert.match(s, /<div className="wk-toolbar" ref=\{toolbarRef\}>/)
|
||||
assert.match(s, /<Button type="button" onClick=\{requestPause\} disabled=\{pausing\} data-wk-pause>/)
|
||||
// die Markierung wird vor setPausing(true) gesetzt, der Fokus erst im Effekt nach dem Ende von pausing (sonst wäre der Knopf noch gesperrt)
|
||||
assert.match(s, /if \(pausing\) return\s*refocusPauseRef\.current = true\s*setPausing\(true\)/)
|
||||
const effect = s.match(/useEffect\(\(\) => \{\s*if \(pausing \|\| !refocusPauseRef\.current\) return([\s\S]*?)\}, \[pausing\]\)/)
|
||||
assert.ok(effect, 'Effekt mit [pausing]')
|
||||
assert.match(effect[1], /refocusPauseRef\.current = false/)
|
||||
// nur wenn der Fokus verloren ist (auf <body> oder nirgends): wer inzwischen woanders ist, wird nicht gestört
|
||||
assert.match(effect[1], /const active = document\.activeElement\s*if \(active && active !== document\.body\) return/)
|
||||
assert.match(effect[1], /toolbarRef\.current\?\.querySelector\('\[data-wk-pause\]'\)/); assert.match(effect[1], /button\.focus\(\)/)
|
||||
// Hook vor der frühen Rückgabe
|
||||
assert.ok(s.indexOf('[data-wk-pause]') < s.indexOf('if (!loaded || !state)'))
|
||||
// das Gesperrt-Sein des Knopfes bleibt (kein Doppelklick während der Anfrage)
|
||||
assert.match(s, /disabled=\{pausing\}/)
|
||||
})
|
||||
Reference in new issue
Block a user