Hustler-Pixelwelt T13: Korrekturen Pult und Keller (Sperre laufender Aktionen, Fokus, Kontrast, Handy-Tippflächen, Pause)
Pult: laufende Anfragen je Eintrags-Id in PultList (laufRef, Brief meldet Beginn/Ende): Brief schließen und denselben Eintrag wieder öffnen sperrt alle Aktionsknöpfe, kein zweites reply/paste/start-build (P1); spätes Ergebnis nennt den Titel (P1b); Original und Entwurf pre-wrap (P3); Überschrift nimmt beim Einhängen den Fokus (M2); „Pult zuklappen“ mit aria-expanded (K4); paused -> hk-steht an der Szene (R3); Brief z-index 9 (R4); Textlinks am Handy 40 px (K7). Keller: Überschrift-Fokus (M2), Reiter-Fokusring innen (M3), Seitenzeile/Punkte/Reiterzahl in Pixelify Sans 13 px (K5), Suchfeld und Akte-Link 40 px am Handy (K7), Ortszeile mit title (W2), paused -> hk-steht (R3), Akte z-index 9 (R4); Arne ohne Eintrag neutral (R5). Tests: Sperre laufender Aktionen, Titel im späten Ergebnis, Fokus, aria-expanded, paused, pre-wrap, z-index, Tippflächen, Reiter-Fokus, Schrift, Kontrast (WCAG) von .hk-zeile-alt/.hk-kl-tag auf Papier und Hover, Arne neutral (alte „Arne schläft ohne Eintrag“-Prüfung ersetzt). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ea1485c27c
commit
0924a8d2ae
13 files changed
+597
-70
No files matched your search
@@ -194,8 +194,8 @@ function fakeFocus() {
|
||||
return f
|
||||
}
|
||||
|
||||
// Ein Lauf des Pults: items gelten für jeden Filter (der Test steuert über die Liste, was sichtbar ist); opts: kpis, onBack, api
|
||||
async function startPult(items, { api = { pultAction: async () => ({}) }, kpis, onBack, feed = {} } = {}) {
|
||||
// Ein Lauf des Pults: items gelten für jeden Filter (der Test steuert über die Liste, was sichtbar ist); opts: kpis, onBack, api, paused
|
||||
async function startPult(items, { api = { pultAction: async () => ({}) }, kpis, onBack, feed = {}, paused } = {}) {
|
||||
const { __mount, jsx, PultList } = await loadUi()
|
||||
const timers = new Map()
|
||||
let nextTimer = 1
|
||||
@@ -209,7 +209,7 @@ async function startPult(items, { api = { pultAction: async () => ({}) }, kpis,
|
||||
globalThis.__pultEmployees = EMPLOYEES
|
||||
globalThis.__pultFeedFor = (statuses) => ({ items: feed.byStatus ? (feed.byStatus[statuses] ?? []) : items, loading: false, error: null, refresh: async () => { refreshes.push(statuses) }, ...feed.extra })
|
||||
delete globalThis.__panel
|
||||
const mounted = __mount(jsx(PultList, { api, me: ME, kpis, onBack }))
|
||||
const mounted = __mount(jsx(PultList, { api, me: ME, kpis, onBack, paused }))
|
||||
const view = {
|
||||
timers, refreshes,
|
||||
tree: () => mounted.tree(),
|
||||
@@ -396,6 +396,120 @@ test('Pult: „Pult zuklappen“ blendet Reiter und Klemmbrett aus (die Szene bl
|
||||
} finally { ui.end() }
|
||||
})
|
||||
|
||||
test('Pult (T13 P1b): ein spätes Ergebnis nennt den Eintrag („Titel“: …), auch ein später Erfolg; ohne Titel bleibt der Satz wie er ist', async () => {
|
||||
const f = fakeFocus()
|
||||
const wartend = []
|
||||
const api = { pultAction: () => new Promise((resolve, reject) => { wartend.push({ resolve, reject }) }) }
|
||||
const items = [entry({ $id: 'a', title: 'Suche Webdesigner', kind: 'unsicher', prospectId: 'p1' }), entry({ $id: 'b', title: ' ', kind: 'unsicher', prospectId: 'p1' })]
|
||||
const ui = await startPult(items, { api })
|
||||
try {
|
||||
const zu = () => ui.press(nodes(ui.brief(), (n) => n.props['aria-label'] === 'Schließen')[0])
|
||||
// Fehler: Titel vorn
|
||||
await ui.open(0, f.el('u0'))
|
||||
await ui.press(btn(ui.brief(), 'Ich kümmere mich')[0]); await zu()
|
||||
wartend[0].reject(Object.assign(new Error('Wird schon von einem Kollegen bearbeitet.'), { status: 409 })); await settle()
|
||||
assert.deepEqual(byClass(ui.tree(), 'hk-hinweis-err').map(textOf), ['„Suche Webdesigner“: Wird schon von einem Kollegen bearbeitet.'])
|
||||
// Erfolg (Antwort kommt nach dem Schließen): „Du kümmerst dich darum.“ gehört zu diesem Eintrag
|
||||
await ui.open(0, f.el('u0b'))
|
||||
await ui.press(btn(ui.brief(), 'Ich kümmere mich')[0]); await zu()
|
||||
wartend[1].resolve({ item: { $id: 'a' } }); await settle()
|
||||
assert.deepEqual(byClass(ui.tree(), 'hk-hinweis-ok').map(textOf), ['„Suche Webdesigner“: Du kümmerst dich darum.'])
|
||||
// leerer Titel: kein „„“: …“ vorn
|
||||
await ui.open(1, f.el('u1'))
|
||||
await ui.press(btn(ui.brief(), 'Ich kümmere mich')[0]); await zu()
|
||||
wartend[2].reject(Object.assign(new Error('Wird schon von einem Kollegen bearbeitet.'), { status: 409 })); await settle()
|
||||
assert.deepEqual(byClass(ui.tree(), 'hk-hinweis-err').map(textOf), ['Wird schon von einem Kollegen bearbeitet.'])
|
||||
} finally { ui.end(); f.restore() }
|
||||
})
|
||||
|
||||
// ---- T13 M2: Bildschirmwechsel – die Überschrift nimmt beim Einhängen den Fokus ----
|
||||
|
||||
test('Pult (T13 M2): beim Einhängen nimmt die Überschrift „Freigabe“ den Fokus (tabIndex -1, preventScroll); ohne document oder Element passiert nichts', async () => {
|
||||
const aufrufe = []
|
||||
const h1 = { focus: (options) => aufrufe.push(options) }
|
||||
globalThis.__fakeRef = (tag) => (tag === 'h1' ? h1 : null)
|
||||
const ui = await startPult([entry()])
|
||||
try {
|
||||
const titel = byClass(ui.tree(), 'hk-pult-h1')
|
||||
assert.equal(titel.length, 1); assert.equal(titel[0].tag, 'h1'); assert.equal(titel[0].props.tabIndex, -1); assert.equal(textOf(titel[0]), 'Freigabe')
|
||||
assert.deepEqual(aufrufe, [{ preventScroll: true }]) // genau einmal, ohne dass die Seite springt
|
||||
// ein Filterwechsel oder ein Brief holt den Fokus nicht noch einmal an die Überschrift
|
||||
await ui.tab('Veraltet'); await ui.tab('Wartet')
|
||||
await ui.open(0)
|
||||
assert.equal(aufrufe.length, 1)
|
||||
} finally { ui.end(); delete globalThis.__fakeRef }
|
||||
// ohne Element (Server, Node: kein ref gesetzt) bricht nichts
|
||||
const ohne = await startPult([entry()])
|
||||
try { assert.equal(byClass(ohne.tree(), 'hk-pult-h1').length, 1) } finally { ohne.end() }
|
||||
// die Überschrift zeigt den Fokus (sichtbarer Ring) wie die anderen Bedienelemente
|
||||
assert.match(read('src/styles/hustler/hustler-pult-ui.css'), /\.hk-pult \.hk-pult-h1:focus-visible\s*\{[^}]*outline:\s*2px solid/)
|
||||
})
|
||||
|
||||
// ---- T13 K4: „Pult zuklappen“ sagt, ob das Pult offen ist ----
|
||||
|
||||
test('Pult (T13 K4): „Pult zuklappen“ trägt aria-expanded (offen = true, zugeklappt = false)', async () => {
|
||||
const ui = await startPult([entry()], { kpis: { pultOpen: 1 } })
|
||||
try {
|
||||
assert.equal(btn(ui.tree(), 'Pult zuklappen')[0].props['aria-expanded'], true)
|
||||
await ui.press(btn(ui.tree(), 'Pult zuklappen')[0])
|
||||
assert.equal(btn(ui.tree(), 'Pult aufklappen')[0].props['aria-expanded'], false)
|
||||
await ui.press(btn(ui.tree(), 'Pult aufklappen')[0])
|
||||
assert.equal(btn(ui.tree(), 'Pult zuklappen')[0].props['aria-expanded'], true)
|
||||
} finally { ui.end() }
|
||||
})
|
||||
|
||||
// ---- T13 R3: paused erreicht die Szene (Boten, Aufzug, Fähnchen, Uhr stehen still) ----
|
||||
|
||||
test('Pult (T13 R3): paused setzt hk-steht an der Szene (mit und ohne Boten-Schleife); sonst nicht; das CSS der Seite hält alles darunter an', async () => {
|
||||
const klassen = async (opts) => {
|
||||
const ui = await startPult([entry()], opts)
|
||||
try { return String(byClass(ui.tree(), 'hk-pult-szene')[0].props.className) } finally { ui.end() }
|
||||
}
|
||||
assert.equal(await klassen({ kpis: { pultOpen: 3 }, paused: true }), 'hk-pult-szene hk-steht')
|
||||
assert.equal(await klassen({ kpis: { pultOpen: 0 }, paused: true }), 'hk-pult-szene hk-pult-ruhe hk-steht')
|
||||
assert.equal(await klassen({ kpis: { pultOpen: 3 }, paused: false }), 'hk-pult-szene')
|
||||
assert.equal(await klassen({ kpis: { pultOpen: 3 } }), 'hk-pult-szene') // ohne die Prop gilt „nicht angehalten“
|
||||
for (const unsinn of ['ja', 1, null, {}]) assert.equal(await klassen({ kpis: { pultOpen: 3 }, paused: unsinn }), 'hk-pult-szene', String(unsinn))
|
||||
// die Regel, die Animationen unter hk-steht anhält, steht in hustler-ui.css unter der Seite (das Pult hängt darin)
|
||||
assert.match(read('src/styles/hustler/hustler-ui.css'), /\.hk-seite \.hk-steht \*[^{]*\{[^}]*animation-play-state:\s*paused\s*!important/)
|
||||
// PultSzene allein (ohne Seite) kennt die Prop ebenfalls
|
||||
const { __mount, jsx, PultSzene } = await loadUi()
|
||||
const szene = __mount(jsx(PultSzene, { laeuft: true, steht: true }))
|
||||
assert.ok(hasClass(szene.tree(), 'hk-steht')); szene.unmount()
|
||||
})
|
||||
|
||||
// ---- T13 P3, R4, K7: CSS des Pults ----
|
||||
|
||||
test('hustler-pult-ui.css (T13 P3): Originalanfrage und Entwurf zum Lesen behalten ihre Zeilenumbrüche und Leerzeichen (pre-wrap, nicht pre-line)', () => {
|
||||
const css = read('src/styles/hustler/hustler-pult-ui.css').replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
for (const klasse of ['hk-brief-zitat', 'hk-brief-lesen']) {
|
||||
const regel = new RegExp(`\\.hk-pult \\.${klasse}\\s*\\{([^}]*)\\}`).exec(css)
|
||||
assert.ok(regel, klasse)
|
||||
assert.match(regel[1], /white-space:\s*pre-wrap\s*;/, klasse); assert.doesNotMatch(regel[1], /pre-line/, klasse)
|
||||
}
|
||||
assert.doesNotMatch(css, /white-space:\s*pre-line/)
|
||||
})
|
||||
|
||||
test('hustler-pult-ui.css (T13 R4): der Brief liegt unter der Handy-Seitenleiste (z-index 20) und ihrem Hintergrund (15), aber über Szene und Klemmbrett', () => {
|
||||
const css = read('src/styles/hustler/hustler-pult-ui.css').replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
const z = (klasse) => Number(new RegExp(`\\.hk-pult \\.${klasse}\\s*\\{[^}]*z-index:\\s*(\\d+)`).exec(css)?.[1])
|
||||
assert.ok(z('hk-brief-ueber') <= 9, `Brief z-index ${z('hk-brief-ueber')}`)
|
||||
assert.ok(z('hk-brief-ueber') > z('hk-brett'), 'über dem Klemmbrett'); assert.ok(z('hk-brief-ueber') > z('hk-reiter-leiste'), 'über den Reitern')
|
||||
// keine Regel dieser Datei legt etwas über die Seitenleiste
|
||||
for (const m of css.matchAll(/z-index:\s*(\d+)/g)) assert.ok(Number(m[1]) <= 9, `z-index ${m[1]}`)
|
||||
})
|
||||
|
||||
test('hustler-pult-ui.css (T13 K7): am Handy sind die Textlinks im Brief („Anzeige öffnen“, „Interessent“) mindestens 40 px hoch', () => {
|
||||
const css = read('src/styles/hustler/hustler-pult-ui.css').replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
const handy = css.slice(css.indexOf('@media (max-width: 719px)'))
|
||||
assert.match(handy, /\.hk-pult \.hk-link\s*\{[^}]*min-height:\s*40px/)
|
||||
// die Link-Regel selbst zentriert den Text in dieser Höhe (inline-flex), die Fläche ist also wirklich antippbar
|
||||
assert.match(css, /\.hk-pult \.hk-link\s*\{[^}]*display:\s*inline-flex[^}]*align-items:\s*center/)
|
||||
// „Anzeige öffnen“ und „Interessent“ tragen genau diese Klasse
|
||||
const brief = read('src/components/workers/pult/Brief.jsx')
|
||||
assert.match(brief, /<a className="hk-link" href=\{row\.sourceHref\}/); assert.match(brief, /<button type="button" className="hk-link" onClick=\{\(\) => onDetails/)
|
||||
})
|
||||
|
||||
test('Pult: „◂ Hochhaus“ ruft onBack; ohne onBack passiert beim Klick nichts', async () => {
|
||||
const calls = []
|
||||
const ui = await startPult([], { onBack: () => calls.push('zurück') })
|
||||
@@ -557,7 +671,8 @@ test('Pult: ein Ergebnis, das nach dem Schließen des Briefs eintrifft, erschein
|
||||
assert.equal(ui.brief(), null)
|
||||
reject(Object.assign(new Error('Der Hustler-Dienst ist gerade nicht erreichbar.'), { status: 0 })); await settle()
|
||||
const alarm = byClass(ui.tree(), 'hk-hinweis-err')
|
||||
assert.deepEqual(alarm.map((n) => [n.props.role, textOf(n)]), [['alert', 'Kann gesendet sein – Pult neu laden.']])
|
||||
// T13 P1b: der Satz nennt den Eintrag (sein Brief ist ja zu), sonst weiß der Leser nicht, wozu er gehört
|
||||
assert.deepEqual(alarm.map((n) => [n.props.role, textOf(n)]), [['alert', '„Eins“: Kann gesendet sein – Pult neu laden.']])
|
||||
// ein neuer Brief räumt die späte Meldung weg
|
||||
await ui.open(1)
|
||||
assert.equal(byClass(ui.tree(), 'hk-hinweis-err').length, 0)
|
||||
|
||||
Reference in new issue
Block a user