Workers P4 Endprüfung W3: Fokus bleibt nach Pult-Aktionen erhalten statt auf <body> zu fallen
Jede Aktion sperrt die Knöpfe der Zeile; Chromium nimmt einem gesperrten Knopf den Fokus und gibt ihn nicht zurück, Tastatur- und Screenreader-Nutzer standen danach am Seitenanfang. begin() merkt sich das fokussierte Bedienelement und das offene Feld; ist die Anfrage vorbei, geht der Fokus zurück an den Knopf, sonst ans Textfeld, an den Knopf, der das Feld öffnete, sonst an die Zeile (tabIndex -1). Verlässt die Zeile die Liste, nimmt die Meldung oben (tabIndex -1) den Fokus, auch bei gleichem Text zweimal nacheinander. Es wird nur eingegriffen, wenn der Fokus wirklich verloren ging und vor der Aktion ein Bedienelement ihn hatte (Safari fokussiert Knöpfe nicht). Neues Modul workersFocus.js (rein, document wird übergeben). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ff3d042c0e
commit
7c974ab6db
4 files changed
+262
-5
No files matched your search
@@ -5,6 +5,7 @@ import EmptyState from '../ui/EmptyState'
|
|||||||
import { copyTextChecked } from '../CopyableCredential'
|
import { copyTextChecked } from '../CopyableCredential'
|
||||||
import { useEmployees } from '../../hooks/useEmployees'
|
import { useEmployees } from '../../hooks/useEmployees'
|
||||||
import { usePultFeed } from '../../hooks/usePultFeed'
|
import { usePultFeed } from '../../hooks/usePultFeed'
|
||||||
|
import { activeControl, restoreFocus } from '../../lib/workersFocus'
|
||||||
import {
|
import {
|
||||||
ACTION_FAILED, PULT_FILTERS, REPLY_TEXT_MAX, actionErrorText, charCount, copyAndOpen, mailtoHref, pasteDoneText, rowModel, validateReplyText,
|
ACTION_FAILED, PULT_FILTERS, REPLY_TEXT_MAX, actionErrorText, charCount, copyAndOpen, mailtoHref, pasteDoneText, rowModel, validateReplyText,
|
||||||
} from '../../lib/workersPult'
|
} from '../../lib/workersPult'
|
||||||
@@ -15,6 +16,11 @@ const CONFIRM_MS = 4000 // so lange gilt die Rückfrage „Wirklich verwerfen?
|
|||||||
const BUILD_HINT = 'Startet einen Opus-Lauf und baut die Vorschau, als hätte der Interessent Ja gesagt.'
|
const BUILD_HINT = 'Startet einen Opus-Lauf und baut die Vorschau, als hätte der Interessent Ja gesagt.'
|
||||||
const NO_FIELDS = Object.freeze({}) // Eintrag ohne bearbeitete Felder
|
const NO_FIELDS = Object.freeze({}) // Eintrag ohne bearbeitete Felder
|
||||||
|
|
||||||
|
// Das document des Browsers; unter Node (Tests, Server) gibt es keins, dann bleibt der Fokus unangetastet
|
||||||
|
const browserDocument = () => (typeof document === 'undefined' ? null : document)
|
||||||
|
// Selektor des Textfelds eines aufgeklappten Feldes (Antwort oder Einfügen)
|
||||||
|
const PANEL_FIELD = '.wk-pult-reply textarea, .wk-pult-paste textarea'
|
||||||
|
|
||||||
// Leerzustand je Filter
|
// Leerzustand je Filter
|
||||||
const EMPTY = {
|
const EMPTY = {
|
||||||
offen: { title: 'Nichts wartet auf dich', hint: 'Neue Antworten erscheinen hier, sobald ein Entwurf bereit ist.' },
|
offen: { title: 'Nichts wartet auf dich', hint: 'Neue Antworten erscheinen hier, sobald ein Entwurf bereit ist.' },
|
||||||
@@ -85,6 +91,9 @@ function PultRow({ api, item, row, text, fields, onText, onField, refresh, onFin
|
|||||||
const busyRef = useRef(false) // sperrt schon im selben Takt, in dem busy noch nicht greift (Doppelklick)
|
const busyRef = useRef(false) // sperrt schon im selben Takt, in dem busy noch nicht greift (Doppelklick)
|
||||||
const aliveRef = useRef(true)
|
const aliveRef = useRef(true)
|
||||||
const actionsRef = useRef(null) // die Aktionsgruppe: darin sucht close() den Knopf, der ein Feld geöffnet hat
|
const actionsRef = useRef(null) // die Aktionsgruppe: darin sucht close() den Knopf, der ein Feld geöffnet hat
|
||||||
|
const rowRef = useRef(null) // die Zeile selbst (tabIndex -1): letzter Ort für den Fokus, wenn der Knopf nach der Aktion weg ist
|
||||||
|
const returnTo = useRef(null) // { control, panel }: wer beim Start der Aktion den Fokus hatte und welches Feld damals offen war
|
||||||
|
const wasBusy = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
aliveRef.current = true
|
aliveRef.current = true
|
||||||
@@ -98,9 +107,33 @@ function PultRow({ api, item, row, text, fields, onText, onField, refresh, onFin
|
|||||||
return () => window.clearTimeout(timer)
|
return () => window.clearTimeout(timer)
|
||||||
}, [confirming])
|
}, [confirming])
|
||||||
|
|
||||||
|
// Fokus nach einer Aktion (Endprüfung W3): die gesperrten Knöpfe verlieren den Fokus an <body>. Ist die Anfrage vorbei (busy wieder false, die Knöpfe sind
|
||||||
|
// frei), geht er zurück: an den Knopf, der ihn hatte; ist der weg, an das offene Feld, an den Knopf, der das Feld öffnete, sonst an die Zeile.
|
||||||
|
// Hat die Person den Fokus inzwischen selbst woanders, bleibt er dort. Eine Zeile, die die Liste verlassen hat, gibt ihn nicht mehr zurück: das tut die
|
||||||
|
// Meldung oben (PultList).
|
||||||
|
useEffect(() => {
|
||||||
|
if (busy) {
|
||||||
|
wasBusy.current = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!wasBusy.current) return
|
||||||
|
wasBusy.current = false
|
||||||
|
const memory = returnTo.current
|
||||||
|
returnTo.current = null
|
||||||
|
if (!memory?.control) return // beim Start hatte nichts den Fokus: es ging nichts verloren
|
||||||
|
const row = rowRef.current
|
||||||
|
restoreFocus(browserDocument(), [
|
||||||
|
memory.control,
|
||||||
|
row?.querySelector(PANEL_FIELD),
|
||||||
|
memory.panel ? actionsRef.current?.querySelector(`[data-opens="${memory.panel}"]`) : null,
|
||||||
|
row,
|
||||||
|
])
|
||||||
|
}, [busy])
|
||||||
|
|
||||||
const begin = () => {
|
const begin = () => {
|
||||||
if (busyRef.current) return false
|
if (busyRef.current) return false
|
||||||
busyRef.current = true
|
busyRef.current = true
|
||||||
|
returnTo.current = { control: activeControl(browserDocument()), panel: open } // ohne Fokus beim Start (Safari fokussiert Knöpfe nicht) gibt es nichts zurückzugeben
|
||||||
setBusy(true)
|
setBusy(true)
|
||||||
setConfirming(null)
|
setConfirming(null)
|
||||||
setNote(null)
|
setNote(null)
|
||||||
@@ -120,7 +153,7 @@ function PultRow({ api, item, row, text, fields, onText, onField, refresh, onFin
|
|||||||
try {
|
try {
|
||||||
const result = await call()
|
const result = await call()
|
||||||
const message = typeof done === 'function' ? done(result) : done
|
const message = typeof done === 'function' ? done(result) : done
|
||||||
if (finished) onFinished(row.id, `„${row.title}“ ${message}`)
|
if (finished) onFinished(row.id, `„${row.title}“ ${message}`, returnTo.current?.control != null)
|
||||||
else if (aliveRef.current) setNote({ kind: 'ok', text: message })
|
else if (aliveRef.current) setNote({ kind: 'ok', text: message })
|
||||||
ok = true
|
ok = true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -228,7 +261,7 @@ function PultRow({ api, item, row, text, fields, onText, onField, refresh, onFin
|
|||||||
: ''
|
: ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="wk-pult-row">
|
<li className="wk-pult-row" ref={rowRef} tabIndex={-1}>
|
||||||
<div className="wk-pult-head">
|
<div className="wk-pult-head">
|
||||||
<Badge tone="blue">{row.kindLabel}</Badge>
|
<Badge tone="blue">{row.kindLabel}</Badge>
|
||||||
<strong className="wk-pult-title">{row.title}</strong>
|
<strong className="wk-pult-title">{row.title}</strong>
|
||||||
@@ -378,20 +411,30 @@ export default function PultList({ api, me }) {
|
|||||||
const [drafts, setDrafts] = useState({}) // bearbeitete Texte je Eintrag; was fehlt, zeigt den Entwurf des Dienstes
|
const [drafts, setDrafts] = useState({}) // bearbeitete Texte je Eintrag; was fehlt, zeigt den Entwurf des Dienstes
|
||||||
const [fields, setFields] = useState({}) // Texte der Felder „Als Mensch antworten“ (reply) und „Antwort einfügen“ (paste) je Eintrag
|
const [fields, setFields] = useState({}) // Texte der Felder „Als Mensch antworten“ (reply) und „Antwort einfügen“ (paste) je Eintrag
|
||||||
const [finished, setFinished] = useState('') // Meldung über den zuletzt abgeschlossenen Eintrag (seine Zeile ist dann schon weg)
|
const [finished, setFinished] = useState('') // Meldung über den zuletzt abgeschlossenen Eintrag (seine Zeile ist dann schon weg)
|
||||||
|
const [focusAsk, setFocusAsk] = useState(0) // zählt die Abschlüsse, bei denen die Person den Fokus auf einem Knopf hatte: auch eine gleiche Meldung zweimal nacheinander holt ihn
|
||||||
|
const finishedRef = useRef(null)
|
||||||
const [detailId, setDetailId] = useState(null) // Interessent, dessen Panel offen ist (null = keins)
|
const [detailId, setDetailId] = useState(null) // Interessent, dessen Panel offen ist (null = keins)
|
||||||
const filter = PULT_FILTERS.find((f) => f.id === filterId) ?? PULT_FILTERS[0]
|
const filter = PULT_FILTERS.find((f) => f.id === filterId) ?? PULT_FILTERS[0]
|
||||||
const { items, loading, error, refresh } = usePultFeed({ api, statuses: filter.statuses })
|
const { items, loading, error, refresh } = usePultFeed({ api, statuses: filter.statuses })
|
||||||
const { employees } = useEmployees()
|
const { employees } = useEmployees()
|
||||||
const nowMs = Date.now()
|
const nowMs = Date.now()
|
||||||
|
|
||||||
|
// Verlässt eine Zeile die Liste, hängt ihr Knopf mit dem Fokus ab. Die Meldung oben nimmt ihn (tabIndex -1), wenn er sonst nirgends liegt:
|
||||||
|
// Tastatur- und Screenreader-Nutzer bleiben in der Nähe der Liste und hören die Bestätigung (Endprüfung W3).
|
||||||
|
useEffect(() => {
|
||||||
|
if (focusAsk === 0) return
|
||||||
|
restoreFocus(browserDocument(), [finishedRef.current])
|
||||||
|
}, [focusAsk])
|
||||||
|
|
||||||
const chooseFilter = (id) => {
|
const chooseFilter = (id) => {
|
||||||
setFilterId(id)
|
setFilterId(id)
|
||||||
setFinished('')
|
setFinished('')
|
||||||
}
|
}
|
||||||
const setText = useCallback((id, value) => setDrafts((d) => ({ ...d, [id]: value })), [])
|
const setText = useCallback((id, value) => setDrafts((d) => ({ ...d, [id]: value })), [])
|
||||||
const setField = useCallback((id, which, value) => setFields((f) => ({ ...f, [id]: { ...f[id], [which]: value } })), [])
|
const setField = useCallback((id, which, value) => setFields((f) => ({ ...f, [id]: { ...f[id], [which]: value } })), [])
|
||||||
const onFinished = useCallback((id, message) => {
|
const onFinished = useCallback((id, message, hadFocus) => {
|
||||||
setFinished(message)
|
setFinished(message)
|
||||||
|
if (hadFocus) setFocusAsk((n) => n + 1)
|
||||||
setDrafts((d) => {
|
setDrafts((d) => {
|
||||||
const { [id]: _gone, ...rest } = d
|
const { [id]: _gone, ...rest } = d
|
||||||
return rest
|
return rest
|
||||||
@@ -457,7 +500,7 @@ export default function PultList({ api, me }) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{finished && <div className="msg ok" role="status">{finished}</div>}
|
{finished && <div className="msg ok" role="status" tabIndex={-1} ref={finishedRef}>{finished}</div>}
|
||||||
{error && rows.length > 0 && <div className="msg warn" role="status">Verbindung gestört: {error} Es wird weiter versucht.</div>}
|
{error && rows.length > 0 && <div className="msg warn" role="status">Verbindung gestört: {error} Es wird weiter versucht.</div>}
|
||||||
{body}
|
{body}
|
||||||
{detailId && <ProspectPanel api={api} prospectId={detailId} onClose={() => setDetailId(null)} />}
|
{detailId && <ProspectPanel api={api} prospectId={detailId} onClose={() => setDetailId(null)} />}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// Fokus nach Pult-Aktionen (Endprüfung P4, W3). Während einer Anfrage sind die Knöpfe der Zeile gesperrt; ein gesperrter Knopf verliert in
|
||||||
|
// Chromium den Fokus an <body> und bekommt ihn nach der Anfrage nicht zurück. Tastatur- und Screenreader-Nutzer stünden nach jeder Aktion
|
||||||
|
// wieder am Seitenanfang. Die Zeile merkt sich beim Start, wer den Fokus hatte, und gibt ihn danach zurück, aber nur, wenn er wirklich
|
||||||
|
// verloren ging (wer inzwischen selbst woandershin gegangen ist, wird nicht gestört).
|
||||||
|
// Rein: das document wird übergeben (unter Node eine Attrappe, im Browser das echte; ohne document passiert nichts).
|
||||||
|
|
||||||
|
// Liegt der Fokus nirgends? body, die Wurzel, nichts, ein gesperrtes oder ausgehängtes Element (der Browser lässt es erst beim nächsten Takt los)
|
||||||
|
export function focusLost(doc) {
|
||||||
|
if (!doc) return false
|
||||||
|
const active = doc.activeElement
|
||||||
|
return !active || active === doc.body || active === doc.documentElement || active.disabled === true || active.isConnected === false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Das Bedienelement mit dem Fokus, sonst null (body und Wurzel sind kein Bedienelement)
|
||||||
|
export function activeControl(doc) {
|
||||||
|
if (!doc) return null
|
||||||
|
const active = doc.activeElement
|
||||||
|
return !active || active === doc.body || active === doc.documentElement ? null : active
|
||||||
|
}
|
||||||
|
|
||||||
|
const usable = (el) => Boolean(el) && typeof el === 'object' && typeof el.focus === 'function' && el.isConnected !== false && el.disabled !== true
|
||||||
|
|
||||||
|
// Gibt den Fokus an den ersten brauchbaren Kandidaten (eingehängt, nicht gesperrt), falls er verloren ging. Ergebnis: ob fokussiert wurde.
|
||||||
|
export function restoreFocus(doc, candidates) {
|
||||||
|
if (!focusLost(doc)) return false
|
||||||
|
const target = candidates.find(usable)
|
||||||
|
if (!target) return false
|
||||||
|
target.focus()
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { loadModule } from './load.mjs'
|
||||||
|
|
||||||
|
// Fokus nach Pult-Aktionen (Endprüfung P4, W3): ein gesperrter Knopf verliert den Fokus an <body>; die Zeile gibt ihn danach zurück,
|
||||||
|
// aber nur, wenn er wirklich verloren ging. Reine Logik mit einer document-Attrappe, kein DOM nötig.
|
||||||
|
const { activeControl, focusLost, restoreFocus } = await loadModule('src/lib/workersFocus.js')
|
||||||
|
|
||||||
|
function fakeDoc() {
|
||||||
|
const body = { name: 'body' }
|
||||||
|
const doc = { body, documentElement: { name: 'html' }, activeElement: body }
|
||||||
|
const el = (name, extra = {}) => ({ name, isConnected: true, disabled: false, focusCalls: 0, focus() { this.focusCalls += 1; doc.activeElement = this }, ...extra })
|
||||||
|
return { doc, body, el }
|
||||||
|
}
|
||||||
|
|
||||||
|
test('focusLost: body, Wurzel, nichts, gesperrtes oder ausgehängtes Element gelten als verloren, ein bedienbares Element nicht', () => {
|
||||||
|
const { doc, body, el } = fakeDoc()
|
||||||
|
assert.equal(focusLost(doc), true) // activeElement = body
|
||||||
|
doc.activeElement = doc.documentElement; assert.equal(focusLost(doc), true)
|
||||||
|
for (const nothing of [null, undefined]) { doc.activeElement = nothing; assert.equal(focusLost(doc), true, String(nothing)) }
|
||||||
|
doc.activeElement = el('a', { disabled: true }); assert.equal(focusLost(doc), true) // der Browser lässt es erst beim nächsten Takt los
|
||||||
|
doc.activeElement = el('b', { isConnected: false }); assert.equal(focusLost(doc), true)
|
||||||
|
doc.activeElement = el('c'); assert.equal(focusLost(doc), false)
|
||||||
|
// ohne document (Node, Server) gibt es nichts zurückzugeben
|
||||||
|
for (const none of [null, undefined]) assert.equal(focusLost(none), false)
|
||||||
|
assert.equal(body.name, 'body')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('activeControl: das fokussierte Bedienelement, sonst null (body und Wurzel zählen nicht)', () => {
|
||||||
|
const { doc, el } = fakeDoc()
|
||||||
|
assert.equal(activeControl(doc), null)
|
||||||
|
doc.activeElement = doc.documentElement; assert.equal(activeControl(doc), null)
|
||||||
|
doc.activeElement = null; assert.equal(activeControl(doc), null)
|
||||||
|
const button = el('button'); doc.activeElement = button
|
||||||
|
assert.equal(activeControl(doc), button)
|
||||||
|
assert.equal(activeControl(null), null); assert.equal(activeControl(undefined), null)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('restoreFocus: nimmt den ersten Kandidaten, der noch da und frei ist; greift nur ein, wenn der Fokus verloren ging', () => {
|
||||||
|
const { doc, body, el } = fakeDoc()
|
||||||
|
const gone = el('gone', { isConnected: false })
|
||||||
|
const locked = el('locked', { disabled: true })
|
||||||
|
const first = el('first')
|
||||||
|
const second = el('second')
|
||||||
|
assert.equal(restoreFocus(doc, [null, undefined, gone, locked, first, second]), true)
|
||||||
|
assert.equal(doc.activeElement, first); assert.deepEqual([first.focusCalls, second.focusCalls, gone.focusCalls, locked.focusCalls], [1, 0, 0, 0])
|
||||||
|
|
||||||
|
// der Fokus liegt woanders (die Person hat inzwischen selbst gewählt): nichts wird angefasst
|
||||||
|
const other = el('other'); doc.activeElement = other
|
||||||
|
assert.equal(restoreFocus(doc, [first]), false)
|
||||||
|
assert.equal(doc.activeElement, other); assert.equal(first.focusCalls, 1)
|
||||||
|
|
||||||
|
// kein brauchbarer Kandidat, kein document, keine Liste: false, nichts passiert
|
||||||
|
doc.activeElement = body
|
||||||
|
assert.equal(restoreFocus(doc, [null, gone, locked]), false); assert.equal(doc.activeElement, body)
|
||||||
|
assert.equal(restoreFocus(null, [first]), false); assert.equal(restoreFocus(doc, []), false)
|
||||||
|
// Dinge ohne focus() (Attrappen, Textknoten) werden übersprungen
|
||||||
|
assert.equal(restoreFocus(doc, [{ isConnected: true }, 'text', 7, second]), true); assert.equal(doc.activeElement, second)
|
||||||
|
})
|
||||||
@@ -487,7 +487,7 @@ test('PultList (P4-T32): Quelltext – Aktionen über pultAction, Prüfung mit v
|
|||||||
"'reply'", "'approve-preview'", "'paste'", "'start-build'", 'validateReplyText', 'canReply', 'canApprovePreview', 'canStartBuild', 'canPaste', 'previewHref',
|
"'reply'", "'approve-preview'", "'paste'", "'start-build'", 'validateReplyText', 'canReply', 'canApprovePreview', 'canStartBuild', 'canPaste', 'previewHref',
|
||||||
'actionErrorText', 'pasteDoneText', 'rel="noopener noreferrer"', 'target="_blank"',
|
'actionErrorText', 'pasteDoneText', 'rel="noopener noreferrer"', 'target="_blank"',
|
||||||
'Als Mensch antworten', 'Jetzt senden', 'Vorschau ansehen', 'Freigeben & senden', 'Wirklich freigeben?', 'Vorschau bauen', 'Wirklich bauen?',
|
'Als Mensch antworten', 'Jetzt senden', 'Vorschau ansehen', 'Freigeben & senden', 'Wirklich freigeben?', 'Vorschau bauen', 'Wirklich bauen?',
|
||||||
'Antwort des Kunden einfügen', 'Einfügen', 'wk-pult-reply', 'wk-pult-paste', 'Abbrechen', 'data-opens', 'Escape',
|
'Antwort des Kunden einfügen', 'Einfügen', 'wk-pult-reply', 'wk-pult-paste', 'Abbrechen', 'data-opens', 'Escape', 'restoreFocus', 'activeControl', 'tabIndex={-1}',
|
||||||
'Kopiere die Antwort aus der Plattform hierher. Nora sortiert sie ein; bei einer Frage schreibt Clara den nächsten Entwurf.',
|
'Kopiere die Antwort aus der Plattform hierher. Nora sortiert sie ein; bei einer Frage schreibt Clara den nächsten Entwurf.',
|
||||||
]) assert.ok(s.includes(t), t)
|
]) assert.ok(s.includes(t), t)
|
||||||
assert.doesNotMatch(s, /dangerouslySetInnerHTML/); assert.doesNotMatch(s, /innerHTML/)
|
assert.doesNotMatch(s, /dangerouslySetInnerHTML/); assert.doesNotMatch(s, /innerHTML/)
|
||||||
@@ -878,6 +878,131 @@ test('PultList (bedient, W4): Esc und „Abbrechen“ schließen auch „Antwort
|
|||||||
} finally { ui.end(); f.restore() }
|
} finally { ui.end(); f.restore() }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Endprüfung P4 (W3): ein gesperrter Knopf verliert den Fokus an <body>. Nach der Anfrage gibt die Zeile ihn zurück (an den Knopf, sonst ans Feld, an den
|
||||||
|
// Knopf, der das Feld öffnete, oder an die Zeile); verlässt eine Zeile die Liste, geht er an die Meldung oben. Wer den Fokus inzwischen selbst
|
||||||
|
// woanders hat, wird nicht gestört. Der Test spielt den Browser nach: activeElement stellt der Test (Klick: Knopf; Sperre: body).
|
||||||
|
const PANEL_FIELD = '.wk-pult-reply textarea, .wk-pult-paste textarea'
|
||||||
|
|
||||||
|
test('PultList (bedient, W3): nach einem Fehler beim Senden steht der Fokus wieder auf „Jetzt senden“; hat die Person inzwischen selbst gewählt, bleibt er dort', async () => {
|
||||||
|
for (const moved of [false, true]) {
|
||||||
|
const f = fakeFocus()
|
||||||
|
let reject
|
||||||
|
const api = scriptedApi(new Promise((_, no) => { reject = no }))
|
||||||
|
const ui = await startList([unsicher], api)
|
||||||
|
try {
|
||||||
|
await ui.click('Als Mensch antworten')
|
||||||
|
const submit = f.el('submit')
|
||||||
|
f.doc.activeElement = submit // der Klick hat den Knopf fokussiert
|
||||||
|
await ui.click('Jetzt senden')
|
||||||
|
const other = f.el('other')
|
||||||
|
f.doc.activeElement = moved ? other : f.body // der gesperrte Knopf verliert den Fokus (Chromium) – oder die Person ist schon woandershin gegangen
|
||||||
|
reject(failure('Wird schon von einem Kollegen bearbeitet.', 409)); await settle()
|
||||||
|
assert.deepEqual(ui.messages(), [{ kind: 'err', text: 'Wird schon von einem Kollegen bearbeitet.', role: 'alert' }]) // die Zeile bleibt
|
||||||
|
if (moved) assert.deepEqual([f.doc.activeElement, submit.focusCalls], [other, 0])
|
||||||
|
else assert.deepEqual([f.doc.activeElement, submit.focusCalls], [submit, 1])
|
||||||
|
} finally { ui.end(); f.restore() }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('PultList (bedient, W3): ist der Knopf nach der Aktion weg („Ich kümmere mich“), geht der Fokus an die Zeile; sie ist mit tabIndex -1 fokussierbar', async () => {
|
||||||
|
const f = fakeFocus()
|
||||||
|
let finish
|
||||||
|
const api = scriptedApi(new Promise((resolve) => { finish = resolve }))
|
||||||
|
const ui = await startList([unsicher], api)
|
||||||
|
try {
|
||||||
|
const li = nodes(ui.tree(), (n) => n.tag === 'li')[0]
|
||||||
|
assert.equal(li.props.tabIndex, -1)
|
||||||
|
const claim = f.el('claim')
|
||||||
|
f.doc.activeElement = claim
|
||||||
|
await ui.click('Ich kümmere mich')
|
||||||
|
f.doc.activeElement = f.body
|
||||||
|
claim.isConnected = false // nach „übernommen“ gibt es den Knopf nicht mehr
|
||||||
|
finish({ item: { $id: 'e1' } }); await settle()
|
||||||
|
const row = f.ref('wk-pult-row')
|
||||||
|
assert.deepEqual([row.focusCalls, f.doc.activeElement], [1, row])
|
||||||
|
assert.equal(claim.focusCalls, 0)
|
||||||
|
} finally { ui.end(); f.restore() }
|
||||||
|
})
|
||||||
|
|
||||||
|
test('PultList (bedient, W3): nach dem Einfügen (Feld klappt zu) geht der Fokus an „Antwort des Kunden einfügen“; nach einem Fehler bleibt er im Feld', async () => {
|
||||||
|
const f = fakeFocus()
|
||||||
|
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' } }
|
||||||
|
let reject
|
||||||
|
let finish
|
||||||
|
const api = scriptedApi(new Promise((_, no) => { reject = no }), new Promise((resolve) => { finish = resolve }))
|
||||||
|
const ui = await startList([plattform], api)
|
||||||
|
try {
|
||||||
|
const opener = f.el('opener'); f.found.set('[data-opens="paste"]', opener)
|
||||||
|
const field = f.el('textarea'); f.found.set(PANEL_FIELD, field)
|
||||||
|
await ui.click('Antwort des Kunden einfügen')
|
||||||
|
await ui.type('Antwort des Kunden', 'Danke, passt.')
|
||||||
|
// Fehler: das Feld bleibt offen, der Knopf „Einfügen“ ist noch da und bekommt den Fokus zurück
|
||||||
|
const insert = f.el('insert'); f.doc.activeElement = insert
|
||||||
|
await ui.click('Einfügen')
|
||||||
|
f.doc.activeElement = f.body
|
||||||
|
reject(failure('Dieser Kontakt ist gesperrt.', 409)); await settle()
|
||||||
|
assert.deepEqual([f.doc.activeElement, insert.focusCalls], [insert, 1])
|
||||||
|
// Erfolg: das Feld klappt zu (Textfeld und Knopf „Einfügen“ sind weg), der Fokus geht an „Antwort des Kunden einfügen“ (vor der Zeile)
|
||||||
|
f.found.delete(PANEL_FIELD)
|
||||||
|
f.doc.activeElement = insert
|
||||||
|
await ui.click('Einfügen')
|
||||||
|
f.doc.activeElement = f.body
|
||||||
|
insert.isConnected = false
|
||||||
|
finish({ status: 'neu' }); await settle()
|
||||||
|
assert.deepEqual([opener.focusCalls, f.doc.activeElement], [1, opener])
|
||||||
|
assert.equal(field.focusCalls, 0); assert.equal(f.ref('wk-pult-row').focusCalls, 0)
|
||||||
|
// und bleibt das Feld offen, aber der Knopf ist weg, nimmt das Textfeld den Fokus (vor dem Auslöser)
|
||||||
|
} finally { ui.end(); f.restore() }
|
||||||
|
})
|
||||||
|
|
||||||
|
test('PultList (bedient, W3): verlässt die Zeile die Liste, geht der Fokus an die Meldung oben (auch bei gleichem Text); sie ist mit tabIndex -1 fokussierbar', async () => {
|
||||||
|
const f = fakeFocus()
|
||||||
|
const zweite = { ...unsicher, $id: 'e2' } // gleicher Titel: dieselbe Meldung zweimal
|
||||||
|
let finish
|
||||||
|
const api = scriptedApi(undefined, undefined, new Promise((resolve) => { finish = resolve }))
|
||||||
|
const ui = await startList([unsicher, zweite], api)
|
||||||
|
try {
|
||||||
|
// der Knopf, den die Anfrage gerade sperrt (disabled): der Browser lässt ihn erst beim nächsten Takt los, dann liegt der Fokus auf <body>
|
||||||
|
const done0 = f.el('done0', { disabled: true }); f.doc.activeElement = done0
|
||||||
|
await ui.click('Erledigt', 0)
|
||||||
|
const status = f.ref('msg')
|
||||||
|
assert.deepEqual(ui.messages(), [{ kind: 'ok', text: '„Unklare Antwort“ erledigt.', role: 'status' }])
|
||||||
|
assert.equal(nodes(ui.tree(), (n) => n.tag === 'div' && hasClass(n, 'msg'))[0].props.tabIndex, -1)
|
||||||
|
assert.deepEqual([status.focusCalls, f.doc.activeElement], [1, status])
|
||||||
|
assert.equal(done0.focusCalls, 0)
|
||||||
|
// zweite Zeile, gleiche Meldung: wieder auf die Meldung
|
||||||
|
f.doc.activeElement = f.el('done1', { disabled: true })
|
||||||
|
await ui.click('Erledigt', 1)
|
||||||
|
assert.equal(status.focusCalls, 2); assert.equal(f.doc.activeElement, status)
|
||||||
|
// hat die Person den Fokus während der Anfrage selbst woanders hingesetzt, bleibt er dort
|
||||||
|
f.doc.activeElement = f.el('done2', { disabled: true })
|
||||||
|
await ui.click('Erledigt', 0)
|
||||||
|
const other = f.el('other'); f.doc.activeElement = other
|
||||||
|
finish({}); await settle()
|
||||||
|
assert.deepEqual([status.focusCalls, f.doc.activeElement], [2, other])
|
||||||
|
} finally { ui.end(); f.restore() }
|
||||||
|
})
|
||||||
|
|
||||||
|
test('PultList (bedient, W3): ohne document (Server, Node) und ohne Fokus vor dem Klick passiert nichts', async () => {
|
||||||
|
const api = scriptedApi()
|
||||||
|
const ui = await startList([unsicher], api) // kein document, kein fakeRef: refs bleiben null
|
||||||
|
try {
|
||||||
|
await ui.click('Ich kümmere mich')
|
||||||
|
await ui.click('Erledigt')
|
||||||
|
assert.equal(api.calls.length, 2)
|
||||||
|
} finally { ui.end() }
|
||||||
|
// das document ist da, aber der Klick hatte keinen Fokus (Safari fokussiert Knöpfe nicht): nichts zurückzugeben, nichts wird bewegt – auch nicht zur Meldung oben
|
||||||
|
const f = fakeFocus()
|
||||||
|
const ui2 = await startList([unsicher], scriptedApi())
|
||||||
|
try {
|
||||||
|
await ui2.click('Ich kümmere mich')
|
||||||
|
assert.deepEqual([f.ref('wk-pult-row').focusCalls, f.doc.activeElement], [0, f.body])
|
||||||
|
await ui2.click('Erledigt')
|
||||||
|
assert.ok(ui2.text().includes('erledigt.'))
|
||||||
|
assert.deepEqual([f.ref('msg').focusCalls, f.doc.activeElement], [0, f.body])
|
||||||
|
} finally { ui2.end(); f.restore() }
|
||||||
|
})
|
||||||
|
|
||||||
// Endprüfung P4 (W2): „Vorschau bauen“ startet einen Opus-Lauf und lässt sich in der Oberfläche nicht zurücknehmen: wie Freigeben und Verwerfen
|
// Endprüfung P4 (W2): „Vorschau bauen“ startet einen Opus-Lauf und lässt sich in der Oberfläche nicht zurücknehmen: wie Freigeben und Verwerfen
|
||||||
// fragt der Knopf zuerst nach (4 s). Ein Fehlklick (Handy: der Knopf liegt in voller Breite zwischen anderen) startet keinen Bau mehr.
|
// fragt der Knopf zuerst nach (4 s). Ein Fehlklick (Handy: der Knopf liegt in voller Breite zwischen anderen) startet keinen Bau mehr.
|
||||||
test('PultList (bedient, W2): „Vorschau bauen“ – erster Klick fragt nach (4 s, Hinweis), zweiter startet den Bau; Fehler und Erfolg wie bisher', async () => {
|
test('PultList (bedient, W2): „Vorschau bauen“ – erster Klick fragt nach (4 s, Hinweis), zweiter startet den Bau; Fehler und Erfolg wie bisher', async () => {
|
||||||
|
|||||||
Reference in new issue
Block a user