Hustler-Pixelwelt T13: Korrekturen Hochhaus (Fokus, Tastatur, weniger Bewegung, Handy-Tippflächen, Schnee per transform, Pause überall, Seite ohne Leerlauf-Blitz)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0924a8d2ae
commit
5e6698f5e0
14 files changed
+994
-52
No files matched your search
@@ -609,6 +609,94 @@ test('Fokus-Rückgabe: Kennungen mit Sonderzeichen gehen nie in einen Selektor',
|
||||
// ---------------------------------------------------------------------------------------------------------------------------------------------
|
||||
// Quelltext und Stil
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------------------------------
|
||||
// Korrekturen der Endprüfung (T13): Ansage beim Blättern, Pause, Handy quer, Überlagerung, weniger Bewegung
|
||||
|
||||
test('M6 Blättern wird angesagt: Live-Region (polite) ist beim Öffnen leer, nach ◂ ▸ und ←/→ steht Name, Status und Nummer darin; Öffnen sagt nichts an', async () => {
|
||||
const workers = { lea: EINTRAG('arbeitet'), bea: EINTRAG('wartet'), sven: EINTRAG('fehler'), otto: EINTRAG('leerlauf'), arne: EINTRAG('grau', { connected: false }) }
|
||||
await lauf({ props: { workerId: 'lea', workers } }, (ui) => {
|
||||
const region = () => ui.one((n) => n.props['aria-live'] === 'polite', 'Live-Region')
|
||||
assert.equal(region().props['aria-atomic'], 'true'); assert.ok(hasClass(region(), 'hk-nur-sr'))
|
||||
assert.equal(textOf(region()), '', 'beim Öffnen nichts ansagen')
|
||||
ui.knopf('Nächster Hustler').props.onClick({})
|
||||
assert.equal(textOf(region()), 'Bea, Wartet auf Auftrag, 10 von 19')
|
||||
assert.deepEqual(ui.wechsel, ['bea'])
|
||||
ui.setProps({ workerId: 'bea' })
|
||||
assert.equal(textOf(region()), 'Bea, Wartet auf Auftrag, 10 von 19', 'bleibt stehen, bis neu geblättert wird')
|
||||
ui.taste('ArrowLeft')
|
||||
assert.equal(textOf(region()), 'Lea, Arbeitet, 9 von 19')
|
||||
ui.setProps({ workerId: 'lea' })
|
||||
ui.taste('ArrowLeft')
|
||||
assert.equal(textOf(region()), 'Sven, Fehler, 8 von 19')
|
||||
ui.setProps({ workerId: 'otto' })
|
||||
ui.knopf('Vorheriger Hustler').props.onClick({})
|
||||
assert.equal(textOf(region()), 'Arne, Noch nicht angeschlossen, 19 von 19', 'Umlauf')
|
||||
// die Ansage ist eine Momentaufnahme: ein späterer Statuswechsel meldet sich nicht von selbst
|
||||
ui.setProps({ workerId: 'arne', workers: { ...workers, arne: EINTRAG('arbeitet') } })
|
||||
assert.equal(textOf(region()), 'Arne, Noch nicht angeschlossen, 19 von 19')
|
||||
// der Dialog selbst kennt den neuen Namen
|
||||
assert.equal(ui.dialog().props['aria-label'], 'Hustler Arne')
|
||||
// ein Hustler ohne Eintrag in der Tabelle: Leerlauf
|
||||
ui.setProps({ workerId: 'lea', workers: {} })
|
||||
ui.knopf('Nächster Hustler').props.onClick({})
|
||||
assert.equal(textOf(region()), 'Bea, Leerlauf, 10 von 19')
|
||||
// Dialog zu und neu auf: wieder leer
|
||||
ui.setProps({ workerId: null })
|
||||
ui.setProps({ workerId: 'kim', workers })
|
||||
assert.equal(textOf(region()), '')
|
||||
})
|
||||
})
|
||||
|
||||
test('R3 Pause: bei paused trägt die Überlagerung hk-steht (alle Animationen im Dialog stehen), sonst nicht', async () => {
|
||||
await lauf({ props: { workerId: 'lea', workers: { lea: EINTRAG('arbeitet') }, paused: false } }, (ui) => {
|
||||
assert.ok(!hasClass(ui.rahmen(), 'hk-steht'))
|
||||
ui.setProps({ paused: true })
|
||||
assert.ok(hasClass(ui.rahmen(), 'hk-steht')); assert.ok(hasClass(ui.rahmen(), 'hk-dialog-ueber'))
|
||||
ui.setProps({ paused: 'ja' })
|
||||
assert.ok(!hasClass(ui.rahmen(), 'hk-steht'), 'nur echtes true')
|
||||
ui.setProps({ paused: undefined })
|
||||
assert.ok(!hasClass(ui.rahmen(), 'hk-steht'))
|
||||
})
|
||||
// die globale Regel hält Animationen unter .hk-steht an, und die Detailansicht liegt unter der Seiten-Wurzel
|
||||
const regel = fs.readFileSync(path.join(ROOT, 'src/styles/hustler/hustler-ui.css'), 'utf8')
|
||||
assert.match(regel, /\.hk-seite \.hk-steht \*[^{]*\{\s*animation-play-state:\s*paused !important/)
|
||||
})
|
||||
|
||||
test('M5 Handy quer: Vollbild auch bei niedriger Höhe (max-width 719 ODER max-height 500), die Zeilen-Anordnung gilt nur quer; Überlagerung unter der Handy-Navigation', () => {
|
||||
const css = fs.readFileSync(path.join(ROOT, 'src/styles/hustler/hustler-detail.css'), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
// Vollbild-Block: beide Bedingungen, durch Komma verbunden
|
||||
const vollbild = css.match(/@media \(max-width: 719px\), \(max-height: 500px\) \{([\s\S]*?)\n\}/)
|
||||
assert.ok(vollbild, 'Media-Abfrage für Vollbild')
|
||||
assert.match(vollbild[1], /\.hk-hochhaus \.hk-dialog-ueber \{ padding: 0; \}/)
|
||||
assert.match(vollbild[1], /width: 100%; max-width: 100%; height: 100%; max-height: 100%/)
|
||||
assert.match(vollbild[1], /container-type: inline-size/)
|
||||
// Quer: Szene links, Daten rechts, nur im Querformat und niedrig
|
||||
const quer = css.match(/@media \(max-height: 500px\) and \(orientation: landscape\) \{([\s\S]*?)\n\}/)
|
||||
assert.ok(quer, 'Media-Abfrage für quer')
|
||||
assert.match(quer[1], /flex-direction: row/); assert.match(quer[1], /45cqw/)
|
||||
// die alte Bedingung (nur schmale Handys) ist weg
|
||||
assert.doesNotMatch(css, /@media \(max-width: 719px\) and \(max-height: 520px\)/)
|
||||
// R4: z-index unter Seitenleiste (20) und Abdunkler (15)
|
||||
assert.match(css, /\.hk-hochhaus \.hk-dialog-ueber \{[^}]*z-index: 9;/)
|
||||
assert.doesNotMatch(css, /z-index:\s*(1[0-9]|[2-9][0-9])/)
|
||||
})
|
||||
|
||||
test('M4 weniger Bewegung: alle Sprites der Detailansicht (Figur, Monitor, Dampf, Zzz, Wirbel) tragen hk-spr und stehen damit auf dem ersten Bild; der Fehler-Marker hat seine eigene Regel', async () => {
|
||||
const css = fs.readFileSync(path.join(ROOT, 'src/styles/hustler/hustler-ui.css'), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
const reduziert = css.slice(css.indexOf('@media (prefers-reduced-motion: reduce)'))
|
||||
assert.match(reduziert, /\.hk-seite \.hk-hochhaus \.hk-spr \{ animation: none; \}/)
|
||||
assert.match(reduziert, /\.hk-seite \.hk-alarm \{ animation: none; \}/)
|
||||
const vorlage = fs.readFileSync(path.join(ROOT, 'src/styles/hustler/hochhaus.css'), 'utf8')
|
||||
const animiert = new Set([...vorlage.replace(/@media \(prefers-reduced-motion: reduce\)\{[\s\S]*?\n\}\n/, '').matchAll(/\.hk-hochhaus (\.hk-[\w-]+)\{[^}]*animation:/g)].map((m) => m[1].slice(1)))
|
||||
for (const [id, status] of [['lea', 'arbeitet'], ['hanna', 'wartet'], ['bea', 'leerlauf'], ['kim', 'grau'], ['otto', 'wartet'], ['pia', 'fehler']]) {
|
||||
await lauf({ props: { workerId: id, workers: { [id]: EINTRAG(status, { connected: status !== 'grau' }) } } }, (ui) => {
|
||||
const bewegt = ui.find((n) => typeof n.tag === 'string' && classes(n).some((c) => animiert.has(c)))
|
||||
assert.ok(bewegt.length >= 1, `${id}: mindestens die Figur`)
|
||||
for (const n of bewegt) assert.ok(classes(n).includes('hk-spr') || classes(n).includes('hk-alarm'), `${id}: ${classes(n).join(' ')} nicht von einer Reduziert-Regel erfasst`)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test('Komponente: kein innerHTML, kein CSS- oder Bild-Import, Links nur über safeLink', () => {
|
||||
const quelle = fs.readFileSync(path.join(ROOT, 'src/components/workers/hochhaus/HustlerDialog.jsx'), 'utf8')
|
||||
const src = quelle.replace(/(^|\s)\/\/.*$/gm, '$1') // Zeilenkommentare dürfen das Wort erwähnen
|
||||
|
||||
@@ -34,7 +34,7 @@ const laden = () => (bundle ??= esbuild.build({
|
||||
"export { default as HochhausWelt } from './src/components/workers/hochhaus/HochhausWelt.jsx'",
|
||||
"export { indexWorkers } from './src/lib/workersEvents.js'",
|
||||
"export { AUFZUG, ETAGEN, GRAUE_RAEUME, HUSTLER, RAEUME, SCHILDER } from './src/lib/hustlerWelt.js'",
|
||||
"export { figurZustand, kameraEtage, kameraGrenzen, kameraMinS, kameraPassend, kameraZoomAuf } from './src/lib/hustlerSicht.js'",
|
||||
"export { figurZustand, kameraEtage, kameraGrenzen, kameraMinS, kameraPassend, kameraZeige, kameraZoomAuf, tippRand } from './src/lib/hustlerSicht.js'",
|
||||
"export { FLOOR_TITLES, ROLES, ROOM_TITLES, SEARCH_ROOMS } from './src/lib/workersRoster.js'",
|
||||
].join('\n'),
|
||||
resolveDir: ROOT, loader: 'js',
|
||||
@@ -865,6 +865,306 @@ test('Kamera: beim Abbau fallen Rad-Zuhörer, Beobachter und Gleit-Timer weg; ei
|
||||
} finally { wechsel.end(); b2.restore(); r2() }
|
||||
})
|
||||
|
||||
// ---------- Korrekturen der Endprüfung (T13): Fokus, Tastatur, Handy, Schnee, weniger Bewegung ----------
|
||||
|
||||
const lies = (datei) => readFileSync(path.join(ROOT, 'src/styles/hustler', datei), 'utf8')
|
||||
const ohneKommentare = (text) => text.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
// Block { ... } ab der Klammer an Position `ab` (geschachtelt)
|
||||
function block(text, ab) {
|
||||
let tiefe = 0
|
||||
for (let i = ab; i < text.length; i += 1) {
|
||||
if (text[i] === '{') tiefe += 1
|
||||
else if (text[i] === '}') { tiefe -= 1; if (tiefe === 0) return text.slice(ab + 1, i) }
|
||||
}
|
||||
throw new Error('Klammer nicht geschlossen')
|
||||
}
|
||||
const mediaBlock = (text, kopf) => {
|
||||
const i = text.indexOf(kopf)
|
||||
assert.ok(i >= 0, kopf)
|
||||
return block(text, text.indexOf('{', i))
|
||||
}
|
||||
const leucht = (hex) => {
|
||||
const c = [1, 3, 5].map((i) => Number.parseInt(hex.slice(i, i + 2), 16) / 255).map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4))
|
||||
return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]
|
||||
}
|
||||
const kontrast = (a, b) => { const [x, y] = [leucht(a), leucht(b)].sort((p, q) => q - p); return (x + 0.05) / (y + 0.05) }
|
||||
|
||||
// Element-Attrappe für den Tastaturfokus: Lage in der Bühne (offset*), :focus-visible und Kind der Gruppe `.hk-etage, .hk-hustler`
|
||||
function fokusZiel({ left, top, width, height }, { figur = true, tastatur = true } = {}) {
|
||||
const el = {
|
||||
offsetLeft: left, offsetTop: top, offsetWidth: width, offsetHeight: height,
|
||||
closest: () => el,
|
||||
matches: (sel) => (sel === ':focus-visible' ? tastatur : sel === '.hk-hustler' ? figur : false),
|
||||
}
|
||||
return el
|
||||
}
|
||||
const fokus = (ziel, sicht) => ({ target: ziel, currentTarget: sicht })
|
||||
|
||||
test('M1 Fokus per Tastatur: eine Figur außerhalb des Bildes holt die Kamera heran (Kopfzeile abgezogen), mit Gleiten; sichtbar, Maus und gesperrt bewegen nichts', async () => {
|
||||
const { HUSTLER, kameraEtage, kameraZeige } = await laden()
|
||||
const restore = fensterAttrappe()
|
||||
const ui = await start()
|
||||
try {
|
||||
const sicht = ui.anhaengen(fakeSicht(390, 844))
|
||||
ui.etage('Werkstatt').props.onClick(ereignis()) // Handy hochkant: Etage heranzoomen
|
||||
await wait(650)
|
||||
const vorher = ui.kamera()
|
||||
nearKamera(vorher, kameraEtage(776, 832, { w: 390, h: 844 }), 'Werkstatt nah')
|
||||
// Ben (Werkstatt, rechts) liegt am rechten Rand, außerhalb der 24 px Luft
|
||||
const k = HUSTLER.ben.knopf
|
||||
const rechts = vorher.x + (k.left + k.w) * vorher.s
|
||||
assert.ok(rechts > 390 - 24, `Ausgangslage: Ben ragt in den Rand (${rechts})`)
|
||||
ui.ruf('onFocus', fokus(fokusZiel({ left: k.left, top: k.top, width: k.w, height: k.h }), sicht))
|
||||
const nachher = ui.kamera()
|
||||
const erwartet = kameraZeige(vorher, { left: k.left, top: k.top - 34 / vorher.s, width: k.w, height: k.h + 34 / vorher.s }, { w: 390, h: 844 }, { oben: 84 })
|
||||
nearKamera(nachher, erwartet, 'Ben sichtbar')
|
||||
assert.equal(nachher.s, vorher.s, 'Zoom bleibt')
|
||||
assert.ok(nachher.x + (k.left + k.w) * nachher.s <= 390 - 24 + 1e-6)
|
||||
assert.ok(hasClass(ui.buehne(), 'hk-gleitet'), 'gleitet')
|
||||
await wait(650)
|
||||
// schon sichtbar: keine Bewegung
|
||||
const still = ui.kamera()
|
||||
ui.ruf('onFocus', fokus(fokusZiel({ left: k.left, top: k.top, width: k.w, height: k.h }), sicht))
|
||||
assert.deepEqual(ui.kamera(), still); assert.ok(!hasClass(ui.buehne(), 'hk-gleitet'))
|
||||
// Mausfokus (kein :focus-visible): kein Schwenk
|
||||
const bob = HUSTLER.bob.knopf
|
||||
ui.ruf('onFocus', fokus(fokusZiel({ left: bob.left - 600, top: bob.top, width: bob.w, height: bob.h }, { tastatur: false }), sicht))
|
||||
assert.deepEqual(ui.kamera(), still)
|
||||
// ohne passendes Ziel (Fokus auf das Sichtfenster selbst): nichts
|
||||
ui.ruf('onFocus', { target: { closest: () => null, matches: () => true }, currentTarget: sicht })
|
||||
ui.ruf('onFocus', { target: null, currentTarget: sicht })
|
||||
assert.deepEqual(ui.kamera(), still)
|
||||
// Detailansicht offen: nichts
|
||||
ui.set({ gesperrt: true })
|
||||
ui.ruf('onFocus', fokus(fokusZiel({ left: bob.left - 600, top: bob.top, width: bob.w, height: bob.h }), sicht))
|
||||
assert.deepEqual(ui.kamera(), still)
|
||||
} finally { ui.end(); restore() }
|
||||
})
|
||||
|
||||
test('M1 Fokus per Tastatur: die Kopfzeile wird gemessen (Unterkante von .hk-kopf, sonst 84 px); bei „weniger Bewegung“ springt die Kamera statt zu gleiten', async () => {
|
||||
const { kameraZeige } = await laden()
|
||||
const g = { w: 1280, h: 720 }
|
||||
const ruhig = fensterAttrappe({ reduziert: true })
|
||||
const ui = await start()
|
||||
try {
|
||||
const sicht = ui.anhaengen(fakeSicht(g.w, g.h))
|
||||
for (let i = 0; i < 3; i += 1) ui.ruf('onKeyDown', ereignis({ key: '+' }))
|
||||
const k0 = ui.kamera()
|
||||
assert.ok(k0.s > 1.5)
|
||||
// Etage, deren Oberkante auf dem Bildschirm bei 130 px liegt: unter 84 + 24 = 108 (Standard), aber über 150 + 24 = 174 (gemessene Kopfzeile)
|
||||
const rechteck = { left: 1000, top: (130 - k0.y) / k0.s, width: 512, height: 56 }
|
||||
assert.equal(kameraZeige(k0, rechteck, g, { oben: 84 }), null, 'mit Standardhöhe nichts zu tun')
|
||||
const erwartet = kameraZeige(k0, rechteck, g, { oben: 150 })
|
||||
assert.notEqual(erwartet, null, 'mit gemessener Kopfzeile muss die Kamera schieben')
|
||||
const ziel = fokusZiel(rechteck, { figur: false })
|
||||
// ohne messbare Kopfzeile (kein Geschwister .hk-kopf): Standardhöhe, keine Bewegung
|
||||
ui.ruf('onFocus', fokus(ziel, sicht))
|
||||
nearKamera(ui.kamera(), k0, 'ohne Messung')
|
||||
// mit Messung: Kopfzeile reicht bis 150 px -> die Kamera schiebt die Etage darunter
|
||||
sicht.parentElement = { querySelector: (sel) => (sel === '.hk-kopf' ? { getBoundingClientRect: () => ({ bottom: 150 }) } : null) }
|
||||
ui.ruf('onFocus', fokus(ziel, sicht))
|
||||
nearKamera(ui.kamera(), erwartet, 'mit Messung')
|
||||
assert.ok(ui.kamera().y + rechteck.top * ui.kamera().s >= 150 + 24 - 1e-6, 'Etage liegt unter Kopfzeile + Luft')
|
||||
assert.equal(ui.kamera().s, k0.s)
|
||||
assert.ok(!hasClass(ui.buehne(), 'hk-gleitet'), 'bei weniger Bewegung springt die Kamera')
|
||||
// eine unsinnig hohe Kopfzeile wird auf die Hälfte des Fensters begrenzt (viele Meldungen)
|
||||
sicht.parentElement = { querySelector: () => ({ getBoundingClientRect: () => ({ bottom: 9999 }) }) }
|
||||
const k1 = ui.kamera()
|
||||
ui.ruf('onFocus', fokus(fokusZiel({ left: 1000, top: (400 - k1.y) / k1.s, width: 24, height: 32 }), sicht))
|
||||
assert.ok(ui.kamera().y + ((400 - k1.y) / k1.s) * ui.kamera().s >= 360 + 24 - 1e-6, 'Obergrenze 50 %')
|
||||
} finally { ui.end(); ruhig() }
|
||||
})
|
||||
|
||||
test('K1 Zeigerdruck in die Welt gibt dem Sichtfenster den Fokus (tabindex -1, preventScroll); auf Figur/Etage und bei gesperrter Kamera nicht', async () => {
|
||||
const restore = fensterAttrappe()
|
||||
const ui = await start()
|
||||
try {
|
||||
assert.equal(ui.tree().props.tabIndex, -1)
|
||||
const sicht = ui.anhaengen()
|
||||
sicht.focusCalls = []
|
||||
sicht.focus = (optionen) => { sicht.focusCalls.push(optionen) }
|
||||
ui.ruf('onPointerDown', zeiger({ clientX: 600, clientY: 400 }))
|
||||
assert.deepEqual(sicht.focusCalls, [{ preventScroll: true }])
|
||||
ui.ruf('onPointerUp', zeiger({ clientX: 600, clientY: 400 }))
|
||||
// auf einem Knopf/Link der Welt fokussiert der Browser selbst: kein zweiter Fokus
|
||||
ui.ruf('onPointerDown', zeiger({ clientX: 600, clientY: 400, target: aufBedienbarem }))
|
||||
assert.equal(sicht.focusCalls.length, 1)
|
||||
ui.ruf('onPointerUp', zeiger({ clientX: 600, clientY: 400 }))
|
||||
// Finger zählt wie die Maus
|
||||
ui.ruf('onPointerDown', finger({ clientX: 600, clientY: 400 }))
|
||||
assert.equal(sicht.focusCalls.length, 2)
|
||||
ui.ruf('onPointerUp', finger({ clientX: 600, clientY: 400 }))
|
||||
// Detailansicht offen: die Welt nimmt dem Dialog den Fokus nicht
|
||||
ui.set({ gesperrt: true })
|
||||
ui.ruf('onPointerDown', zeiger({ clientX: 600, clientY: 400 }))
|
||||
assert.equal(sicht.focusCalls.length, 2)
|
||||
} finally { ui.end(); restore() }
|
||||
// Sichtfenster ohne focus() (Attrappe, alter Browser): kein Absturz
|
||||
const r2 = fensterAttrappe()
|
||||
const ohne = await start()
|
||||
try { ohne.anhaengen(); ohne.ruf('onPointerDown', zeiger({ clientX: 1, clientY: 1 })) } finally { ohne.end(); r2() }
|
||||
})
|
||||
|
||||
test('K3 Figuren melden einen Dialog (aria-haspopup), das Neon „HUSTLER“ ist für Screenreader versteckt; Etagen tragen ihre Kennung für den Rückweg', async () => {
|
||||
const { ETAGEN } = await laden()
|
||||
const ui = await start()
|
||||
try {
|
||||
const knoepfe = nodes(ui.tree(), (n) => n.tag === 'button' && n.props['data-hk-hustler'] !== undefined)
|
||||
assert.equal(knoepfe.length, 19)
|
||||
for (const b of knoepfe) assert.equal(b.props['aria-haspopup'], 'dialog', b.props['data-hk-hustler'])
|
||||
// Etagen (die zoomen) melden keinen Dialog
|
||||
for (const e of byClass(ui.tree(), 'hk-etage')) assert.equal(e.props['aria-haspopup'], undefined)
|
||||
assert.equal(byClass(ui.tree(), 'hk-neon')[0].props['aria-hidden'], 'true')
|
||||
assert.deepEqual(byClass(ui.tree(), 'hk-etage').map((n) => n.props['data-hk-etage']), ETAGEN.map((e) => e.id))
|
||||
} finally { ui.end() }
|
||||
})
|
||||
|
||||
test('M8 Tippflächen: die Bühne setzt die Überstände aus tippRand(Zoom) (--hk-tipp-x/-oben/-unten); sie wachsen beim Herauszoomen, hustler-welt.css wertet sie am ::after aus', async () => {
|
||||
const { tippRand } = await laden()
|
||||
const restore = fensterAttrappe()
|
||||
const ui = await start()
|
||||
try {
|
||||
ui.anhaengen(fakeSicht(390, 844))
|
||||
const gleich = () => {
|
||||
const t = tippRand(ui.kamera().s)
|
||||
const stil = ui.buehne().props.style
|
||||
assert.equal(stil['--hk-tipp-x'], `${t.x.toFixed(2)}px`); assert.equal(stil['--hk-tipp-oben'], `${t.oben.toFixed(2)}px`); assert.equal(stil['--hk-tipp-unten'], `${t.unten.toFixed(2)}px`)
|
||||
return t
|
||||
}
|
||||
const ganz = gleich()
|
||||
assert.ok(ganz.x > 12, 'ganzes Haus am Handy: breiter als die alten 12 px')
|
||||
ui.etage('Werkstatt').props.onClick(ereignis())
|
||||
gleich()
|
||||
await wait(650)
|
||||
for (let i = 0; i < 40; i += 1) ui.rad(ereignis({ clientX: 195, clientY: 400, deltaY: -400, deltaMode: 0, ctrlKey: false }))
|
||||
assert.equal(ui.kamera().s, 5)
|
||||
assert.deepEqual({ x: gleich().x, oben: gleich().oben, unten: gleich().unten }, { x: 12, oben: 8, unten: 6 })
|
||||
} finally { ui.end(); restore() }
|
||||
const css = ohneKommentare(lies('hustler-welt.css'))
|
||||
const regel = css.match(/\.hk-hochhaus \.hk-buehne \.hk-hustler::after\s*\{([^}]*)\}/)
|
||||
assert.ok(regel, '::after-Regel der Figuren')
|
||||
assert.match(regel[1], /left:\s*calc\(-1 \* var\(--hk-tipp-x, 12px\)\)/); assert.match(regel[1], /right:\s*calc\(-1 \* var\(--hk-tipp-x, 12px\)\)/)
|
||||
assert.match(regel[1], /top:\s*calc\(-1 \* var\(--hk-tipp-oben, 8px\)\)/); assert.match(regel[1], /bottom:\s*calc\(-1 \* var\(--hk-tipp-unten, 6px\)\)/)
|
||||
// und die Vorgabe bleibt die alte Regel aus hochhaus.css (-12/-12/-8/-6), wenn die Bühne nichts setzt
|
||||
assert.match(ohneKommentare(lies('hochhaus.css')), /\.hk-hustler::after\{content:""; position:absolute; left:-12px; right:-12px; top:-8px; bottom:-6px\}/)
|
||||
})
|
||||
|
||||
test('Schnee (R1): Bewegung per transform statt background-position, gleiche Richtung, Strecke und Dauer wie die Vorlage; Ebene um genau diese Strecke größer; Container schneidet ab', () => {
|
||||
const welt = ohneKommentare(lies('hustler-welt.css'))
|
||||
const vorlage = ohneKommentare(lies('hochhaus.css'))
|
||||
const dauer = (text, klasse, name) => {
|
||||
const m = text.match(new RegExp(`${klasse}[^{]*\\{[^}]*animation:\\s*${name}\\s+(\\d+)s linear infinite`))
|
||||
assert.ok(m, `${klasse} ${name}`)
|
||||
return Number(m[1])
|
||||
}
|
||||
for (const [i, kachel] of [[1, 512], [2, 512], [3, 768]]) {
|
||||
// Vorlage: background-position: dx dy (Bild wandert um genau diese Strecke)
|
||||
const v = vorlage.match(new RegExp(`@keyframes hk-h-schnee${i}\\{to\\{background-position:(-?\\d+)px (-?\\d+)px\\}\\}`))
|
||||
assert.ok(v, `Vorlage hk-h-schnee${i}`)
|
||||
const [dx, dy] = [Number(v[1]), Number(v[2])]
|
||||
// neu: dieselbe Strecke als transform
|
||||
const k = welt.match(new RegExp(`@keyframes hk-welt-schnee${i}\\s*\\{\\s*to\\s*\\{\\s*transform:\\s*translate\\((-?\\d+)px,\\s*(-?\\d+)px\\);\\s*\\}\\s*\\}`))
|
||||
assert.ok(k, `Keyframes hk-welt-schnee${i} mit transform`)
|
||||
assert.deepEqual([Number(k[1]), Number(k[2])], [dx, dy], `Strecke ${i}`)
|
||||
assert.ok(!/background-position/.test(welt.slice(welt.indexOf(`@keyframes hk-welt-schnee${i}`), welt.indexOf(`@keyframes hk-welt-schnee${i}`) + 120)), 'kein background-position')
|
||||
// gleiche Dauer wie die Vorlage
|
||||
assert.equal(dauer(welt, `\\.hk-schnee-${i}::before`, `hk-welt-schnee${i}`), dauer(vorlage, `\\.hk-schnee-${i}`, `hk-h-schnee${i}`), `Dauer ${i}`)
|
||||
// Kachelgröße wie die Vorlage, die Strecke ist ein Vielfaches (nahtlos)
|
||||
assert.match(vorlage, new RegExp(`\\.hk-schnee-${i}\\{background:url\\([^)]*\\) 0 0 / ${kachel}px ${kachel}px repeat`))
|
||||
assert.equal(Math.abs(dx) % kachel, 0); assert.equal(Math.abs(dy) % kachel, 0)
|
||||
// Ebene: um |dx| breiter, um |dy| höher als das Fenster; Anfangslage so, dass die Bewegung sie nie vom Fenster wegzieht
|
||||
const regel = welt.match(new RegExp(`\\.hk-schnee-${i}::before\\s*\\{([^}]*)\\}`))[1]
|
||||
assert.match(regel, new RegExp(`width:\\s*calc\\(100% \\+ ${Math.abs(dx)}px\\)`)); assert.match(regel, new RegExp(`height:\\s*calc\\(100% \\+ ${Math.abs(dy)}px\\)`))
|
||||
assert.match(regel, new RegExp(`left:\\s*${dx > 0 ? `-${dx}px` : '0'}[;\\s]`)); assert.match(regel, new RegExp(`top:\\s*-${dy}px`))
|
||||
assert.match(regel, new RegExp(`background-size:\\s*${kachel}px ${kachel}px`))
|
||||
assert.match(regel, new RegExp(`url\\(\\.\\./\\.\\./assets/hustler/welt/schnee-${i}\\.png\\)`))
|
||||
}
|
||||
// der Container hat kein eigenes Bild und keine eigene Animation mehr und schneidet ab
|
||||
const container = welt.match(/\.hk-hochhaus \.hk-sicht > \.hk-schnee\s*\{([^}]*)\}/)[1]
|
||||
assert.match(container, /background:\s*none/); assert.match(container, /animation:\s*none/); assert.match(container, /overflow:\s*hidden/); assert.match(container, /overflow:\s*clip/)
|
||||
// Detailansicht offen: Schnee weg; weniger Bewegung: keine Animation; Pause: auch Pseudo-Elemente stehen
|
||||
assert.match(welt, /\.hk-sicht\.hk-verdeckt > \.hk-schnee\s*\{\s*display:\s*none;?\s*\}/)
|
||||
assert.match(mediaBlock(welt, '@media (prefers-reduced-motion: reduce)'), /\.hk-schnee::before\s*\{\s*animation:\s*none/)
|
||||
assert.match(mediaBlock(vorlage, '@media (prefers-reduced-motion: reduce)'), /\.hk-hochhaus \.hk-schnee\{display:none\}/)
|
||||
const ui = ohneKommentare(lies('hustler-ui.css'))
|
||||
assert.match(ui, /\.hk-seite \.hk-steht \*::before/); assert.match(ui, /\.hk-seite \.hk-steht \*::after/)
|
||||
assert.match(ui.match(/\.hk-seite \.hk-steht,[^{]*\{([^}]*)\}/)[1], /animation-play-state:\s*paused !important/)
|
||||
})
|
||||
|
||||
test('Schnee (R1): in der Welt hängen die drei Schnee-Ebenen im Sichtfenster; die Pause (hk-steht) und die offene Detailansicht (hk-verdeckt) stehen auf demselben Element', async () => {
|
||||
const ui = await start({ state: makeState({ paused: true }), gesperrt: true })
|
||||
try {
|
||||
const wurzel = ui.tree()
|
||||
assert.ok(hasClass(wurzel, 'hk-steht')); assert.ok(hasClass(wurzel, 'hk-verdeckt'))
|
||||
assert.deepEqual(wurzel.children.filter((k) => hasClass(k, 'hk-schnee')).map((k) => classes(k)[1]), ['hk-schnee-1', 'hk-schnee-2', 'hk-schnee-3'])
|
||||
ui.set({ gesperrt: false })
|
||||
assert.ok(hasClass(ui.tree(), 'hk-steht')); assert.ok(!hasClass(ui.tree(), 'hk-verdeckt'))
|
||||
} finally { ui.end() }
|
||||
})
|
||||
|
||||
test('M4 weniger Bewegung: alle Sprite-Schleifen im Hochhaus (und in der Detailansicht) stehen auf dem ersten Bild; jedes animierte Element der Welt ist von einer Reduziert-Regel erfasst', async () => {
|
||||
const ui = css => ohneKommentare(lies(css))
|
||||
const vorlage = ui('hochhaus.css')
|
||||
const regel = mediaBlock(ui('hustler-ui.css'), '@media (prefers-reduced-motion: reduce)')
|
||||
assert.match(regel, /\.hk-seite \.hk-hochhaus \.hk-spr\s*\{\s*animation:\s*none;?\s*\}/)
|
||||
// mit höherer Spezifität als die Sprite-Regeln der Vorlage (.hk-hochhaus .hk-fig-… = 0,2,0): .hk-seite .hk-hochhaus .hk-spr = 0,3,0
|
||||
assert.match(vorlage, /\.hk-hochhaus \.hk-fig-muetze-arbeiten\{[^}]*animation:/)
|
||||
// Klassen mit Animation in der Vorlage (außerhalb der Reduziert-Regeln)
|
||||
const reduziert = mediaBlock(vorlage, '@media (prefers-reduced-motion: reduce)')
|
||||
const ohneReduziert = vorlage.replace(reduziert, '')
|
||||
const animiert = new Set([...ohneReduziert.matchAll(/\.hk-hochhaus (\.hk-[\w-]+)\{[^}]*animation:/g)].map((m) => m[1].slice(1)))
|
||||
assert.ok(animiert.has('hk-fig-muetze-arbeiten') && animiert.has('hk-gr-muetze-arbeiten') && animiert.has('hk-dampf') && animiert.has('hk-bildschirm'))
|
||||
// erfasst: Elemente mit hk-spr, oder eine Klasse aus den Reduziert-Regeln (Vorlage, hustler-ui, hustler-welt)
|
||||
const reduzierteKlassen = new Set([...[reduziert, regel, mediaBlock(ui('hustler-welt.css'), '@media (prefers-reduced-motion: reduce)')].join('\n').matchAll(/\.hk-[\w-]+/g)].map((m) => m[0].slice(1)))
|
||||
const zustaende = [
|
||||
makeState({ statuses: { rita: 'arbeitet', pia: 'fehler', ali: 'wartet', hanna: 'leerlauf', otto: 'arbeitet' } }),
|
||||
makeState({ statuses: { otto: 'wartet' }, connected: { kim: false, lea: false } }),
|
||||
]
|
||||
for (const state of zustaende) {
|
||||
const lauf = await start({ state })
|
||||
try {
|
||||
const knoten = nodes(lauf.tree(), (n) => typeof n.tag === 'string' && classes(n).some((c) => animiert.has(c)))
|
||||
assert.ok(knoten.length > 20)
|
||||
for (const n of knoten) {
|
||||
const erfasst = hasClass(n, 'hk-spr') || classes(n).some((c) => reduzierteKlassen.has(c))
|
||||
assert.ok(erfasst, `nicht erfasst: ${classes(n).join(' ')}`)
|
||||
}
|
||||
} finally { lauf.end() }
|
||||
}
|
||||
})
|
||||
|
||||
test('K6 Weiß auf Rot: Briefkasten-Zähler und Fehler-Marker nutzen #c8372d (Kontrast zu Weiß mindestens 4,5:1; #e2483d reichte nicht)', () => {
|
||||
assert.ok(kontrast('#e2483d', '#ffffff') < 4.5, 'Ausgangsfarbe war zu hell')
|
||||
assert.ok(kontrast('#c8372d', '#ffffff') >= 4.5)
|
||||
assert.ok(Math.abs(kontrast('#c8372d', '#ffffff') - 5.2) < 0.1)
|
||||
const briefkasten = ohneKommentare(lies('hustler-welt.css')).match(/\.hk-hochhaus \.hk-briefkasten\s*\{([^}]*)\}/)[1]
|
||||
const alarm = ohneKommentare(lies('hustler-ui.css')).match(/\.hk-seite \.hk-alarm\s*\{([^}]*)\}/)[1]
|
||||
for (const [wo, regel] of [['Briefkasten', briefkasten], ['Fehler-Marker', alarm]]) {
|
||||
const hg = regel.match(/background:\s*(#[0-9a-f]{6})/i)[1]; const vg = regel.match(/[^-]color:\s*(#[0-9a-f]{6})/i)[1]
|
||||
assert.equal(hg.toLowerCase(), '#c8372d', wo)
|
||||
assert.ok(kontrast(hg, vg) >= 4.5, `${wo}: ${kontrast(hg, vg).toFixed(2)}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('R4 Überlagerungen: z-index der Detailansicht liegt unter Seitenleiste (20) und Abdunkler (15) der Handy-Navigation, aber über der Kopfzeile (5)', () => {
|
||||
const global = readFileSync(path.join(ROOT, 'src/styles/global.css'), 'utf8')
|
||||
const sidebar = Number(global.match(/\.sidebar \{[^}]*z-index:\s*(\d+)/)?.[1] ?? 20)
|
||||
const scrim = Number(global.match(/\.shell\.menu-open \.scrim \{[^}]*z-index:\s*(\d+)/)[1])
|
||||
assert.equal(scrim, 15); assert.equal(sidebar, 20)
|
||||
const detail = ohneKommentare(lies('hustler-detail.css')).match(/\.hk-hochhaus \.hk-dialog-ueber\s*\{([^}]*)\}/)[1]
|
||||
const z = Number(detail.match(/z-index:\s*(\d+)/)[1])
|
||||
assert.ok(z <= 9 && z > 5, `z-index ${z}`)
|
||||
const kopf = Number(ohneKommentare(lies('hustler-kopf.css')).match(/\.hk-hochhaus \.hk-kopf\s*\{[^}]*z-index:\s*(\d+)/)[1])
|
||||
assert.ok(z > kopf)
|
||||
})
|
||||
|
||||
test('R7 Hover in WOMS-Bausteinen: Links im WOMS-Dialog bleiben beim Überfahren in der WOMS-Akzentfarbe (nicht gelb wie .hk-hochhaus a:hover)', () => {
|
||||
const ui = ohneKommentare(lies('hustler-ui.css'))
|
||||
assert.match(ui, /\.hk-seite \.hk-woms a:hover\s*\{\s*color:\s*var\(--accent\);?\s*\}/)
|
||||
assert.match(ohneKommentare(lies('hochhaus.css')), /\.hk-hochhaus a:hover\{color:#ffe9a0\}/)
|
||||
// höhere Spezifität als die Vorlage: .hk-seite .hk-woms a:hover (0,3,1) gegen .hk-hochhaus a:hover (0,2,1)
|
||||
})
|
||||
|
||||
// ---------- Aufbau ----------
|
||||
|
||||
test('Aufbau: kein CSS-Import, keine PNG-Importe, kein innerHTML; jede gerenderte Klasse steht in einer der Hustler-Stilregeln', async () => {
|
||||
@@ -888,6 +1188,10 @@ test('Aufbau: kein CSS-Import, keine PNG-Importe, kein innerHTML; jede gerendert
|
||||
for (const c of allClasses(ui.tree())) gesehen.add(c)
|
||||
} finally { ui.end() }
|
||||
}
|
||||
// Detailansicht offen (gesperrt): die Welt trägt hk-verdeckt
|
||||
const gesperrt = await start({ gesperrt: true })
|
||||
try { for (const c of allClasses(gesperrt.tree())) gesehen.add(c) } finally { gesperrt.end() }
|
||||
assert.ok(gesehen.has('hk-verdeckt'))
|
||||
// Klassen, die nur im Lauf entstehen: Ziehen und Gleiten
|
||||
gesehen.add('hk-zieht'); gesehen.add('hk-gleitet')
|
||||
assert.ok(SEARCH_ROOMS.every((r) => gesehen.has(`hk-raum-grau-${r}`)))
|
||||
|
||||
@@ -215,9 +215,10 @@ const STUBS = {
|
||||
return hk.feed
|
||||
}
|
||||
`,
|
||||
hochhaus: "import { jsx } from 'react'\nexport default function Hochhaus(props) { globalThis.__hk.props.hochhaus = props; return jsx('div', { 'data-screen': 'hochhaus' }) }",
|
||||
pult: "import { jsx } from 'react'\nexport default function PultList(props) { globalThis.__hk.props.pult = props; return jsx('div', { 'data-screen': 'pult' }) }",
|
||||
keller: "import { jsx } from 'react'\nexport default function BotLeadList(props) { globalThis.__hk.props.keller = props; return jsx('div', { 'data-screen': 'keller' }) }",
|
||||
// renders: jede Darstellung eines Bildschirms (nicht nur die letzte), damit der Test auch Zwischenstände sieht (R5)
|
||||
hochhaus: "import { jsx } from 'react'\nexport default function Hochhaus(props) { globalThis.__hk.props.hochhaus = props; globalThis.__hk.renders?.hochhaus.push(props); return jsx('div', { 'data-screen': 'hochhaus' }) }",
|
||||
pult: "import { jsx } from 'react'\nexport default function PultList(props) { globalThis.__hk.props.pult = props; globalThis.__hk.renders?.pult.push(props); return jsx('div', { 'data-screen': 'pult' }) }",
|
||||
keller: "import { jsx } from 'react'\nexport default function BotLeadList(props) { globalThis.__hk.props.keller = props; globalThis.__hk.renders?.keller.push(props); return jsx('div', { 'data-screen': 'keller' }) }",
|
||||
}
|
||||
const stubPlugin = {
|
||||
name: 'seite-attrappen',
|
||||
@@ -255,6 +256,7 @@ async function seite(query, { admin = false, feed } = {}) {
|
||||
api: globalThis.__hkApi,
|
||||
feed: feed ?? { state: null, error: null, loaded: false, refresh: () => {} },
|
||||
feedCalls: [], feedMounts: 0, feedUnmounts: 0, props: {}, onEvents: null,
|
||||
renders: { hochhaus: [], pult: [], keller: [] },
|
||||
}
|
||||
const savedHk = globalThis.__hk
|
||||
const hadDocument = Object.hasOwn(globalThis, 'document')
|
||||
@@ -454,3 +456,176 @@ test('Seite: ohne angemeldeten Nutzer bleibt me ohne Id und Name (kein Absturz)'
|
||||
assert.deepEqual(ui.hk.props.pult.me, { id: undefined, name: undefined })
|
||||
} finally { ui.end() }
|
||||
})
|
||||
|
||||
// ---------- Korrekturen der Endprüfung (T13) ----------
|
||||
|
||||
test('R5 Seite: die Hustler-Tabelle liegt nie einen Render hinter dem Schnappschuss (kein „Leerlauf“-Blitz der Figuren, in keinem Bildschirm)', async () => {
|
||||
const state = makeState({ statuses: { lea: 'wartet', kim: 'fehler' }, connected: { pia: false } })
|
||||
const ui = await seite('tab=hochhaus', { feed: { state, error: null, loaded: true, refresh() {} } })
|
||||
try {
|
||||
// schon die allererste Darstellung mit Zustand hat die Tabelle daraus
|
||||
const renders = ui.hk.renders.hochhaus
|
||||
assert.ok(renders.length >= 1)
|
||||
for (const p of renders) {
|
||||
assert.equal(Object.keys(p.workers).length, 19)
|
||||
assert.equal(p.workers.lea.status, 'wartet'); assert.equal(p.workers.kim.status, 'fehler'); assert.equal(p.workers.pia.status, 'grau')
|
||||
}
|
||||
// neuer Schnappschuss mitten im Lauf: jede weitere Darstellung zeigt schon die neue Lage (nie die alte Tabelle zum neuen Zustand)
|
||||
renders.length = 0
|
||||
const zweiter = makeState({ statuses: { lea: 'leerlauf' } })
|
||||
ui.setFeed({ state: zweiter })
|
||||
assert.ok(renders.length >= 1)
|
||||
for (const p of renders) { assert.equal(p.feed.state, zweiter); assert.equal(p.workers.lea.status, 'leerlauf'); assert.equal(p.workers.kim.status, 'arbeitet') }
|
||||
// dasselbe für Pult und Keller (sie bekommen dieselbe Tabelle)
|
||||
ui.go('tab=freigabe')
|
||||
ui.hk.renders.pult.length = 0
|
||||
const dritter = makeState({ statuses: { lea: 'wartet' } })
|
||||
ui.setFeed({ state: dritter })
|
||||
for (const p of ui.hk.renders.pult) assert.equal(p.workers.lea.status, 'wartet')
|
||||
ui.go('tab=botleads')
|
||||
ui.hk.renders.keller.length = 0
|
||||
ui.setFeed({ state: makeState({ statuses: { lea: 'fehler' } }) })
|
||||
for (const p of ui.hk.renders.keller) assert.equal(p.workers.lea.status, 'fehler')
|
||||
assert.ok(ui.hk.renders.keller.length >= 1)
|
||||
// Ereignisse wirken weiter auf die Tabelle und überleben das Neu-Rendern desselben Schnappschusses
|
||||
ui.hk.onEvents([ev('leerlauf', 'lea', { text: 'Pause' })])
|
||||
assert.equal(ui.hk.props.keller.workers.lea.status, 'leerlauf')
|
||||
ui.setFeed({})
|
||||
assert.equal(ui.hk.props.keller.workers.lea.status, 'leerlauf')
|
||||
} finally { ui.end() }
|
||||
// Erster Schnappschuss kommt erst später: vorher leer, danach nie eine Darstellung mit Zustand und leerer Tabelle
|
||||
const spaet = await seite('tab=hochhaus')
|
||||
try {
|
||||
assert.deepEqual(spaet.hk.props.hochhaus.workers, {})
|
||||
spaet.hk.renders.hochhaus.length = 0
|
||||
spaet.setFeed({ state: makeState({ statuses: { lea: 'wartet' } }), loaded: true })
|
||||
for (const p of spaet.hk.renders.hochhaus) { assert.ok(p.feed.state); assert.equal(Object.keys(p.workers).length, 19) }
|
||||
assert.ok(spaet.hk.renders.hochhaus.length >= 1)
|
||||
} finally { spaet.end() }
|
||||
})
|
||||
|
||||
test('R3 Seite: Pult und Keller bekommen paused aus state.paused (nur ein echtes true), Pause schaltet mit dem Zustand um', async () => {
|
||||
const ui = await seite('tab=freigabe', { feed: { state: makeState({ paused: false }), error: null, loaded: true, refresh() {} } })
|
||||
try {
|
||||
assert.equal(ui.hk.props.pult.paused, false)
|
||||
ui.setFeed({ state: makeState({ paused: true }) })
|
||||
assert.equal(ui.hk.props.pult.paused, true)
|
||||
ui.go('tab=botleads')
|
||||
assert.equal(ui.hk.props.keller.paused, true)
|
||||
ui.setFeed({ state: makeState({ paused: false }) })
|
||||
assert.equal(ui.hk.props.keller.paused, false)
|
||||
ui.setFeed({ state: { ...makeState(), paused: 'ja' } })
|
||||
assert.equal(ui.hk.props.keller.paused, false)
|
||||
// vor dem ersten Schnappschuss: nicht angehalten
|
||||
ui.setFeed({ state: null, loaded: false })
|
||||
assert.equal(ui.hk.props.keller.paused, false)
|
||||
} finally { ui.end() }
|
||||
})
|
||||
|
||||
test('M2 Seite: das Hochhaus bekommt vonTab (der Bildschirm, von dem man zurückkommt); beim ersten Besuch null', async () => {
|
||||
const ui = await seite('tab=hochhaus', { feed: { state: makeState(), error: null, loaded: true, refresh() {} } })
|
||||
try {
|
||||
assert.equal(ui.hk.props.hochhaus.vonTab, null)
|
||||
ui.go('tab=freigabe'); assert.deepEqual(ui.screen(), ['pult'])
|
||||
ui.go('tab=hochhaus'); assert.equal(ui.hk.props.hochhaus.vonTab, 'freigabe')
|
||||
// schon der erste Render nach dem Wechsel kennt vonTab (das Hochhaus fokussiert beim Einhängen)
|
||||
assert.ok(ui.hk.renders.hochhaus.every((p, i) => i === 0 || p.vonTab === null || p.vonTab === 'freigabe'))
|
||||
ui.go('tab=botleads'); assert.deepEqual(ui.screen(), ['keller'])
|
||||
ui.go('tab=hochhaus'); assert.equal(ui.hk.props.hochhaus.vonTab, 'botleads')
|
||||
// Neu-Rendern im Hochhaus ändert nichts
|
||||
ui.setFeed({}); assert.equal(ui.hk.props.hochhaus.vonTab, 'botleads')
|
||||
} finally { ui.end() }
|
||||
// direkt im Pult gestartet, dann zurück
|
||||
const direkt = await seite('tab=freigabe', { feed: { state: makeState(), error: null, loaded: true, refresh() {} } })
|
||||
try {
|
||||
direkt.go('tab=hochhaus')
|
||||
assert.equal(direkt.hk.props.hochhaus.vonTab, 'freigabe')
|
||||
} finally { direkt.end() }
|
||||
})
|
||||
|
||||
// Hochhaus mit Wurzel-Attrappe (querySelector, focus) für den Rückweg und die Überschrift
|
||||
function wurzelAttrappe() {
|
||||
const gefragt = []
|
||||
const etagen = new Map()
|
||||
const wurzel = {
|
||||
querySelector(sel) {
|
||||
gefragt.push(sel)
|
||||
const m = /^\[data-hk-etage="([\w-]+)"\]$/.exec(sel)
|
||||
if (!m) return null
|
||||
if (!etagen.has(m[1])) etagen.set(m[1], { id: m[1], focusCalls: [], focus(o) { this.focusCalls.push(o) } })
|
||||
return etagen.get(m[1])
|
||||
},
|
||||
}
|
||||
const gehabt = Object.hasOwn(globalThis, '__fakeRef')
|
||||
const alt = globalThis.__fakeRef
|
||||
globalThis.__fakeRef = (tag, props) => (tag === 'div' && props.className === 'hk-hochhaus' ? wurzel : null)
|
||||
return { gefragt, etagen, restore() { if (gehabt) globalThis.__fakeRef = alt; else delete globalThis.__fakeRef } }
|
||||
}
|
||||
|
||||
test('M2 Hochhaus: nach dem Rückweg aus dem Pult fokussiert die Etage „freigabe“, aus dem Keller die Etage „keller“ (ohne Scrollen), erst wenn die Welt steht; sonst nichts', async () => {
|
||||
const ready = { state: makeState(), error: null, loaded: true, refresh() {} }
|
||||
for (const [von, etage] of [['freigabe', 'freigabe'], ['botleads', 'keller']]) {
|
||||
const w = wurzelAttrappe()
|
||||
const view = await hochhaus(ready, { vonTab: von })
|
||||
try {
|
||||
assert.deepEqual(w.gefragt, [`[data-hk-etage="${etage}"]`])
|
||||
assert.deepEqual(w.etagen.get(etage).focusCalls, [{ preventScroll: true }])
|
||||
// Neu-Rendern (Feed-Aktualisierung) holt den Fokus nicht noch einmal
|
||||
view.setFeed({ state: makeState() })
|
||||
assert.equal(w.etagen.get(etage).focusCalls.length, 1)
|
||||
} finally { view.end(); w.restore() }
|
||||
}
|
||||
// kein vonTab / unbekannter Wert: kein Fokus
|
||||
for (const von of [null, undefined, 'hochhaus', 'toString', '__proto__']) {
|
||||
const w = wurzelAttrappe()
|
||||
const view = await hochhaus(ready, { vonTab: von })
|
||||
try { assert.deepEqual(w.gefragt, [], String(von)) } finally { view.end(); w.restore() }
|
||||
}
|
||||
// noch nicht geladen: erst wenn der Zustand da ist
|
||||
const w = wurzelAttrappe()
|
||||
const view = await hochhaus({ state: null, error: null, loaded: false, refresh() {} }, { vonTab: 'freigabe' })
|
||||
try {
|
||||
assert.deepEqual(w.gefragt, [])
|
||||
view.setFeed({ state: makeState(), loaded: true })
|
||||
assert.deepEqual(w.etagen.get('freigabe').focusCalls, [{ preventScroll: true }])
|
||||
} finally { view.end(); w.restore() }
|
||||
})
|
||||
|
||||
test('M2/M7 Hochhaus: Überschrift „Hustler – Hochhaus“ (h1, nur für Screenreader) in jedem Zustand; die Kopfzeile steht im DOM vor der Welt', async () => {
|
||||
const ready = { state: makeState(), error: null, loaded: true, refresh() {} }
|
||||
const faelle = [
|
||||
['geladen', ready],
|
||||
['lädt', { state: null, error: null, loaded: false, refresh() {} }],
|
||||
['Dienst nicht erreichbar', { state: null, error: 'weg', loaded: false, refresh() {} }],
|
||||
]
|
||||
for (const [wo, feed] of faelle) {
|
||||
const view = await hochhaus(feed)
|
||||
try {
|
||||
const h1 = nodes(view.tree(), (n) => n.tag === 'h1')
|
||||
assert.equal(h1.length, 1, wo)
|
||||
assert.equal(textOf(h1[0]), 'Hustler – Hochhaus', wo); assert.equal(h1[0].props.className, 'hk-nur-sr', wo)
|
||||
assert.equal(view.tree().children[0], h1[0], `${wo}: die Überschrift steht zuerst`)
|
||||
} finally { view.end() }
|
||||
}
|
||||
const view = await hochhaus(ready)
|
||||
try {
|
||||
const reihenfolge = view.tree().children.filter((k) => k && k.props && k.props['data-teil']).map((k) => k.props['data-teil'])
|
||||
assert.deepEqual(reihenfolge, ['kopf', 'welt'], 'Tab-Reihenfolge: Kopfzeile vor der Welt')
|
||||
view.hh.welt.onZeige('lea')
|
||||
await settle()
|
||||
assert.deepEqual(view.tree().children.filter((k) => k && k.props && k.props['data-teil']).map((k) => k.props['data-teil']), ['kopf', 'welt', 'dialog'])
|
||||
} finally { view.end() }
|
||||
})
|
||||
|
||||
test('R3 Hochhaus: paused des Dienstes geht an die Detailansicht (nur ein echtes true)', async () => {
|
||||
const view = await hochhaus({ state: makeState({ paused: false }), error: null, loaded: true, refresh() {} })
|
||||
try {
|
||||
view.hh.welt.onZeige('lea')
|
||||
await settle()
|
||||
assert.equal(view.hh.dialog.paused, false)
|
||||
view.setFeed({ state: makeState({ paused: true }) })
|
||||
assert.equal(view.hh.dialog.paused, true)
|
||||
view.setFeed({ state: { ...makeState(), paused: 'ja' } })
|
||||
assert.equal(view.hh.dialog.paused, false)
|
||||
} finally { view.end() }
|
||||
})
|
||||
@@ -12,11 +12,11 @@ const W = await loadModule('src/lib/hustlerWelt.js')
|
||||
const {
|
||||
HUT, STATUS_ANZEIGE, POSE_NACH_STATUS, AUFGABEN, ZOOM_MAX, SICHT_STANDARD,
|
||||
anzeigeName, modellKurz, modellName, statusVon, figurZustand, detailModell, blaettern, hudWerte, schilder,
|
||||
kameraMinS, kameraGrenzen, kameraPassend, kameraZoomAuf, kameraEtage,
|
||||
kameraMinS, kameraGrenzen, kameraPassend, kameraZoomAuf, kameraEtage, kameraZeige, tippRand,
|
||||
} = S
|
||||
const { ROLES, SEARCH_ROOMS, FLOOR_TITLES, ROOM_TITLES } = R
|
||||
const { formatEur, formatUsd, formatCount, kpiTiles, traceRows } = V
|
||||
const { ETAGEN } = W
|
||||
const { ETAGEN, HUSTLER } = W
|
||||
|
||||
const NBSP = String.fromCharCode(160)
|
||||
const STATUS = ['arbeitet', 'wartet', 'leerlauf', 'grau', 'fehler']
|
||||
@@ -691,8 +691,18 @@ const VL = (() => {
|
||||
const FENSTER = [
|
||||
{ w: 1280, h: 720 }, { w: 1920, h: 1080 }, { w: 390, h: 700 }, { w: 390, h: 844 }, { w: 2560, h: 1440 },
|
||||
{ w: 800, h: 400 }, { w: 3840, h: 2160 }, { w: 2400, h: 1200 }, { w: 1024, h: 1366 }, { w: 1500, h: 500 },
|
||||
{ w: 844, h: 390 }, { w: 360, h: 740 }, { w: 768, h: 1024 },
|
||||
]
|
||||
|
||||
// Einzige gewollte Abweichung von der Vorlage (M8): „Etage heranzoomen“ vergrößert mindestens das 1,6-fache von „ganzes Haus“ (höchstens 5-fach).
|
||||
// Wo die Vorlage ohnehin mehr zoomt (Desktop, Handy quer), bleibt es bei ihrer Zahl; sonst gilt der Boden, gerechnet mit den Grenzen der Vorlage.
|
||||
function etageErwartet(oben, unten, g) {
|
||||
const vorlage = VL.etage(oben, unten, g)
|
||||
const boden = Math.min(5, VL.passend(g).s * 1.6)
|
||||
if (vorlage.s >= boden) return vorlage
|
||||
return VL.grenzen({ s: boden, x: g.w / 2 - 1216 * boden, y: 42 + g.h / 2 - ((oben + unten) / 2) * boden }, g)
|
||||
}
|
||||
|
||||
// Gleichmäßige Zufallszahlen ohne Math.random, damit der Test immer dasselbe prüft
|
||||
function zufall(seed) {
|
||||
let z = seed >>> 0
|
||||
@@ -704,7 +714,7 @@ test('Kamera: gleiche Zahlen wie die Formeln der Zeichenfläche', () => {
|
||||
for (const g of FENSTER) {
|
||||
assert.equal(kameraMinS(g), VL.minS(g), `minS ${g.w}x${g.h}`)
|
||||
assert.deepEqual(kameraPassend(g), VL.passend(g), `passend ${g.w}x${g.h}`)
|
||||
for (const e of ETAGEN) assert.deepEqual(kameraEtage(e.oben, e.unten, g), VL.etage(e.oben, e.unten, g), `etage ${e.id} ${g.w}x${g.h}`)
|
||||
for (const e of ETAGEN) assert.deepEqual(kameraEtage(e.oben, e.unten, g), etageErwartet(e.oben, e.unten, g), `etage ${e.id} ${g.w}x${g.h}`)
|
||||
for (let i = 0; i < 40; i += 1) {
|
||||
const st = { s: rnd() * 8, x: (rnd() - 0.7) * 6000, y: (rnd() - 0.6) * 3000 }
|
||||
assert.deepEqual(kameraGrenzen(st, g), VL.grenzen(st, g), `grenzen ${JSON.stringify(st)}`)
|
||||
@@ -846,6 +856,133 @@ test('kameraEtage: die Etage liegt mittig im Bild, höchstens 3-fach, nie kleine
|
||||
assert.equal(kameraEtage(720, 776, { w: 1920, h: 1080 }).s, 3)
|
||||
})
|
||||
|
||||
test('kameraEtage (M8): mindestens 1,6 x „ganzes Haus“ (höchstens ZOOM_MAX); Handy hochkant zoomt jetzt sichtbar, Desktop und Handy quer bleiben wie die Vorlage', () => {
|
||||
for (const g of FENSTER) {
|
||||
const ganz = kameraPassend(g).s
|
||||
for (const e of ETAGEN) {
|
||||
const k = kameraEtage(e.oben, e.unten, g)
|
||||
const wo = `${e.id} ${g.w}x${g.h} ${JSON.stringify(k)}`
|
||||
assert.ok(k.s >= Math.min(ZOOM_MAX, ganz * 1.6) - 1e-9, `mindestens 1,6 x ganz: ${wo}`)
|
||||
assert.ok(k.s <= ZOOM_MAX, `höchstens ZOOM_MAX: ${wo}`)
|
||||
}
|
||||
}
|
||||
const nahe = (a, b, wo) => assert.ok(Math.abs(a - b) < 1e-9, `${wo}: ${a} ≈ ${b}`)
|
||||
// 390 x 844: ganzes Haus = cover = 844 / 1200; vorher änderte „heranzoomen“ den Zoom gar nicht (Breite begrenzte auf unter cover), jetzt 1,6 x
|
||||
const handy = { w: 390, h: 844 }
|
||||
nahe(kameraPassend(handy).s, 844 / 1200, 'ganz 390x844')
|
||||
nahe(kameraEtage(552, 608, handy).s, (844 / 1200) * 1.6, 'Etage 390x844')
|
||||
assert.ok(kameraEtage(552, 608, handy).s > kameraPassend(handy).s * 1.5)
|
||||
// Desktop: die Breite begrenzt, das ist schon mehr als 1,6 x
|
||||
nahe(kameraEtage(720, 776, { w: 1280, h: 720 }).s, (1280 - 96) / 560, 'Etage 1280x720')
|
||||
assert.equal(kameraEtage(720, 776, { w: 1920, h: 1080 }).s, 3)
|
||||
// Handy quer 844 x 390: 1,336 (Breite) bleibt, ganz = 0,41
|
||||
nahe(kameraEtage(776, 832, { w: 844, h: 390 }).s, (844 - 96) / 560, 'Etage 844x390')
|
||||
// Tablet hochkant: vorher kleiner als „ganzes Haus“ (1,2 gegen 1,26)
|
||||
assert.ok(kameraEtage(776, 832, { w: 768, h: 1024 }).s > kameraPassend({ w: 768, h: 1024 }).s * 1.59)
|
||||
// Die Etage bleibt mittig und die Kamera in den Grenzen
|
||||
for (const e of ETAGEN) {
|
||||
const k = kameraEtage(e.oben, e.unten, handy)
|
||||
assert.deepEqual(kameraGrenzen(k, handy), k, e.id)
|
||||
}
|
||||
})
|
||||
|
||||
test('kameraZeige (M1): holt ein Rechteck der Welt ins Bild (unter der Kopfzeile, mit Rand), Zoom bleibt; schon Sichtbares und Unmögliches geben null', () => {
|
||||
const g = { w: 1280, h: 720 }
|
||||
const k = { s: 1, x: -600, y: -300 } // sichtbar: Welt x 600..1880, y 300+kopf..1020
|
||||
const figur = { left: 1000, top: 500, width: 24, height: 32 }
|
||||
assert.equal(kameraZeige(k, figur, g), null)
|
||||
// rechts außerhalb: rechter Rand der Figur kommt auf Breite - 24
|
||||
const rechts = kameraZeige(k, { ...figur, left: 2000 }, g)
|
||||
assert.deepEqual(rechts, { s: 1, x: 1280 - 24 - (2000 + 24), y: -300 })
|
||||
// links außerhalb: linker Rand auf 24
|
||||
const links = kameraZeige(k, { ...figur, left: 400 }, g)
|
||||
assert.deepEqual(links, { s: 1, x: 24 - 400, y: -300 })
|
||||
// oben: unter der Kopfzeile (Standard 84 px) plus Rand 24
|
||||
const oben = kameraZeige({ s: 1, x: -600, y: -300 }, { ...figur, top: 340 }, g)
|
||||
assert.deepEqual(oben, { s: 1, x: -600, y: 84 + 24 - 340 })
|
||||
// eine höhere Kopfzeile (2 Reihen am Handy) verschiebt die Grenze
|
||||
assert.deepEqual(kameraZeige({ s: 1, x: -600, y: -300 }, { ...figur, top: 340 }, g, { oben: 150 }), { s: 1, x: -600, y: 150 + 24 - 340 })
|
||||
// ein Rechteck dicht unter der Kopfzeile, das die Standard-Zeile nicht stört, aber eine hohe: wird weggeholt
|
||||
assert.equal(kameraZeige({ s: 1, x: -600, y: -300 }, { ...figur, top: 420 }, g), null)
|
||||
assert.notEqual(kameraZeige({ s: 1, x: -600, y: -300 }, { ...figur, top: 420 }, g, { oben: 150 }), null)
|
||||
// unten
|
||||
assert.deepEqual(kameraZeige(k, { ...figur, top: 1100 }, g), { s: 1, x: -600, y: 720 - 24 - (1100 + 32) })
|
||||
// Zoom wirkt: bei s = 2 zählt das Rechteck doppelt
|
||||
assert.deepEqual(kameraZeige({ s: 2, x: 0, y: 0 }, { left: 700, top: 200, width: 24, height: 32 }, g), { s: 2, x: 1280 - 24 - (700 + 24) * 2, y: 0 })
|
||||
// größer als der sichtbare Bereich: die Anfangskante kommt an den Rand
|
||||
const breit = kameraZeige({ s: 3, x: -2000, y: -300 }, { left: 960, top: 888, width: 512, height: 56 }, g)
|
||||
assert.equal(breit.x, 24 - 960 * 3 - 0) // 960*3 - 2000 = 880 > 24 (sichtbar), Rechteck 1536 breit > 1232 -> linke Kante auf 24
|
||||
// Ergebnis liegt in den Grenzen (die Welt endet links bei 0, rechts bei Breite - Welt)
|
||||
const ganzRechts = kameraZeige({ s: 1, x: 0, y: 0 }, { left: 2390, top: 500, width: 24, height: 32 }, g)
|
||||
assert.deepEqual(kameraGrenzen(ganzRechts, g), ganzRechts)
|
||||
assert.equal(ganzRechts.x, 1280 - 2400)
|
||||
// Eingabe unverändert, kaputte Eingaben geben null
|
||||
const eingabe = Object.freeze({ s: 1, x: -600, y: -300 })
|
||||
kameraZeige(eingabe, { ...figur, left: 2000 }, g)
|
||||
assert.deepEqual({ ...eingabe }, { s: 1, x: -600, y: -300 })
|
||||
for (const kaputt of [null, undefined, {}, { left: 1 }, { left: NaN, top: 0, width: 1, height: 1 }, 'x']) assert.equal(kameraZeige(k, kaputt, g), null)
|
||||
assert.equal(kameraZeige(null, figur, g), null)
|
||||
assert.equal(kameraZeige({ s: 0, x: 0, y: 0 }, figur, g), null)
|
||||
// kaputtes Fenster fällt auf die Entwurfsgröße zurück
|
||||
assert.deepEqual(kameraZeige(k, { ...figur, left: 2000 }, null), kameraZeige(k, { ...figur, left: 2000 }, SICHT_STANDARD))
|
||||
})
|
||||
|
||||
test('tippRand (M8): Tippfläche der Figuren wächst mit dem Zoom auf 40 Bildschirm-Pixel, gedeckelt gegen Überlappung; bei starkem Zoom die alten 12/8/6', () => {
|
||||
const nahe = (a, b, wo) => assert.ok(Math.abs(a - b) < 1e-9, `${wo}: ${a} ≈ ${b}`)
|
||||
// starker Zoom: bisherige Überstände von hochhaus.css (-12/-12/-8/-6)
|
||||
for (const z of [1.7, 2, 3, 5]) assert.deepEqual(tippRand(z), { x: 12, oben: 8, unten: 6 }, `Zoom ${z}`)
|
||||
// Handy hochkant, ganzes Haus (0,7033): 40 px = 56,9 Weltpixel -> 24 + 2 x 16,4; senkrecht auf 12 gedeckelt (32 + 2 x 12 = 56 = Etagenabstand)
|
||||
const z = 844 / 1200
|
||||
const t = tippRand(z)
|
||||
nahe(t.x, (40 / z - 24) / 2, 'x 0,7033')
|
||||
assert.equal(t.oben, 12); assert.equal(t.unten, 12)
|
||||
nahe((24 + 2 * t.x) * z, 40, 'Breite am Bildschirm')
|
||||
nahe((32 + t.oben + t.unten) * z, 56 * z, 'Höhe am Bildschirm')
|
||||
assert.ok((32 + t.oben + t.unten) * z > 39 && (32 + t.oben + t.unten) * z < 40)
|
||||
// sehr klein (Handy quer, ganzes Haus 0,41; cover 0,35): seitlich auf 36 gedeckelt (24 + 72 = 96 < 100 Weltpixel Figurenabstand)
|
||||
assert.equal(tippRand(0.41).x, 36); assert.equal(tippRand(0.3517).x, 36)
|
||||
assert.ok((24 + 2 * 36) * 0.3517 >= 32, 'auch beim kleinsten Zoom mindestens 32 px breit')
|
||||
// nie kleiner als die bisherigen Werte, immer endlich, auch bei kaputtem Zoom
|
||||
for (const kaputt of [0, -1, NaN, Infinity, undefined, null, 'x']) {
|
||||
const k = tippRand(kaputt)
|
||||
for (const v of [k.x, k.oben, k.unten]) assert.ok(Number.isFinite(v) && v > 0, String(kaputt))
|
||||
}
|
||||
for (let zoom = 0.3; zoom <= 5; zoom += 0.05) {
|
||||
const k = tippRand(zoom)
|
||||
assert.ok(k.x >= 12 && k.oben >= 8 && k.unten >= 6 && k.x <= 36 && k.oben <= 12 && k.unten <= 12, `Zoom ${zoom}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('tippRand (M8): die gewachsenen Tippflächen überdecken sich bei keinem Zoom (alle 19 Figuren, echte Lage), und am Handy sind sie breit genug', () => {
|
||||
const figuren = Object.entries(HUSTLER).map(([id, h]) => ({ id, ...h.knopf }))
|
||||
assert.equal(figuren.length, 19)
|
||||
for (let zoom = 0.35; zoom <= 5; zoom += 0.01) {
|
||||
const t = tippRand(zoom)
|
||||
const flaechen = figuren.map((f) => ({ id: f.id, l: f.left - t.x, r: f.left + f.w + t.x, o: f.top - t.oben, u: f.top + f.h + t.unten }))
|
||||
for (let i = 0; i < flaechen.length; i += 1) {
|
||||
for (let j = i + 1; j < flaechen.length; j += 1) {
|
||||
const a = flaechen[i]; const b = flaechen[j]
|
||||
const ueberlappt = a.l < b.r - 1e-9 && b.l < a.r - 1e-9 && a.o < b.u - 1e-9 && b.o < a.u - 1e-9
|
||||
assert.ok(!ueberlappt, `${a.id} und ${b.id} überdecken sich bei Zoom ${zoom.toFixed(2)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 390 x 844 (ganzes Haus 0,7033): jede Figur ≥ 40 px breit, ≥ 32 px hoch (senkrecht begrenzt der Etagenabstand 56 Weltpixel)
|
||||
const z = kameraPassend({ w: 390, h: 844 }).s
|
||||
const t = tippRand(z)
|
||||
for (const f of figuren) {
|
||||
assert.ok((f.w + 2 * t.x) * z >= 40 - 1e-9, `${f.id} Breite`)
|
||||
assert.ok((f.h + t.oben + t.unten) * z >= 32, `${f.id} Höhe`)
|
||||
}
|
||||
// nach „Etage heranzoomen“ (1,125): ≥ 40 in beide Richtungen
|
||||
const nah = kameraEtage(552, 608, { w: 390, h: 844 }).s
|
||||
const tn = tippRand(nah)
|
||||
for (const f of figuren) {
|
||||
assert.ok((f.w + 2 * tn.x) * nah >= 40 - 1e-9, `${f.id} Breite nah`)
|
||||
assert.ok((f.h + tn.oben + tn.unten) * nah >= 40 - 1e-9, `${f.id} Höhe nah`)
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// Aufbau des Moduls
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ test('WorkersPage: der Feed läuft einmal pro Seite und füllt die Hustler-Tabel
|
||||
const s = readFileSync(new URL('../../src/pages/WorkersPage.jsx', import.meta.url), 'utf8')
|
||||
assert.equal(s.match(/useWorkersFeed\(/g).length, 1)
|
||||
assert.match(s, /const \{ state, error, loaded, refresh \} = useWorkersFeed\(api, \{ onEvents \}\)/)
|
||||
assert.match(s, /indexWorkers\(state\)/); assert.match(s, /reduceEvents\(w, events\)/)
|
||||
assert.match(s, /indexWorkers\(state\)/); assert.match(s, /reduceEvents\(t\.workers, events\)/) // die Tabelle merkt sich ihren Schnappschuss (R5) und wird bei Ereignissen fortgeschrieben
|
||||
assert.match(s, /const regieRef = useRef\(null\)/); assert.match(s, /regieRef\.current\.push\(events\)/); assert.match(s, /<Hochhaus[^>]*regieRef=\{regieRef\}/)
|
||||
assert.match(s, /feed=\{\{ state, error, loaded, refresh \}\}/); assert.match(s, /<Hochhaus[^>]*workers=\{workers\}/)
|
||||
for (const f of ['Hochhaus', 'PultList', 'BotLeadList']) assert.equal(readFileSync(new URL(`../../src/components/workers/${f === 'Hochhaus' ? 'hochhaus/' : ''}${f}.jsx`, import.meta.url), 'utf8').includes('useWorkersFeed('), false, `${f} startet keinen eigenen Feed`)
|
||||
@@ -42,7 +42,8 @@ test('WorkersPage: freigabe zeigt die PultList, botleads die BotLeadList (api, b
|
||||
assert.match(s, /params\.get\('bot'\)/); assert.match(s, /<BotLeadList[^>]*\bbot=\{bot\}/); assert.match(s, /<BotLeadList[^>]*onClearBot=\{\(\) => setParams\(\{ tab: 'botleads' \}\)\}/)
|
||||
assert.match(s, /onOpen=\{\(tab\) => setParams\(\{ tab \}\)\}/) // setParams ersetzt alle Parameter: bot fällt beim Wechsel weg
|
||||
assert.equal(s.match(/onBack=\{\(\) => setParams\(\{ tab: 'hochhaus' \}\)\}/g).length, 2) // „◂ Hochhaus“ in Pult und Keller
|
||||
for (const name of ['PultList', 'BotLeadList']) assert.match(s, new RegExp(`<${name}[\\s\\S]*?kpis=\\{state\\?\\.kpis\\}[\\s\\S]*?workers=\\{workers\\}[\\s\\S]*?/>`))
|
||||
for (const name of ['PultList', 'BotLeadList']) assert.match(s, new RegExp(`<${name}[\\s\\S]*?kpis=\\{state\\?\\.kpis\\}[\\s\\S]*?workers=\\{workers\\}[\\s\\S]*?paused=\\{paused\\}[\\s\\S]*?/>`)) // Pause (R3) geht an Pult und Keller
|
||||
assert.match(s, /const paused = state\?\.paused === true/)
|
||||
assert.doesNotMatch(s, /Später kommt botleads/)
|
||||
})
|
||||
test('CSS-Importe der Hustler-Oberfläche stehen nur in WorkersPage, nicht in den Komponenten (sonst nicht unter Node mit fakeReact ladbar)', () => {
|
||||
|
||||
Reference in new issue
Block a user