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)