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
@@ -1,6 +1,7 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { existsSync, readFileSync, statSync } from 'node:fs'
|
||||
import { inflateSync as zlibSync } from 'node:zlib'
|
||||
import path from 'node:path'
|
||||
import { loadModule } from './load.mjs'
|
||||
import {
|
||||
@@ -101,19 +102,104 @@ test('Arne arbeitet nicht: Haus-Figur in der Pose des Status am Pult, Rohrpost,
|
||||
}
|
||||
})
|
||||
|
||||
test('Arne ohne Zustand (Tabelle noch leer, kein Eintrag): er schläft; der Status wechselt mit den Daten der Seite', async () => {
|
||||
// T13 R5 (ersetzt „er schläft“): solange es keinen Eintrag für Arne gibt, weiß die Seite nichts über ihn; er steht neutral am Pult – ohne Zzz (schläft), ohne Kaffeepunkte
|
||||
// (wartet auf Auftrag), ohne Marker und ohne die „nicht“-Pose (nicht angeschlossen). Mit den ersten Daten wechselt er in den echten Zustand.
|
||||
test('Arne ohne Zustand (Tabelle noch leer, kein Eintrag): er steht neutral am Pult, ohne Zzz, Punkte, Marker und „nicht angeschlossen“; der Status wechselt mit den Daten der Seite', async () => {
|
||||
const neutral = (ui, wo) => {
|
||||
assert.equal(sz(ui).props['data-hk-arne'], 'unbekannt', wo)
|
||||
assert.deepEqual(byClass(sz(ui), 'hk-kl-fig').map(classesOf), [['hk-spr', 'hk-kl-fig', 'hk-kl-fig-warten']], wo) // ruhige Haltung, keine Schlaf- oder „nicht“-Pose
|
||||
for (const c of ['hk-kl-zzz', 'hk-kl-punkte', 'hk-alarm', 'hk-kl-fig-schlafen', 'hk-kl-fig-nicht', 'hk-arne-weg', 'hk-arne-bild']) assert.equal(has(ui, c), false, `${wo}: ${c}`)
|
||||
for (const c of RUHENDE) assert.ok(classesOf(byClass(sz(ui), c)[0]).includes('hk-kl-ruht'), `${wo}: ${c} läuft`) // Rohrpost, Stempel und Schublade ruhen
|
||||
}
|
||||
const { ui } = await szene(undefined)
|
||||
try {
|
||||
assert.equal(sz(ui).props['data-hk-arne'], 'leerlauf')
|
||||
assert.ok(has(ui, 'hk-kl-fig-schlafen') && has(ui, 'hk-kl-zzz'))
|
||||
neutral(ui, 'ohne Tabelle')
|
||||
ui.setProps({ workers: { arne: eintrag('arbeitet') } })
|
||||
assert.ok(has(ui, 'hk-arne-weg') && !has(ui, 'hk-kl-fig'))
|
||||
ui.setProps({ workers: { arne: eintrag('fehler') } })
|
||||
assert.ok(!has(ui, 'hk-arne-weg') && has(ui, 'hk-kl-fig-nicht') && has(ui, 'hk-alarm'))
|
||||
ui.setProps({ workers: { arne: eintrag('wartet') } })
|
||||
assert.ok(has(ui, 'hk-kl-fig-warten') && has(ui, 'hk-kl-punkte') && !has(ui, 'hk-alarm'))
|
||||
ui.setProps({ workers: {} })
|
||||
assert.ok(has(ui, 'hk-kl-fig-schlafen'))
|
||||
ui.setProps({ workers: { arne: eintrag('leerlauf') } })
|
||||
assert.ok(has(ui, 'hk-kl-fig-schlafen') && has(ui, 'hk-kl-zzz') && !has(ui, 'hk-kl-punkte')) // ein echter Leerlauf schläft weiterhin
|
||||
ui.setProps({ workers: { arne: eintrag('grau') } })
|
||||
assert.ok(has(ui, 'hk-kl-fig-nicht') && !has(ui, 'hk-kl-zzz') && !has(ui, 'hk-alarm'))
|
||||
// Tabelle leer ({}), Eintrag für Arne fehlt (nur andere Hustler), Eintrag ist keine Tabellenzeile: neutral
|
||||
for (const workers of [{}, { otto: eintrag('arbeitet') }, { arne: null }, { arne: 'arbeitet' }, undefined]) {
|
||||
ui.setProps({ workers })
|
||||
neutral(ui, JSON.stringify(workers))
|
||||
}
|
||||
} finally { ui.end() }
|
||||
})
|
||||
|
||||
// T13 R3: Prop paused (Hustler angehalten) → hk-steht an der Szene; die Regel der Seite (hustler-ui.css) hält darunter alle Animationen an (Arne, Rohrpost, Maus, Licht)
|
||||
test('Szene (T13 R3): paused setzt hk-steht an der Keller-Szene (auch bei arbeitendem Arne), sonst nicht; unbrauchbare Werte gelten als „läuft“', async () => {
|
||||
const klasse = async (initial) => {
|
||||
const { ui } = await szene(undefined)
|
||||
ui.end()
|
||||
const lauf = await start(fakeApi(), initial)
|
||||
try { return classesOf(sz(lauf)).join(' ') } finally { lauf.end() }
|
||||
}
|
||||
assert.equal(await klasse({ paused: true }), 'hk-kl-szene hk-steht')
|
||||
assert.equal(await klasse({ paused: true, workers: { arne: eintrag('arbeitet') } }), 'hk-kl-szene hk-steht')
|
||||
assert.equal(await klasse({ paused: false }), 'hk-kl-szene')
|
||||
assert.equal(await klasse({}), 'hk-kl-szene')
|
||||
for (const unsinn of ['ja', 1, null, {}]) assert.equal(await klasse({ paused: unsinn }), 'hk-kl-szene', String(unsinn))
|
||||
// die Szene ist die Wurzel dieser Animationen und steckt unter .hk-seite (WorkersPage), wo die Regel gilt
|
||||
assert.match(read('src/styles/hustler/hustler-ui.css'), /\.hk-seite \.hk-steht \*[^{]*\{[^}]*animation-play-state:\s*paused\s*!important/)
|
||||
// der Wechsel von „läuft“ zu „steht“ und zurück gilt sofort
|
||||
const lauf = await start(fakeApi(), {})
|
||||
try {
|
||||
assert.equal(classesOf(sz(lauf)).includes('hk-steht'), false)
|
||||
lauf.setProps({ paused: true }); assert.equal(classesOf(sz(lauf)).includes('hk-steht'), true)
|
||||
lauf.setProps({ paused: false }); assert.equal(classesOf(sz(lauf)).includes('hk-steht'), false)
|
||||
} finally { lauf.end() }
|
||||
})
|
||||
|
||||
// T13 M2: Bildschirmwechsel – die Überschrift „Bot-Leads“ nimmt beim Einhängen den Fokus
|
||||
test('Keller (T13 M2): beim Einhängen nimmt die Überschrift „Bot-Leads“ den Fokus (tabIndex -1, preventScroll), nur einmal; ohne Element bricht nichts', async () => {
|
||||
const aufrufe = []
|
||||
const h1 = { focus: (options) => aufrufe.push(options) }
|
||||
globalThis.__fakeRef = (tag) => (tag === 'h1' ? h1 : null)
|
||||
const api = fakeApi()
|
||||
const ui = await start(api, {})
|
||||
try {
|
||||
const titel = nodes(ui.tree(), (n) => n.tag === 'h1')
|
||||
assert.equal(titel.length, 1); assert.equal(titel[0].props.tabIndex, -1); assert.equal(textOf(titel[0]), 'Bot-Leads')
|
||||
assert.deepEqual(aufrufe, [{ preventScroll: true }])
|
||||
// Laden, Raumwechsel, Suche und Akte holen den Fokus nicht wieder an die Überschrift
|
||||
await ui.answer(0, { items: [ITEM], total: 120, pageSize: 50 })
|
||||
await ui.pick(1); await ui.pick(0)
|
||||
await ui.type('Elektro'); ui.fireTimers(); await settle()
|
||||
assert.equal(aufrufe.length, 1)
|
||||
} finally { ui.end(); delete globalThis.__fakeRef }
|
||||
const ohne = await start(fakeApi(), {}) // kein ref: nichts zu fokussieren
|
||||
try { assert.equal(nodes(ohne.tree(), (n) => n.tag === 'h1').length, 1) } finally { ohne.end() }
|
||||
assert.match(cssOhne('src/styles/hustler/hustler-keller-ui.css'), /\.hk-keller \.hk-kl-titel:focus-visible\s*\{[^}]*outline:\s*2px solid/)
|
||||
})
|
||||
|
||||
// T13 K4 (Keller): „Register zuklappen“ sagt, ob das Register offen ist – die Prüfung war schon da, hier wird sie festgehalten
|
||||
test('Keller (T13 K4): „Register zuklappen“ trägt aria-expanded und folgt dem Zustand', async () => {
|
||||
const ui = await start(fakeApi(), {})
|
||||
try {
|
||||
assert.equal(buttons(ui.tree(), 'Register zuklappen')[0].props['aria-expanded'], true)
|
||||
await ui.click('Register zuklappen')
|
||||
assert.equal(buttons(ui.tree(), 'Register aufklappen')[0].props['aria-expanded'], false)
|
||||
} finally { ui.end() }
|
||||
})
|
||||
|
||||
// T13 W2: die Ortszeile kürzt mit „…“, der Browser-Tooltip nennt den vollen Ort samt Datum
|
||||
test('Zeile (T13 W2): die Ortszeile trägt title mit vollem Ort und Funddatum; ohne Ort und Datum kein leeres title', async () => {
|
||||
const ui = await start(fakeApi(), {})
|
||||
try {
|
||||
await ui.answer(0, { items: [{ ...ITEM, city: 'Kaiserslautern-Hohenecken-Am-Lauterstrom', postalCode: '67655' }, { ...ITEM, id: 'p9', city: '', postalCode: '', foundAt: '' }], total: 2 })
|
||||
const zeilen = ui.rows()
|
||||
const klein = (r) => byClass(r, 'hk-klein')[0]
|
||||
assert.equal(klein(zeilen[0]).props.title, '67655 Kaiserslautern-Hohenecken-Am-Lauterstrom · 19.10.2026')
|
||||
assert.equal(klein(zeilen[0]).props.title, textOf(klein(zeilen[0]))) // title = sichtbarer Text, nur ungekürzt
|
||||
// ohne Ort und Datum: botleadRow liefert Platzhalter oder nichts; ein title ist nie leer
|
||||
assert.ok(klein(zeilen[1]).props.title === undefined || klein(zeilen[1]).props.title !== '')
|
||||
assert.equal(klein(zeilen[1]).props.title, textOf(klein(zeilen[1])) === '' ? undefined : textOf(klein(zeilen[1])))
|
||||
} finally { ui.end() }
|
||||
})
|
||||
|
||||
@@ -740,3 +826,124 @@ test('hustler-keller-ui.css: Überschreibungen erzeugter Regeln haben höhere Sp
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// T13 (Endprüfung): Schrift, Tippflächen, Fokusring der Reiter, Stapelreihenfolge, Kontrast
|
||||
|
||||
const eigeneRegeln = (selektor, kontext = '') => ui.regeln.filter((r) => r.kontext === kontext && r.selektor.split(',').map((t) => t.trim()).includes(selektor))
|
||||
const handyRegeln = (selektor) => ui.regeln.filter((r) => /max-width:\s*719px/.test(r.kontext) && r.selektor.split(',').map((t) => t.trim()).includes(selektor))
|
||||
const eigenschaft = (regel, name) => new RegExp(`(?:^|;)\\s*${name}\\s*:\\s*([^;]+)`).exec(regel.koerper)?.[1].trim()
|
||||
const pixel = (wert) => Number.parseFloat(wert)
|
||||
|
||||
test('hustler-keller-ui.css (T13 K5): Seitenzeile, Punkte-Zahl und Reiterzahl stehen in Pixelify Sans ab 13 px, nicht in Press Start 2P mit 8 px', () => {
|
||||
for (const klasse of ['hk-kl-seiten', 'hk-kl-punkte-text', 'hk-kl-reiter-zahl']) {
|
||||
const regeln = ui.regeln.filter((r) => r.selektor.split(',').some((t) => t.trim().endsWith(`.${klasse}`)))
|
||||
assert.ok(regeln.length >= 1, klasse)
|
||||
for (const r of regeln) {
|
||||
const schrift = eigenschaft(r, 'font-family'); const groesse = eigenschaft(r, 'font-size')
|
||||
if (r.kontext === '') { assert.ok(schrift && groesse, `${klasse}: Schrift und Größe stehen in der Grundregel`) }
|
||||
if (schrift) { assert.match(schrift, /^'Pixelify Sans'/, klasse); assert.doesNotMatch(schrift, /Press Start 2P/, klasse) }
|
||||
if (groesse) {
|
||||
assert.ok(pixel(groesse) >= 13, `${klasse}: ${groesse}`)
|
||||
const zeile = eigenschaft(r, 'line-height'); if (zeile) assert.ok(pixel(zeile) >= pixel(groesse), `${klasse}: Zeilenhöhe ${zeile}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('hustler-keller-ui.css (T13 K7): am Handy sind Suchfeld und der Textlink der Akte mindestens 40 px hoch (Tippfläche), die Lupe bleibt im Feld mittig', () => {
|
||||
const suche = handyRegeln('.hk-keller input.hk-suche')
|
||||
assert.equal(suche.length, 1); assert.ok(pixel(eigenschaft(suche[0], 'min-height')) >= 40)
|
||||
const link = handyRegeln('.hk-keller .hk-akte-link')
|
||||
assert.equal(link.length, 1); assert.ok(pixel(eigenschaft(link[0], 'min-height')) >= 40)
|
||||
// das Suchfeld ist am Desktop 36 px hoch (erzeugt); min-height gewinnt dort, wo es nötig ist; die Spezifität der Handy-Regel schlägt die erzeugte (input.hk-suche, 2 Klassen + Element)
|
||||
assert.match(cssOhne('src/styles/hustler/keller.css'), /\.hk-keller \.hk-suche\{[^}]*height:36px/)
|
||||
const lupe = handyRegeln('.hk-keller .hk-kl-lupe')
|
||||
assert.equal(lupe.length, 1); assert.equal(pixel(eigenschaft(lupe[0], 'top')), 4) // 40 px Feld: 4 px oben, das 32-px-Symbol sitzt mittig (vorher 2 px bei 36 px)
|
||||
// der Link steht in der Zeile des Textes und zentriert ihn (inline-flex), die 40 px sind also Fläche des Links; „Quelle öffnen“ trägt genau diese Klasse
|
||||
assert.match(cssOhne('src/styles/hustler/keller.css'), /\.hk-keller \.hk-akte-link\{[^}]*display:inline-flex[^}]*align-items:center/)
|
||||
assert.match(read('src/components/workers/keller/Akte.jsx'), /<a className="hk-akte-link" href=\{e\.sourceHref\}/)
|
||||
// am Desktop ändert sich nichts: die Mindesthöhe steht nur im Handy-Block
|
||||
for (const sel of ['.hk-keller input.hk-suche', '.hk-keller .hk-akte-link']) assert.equal(eigeneRegeln(sel).filter((r) => /min-height/.test(r.koerper)).length, 0, sel)
|
||||
})
|
||||
|
||||
// Spezifität grob: Zahl der Klassen und Pseudoklassen (Elementnamen zählen weniger und sind hier nicht nötig)
|
||||
const spezifitaet = (selektor) => (selektor.match(/\.[\w-]+/g) ?? []).length + (selektor.match(/:[\w-]+/g) ?? []).length
|
||||
|
||||
test('hustler-keller-ui.css (T13 M3): Raum-Reiter zeigen den Fokus innen (Umriss nach innen, gelbe Innenkante), auch der aktive; die Regel schlägt die erzeugte', () => {
|
||||
const erzeugt = zerlege(cssOhne('src/styles/hustler/keller.css')).regeln.find((r) => r.selektor === '.hk-keller .hk-reiter:focus-visible')
|
||||
assert.ok(erzeugt)
|
||||
assert.match(erzeugt.koerper, /outline-offset:\s*2px/) // das war der Fehler: außen, am Handy vom wischbaren Reiterstreifen abgeschnitten
|
||||
const normal = eigeneRegeln('.hk-keller .hk-kl-reiter .hk-reiter:focus-visible')
|
||||
const aktiv = eigeneRegeln('.hk-keller .hk-kl-reiter .hk-reiter-an:focus-visible')
|
||||
assert.equal(normal.length, 1); assert.equal(aktiv.length, 1)
|
||||
assert.equal(eigenschaft(normal[0], 'outline-offset'), '-2px'); assert.match(eigenschaft(normal[0], 'outline'), /^2px solid #1a1630$/)
|
||||
// der Ring ist innen (inset) und gelb; der Kasten-Schatten, der den Rand des Reiters bildet, bleibt erhalten (box-shadow ersetzt, nicht ergänzt)
|
||||
assert.match(eigenschaft(normal[0], 'box-shadow'), /inset 0 0 0 4px #ffd25a$/); assert.match(eigenschaft(normal[0], 'box-shadow'), /^0 -2px 0 0 #2b2230, -2px 0 0 0 #2b2230, 2px 0 0 0 #2b2230/)
|
||||
assert.match(eigenschaft(aktiv[0], 'box-shadow'), /inset 0 0 0 4px #ffd25a$/); assert.match(eigenschaft(aktiv[0], 'box-shadow'), /^0 -2px 0 0 #7a2e3a, -2px 0 0 0 #7a2e3a, 2px 0 0 0 #7a2e3a/)
|
||||
// höhere Spezifität als die erzeugten Regeln (auch .hk-reiter-an:hover bleibt unberührt)
|
||||
assert.ok(spezifitaet('.hk-keller .hk-kl-reiter .hk-reiter:focus-visible') > spezifitaet(erzeugt.selektor))
|
||||
assert.ok(spezifitaet('.hk-keller .hk-kl-reiter .hk-reiter-an:focus-visible') > spezifitaet('.hk-keller .hk-reiter-an,.hk-keller .hk-reiter-an:hover'.split(',')[1]))
|
||||
// der Reiter steckt wirklich im Streifen .hk-kl-reiter (RegisterBuch)
|
||||
assert.match(read('src/components/workers/keller/RegisterBuch.jsx'), /className="hk-kl-reiter"[\s\S]*REITER\.map/)
|
||||
})
|
||||
|
||||
test('hustler-keller-ui.css (T13 R4): die Akte liegt unter der Handy-Seitenleiste (z-index 20) und ihrem Hintergrund (15), aber über Szene und Buch', () => {
|
||||
const akte = eigeneRegeln('.hk-keller .hk-kl-akte-ueber')[0]
|
||||
const z = Number(eigenschaft(akte, 'z-index'))
|
||||
assert.ok(z <= 9, `Akte z-index ${z}`)
|
||||
const buch = Number(eigenschaft(eigeneRegeln('.hk-keller .hk-kl-buch')[0], 'z-index'))
|
||||
assert.ok(z > buch, 'über dem Buch')
|
||||
for (const r of ui.regeln) { const w = eigenschaft(r, 'z-index'); if (w) assert.ok(Number(w) <= 9, `${r.selektor}: z-index ${w}`) }
|
||||
})
|
||||
|
||||
// Kontrast nach WCAG 2.x: relative Leuchtdichte, (hell + 0,05) / (dunkel + 0,05)
|
||||
const kanal = (c) => { const v = c / 255; return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4 }
|
||||
const leuchtdichte = ([r, g, b]) => 0.2126 * kanal(r) + 0.7152 * kanal(g) + 0.0722 * kanal(b)
|
||||
const kontrast = (a, b) => { const [x, y] = [leuchtdichte(a), leuchtdichte(b)]; return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05) }
|
||||
const hex = (t) => { const m = /#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})\b/i.exec(t); assert.ok(m, t); return [1, 2, 3].map((i) => Number.parseInt(m[i], 16)) }
|
||||
const mische = (vorn, alpha, hinten) => vorn.map((v, i) => v * alpha + hinten[i] * (1 - alpha)) // Farbe mit Deckkraft über einem Grund
|
||||
|
||||
// RGBA-Pixel einer 8-Bit-PNG (nicht verzahnt; Filter 0–4): das Papier des Buchs liegt als Bild vor (buch.png, „fill“ in der Mitte)
|
||||
function pngPixel(datei, x, y) {
|
||||
const d = readFileSync(path.join(ROOT, datei))
|
||||
const breite = d.readUInt32BE(16); const farbtyp = d[25]; const kanaele = { 2: 3, 6: 4 }[farbtyp]
|
||||
assert.ok(kanaele && d[24] === 8 && d[28] === 0, 'PNG: 8 Bit, RGB oder RGBA, nicht verzahnt')
|
||||
const teile = []
|
||||
for (let i = 8; i < d.length;) { const l = d.readUInt32BE(i); if (d.toString('latin1', i + 4, i + 8) === 'IDAT') teile.push(d.subarray(i + 8, i + 8 + l)); i += 12 + l }
|
||||
const roh = zlibSync(Buffer.concat(teile))
|
||||
const zeile = breite * kanaele
|
||||
let davor = Buffer.alloc(zeile)
|
||||
for (let row = 0; row <= y; row += 1) {
|
||||
const filter = roh[row * (zeile + 1)]; const aktuell = Buffer.from(roh.subarray(row * (zeile + 1) + 1, (row + 1) * (zeile + 1)))
|
||||
for (let i = 0; i < zeile; i += 1) {
|
||||
const a = i >= kanaele ? aktuell[i - kanaele] : 0; const b = davor[i]; const c = i >= kanaele ? davor[i - kanaele] : 0
|
||||
const p = a + b - c; const pa = Math.abs(p - a); const pb = Math.abs(p - b); const pc = Math.abs(p - c)
|
||||
aktuell[i] = (aktuell[i] + [0, a, b, (a + b) >> 1, pa <= pb && pa <= pc ? a : pb <= pc ? b : c][filter]) & 255
|
||||
}
|
||||
davor = aktuell
|
||||
}
|
||||
return [...davor.subarray(x * kanaele, x * kanaele + 3)]
|
||||
}
|
||||
|
||||
test('Kontrast (T13 C5): grau gestellte Einträge („gekürzt“: Zeilentext und Marke) haben auf dem Papier des Buchs und auf der Hover-Tönung mindestens 4,5 : 1', () => {
|
||||
const erzeugt = zerlege(cssOhne('src/styles/hustler/keller.css')).regeln
|
||||
const alt = erzeugt.find((r) => r.selektor === '.hk-keller .hk-zeile-alt')
|
||||
const marke = eigeneRegeln('.hk-keller .hk-kl-tag')[0]
|
||||
const hover = erzeugt.find((r) => r.selektor === '.hk-keller .hk-zeile:hover')
|
||||
assert.ok(alt && marke && hover)
|
||||
const vordergruende = { 'hk-zeile-alt': hex(eigenschaft(alt, 'color')), 'hk-kl-tag': hex(eigenschaft(marke, 'color')) }
|
||||
// Papier: Mitte des Buchs (buch.png, 48 x 48); dieselbe Farbe steht als Papierton im CSS (Reiter „Alle“, großer Punkte-Balken)
|
||||
const papier = pngPixel('src/assets/hustler/keller/ui/buch.png', 24, 24)
|
||||
assert.deepEqual(papier, hex(eigenschaft(erzeugt.find((r) => r.selektor === '.hk-keller .hk-reiter-alle'), 'background')))
|
||||
const m = /rgba\(\s*(\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\s*\)/.exec(eigenschaft(hover, 'background'))
|
||||
assert.ok(m, 'Hover-Tönung ist rgba(…)')
|
||||
const hoverGrund = mische([Number(m[1]), Number(m[2]), Number(m[3])], Number(m[4]), papier)
|
||||
for (const [name, farbe] of Object.entries(vordergruende)) {
|
||||
const aufPapier = kontrast(farbe, papier); const aufHover = kontrast(farbe, hoverGrund)
|
||||
assert.ok(aufPapier >= 4.5, `${name} auf Papier: ${aufPapier.toFixed(2)} : 1`)
|
||||
assert.ok(aufHover >= 4.5, `${name} auf Hover: ${aufHover.toFixed(2)} : 1`)
|
||||
}
|
||||
// die Rechnung selbst stimmt (Gegenprobe mit bekannten Werten: Schwarz auf Weiß 21 : 1, gleiche Farben 1 : 1)
|
||||
assert.ok(Math.abs(kontrast([0, 0, 0], [255, 255, 255]) - 21) < 1e-9); assert.equal(kontrast([10, 20, 30], [10, 20, 30]), 1)
|
||||
})
|
||||
@@ -111,8 +111,8 @@ test('arneAnsicht: arbeitet ⇒ Ablauf-Schleife; sonst Haus-Figur nach Status (w
|
||||
assert.deepEqual(arneAnsicht({ arne: eintrag('fehler') }), { status: 'fehler', arbeitet: false, pose: 'nicht', alarm: true, punkte: false, zzz: false, label: 'Arne, Haiku, Fehler' })
|
||||
})
|
||||
|
||||
test('arneAnsicht: nur „arbeitet“ arbeitet; ohne Tabelle, ohne Eintrag oder mit Unsinn schläft Arne (Leerlauf)', () => {
|
||||
for (const workers of [undefined, null, {}, [], 'x', 5, { otto: eintrag('arbeitet') }, { arne: null }, { arne: 'arbeitet' }, { arne: { status: 'irgendwas' } }, { arne: {} }]) {
|
||||
test('arneAnsicht: nur „arbeitet“ arbeitet; ein Eintrag mit unbrauchbarem Status schläft (Leerlauf)', () => {
|
||||
for (const workers of [{ arne: { status: 'irgendwas' } }, { arne: {} }]) {
|
||||
const a = arneAnsicht(workers)
|
||||
assert.equal(a.arbeitet, false, JSON.stringify(workers))
|
||||
assert.equal(a.status, 'leerlauf')
|
||||
@@ -125,8 +125,18 @@ test('arneAnsicht: nur „arbeitet“ arbeitet; ohne Tabelle, ohne Eintrag oder
|
||||
assert.equal(arneAnsicht({ arne: { ...eintrag('arbeitet'), connected: false } }).status, 'grau')
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// Register-Buch
|
||||
// T13 R5: ohne Eintrag für Arne (Tabelle leer, die Seite hat den ersten Zustand noch nicht) ist nichts über ihn bekannt: neutral, kein Schlaf, keine „nicht angeschlossen“-Pose
|
||||
test('arneAnsicht (T13 R5): ohne Tabelle oder ohne Eintrag für Arne steht er neutral da – nicht arbeitend, ohne Zzz, ohne Punkte, ohne Marker, nicht „nicht angeschlossen“', () => {
|
||||
const neutral = { status: 'unbekannt', arbeitet: false, pose: 'warten', alarm: false, punkte: false, zzz: false, label: 'Arne, Haiku' }
|
||||
for (const workers of [undefined, null, {}, [], 'x', 5, { otto: eintrag('arbeitet') }, { ali: eintrag('fehler') }, { arne: null }, { arne: undefined }, { arne: 'arbeitet' }, { arne: 7 }, { arne: [] }]) {
|
||||
assert.deepEqual(arneAnsicht(workers), neutral, JSON.stringify(workers))
|
||||
}
|
||||
// neutral ist weder einer der Status des Dienstes noch „grau“
|
||||
for (const status of ['arbeitet', 'wartet', 'leerlauf', 'fehler', 'grau']) assert.notEqual(arneAnsicht({ arne: eintrag(status) }).status, 'unbekannt', status)
|
||||
assert.equal(arneAnsicht({ arne: eintrag('grau') }).status, 'grau')
|
||||
// die Pose „warten“ gibt es als Sprite und als Klasse im CSS (sonst stünde Arne unsichtbar da)
|
||||
assert.ok(CSS.has('hk-kl-fig-warten'))
|
||||
})
|
||||
|
||||
test('REITER: Reihenfolge, Werte und Beschriftungen von ROOM_FILTERS, kurze sichtbare Namen, Symbol und Farbe je Reiter aus keller.css', () => {
|
||||
assert.deepEqual(REITER.map((r) => r.value), ROOM_FILTERS.map((f) => f.value))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -758,6 +758,108 @@ test('Pult (bedient, P4-T32): während einer Anfrage sind alle Knöpfe des Brief
|
||||
} finally { ui.end() }
|
||||
})
|
||||
|
||||
// T13 P1: die Sperre gehört dem Eintrag, nicht dem Brief. Wer den Brief während einer Anfrage schließt und denselben Eintrag wieder öffnet, darf nichts zweimal auslösen
|
||||
// (sonst fügte „Antwort einfügen“ den Text doppelt ein). Aufruf der Knopf-Funktionen von Hand: ein gesperrter Knopf löst im Browser nichts aus, aber die Funktion dahinter
|
||||
// darf auch auf anderem Weg (alter Stand, Hilfstechnik) nichts senden.
|
||||
test('Pult (bedient, T13 P1): Brief schließen, während eine Anfrage läuft → denselben Eintrag öffnen → Knöpfe gesperrt, kein zweiter Aufruf → Anfrage endet → frei bzw. Brief/Meldung wie vorgesehen', async () => {
|
||||
const plattform = { ...unsicher, $id: 'k1', kind: 'plattform_antwort', title: 'Suche Webdesigner', draft: 'Claras Entwurf', sourceUrl: 'https://www.kleinanzeigen.de/s-anzeige/x/1-2-3', prospect: { $id: 'p1', name: 'Verein' } }
|
||||
const zweite = { ...unsicher, $id: 'e2', title: 'Zweite Rückfrage' }
|
||||
const faelle = [
|
||||
{ name: 'start-build', item: unsicher, aktion: 'start-build', schliesst: true, ok: 'wird gebaut.',
|
||||
start: async (ui) => { await ui.click('Vorschau bauen'); await ui.click('Wirklich bauen?') },
|
||||
gesperrt: ['Als Mensch antworten', 'Vorschau bauen', 'Erledigt', 'Verwerfen', 'Ich kümmere mich'] },
|
||||
{ name: 'reply', item: unsicher, aktion: 'reply', schliesst: true, ok: 'gesendet.',
|
||||
start: async (ui) => { await ui.click('Als Mensch antworten'); await ui.click('Jetzt senden') },
|
||||
gesperrt: ['Als Mensch antworten', 'Vorschau bauen', 'Erledigt', 'Verwerfen', 'Ich kümmere mich'] },
|
||||
{ name: 'paste', item: plattform, aktion: 'paste', schliesst: false, ok: 'Eingefügt. Nora sortiert die Antwort ein.',
|
||||
start: async (ui) => { await ui.click('Antwort des Kunden einfügen'); await ui.type('Antwort des Kunden', 'Ja, gern'); await ui.click('Einfügen') },
|
||||
gesperrt: ['Antwort des Kunden einfügen', 'Kopieren & öffnen', 'Als gesendet markieren', 'Verwerfen', 'Ich kümmere mich'] },
|
||||
]
|
||||
for (const fall of faelle) {
|
||||
for (const ende of ['fehler', 'erfolg']) {
|
||||
const wo = `${fall.name} / ${ende}`
|
||||
let finish
|
||||
let reject
|
||||
const api = scriptedApi(new Promise((resolve, no) => { finish = resolve; reject = no }))
|
||||
const ui = await startList([fall.item, zweite], api)
|
||||
const stale = (label) => clickable(ui.brief(), label)[0].props.onClick?.({ preventDefault() {} }) // wie ein Klick auf einen Knopf, den der Browser nicht auslöst
|
||||
try {
|
||||
// 1. die Anfrage läuft, der Brief wird geschlossen
|
||||
await ui.open(0)
|
||||
await fall.start(ui)
|
||||
assert.equal(api.calls.length, 1, wo); assert.equal(api.calls[0][1], fall.aktion, wo)
|
||||
await ui.close()
|
||||
assert.equal(ui.brief(), null, wo)
|
||||
// 2. derselbe Eintrag, neuer Brief: alles gesperrt, ein Hinweis nennt den Grund
|
||||
await ui.open(0)
|
||||
for (const label of fall.gesperrt) assert.deepEqual(ui.disabled(label), [true], `${wo}: ${label}`)
|
||||
assert.ok(ui.text().includes('Für diesen Eintrag läuft noch eine Anfrage.'), wo)
|
||||
// 3. auch die Funktionen hinter den Knöpfen senden nichts (kein zweites reply, paste oder start-build, keine Rückfrage, kein Feld, kein Öffnen der Anzeige)
|
||||
for (const label of fall.gesperrt) { stale(label); stale(label); await settle() }
|
||||
assert.equal(api.calls.length, 1, `${wo}: zweiter Aufruf`)
|
||||
assert.ok(!ui.has('Wirklich bauen?') && !ui.has('Wirklich verwerfen?') && !ui.has('Wirklich freigeben?'), `${wo}: Rückfrage`)
|
||||
assert.deepEqual([ui.value('Text der Antwort'), ui.value('Antwort des Kunden')], [[], []], `${wo}: Feld`)
|
||||
assert.equal(ui.opened.length, 0, `${wo}: Anzeige geöffnet`)
|
||||
// 4. andere Einträge sind nicht betroffen
|
||||
await ui.close(); await ui.open(1)
|
||||
assert.deepEqual(ui.disabled('Erledigt'), [false], wo); assert.ok(!ui.text().includes('Für diesen Eintrag läuft noch eine Anfrage.'), wo)
|
||||
await ui.close(); await ui.open(0)
|
||||
assert.deepEqual(ui.disabled(fall.gesperrt[0]), [true], wo)
|
||||
|
||||
// 5. die Anfrage endet
|
||||
if (ende === 'fehler') {
|
||||
reject(failure('Wird schon von einem Kollegen bearbeitet.', 409)); await settle()
|
||||
// der Brief bleibt offen, alles ist frei, der Hinweis ist weg; der späte Fehler steht bei der Liste und nennt den Eintrag
|
||||
assert.ok(ui.brief(), wo)
|
||||
for (const label of fall.gesperrt) assert.deepEqual(ui.disabled(label), [false], `${wo}: ${label}`)
|
||||
assert.ok(!ui.text().includes('Für diesen Eintrag läuft noch eine Anfrage.'), wo)
|
||||
assert.deepEqual(ui.messages(), [{ kind: 'err', text: `„${fall.item.title}“: Wird schon von einem Kollegen bearbeitet.`, role: 'alert' }], wo)
|
||||
assert.equal(api.calls.length, 1, wo)
|
||||
// wieder frei: die nächste Aktion geht hinaus
|
||||
await ui.click('Ich kümmere mich')
|
||||
assert.deepEqual(api.calls[1], [fall.item.$id, 'claim'], wo)
|
||||
} else {
|
||||
finish({ status: 'neu', item: { $id: fall.item.$id } }); await settle()
|
||||
assert.equal(api.calls.length, 1, wo)
|
||||
if (fall.schliesst) {
|
||||
// der Eintrag hat die Liste verlassen: der wieder geöffnete Brief schließt, die Meldung nennt den Titel
|
||||
assert.equal(ui.brief(), null, wo)
|
||||
assert.deepEqual(ui.messages(), [{ kind: 'ok', text: `„${fall.item.title}“ ${fall.ok}`, role: 'status' }], wo)
|
||||
} else {
|
||||
// der Eintrag bleibt: der Brief ist offen und frei, das späte Ergebnis steht mit dem Titel in der Liste
|
||||
assert.ok(ui.brief(), wo)
|
||||
for (const label of fall.gesperrt) assert.deepEqual(ui.disabled(label), [false], `${wo}: ${label}`)
|
||||
assert.deepEqual(ui.messages(), [{ kind: 'ok', text: `„${fall.item.title}“: ${fall.ok}`, role: 'status' }], wo)
|
||||
}
|
||||
}
|
||||
} finally { ui.end() }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('Pult (bedient, T13 P1): ohne Anfrage ist nichts gesperrt; die Sperre hält nur während der Anfrage und gilt je Eintrag (Brief zu, Brief auf, andere Einträge frei)', async () => {
|
||||
const zweite = { ...unsicher, $id: 'e2', title: 'Zweite Rückfrage' }
|
||||
let finish
|
||||
const api = scriptedApi(new Promise((resolve) => { finish = resolve }))
|
||||
const ui = await startList([unsicher, zweite], api)
|
||||
try {
|
||||
await ui.open(0)
|
||||
for (const label of ['Als Mensch antworten', 'Vorschau bauen', 'Erledigt', 'Verwerfen', 'Ich kümmere mich']) assert.deepEqual(ui.disabled(label), [false], label)
|
||||
assert.ok(!ui.text().includes('Für diesen Eintrag läuft noch eine Anfrage.'))
|
||||
await ui.click('Ich kümmere mich') // Anfrage zu e1 läuft
|
||||
assert.equal(api.calls.length, 1)
|
||||
// der erste Brief sperrt sich selbst (wie bisher), der Hinweis erscheint dort nicht: die Sperre ist die eigene
|
||||
assert.deepEqual(ui.disabled('Erledigt'), [true]); assert.ok(!ui.text().includes('Für diesen Eintrag läuft noch eine Anfrage.'))
|
||||
// ein zweiter Eintrag kann währenddessen bearbeitet werden (eigene Id)
|
||||
await ui.close(); await ui.open(1)
|
||||
await ui.click('Ich kümmere mich')
|
||||
assert.deepEqual(api.calls.map((c) => c[0]), ['e1', 'e2'])
|
||||
finish({ item: { $id: 'e1' } }); await settle()
|
||||
await ui.close(); await ui.open(0)
|
||||
assert.deepEqual(ui.disabled('Erledigt'), [false]) // Anfrage zu e1 ist zu Ende
|
||||
} finally { ui.end() }
|
||||
})
|
||||
|
||||
test('Pult (bedient, P4-T32): ein „unsicher“-Eintrag ohne Entwurf öffnet „Als Mensch antworten“ mit leerem Feld', async () => {
|
||||
const api = scriptedApi()
|
||||
const ui = await startList([{ ...unsicher, draft: undefined }], api)
|
||||
|
||||
Reference in new issue
Block a user