From 453d3585badd508c982c04fcf6a40f1a5ccb1851 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 16:15:43 +0000 Subject: [PATCH] =?UTF-8?q?workers:=20Bot-Leads=20Fokus=20beim=20Bl=C3=A4t?= =?UTF-8?q?tern=20ohne=20Scrollen=20und=20ohne=20Fokusdiebstahl=20(N5),=20?= =?UTF-8?q?=E2=80=9EErneut=20versuchen=E2=80=9C=20nach=20einem=20Fehler=20?= =?UTF-8?q?(N6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit N5: Der Fokus geht nach dem Blättern über restoreFocus (workersFocus.js) mit preventScroll an die Seitenzeile zurück, aber nur, wenn er wirklich verloren ging (body, nirgends, alter Knopf ausgehängt). Hat der Nutzer inzwischen Suchfeld oder Raumfilter angesteuert, bleibt er dort. N6: Scheitert eine Anfrage, steht unter der Meldung „Erneut versuchen“; der Knopf stellt genau diese Anfrage (Raum, Suche, Seite oder Eintrag) noch einmal und die Seitenzeile bekommt danach den Fokus zurück. CSS: .wk-bl-retry, am Handy 40 px. Co-Authored-By: Claude Sonnet 5.5 --- src/components/workers/BotLeadList.jsx | 32 +++-- src/styles/workers.css | 5 +- tests/unit/botleadList.test.mjs | 190 +++++++++++++++++++++++-- tests/unit/workersCss.test.mjs | 4 +- 4 files changed, 207 insertions(+), 24 deletions(-) diff --git a/src/components/workers/BotLeadList.jsx b/src/components/workers/BotLeadList.jsx index c8d3165..2effb3c 100644 --- a/src/components/workers/BotLeadList.jsx +++ b/src/components/workers/BotLeadList.jsx @@ -1,16 +1,19 @@ import { useEffect, useRef, useState } from 'react' import EmptyState from '../ui/EmptyState' import { ROOM_FILTERS, botleadRow, pageInfo } from '../../lib/botleadsView' +import { restoreFocus } from '../../lib/workersFocus' const SEARCH_DELAY_MS = 300 // so lange wartet die Suche nach dem letzten Tastendruck, bevor sie den Dienst fragt const SEARCH_MAX = 100 // der Dienst kürzt die Suche auf 100 Zeichen const UNKNOWN_ERROR = 'Unbekannter Fehler' const COLUMNS = ['Betrieb', 'Raum', 'Punkte', 'Grund', 'Telefon', 'Quelle', 'Gefunden'] const START_VIEW = Object.freeze({ room: '', q: '', page: 1 }) // Raumfilter, geltende Suche, Seite: ein Wert, damit eine Änderung genau eine Anfrage auslöst +const browserDocument = () => (typeof document === 'undefined' ? null : document) // unter Node (Tests, Server) gibt es keins, dann bleibt der Fokus unangetastet // Reiter „Bot-Leads“: Betriebe, die nicht angeschrieben werden (z. B. keine veröffentlichte Mail), mit Grund. Reine Liste, nichts wird gesendet oder angerufen. // Die Liste holt sich ihre Daten bei jeder Änderung von Raum, Suche, Seite oder bot selbst; es gibt kein Polling. Nur die jüngste Anfrage zählt: eine späte -// Antwort (auch ein später Fehler) einer älteren Anfrage oder nach dem Abbau wird verworfen. +// Antwort (auch ein später Fehler) einer älteren Anfrage oder nach dem Abbau wird verworfen. Scheitert eine Anfrage, steht der Satz des Dienstes mit „Erneut versuchen“, +// das genau diese Anfrage (Raum, Suche, Seite oder Eintrag) noch einmal stellt; Raum und Suche gehen weiterhin. // bot (aus ?bot= der Adresse, Link aus dem Ereignisstrom): zeigt genau diesen Eintrag, hervorgehoben, mit „Alle anzeigen“ (onClearBot nimmt bot aus der Adresse). // Alle Texte der Zeilen sind Textknoten; Links entstehen nur aus row.sourceHref (botleadRow prüft die Adresse mit safeHref: nur http und https, keine Zugangsdaten). export default function BotLeadList({ api, bot, onClearBot }) { @@ -19,6 +22,7 @@ export default function BotLeadList({ api, bot, onClearBot }) { const [view, setView] = useState(START_VIEW) const [draft, setDraft] = useState('') // Inhalt des Suchfelds; in view.q steht erst die Suche, die nach der Verzögerung gilt const [result, setResult] = useState({ key: null, items: [], total: 0, pageSize: undefined, error: null }) + const [attempt, setAttempt] = useState(0) // zählt „Erneut versuchen“: gehört zum Schlüssel, damit dieselbe Anfrage noch einmal läuft und „Wird geladen“ zeigt const timerRef = useRef(null) const seqRef = useRef(0) // Tastaturfokus: Wer „Alle anzeigen“ oder „Zurück“/„Weiter“ drückt, verliert den Knopf mit dem Fokus (Einzelansicht und Seitenzeile werden ausgetauscht oder @@ -27,9 +31,9 @@ export default function BotLeadList({ api, bot, onClearBot }) { const clearedRef = useRef(false) // „Alle anzeigen“ gedrückt: erst dann (nie schon beim Einhängen, das öffnete am Handy die Auswahl) bekommt der Raumfilter den Fokus const prevRef = useRef(null) const nextRef = useRef(null) - const pagedRef = useRef('') // 'prev' oder 'next': welcher Blätterknopf zuletzt gedrückt wurde; nach dem Laden bekommt die Seitenzeile den Fokus zurück + const pagedRef = useRef('') // 'prev' oder 'next': welcher Blätterknopf zuletzt gedrückt wurde; nach dem Laden bekommt die Seitenzeile den Fokus zurück (auch nach „Erneut versuchen“) - const key = JSON.stringify(single ? [botId] : [view.room, view.q, view.page]) + const key = JSON.stringify(single ? [botId, attempt] : [view.room, view.q, view.page, attempt]) useEffect(() => { seqRef.current += 1 @@ -60,6 +64,7 @@ export default function BotLeadList({ api, bot, onClearBot }) { useEffect(() => () => window.clearTimeout(timerRef.current), []) + const onRetry = () => setAttempt((n) => n + 1) const onClear = () => { clearedRef.current = true onClearBot() @@ -92,16 +97,16 @@ export default function BotLeadList({ api, bot, onClearBot }) { }, [single]) // Die neue Seite ist da und die Seitenzeile wieder eingehängt: der Fokus geht an den gedrückten Knopf, ist er gesperrt (erste oder letzte Seite) an den anderen. - // Scheiterte die Anfrage, gibt es keine Seitenzeile (die Refs sind null) und nichts geschieht. + // Nur wenn er wirklich verloren ging (body, nirgends, der alte Knopf ist ausgehängt): wer inzwischen selbst woandershin gegangen ist (Suchfeld, Raumfilter), wird + // nicht gestört. Ohne preventScroll spränge die Seite ans Ende der neuen Seite, denn die Seitenzeile steht unter der Tabelle (restoreFocus in workersFocus.js). + // Scheiterte die Anfrage, gibt es keine Seitenzeile; die Merkung bleibt dann für „Erneut versuchen“ stehen (Raum, Suche und neues Blättern setzen sie neu oder zurück). useEffect(() => { - if (loading) return + if (loading || result.error) return const dir = pagedRef.current pagedRef.current = '' if (dir === '') return - const [first, second] = dir === 'next' ? [nextRef.current, prevRef.current] : [prevRef.current, nextRef.current] - const target = first && !first.disabled ? first : second - target?.focus() - }, [loading]) + restoreFocus(browserDocument(), dir === 'next' ? [nextRef.current, prevRef.current] : [prevRef.current, nextRef.current]) + }, [loading, result.error]) const rows = result.items.map((item) => botleadRow(item)) const info = pageInfo({ total: result.total, page: view.page, pageSize: result.pageSize }) @@ -111,7 +116,14 @@ export default function BotLeadList({ api, bot, onClearBot }) { if (loading) { body =
Wird geladen …
} else if (result.error) { - body =
{result.error}
+ body = ( + <> +
{result.error}
+
+ +
+ + ) } else if (rows.length === 0) { if (single) body = else if (filtered) body = diff --git a/src/styles/workers.css b/src/styles/workers.css index 00546bd..79c555f 100644 --- a/src/styles/workers.css +++ b/src/styles/workers.css @@ -264,6 +264,8 @@ color: var(--grey-fg); } .wk-bl-pager { display: flex; gap: 8px; } +/* „Erneut versuchen“ unter der Fehlermeldung (die Meldung selbst hat keinen Abstand nach unten) */ +.wk-bl-retry { margin-top: 8px; } /* ---------- Schmale Bildschirme ---------- */ @@ -293,10 +295,11 @@ Der Dialog hängt nicht unter einer wk-Klasse, die Regel beginnt deshalb mit der Klasse des Knopfs. Mit (0,3,0) (Klasse, Klasse, Attribut) gewinnt sie gegen (0,2,0), gleich in welcher Reihenfolge die Dateien geladen werden. */ .wk-prospect-close.btn[type="button"] { width: 100%; min-height: 40px; justify-content: center; } - /* Bot-Leads: Blätterknöpfe und „Alle anzeigen“ der Einzelansicht mit 40 px Tippfläche wie die Knöpfe des Pults; min-height schlägt height: 32px von .btn, und mit + /* Bot-Leads: Blätterknöpfe, „Alle anzeigen“ der Einzelansicht und „Erneut versuchen“ mit 40 px Tippfläche wie die Knöpfe des Pults; min-height schlägt height: 32px von .btn, und mit zwei Klassen (0,2,0) gegen (0,1,0) braucht es kein !important. Keine globale .btn-Regel: Dialoge und andere Seiten behalten ihre Höhe. Die Zeile darf umbrechen */ .wk-bl-pager .btn { min-height: 40px; } .wk-bl-single .btn { min-height: 40px; } + .wk-bl-retry .btn { min-height: 40px; } .wk-bl .table-foot { flex-wrap: wrap; gap: 8px 16px; } } diff --git a/tests/unit/botleadList.test.mjs b/tests/unit/botleadList.test.mjs index d1814db..8fe865e 100644 --- a/tests/unit/botleadList.test.mjs +++ b/tests/unit/botleadList.test.mjs @@ -36,20 +36,47 @@ function fakeApi() { return { calls, botleads: (params) => new Promise((resolve, reject) => { calls.push({ params, resolve, reject }) }) } } -// DOM-Attrappe für den Fokus (Muster workersPultUi.test.mjs): ref an select und den Blätterknöpfen liefert __fakeRef. Ein Element zählt den Aufruf von focus() nur, -// solange es im Baum steht; React setzt ref.current beim Abbau auf null, dann wäre ref.current?.focus() wirkungslos. Der Schlüssel ist „select“ oder die Beschriftung des Knopfs. +// DOM-Attrappe für den Fokus (Muster workersPultUi.test.mjs): document mit body und activeElement, ref an select und den Blätterknöpfen liefert __fakeRef. +// Der Schlüssel ist „select“ oder die Beschriftung des Knopfs. Ein Element gilt nur, solange es im Baum steht (isConnected, wie im Browser); steht es beim +// nächsten Render nicht im Baum der Runde davor (Seitenzeile während des Ladens), ist es danach ein neues Element, so wie React die Knöpfe neu einhängt. +// focus() zählt nur für ein eingehängtes Element, merkt sich Schlüssel und Argument und setzt activeElement (Fokus liegt dann auf dem Element). function fakeFocus() { - const f = { focused: [], tree: () => null, made: new Map() } + const body = { name: 'body' } + const doc = { body, documentElement: { name: 'html' }, activeElement: body } + const f = { doc, body, focused: [], options: [], tree: () => null, view: null, made: new Map() } const keyOf = (tag, props) => (tag === 'button' ? String(props.children) : tag) const live = (tag, key) => nodes(f.tree(), (n) => n.tag === tag && keyOf(tag, n.props) === key).length > 0 + const had = Object.hasOwn(globalThis, 'document') + const saved = globalThis.document + globalThis.document = doc globalThis.__fakeRef = (tag, props) => { const key = keyOf(tag, props) - if (!f.made.has(key)) f.made.set(key, { key, props, get disabled() { return this.props.disabled === true }, focus() { if (live(tag, key)) f.focused.push(key) } }) + const known = f.made.get(key) + if (!known || !live(tag, key)) { // während dieser Runde zeigt f.tree() noch die Runde davor + f.made.set(key, { + key, props, + get disabled() { return this.props.disabled === true }, + get isConnected() { return f.made.get(key) === this && live(tag, key) }, + focus(options) { if (this.isConnected) { f.focused.push(key); f.options.push(options); doc.activeElement = this } }, + }) + } f.made.get(key).props = props return f.made.get(key) } - f.attach = (view) => { f.tree = () => view.tree() } - f.restore = () => { delete globalThis.__fakeRef } + // ein Element, das der Nutzer von Hand ansteuert (Suchfeld, Link …): eingehängt, bedienbar + f.other = (name) => ({ name, isConnected: true, disabled: false, focus() { doc.activeElement = this } }) + f.attach = (view) => { f.view = view; f.tree = () => view.tree() } + // Der Nutzer drückt einen Knopf: der Browser gibt ihm den Fokus, dann läuft der Klick + // (Knöpfe ohne ref, z. B. „Erneut versuchen“, sind nur so lange Element, wie ein Knopf dieser Beschriftung im Baum steht) + f.press = async (label) => { + doc.activeElement = f.made.get(label) ?? { name: label, disabled: false, get isConnected() { return buttons(f.tree(), label).length > 0 }, focus() { doc.activeElement = this } } + await f.view.click(label) + } + f.restore = () => { + if (had) globalThis.document = saved + else delete globalThis.document + delete globalThis.__fakeRef + } return f } @@ -344,7 +371,7 @@ test('nach dem Abbau setzt eine späte Antwort nichts mehr, ein offener Zeitgebe assert.equal(ui.lateUpdates(), 0) }) -test('Blättern (C5): nach dem Laden der neuen Seite bekommt die Seitenzeile den Tastaturfokus zurück; ist der Knopf gesperrt, der andere', async () => { +test('Blättern (C5): nach dem Laden der neuen Seite bekommt die Seitenzeile den Tastaturfokus zurück, ohne dass die Seite scrollt (N5); ist der Knopf gesperrt, der andere', async () => { const api = fakeApi() const dom = fakeFocus() const ui = await start(api) @@ -352,19 +379,63 @@ test('Blättern (C5): nach dem Laden der neuen Seite bekommt die Seitenzeile den try { await ui.answer(0, { items: [ITEM], total: 120, pageSize: 50 }) assert.deepEqual(dom.focused, []) // nicht schon beim ersten Laden - await ui.click('Weiter') + await dom.press('Weiter') assert.deepEqual(dom.focused, []) // während der Anfrage ist die Seitenzeile weg + assert.equal(dom.doc.activeElement.isConnected, false) // und mit ihr der Knopf, der den Fokus hatte: der Fokus ist verloren await ui.answer(1, { items: [ITEM], total: 120, pageSize: 50 }) // Seite 2 von 3 assert.deepEqual(dom.focused, ['Weiter']) - await ui.click('Weiter') + await dom.press('Weiter') await ui.answer(2, { items: [ITEM], total: 120, pageSize: 50 }) // Seite 3 von 3: „Weiter“ ist gesperrt ⇒ „Zurück“ assert.deepEqual(dom.focused, ['Weiter', 'Zurück']) - await ui.click('Zurück') + await dom.press('Zurück') await ui.answer(3, { items: [ITEM], total: 120, pageSize: 50 }) // Seite 2 assert.deepEqual(dom.focused, ['Weiter', 'Zurück', 'Zurück']) - await ui.click('Zurück') + await dom.press('Zurück') await ui.answer(4, { items: [ITEM], total: 120, pageSize: 50 }) // Seite 1: „Zurück“ ist gesperrt ⇒ „Weiter“ assert.deepEqual(dom.focused, ['Weiter', 'Zurück', 'Zurück', 'Weiter']) + // N5: ohne { preventScroll: true } sprängen Chromium und Firefox ans Ende der neuen Seite (der Knopf steht unter der Tabelle) + assert.deepEqual(dom.options, [{ preventScroll: true }, { preventScroll: true }, { preventScroll: true }, { preventScroll: true }]) + } finally { dom.restore(); ui.end() } +}) + +test('Blättern (N5): hat der Nutzer den Fokus inzwischen selbst woandershin gesetzt, wird er nicht gestohlen; liegt er auf body oder nirgends, kommt er zurück', async () => { + const api = fakeApi() + const dom = fakeFocus() + const ui = await start(api) + dom.attach(ui) + try { + await ui.answer(0, { items: [ITEM], total: 400, pageSize: 50 }) + // 1. Der Nutzer klickt während des Ladens ins Suchfeld (Fokus dort, aber die Zeitgeber der Suche laufen noch nicht) + await dom.press('Weiter') + const search = dom.other('Suchfeld') + search.focus() + await ui.answer(1, { items: [ITEM], total: 400, pageSize: 50 }) + assert.deepEqual(dom.focused, []) + assert.equal(dom.doc.activeElement, search) + // 2. … oder er wählt den Raumfilter mit der Tastatur an, ohne etwas zu ändern + await dom.press('Weiter') + dom.made.get('select').focus() + assert.deepEqual(dom.focused, ['select']) + await ui.answer(2, { items: [ITEM], total: 400, pageSize: 50 }) + assert.deepEqual(dom.focused, ['select']) // kein weiterer Fokus auf die Seitenzeile + assert.equal(dom.doc.activeElement.key, 'select') + // 3. Fokus auf body: zurück an die Seitenzeile + dom.focused.length = 0; dom.options.length = 0 + await dom.press('Weiter') + dom.doc.activeElement = dom.body + await ui.answer(3, { items: [ITEM], total: 400, pageSize: 50 }) + assert.deepEqual(dom.focused, ['Weiter']) + assert.deepEqual(dom.options, [{ preventScroll: true }]) + // 4. Fokus auf nichts (activeElement null, etwa ein Dokument ohne Fokus): ebenfalls zurück + await dom.press('Weiter') + dom.doc.activeElement = null + await ui.answer(4, { items: [ITEM], total: 400, pageSize: 50 }) + assert.deepEqual(dom.focused, ['Weiter', 'Weiter']) + // 5. ohne document (Server, Node): nichts geschieht, nichts bricht + delete globalThis.document + await ui.click('Weiter') + await ui.answer(5, { items: [ITEM], total: 400, pageSize: 50 }) + assert.deepEqual(dom.focused, ['Weiter', 'Weiter']) } finally { dom.restore(); ui.end() } }) @@ -398,6 +469,103 @@ test('Blättern (C5): der Fokus springt nicht, wenn der Nutzer inzwischen Raum o } finally { dom.restore(); ui.end() } }) +test('Fehler beim Blättern (N6): „Erneut versuchen“ wiederholt genau die Seitenanfrage; Fehler und Knopf verschwinden, die Seitenzeile ist wieder da', async () => { + const api = fakeApi() + const ui = await start(api) + try { + await ui.answer(0, { items: [ITEM], total: 120, pageSize: 50 }) + await ui.click('Weiter') + assert.deepEqual(api.calls[1].params, { room: '', q: '', page: 2 }) + assert.equal(buttons(ui.tree(), 'Erneut versuchen').length, 0) // beim Laden gibt es keinen Knopf + await ui.fail(1, Object.assign(new Error('Der Hustler-Dienst ist gerade nicht erreichbar.'), { status: 0 })) + assert.deepEqual(ui.errors(), [{ text: 'Der Hustler-Dienst ist gerade nicht erreichbar.', role: 'alert' }]) // der Satz bleibt unverändert im Alarm + const retry = buttons(ui.tree(), 'Erneut versuchen') + assert.equal(retry.length, 1) + assert.equal(retry[0].props.type, 'button') + assert.equal(retry[0].props.disabled, undefined) + assert.equal(api.calls.length, 2) + await ui.click('Erneut versuchen') + assert.equal(api.calls.length, 3) // genau eine neue Anfrage, dieselbe Seite + assert.deepEqual(api.calls[2].params, { room: '', q: '', page: 2 }) + assert.ok(ui.text().includes('Wird geladen')) + assert.deepEqual(ui.errors(), []) + assert.equal(buttons(ui.tree(), 'Erneut versuchen').length, 0) + await ui.answer(2, { items: [PRAXIS], total: 120, pageSize: 50 }) + assert.equal(api.calls.length, 3) + assert.deepEqual(ui.errors(), []) + assert.deepEqual(ui.rows().map((r) => ui.cells(r)[0]), ['Praxis Dr. Muster67655 Kaiserslautern']) + assert.ok(ui.text().includes('Seite 2 von 3 · 120 Einträge')) + assert.deepEqual([ui.disabled('Zurück'), ui.disabled('Weiter')], [[false], [false]]) + assert.equal(buttons(ui.tree(), 'Erneut versuchen').length, 0) + } finally { ui.end() } +}) + +test('Fehler beim Blättern (N6): scheitert auch der zweite Versuch, bleibt der Knopf; Raumfilter und Suche gehen weiterhin, mit Seite 1', async () => { + const api = fakeApi() + const ui = await start(api) + try { + await ui.answer(0, { items: [ITEM], total: 120, pageSize: 50 }) + await ui.click('Weiter') + await ui.fail(1, new Error('zu langsam')) + await ui.click('Erneut versuchen') + await ui.fail(2, new Error('immer noch nicht')) + assert.deepEqual(ui.errors().map((e) => e.text), ['immer noch nicht']) + assert.equal(buttons(ui.tree(), 'Erneut versuchen').length, 1) + await ui.click('Erneut versuchen') + assert.deepEqual(api.calls[3].params, { room: '', q: '', page: 2 }) + // statt eines weiteren Versuchs ändert der Nutzer den Raum: neue Anfrage mit Seite 1, die späte Antwort des Versuchs zählt nicht + await ui.pick('handwerk') + assert.deepEqual(api.calls[4].params, { room: 'handwerk', q: '', page: 1 }) + await ui.answer(3, { items: [PRAXIS], total: 120, pageSize: 50 }) + assert.equal(ui.rows().length, 0) + await ui.answer(4, { items: [ITEM], total: 1, pageSize: 50 }) + assert.deepEqual(ui.errors(), []) + assert.deepEqual(ui.rows().map((r) => ui.cells(r)[0]), ['Elektro Beispiel GmbH67655 Kaiserslautern']) + assert.equal(buttons(ui.tree(), 'Erneut versuchen').length, 0) + } finally { ui.end() } +}) + +test('Fehler beim Blättern (N6): „Erneut versuchen“ gibt es auch beim ersten Laden und in der Einzelansicht und wiederholt dort die jeweilige Anfrage', async () => { + const api = fakeApi() + const ui = await start(api) + try { + await ui.fail(0, new Error('Der Hustler-Dienst ist gerade nicht erreichbar.')) + await ui.click('Erneut versuchen') + assert.deepEqual(api.calls[1].params, { room: '', q: '', page: 1 }) + await ui.answer(1, { items: [ITEM], total: 1, pageSize: 50 }) + assert.equal(ui.rows().length, 1) + } finally { ui.end() } + const api2 = fakeApi() + const one = await start(api2, { bot: 'p1', onClearBot: () => {} }) + try { + await one.fail(0, Object.assign(new Error('Der Hustler-Dienst ist gerade nicht erreichbar.'), { status: 0 })) + assert.equal(buttons(one.tree(), 'Alle anzeigen').length, 1) // der Weg zurück bleibt offen + await one.click('Erneut versuchen') + assert.deepEqual(api2.calls.map((c) => c.params), [{ id: 'p1' }, { id: 'p1' }]) + await one.answer(1, { items: [ITEM], total: 1 }) + assert.equal(one.rows().length, 1) + assert.equal(one.rows()[0].props['aria-current'], 'true') + } finally { one.end() } +}) + +test('Fehler beim Blättern (N6): nach einem gelungenen Wiederholen bekommt die Seitenzeile den Fokus zurück (ohne Scrollen), wenn zuvor geblättert wurde', async () => { + const api = fakeApi() + const dom = fakeFocus() + const ui = await start(api) + dom.attach(ui) + try { + await ui.answer(0, { items: [ITEM], total: 120, pageSize: 50 }) + await dom.press('Weiter') + await ui.fail(1, new Error('Der Hustler-Dienst ist gerade nicht erreichbar.')) + assert.deepEqual(dom.focused, []) // keine Seitenzeile, nichts zu fokussieren + await dom.press('Erneut versuchen') // der Knopf verschwindet beim Laden wie vorher die Seitenzeile + assert.deepEqual(dom.focused, []) + await ui.answer(2, { items: [ITEM], total: 120, pageSize: 50 }) + assert.deepEqual(dom.focused, ['Weiter']) + assert.deepEqual(dom.options, [{ preventScroll: true }]) + } finally { dom.restore(); ui.end() } +}) + test('Alle anzeigen (W1): nach dem Verlassen der Einzelansicht bekommt der Raumfilter den Fokus, nicht beim Einhängen und nicht bei einem anderen Weg zur Liste', async () => { const api = fakeApi() const dom = fakeFocus() diff --git a/tests/unit/workersCss.test.mjs b/tests/unit/workersCss.test.mjs index 987a806..59109a3 100644 --- a/tests/unit/workersCss.test.mjs +++ b/tests/unit/workersCss.test.mjs @@ -72,10 +72,10 @@ test('workers.css: gekürzte Zeilen der Bot-Leads bleiben lesbar (Text >= 4,5:1 } }) -test('workers.css: am Handy sind die Blätterknöpfe und „Alle anzeigen“ der Bot-Leads 40 px hoch, ohne die globale .btn-Regel zu ändern (C5, W1)', () => { +test('workers.css: am Handy sind die Blätterknöpfe, „Alle anzeigen“ und „Erneut versuchen“ der Bot-Leads 40 px hoch, ohne die globale .btn-Regel zu ändern (C5, W1)', () => { const css = stripped('workers.css') const phone = css.slice(css.indexOf('@media (max-width: 860px)')) - for (const selector of ['.wk-bl-pager .btn', '.wk-bl-single .btn']) { + for (const selector of ['.wk-bl-pager .btn', '.wk-bl-single .btn', '.wk-bl-retry .btn']) { assert.equal(decl(body(phone, selector), 'min-height'), '40px', selector) // min-height schlägt height: 32px der globalen .btn-Regel, und die Spezifität (0,2,0) schlägt .btn (0,1,0): kein !important nötig assert.ok(beats(specificity(selector), specificity('.btn')), selector)