Workers P4 Endprüfung W4: Esc und „Abbrechen“ schließen die Felder und geben den Fokus an den Knopf zurück

„Als Mensch antworten“ und „Antwort des Kunden einfügen“ ließen sich nur durch erneuten Klick auf den
Auslöser schließen. Esc (aus dem ganzen Feld, mit preventDefault für die Handy-Leiste) und der neue Knopf
„Abbrechen“ schließen das Feld, der Text bleibt in der Liste erhalten, der Fokus geht an den Auslöser
zurück (data-opens am Knopf, gesucht in der Aktionsgruppe: Button reicht kein ref durch). Während einer
Anfrage sind beide gesperrt. Neue Gruppe .wk-pult-panel-btns, auf dem Handy mit dem Hauptknopf oben.
Test-Läufer: ref-Objekte an Tags und eine document-Attrappe für den Fokus.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-18 23:11:38 +00:00
1 parent c7061837fb
commit ff3d042c0e
4 files changed
+141 -11

No files matched your search

+107 -2
View File
@@ -92,6 +92,8 @@ export function __mount(element) {
return out === null || out === undefined || out === false ? null : expand(out, path + '/c', visited)
}
const children = flat(props.children).map((c, i) => expand(c, path + '/' + (c && c.key != null ? 'k:' + c.key : i), visited))
// ref-Objekt an einem Tag: wie React vor den Effekten gesetzt; das Element liefert der Test (globalThis.__fakeRef), sonst null
if (typeof type === 'string' && props.ref && typeof props.ref === 'object') props.ref.current = globalThis.__fakeRef ? globalThis.__fakeRef(type, props) : null
return { tag: type === Fragment ? 'fragment' : type, props, children }
}
@@ -485,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',
'actionErrorText', 'pasteDoneText', 'rel="noopener noreferrer"', 'target="_blank"',
'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',
'Antwort des Kunden einfügen', 'Einfügen', 'wk-pult-reply', 'wk-pult-paste', 'Abbrechen', 'data-opens', 'Escape',
'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.doesNotMatch(s, /dangerouslySetInnerHTML/); assert.doesNotMatch(s, /innerHTML/)
@@ -517,9 +519,11 @@ test('CSS (P4-T32): .wk-pult-reply und .wk-pult-paste mit Haarlinie, volle Breit
assert.match(css, /\.wk-pult-reply/); assert.match(css, /\.wk-pult-paste/)
const rule = css.match(/\.wk-pult-row \.wk-pult-reply[^{]*\{[^}]*\}/)?.[0] ?? ''
assert.match(rule, /\.wk-pult-paste/); assert.match(rule, /border-top: 1px solid var\(--line\)/); assert.match(rule, /width: 100%/)
// schmale Bildschirme: die Fußzeile des Feldes (Zähler, Knopf) steht untereinander
// schmale Bildschirme: die Fußzeile des Feldes (Zähler, Knöpfe) steht untereinander
const phone = css.slice(css.indexOf('@media (max-width: 860px)'))
assert.match(phone, /\.wk-pult-panel-foot/); assert.match(phone, /flex-direction: column/)
// Endprüfung W4: „Abbrechen“ und „Jetzt senden“ stehen nebeneinander (Gruppe .wk-pult-panel-btns), auf dem Handy untereinander über die volle Breite
assert.match(css, /\.wk-pult-panel-btns\s*\{[^}]*display: flex/); assert.match(phone, /\.wk-pult-panel-btns\s*\{[^}]*flex-direction: column-reverse/)
})
test('PultList (gerendert, P4-T32): Aktionen je Art – Antworten, Freigeben, Bauen, Einfügen; Felder bleiben zu, Fremdtext nur als Text', async () => {
@@ -664,6 +668,15 @@ async function startList(items, api) {
if (!found[0].props.disabled) found[0].props.onClick?.({ preventDefault() {} })
await settle()
},
// Taste im Feld mit dieser Klasse (wk-pult-reply, wk-pult-paste); Ergebnis: das Ereignis, damit der Test preventDefault sehen kann
async key(panelClass, key = 'Escape') {
const panel = nodes(view.tree(), (n) => n.tag === 'div' && hasClass(n, panelClass))[0]
assert.ok(panel, 'Feld ' + panelClass)
const event = { key, defaultPrevented: false, stopped: false, preventDefault() { this.defaultPrevented = true }, stopPropagation() { this.stopped = true } }
panel.props.onKeyDown?.(event)
await settle()
return event
},
async tab(label) {
const found = clickable(inFilters(view.tree()), label)
assert.equal(found.length, 1, `Reiter „${label}“`)
@@ -703,6 +716,30 @@ function scriptedApi(...outcomes) {
}
const failure = (message, status) => Object.assign(new Error(message), { status })
// document-Attrappe für den Fokus (nur für die Dauer eines Tests): activeElement stellt der Test, focus() eines Elements setzt es. Elemente, die der Bauteil über
// ref anfasst (Zeile, Aktionsgruppe, Meldung), liefert __fakeRef je Klasse; querySelector antwortet aus f.found (Selektor -> Element), sonst null.
function fakeFocus() {
const body = { name: 'body' }
const doc = { body, documentElement: { name: 'html' }, activeElement: body }
const f = { doc, body, found: new Map(), made: new Map() }
f.el = (name, extra = {}) => ({ name, isConnected: true, disabled: false, focusCalls: 0, focus() { this.focusCalls += 1; doc.activeElement = this }, querySelector: (selector) => f.found.get(selector) ?? null, ...extra })
f.ref = (key) => f.made.get(key) // das Element hinter ref an Tag/Klasse key
const had = Object.hasOwn(globalThis, 'document')
const saved = globalThis.document
globalThis.document = doc
globalThis.__fakeRef = (tag, props) => {
const key = String(props.className ?? tag).split(' ')[0]
if (!f.made.has(key)) f.made.set(key, f.el(key))
return f.made.get(key)
}
f.restore = () => {
if (had) globalThis.document = saved
else delete globalThis.document
delete globalThis.__fakeRef
}
return f
}
const unsicher = { $id: 'e1', kind: 'unsicher', status: 'offen', title: 'Unklare Antwort', draft: 'Entwurf von Clara', createdAt: new Date().toISOString(), prospectId: 'p1', prospect: { $id: 'p1', name: 'Verein', email: 'x@verein.example' } }
test('PultList (bedient, P4-T32): „Als Mensch antworten“ klappt auf, ist mit dem Entwurf vorbelegt, prüft den Text und zeigt die Sätze des Dienstes', async () => {
@@ -773,6 +810,74 @@ test('PultList (bedient, P4-T32): ein „unsicher“-Eintrag ohne Entwurf öffne
} finally { ui.end() }
})
// Endprüfung P4 (W4): die aufgeklappten Felder schließen mit Esc und mit „Abbrechen“; der Fokus geht an den Knopf zurück, der sie öffnete, der Text bleibt.
// Während einer Anfrage bleiben beide gesperrt (das Feld soll nicht mitten im Senden verschwinden).
test('PultList (bedient, W4): Esc und „Abbrechen“ schließen „Als Mensch antworten“, geben den Fokus an den Knopf zurück und behalten den Text', async () => {
const f = fakeFocus()
let finish
const api = scriptedApi(new Promise((resolve) => { finish = resolve }))
const ui = await startList([unsicher], api)
try {
const opener = f.el('opener')
f.found.set('[data-opens="reply"]', opener)
assert.equal(clickable(inList(ui.tree()), 'Als Mensch antworten')[0].props['data-opens'], 'reply') // der Auslöser trägt seine Kennung
assert.ok(!ui.has('Abbrechen'))
await ui.click('Als Mensch antworten')
assert.ok(ui.has('Abbrechen'))
await ui.type('Text der Antwort', 'Mein Text')
// eine andere Taste ändert nichts
let event = await ui.key('wk-pult-reply', 'a')
assert.deepEqual(ui.value('Text der Antwort'), ['Mein Text']); assert.equal(event.defaultPrevented, false)
// Esc: Feld zu, Fokus auf dem Auslöser, das Ereignis ist behandelt (die Handy-Leiste schließt sonst mit)
event = await ui.key('wk-pult-reply')
assert.deepEqual(ui.value('Text der Antwort'), []); assert.equal(opener.focusCalls, 1); assert.equal(f.doc.activeElement, opener)
assert.ok(event.defaultPrevented); assert.ok(event.stopped)
assert.equal(nodes(ui.tree(), (n) => n.tag === 'button' && textOf(n) === 'Als Mensch antworten')[0].props['aria-expanded'], false)
// der Text bleibt erhalten
await ui.click('Als Mensch antworten')
assert.deepEqual(ui.value('Text der Antwort'), ['Mein Text'])
// Abbrechen: wie Esc
await ui.click('Abbrechen')
assert.deepEqual(ui.value('Text der Antwort'), []); assert.equal(opener.focusCalls, 2); assert.equal(api.calls.length, 0)
// ein zweiter Klick auf den Auslöser schließt wie bisher, ohne den Fokus zu bewegen (er liegt schon dort)
await ui.click('Als Mensch antworten'); await ui.click('Als Mensch antworten')
assert.deepEqual(ui.value('Text der Antwort'), []); assert.equal(opener.focusCalls, 2)
// während der Anfrage sind Esc und Abbrechen gesperrt
await ui.click('Als Mensch antworten')
await ui.click('Jetzt senden')
assert.equal(api.calls.length, 1); assert.deepEqual(ui.disabled('Abbrechen'), [true])
event = await ui.key('wk-pult-reply')
assert.deepEqual(ui.value('Text der Antwort'), ['Mein Text']); assert.equal(event.defaultPrevented, false); assert.equal(opener.focusCalls, 2)
await ui.click('Abbrechen')
assert.deepEqual(ui.value('Text der Antwort'), ['Mein Text'])
finish({ item: { $id: 'e1' } }); await settle()
} finally { ui.end(); f.restore() }
})
test('PultList (bedient, W4): Esc und „Abbrechen“ schließen auch „Antwort des Kunden einfügen“ und kehren an dessen Knopf zurück', 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' } }
const api = scriptedApi()
const ui = await startList([plattform], api)
try {
const opener = f.el('opener')
f.found.set('[data-opens="paste"]', opener)
assert.equal(clickable(inList(ui.tree()), 'Antwort des Kunden einfügen')[0].props['data-opens'], 'paste')
await ui.click('Antwort des Kunden einfügen')
await ui.type('Antwort des Kunden', 'Noch nicht gesendet')
await ui.key('wk-pult-paste')
assert.deepEqual(ui.value('Antwort des Kunden'), []); assert.equal(opener.focusCalls, 1)
await ui.click('Antwort des Kunden einfügen')
assert.deepEqual(ui.value('Antwort des Kunden'), ['Noch nicht gesendet']) // der Text bleibt
await ui.click('Abbrechen')
assert.deepEqual(ui.value('Antwort des Kunden'), []); assert.equal(opener.focusCalls, 2); assert.equal(api.calls.length, 0)
// „Abbrechen“ liegt vor dem Absenden (Einfügen bleibt der Hauptknopf)
await ui.click('Antwort des Kunden einfügen')
const names = nodes(ui.tree(), (n) => n.tag === 'div' && hasClass(n, 'wk-pult-panel-btns'))[0].children.map(textOf)
assert.deepEqual(names, ['Abbrechen', 'Einfügen'])
} finally { ui.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
// 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 () => {