From 1ab760be0c9a10528bd5c3106dcd9332968b8fb3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 12:23:03 +0000 Subject: [PATCH] Hustler-Keller: unter Stufe 6 leeres Register ohne Abruf, mit ehrlichem Text BotLeadList bekommt die Stufe (WorkersPage reicht sie durch). Unterhalb von BOTLEADS_AB (Stufe 6) ruft sie weder api.botleads noch ein JWT, zeigt keine Ladeanzeige und keinen Fehlerzustand: Szene mit Arne, Register-Buch, Reiter und Suche stehen da, das Register ist leer ("Noch keine Bot-Leads. Hier legt Arne Betriebe ohne Website ab, die Bea nicht fuer eine Mail ausgewaehlt hat. Die Branchen-Raeume ... suchen noch nicht."), ohne Zahl am Reiter. Reiter, Suche, Zuklappen und Alle anzeigen bleiben bedienbar und loesen nie einen Abruf aus. Ab Stufe 6 unveraendert (Live-Daten); der Test-Laeufer startet den Keller standardmaessig mit Stufe 6. Co-Authored-By: Claude Sonnet 5.5 --- src/components/workers/BotLeadList.jsx | 18 ++- .../workers/keller/RegisterBuch.jsx | 23 +++- tests/unit/botleadList.test.mjs | 128 ++++++++++++++++++ tests/unit/helpers/kellerRunner.mjs | 3 +- tests/unit/workersRoute.test.mjs | 4 +- 5 files changed, 161 insertions(+), 15 deletions(-) diff --git a/src/components/workers/BotLeadList.jsx b/src/components/workers/BotLeadList.jsx index 57e2f8e..0e85e75 100644 --- a/src/components/workers/BotLeadList.jsx +++ b/src/components/workers/BotLeadList.jsx @@ -4,6 +4,7 @@ import KellerSzene from './keller/KellerSzene' import RegisterBuch from './keller/RegisterBuch' import { pageInfo } from '../../lib/botleadsView' import { eintragModell } from '../../lib/hustlerKeller' +import { HUSTLER_STUFE, botleadsAbrufbar } from '../../lib/hustlerStufe' import { formatCount } from '../../lib/workersView' import { restoreFocus } from '../../lib/workersFocus' @@ -14,15 +15,20 @@ const browserDocument = () => (typeof document === 'undefined' ? null : document // Bildschirm „Keller“ (Hustler-Pixelwelt T10, Spec E12/E13; Dateiname bleibt BotLeadList): die Archiv-Szene mit Arne (keller/KellerSzene), davor Arnes Register-Buch // (keller/RegisterBuch) mit den Betrieben, die nicht angeschrieben werden, samt Grund, und die Akte zu einem Eintrag (keller/Akte). Reine Liste, nichts wird gesendet -// oder angerufen. Props: api, bot, onClearBot, kpis (botLeads für die Zahl am Schild), workers (Hustler-Tabelle der Seite: Arnes Status), paused (Hustler angehalten: -// die Szene steht still, Klasse hk-steht), onBack (◂ Hochhaus). Beim Einhängen nimmt die Überschrift „Bot-Leads“ (tabIndex -1) den Fokus, ohne dass die Seite springt (T13 M2). +// oder angerufen. Props: api, stufe (Standard HUSTLER_STUFE), bot, onClearBot, kpis (botLeads für die Zahl am Schild), workers (Hustler-Tabelle der Seite: Arnes Status), +// paused (Hustler angehalten: die Szene steht still, Klasse hk-steht), onBack (◂ Hochhaus). Beim Einhängen nimmt die Überschrift „Bot-Leads“ (tabIndex -1) den Fokus, +// ohne dass die Seite springt (T13 M2). +// Stufen-Sperre (hustlerStufe.js): unterhalb von Stufe 6 (BOTLEADS_AB) gibt es GET /botleads im Dienst noch nicht. Der Keller steht dann trotzdem da (Arne, Regale, Register-Buch, +// Reiter, Suche), das Register ist leer und sagt ehrlich warum (RegisterBuch, ohneDienst); es gibt keinen Abruf (auch kein JWT), keine Ladeanzeige, keinen Fehler. Reiter und Suche +// bleiben bedienbar und ändern nur die Anzeige (es gibt nichts zu filtern); die Bedienung entspricht sonst dem Keller mit Daten, also ohne Fokusfalle. // 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. 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), und zwar // nur in der Akte. Kein CSS-Import (WorkersPage hängt die Stile ein), keine Bilder, kein HTML-Einschub. -export default function BotLeadList({ api, bot, onClearBot, kpis, workers, paused = false, onBack }) { +export default function BotLeadList({ api, stufe = HUSTLER_STUFE, bot, onClearBot, kpis, workers, paused = false, onBack }) { + const abruf = botleadsAbrufbar(stufe) const botId = typeof bot === 'string' ? bot : '' const single = botId !== '' const [view, setView] = useState(START_VIEW) @@ -45,6 +51,7 @@ export default function BotLeadList({ api, bot, onClearBot, kpis, workers, pause const key = JSON.stringify(single ? [botId, attempt] : [view.room, view.q, view.page, attempt]) useEffect(() => { + if (!abruf) return undefined // Stufe unter 6: kein Abruf, das Register bleibt leer seqRef.current += 1 const mine = seqRef.current // Ein früheres Ergebnis zu genau diesem Schlüssel (hin und zurück gewechselt, bevor der andere Schlüssel antwortete) ist veraltet: erst die neue Antwort zählt @@ -70,7 +77,7 @@ export default function BotLeadList({ api, bot, onClearBot, kpis, workers, pause } load() return () => { seqRef.current += 1 } // eine noch offene Anfrage zählt nicht mehr - }, [api, key]) + }, [api, key, abruf]) useEffect(() => () => window.clearTimeout(timerRef.current), []) @@ -101,7 +108,7 @@ export default function BotLeadList({ api, bot, onClearBot, kpis, workers, pause timerRef.current = window.setTimeout(() => setView((v) => ({ ...v, q: value.trim(), page: 1 })), SEARCH_DELAY_MS) } - const loading = result.key !== key + const loading = abruf && result.key !== key // ohne Abruf gibt es nichts zu laden // Die Einzelansicht ist weg (die Seite hat bot aus der Adresse genommen): der Raum-Reiter ersetzt den gedrückten Knopf und bekommt den Fokus. // Ohne den Knopf (Zurück-Taste des Browsers) bleibt der Fokus, wo er ist. @@ -159,6 +166,7 @@ export default function BotLeadList({ api, bot, onClearBot, kpis, workers, pause draft={draft} filtered={filtered} laden={loading} + ohneDienst={!abruf} fehler={result.error} zeilen={zeilen} total={result.total} diff --git a/src/components/workers/keller/RegisterBuch.jsx b/src/components/workers/keller/RegisterBuch.jsx index 62fe95b..ba26797 100644 --- a/src/components/workers/keller/RegisterBuch.jsx +++ b/src/components/workers/keller/RegisterBuch.jsx @@ -1,12 +1,18 @@ // Das Register-Buch des Kellers (Hustler-Pixelwelt T10, Spec E12/E13): Raum-Reiter, das aufgeschlagene Buch mit Titel, Hinweis, Suchfeld (oder, bei einem einzelnen // Eintrag, „Alle anzeigen“), Kopfzeile, den Einträgen der Dienst-Seite (im Buch scrollbar), Leer-, Lade- und Fehlerzustand und der Seitenzeile mit ◂ ▸. // Aufbau und Klassen wie keller/entwurf/BotLeads.dc.html mit hk- davor; die Inline-Stile der Vorlage stehen als Klassen in styles/hustler/hustler-keller-ui.css. +// ohneDienst (Stufe unter 6, der Dienst kennt GET /botleads noch nicht): das Register ist leer und sagt ehrlich warum; weder Fehler noch Ladeanzeige noch Zahl am Reiter. // Rein darstellend: Abruf, Suche, Seiten und Fokus gehören BotLeadList. Alle Texte der Zeilen sind Textknoten; Links gibt es hier nicht (der Link zur Quelle steht in der // Akte und kommt nur aus row.sourceHref). Kein CSS-Import, keine Bilder, kein HTML-Einschub. import { REITER } from '../../../lib/hustlerKeller' import { formatCount } from '../../../lib/workersView' const SEARCH_MAX = 100 // der Dienst kürzt die Suche auf 100 Zeichen +// Leer-Text, solange der Dienst keine Bot-Leads liefert (Stufe unter 6): Bot-Leads entstehen erst, wenn die Branchen-Räume (P6) suchen +const LEER_OHNE_DIENST = [ + 'Noch keine Bot-Leads.', + 'Hier legt Arne Betriebe ohne Website ab, die Bea nicht für eine Mail ausgewählt hat. Die Branchen-Räume (Ärzte, Handwerk, Selbstständige, Läden) suchen noch nicht.', +] // Eine Zeile des Buchs: ein Knopf, der die Akte öffnet. Spalten: Raum-Symbol, Name (+ „gekürzt“) mit Ort und Funddatum, Punkte, Grund mit Symbol, Telefon. function Zeile({ z, nr, aktuell, onOpen }) { @@ -42,11 +48,12 @@ function Zeile({ z, nr, aktuell, onOpen }) { // Props: // einzeln (Einzelansicht über ?bot=), botId, room (gewählter Raum-Schlüssel), draft (Inhalt des Suchfelds), filtered (Raum oder Suche gesetzt) +// ohneDienst (der Dienst liefert noch keine Bot-Leads: ehrlicher Leer-Text) // laden, fehler (Satz des Dienstes oder null), zeilen (eintragModell je Eintrag der Seite), total, info (pageInfo) // onRoom(value), onSearch(ev), onClear(), onPage('prev' | 'next'), onRetry(), onOpen(nr, ev) // reiterRef (am aktiven Reiter), zurueckRef, weiterRef (Blätterknöpfe): Fokus-Ziele von BotLeadList -export default function RegisterBuch({ einzeln, botId, room, draft, filtered, laden, fehler, zeilen, total, info, onRoom, onSearch, onClear, onPage, onRetry, onOpen, reiterRef, zurueckRef, weiterRef }) { - const zahl = laden || fehler ? null : total // die Zahl steht nur am aktiven Reiter und erst, wenn die Antwort da ist +export default function RegisterBuch({ einzeln, botId, room, draft, filtered, ohneDienst = false, laden, fehler, zeilen, total, info, onRoom, onSearch, onClear, onPage, onRetry, onOpen, reiterRef, zurueckRef, weiterRef }) { + const zahl = laden || fehler || ohneDienst ? null : total // die Zahl steht nur am aktiven Reiter und erst, wenn die Antwort da ist (ohne Dienst gibt es keine) let koerper if (laden) { @@ -59,11 +66,13 @@ export default function RegisterBuch({ einzeln, botId, room, draft, filtered, la ) } else if (zeilen.length === 0) { - const leer = einzeln - ? ['Dieser Eintrag ist nicht (mehr) in der Liste.', 'Er wurde gekürzt oder ist kein Bot-Lead.'] - : filtered - ? ['Keine Treffer.', 'Für diesen Raum und diese Suche gibt es keine Bot-Leads.'] - : ['Noch keine Bot-Leads.', 'Betriebe, die nicht angeschrieben werden, erscheinen hier mit ihrem Grund.'] + const leer = ohneDienst + ? LEER_OHNE_DIENST + : einzeln + ? ['Dieser Eintrag ist nicht (mehr) in der Liste.', 'Er wurde gekürzt oder ist kein Bot-Lead.'] + : filtered + ? ['Keine Treffer.', 'Für diesen Raum und diese Suche gibt es keine Bot-Leads.'] + : ['Noch keine Bot-Leads.', 'Betriebe, die nicht angeschrieben werden, erscheinen hier mit ihrem Grund.'] koerper = (

{leer[0]}

diff --git a/tests/unit/botleadList.test.mjs b/tests/unit/botleadList.test.mjs index 0b119b3..639877f 100644 --- a/tests/unit/botleadList.test.mjs +++ b/tests/unit/botleadList.test.mjs @@ -555,3 +555,131 @@ test('aria-current (C5): nur die Zeile mit der Kennung aus bot, nie eine Zeile o assert.deepEqual(one.rows().map((r) => r.props['aria-current']), [undefined, 'true', undefined]) } finally { one.end() } }) + +// ---------- Stufen-Sperre (hustlerStufe.js): unterhalb von Stufe 6 ist der Keller begehbar, aber leer und ohne Abruf ---------- +// Der Läufer baut sonst mit stufe: 6 (Live-Daten, alle Tests oben). Hier ausdrücklich Stufe 3 (Produktion: Dienst P3, GET /botleads gibt es noch nicht). + +const OHNE_DIENST_TITEL = 'Noch keine Bot-Leads.' +const OHNE_DIENST_TEXT = 'Hier legt Arne Betriebe ohne Website ab, die Bea nicht für eine Mail ausgewählt hat. Die Branchen-Räume (Ärzte, Handwerk, Selbstständige, Läden) suchen noch nicht.' + +test('Stufe 3: der Keller steht mit Szene, Register-Buch, Reitern und Suche da, zeigt den ehrlichen Leer-Text, fragt nichts ab und zeigt weder Laden noch Fehler', async () => { + for (const stufe of [3, 4, 5]) { + const api = fakeApi() + const ui = await start(api, { stufe }) + try { + assert.equal(api.calls.length, 0, `Stufe ${stufe}: kein Abruf`) + assert.equal(byClass(ui.tree(), 'hk-kl-szene').length, 1, 'Szene mit Arne') + assert.equal(textOf(byClass(ui.tree(), 'hk-kl-titel')[0]), 'Bot-Leads') + assert.equal(textOf(byClass(ui.tree(), 'hk-kl-buchtitel')[0]), 'Nie-doppelt-Register') + assert.equal(ui.reiter().length, ROOM_VALUES.length); assert.equal(ui.activeIndex(), 0) + assert.ok(ui.search(), 'Suchfeld') + const leer = byClass(ui.tree(), 'hk-kl-leer') + assert.equal(leer.length, 1) + assert.equal(textOf(byClass(leer[0], 'hk-kl-leer-titel')[0]), OHNE_DIENST_TITEL) + assert.equal(textOf(byClass(leer[0], 'hk-kl-leer-text')[0]), OHNE_DIENST_TEXT) + // kein Fehlerzustand, keine Ladeanzeige, kein „Erneut versuchen“, keine Zeilen, keine Kopfzeile, keine Seitenzeile, keine Zahl am Reiter + assert.ok(!ui.text().includes('Wird geladen')); assert.equal(byClass(ui.tree(), 'hk-kl-laden').length, 0) + assert.equal(ui.errors().length, 0); assert.equal(buttons(ui.tree(), 'Erneut versuchen').length, 0) + assert.equal(nodes(ui.tree(), (n) => n.props && (n.props.role === 'alert' || n.props.role === 'status')).length, 0) + assert.equal(ui.rows().length, 0); assert.equal(byClass(ui.tree(), 'hk-kopfzeile').length, 0); assert.equal(byClass(ui.tree(), 'hk-kl-fuss').length, 0) + assert.equal(byClass(ui.tree(), 'hk-kl-reiter-zahl').length, 0) + assert.ok(!ui.text().includes('Keine Treffer')) + } finally { ui.end() } + } +}) + +test('Stufe 3: ohne Stufen-Angabe gilt HUSTLER_STUFE (Produktion = 3): leeres Register, kein Abruf', async () => { + const { HUSTLER_STUFE, botleadsAbrufbar } = await loadModule('src/lib/hustlerStufe.js') + const api = fakeApi() + const ui = await start(api, { stufe: undefined }) + try { + assert.equal(api.calls.length, botleadsAbrufbar(HUSTLER_STUFE) ? 1 : 0) + if (!botleadsAbrufbar(HUSTLER_STUFE)) assert.ok(ui.text().includes(OHNE_DIENST_TITEL) && ui.text().includes(OHNE_DIENST_TEXT)) + } finally { ui.end() } +}) + +test('Stufe 3: Reiter und Suche bleiben bedienbar, ändern nur die Anzeige und lösen nie einen Abruf aus; die Leer-Auskunft bleibt dieselbe', async () => { + const api = fakeApi() + const dom = fakeFocus() + const ui = await start(api, { stufe: 3 }) + dom.attach(ui) + try { + await ui.pick(idx('handwerk')) + assert.equal(ui.activeIndex(), idx('handwerk')); assert.equal(dom.focused.length, 0) + await ui.type('Elektro'); ui.fireTimers(); await settle() + assert.equal(ui.search().props.value, 'Elektro') + await ui.pick(idx('laden')); await ui.pick(idx('')) + assert.equal(ui.activeIndex(), 0) + assert.equal(api.calls.length, 0, 'weder Reiter noch Suche rufen etwas ab') + assert.equal(ui.timers.size, 0) + assert.ok(ui.text().includes(OHNE_DIENST_TITEL) && ui.text().includes(OHNE_DIENST_TEXT)) + assert.ok(!ui.text().includes('Keine Treffer') && !ui.text().includes('Wird geladen')) + assert.equal(ui.errors().length, 0); assert.equal(byClass(ui.tree(), 'hk-kl-reiter-zahl').length, 0) + // keine Fokusfalle: nichts wird dem Nutzer weggenommen oder von der Komponente an sich gezogen (nur die Überschrift beim Einhängen, ohne Seitensprung) + assert.ok(dom.focused.every((k) => k !== 'reiter')) + } finally { dom.restore(); ui.end() } +}) + +test('Stufe 3: Register zuklappen und aufklappen funktionieren wie mit Daten; ◂ Hochhaus ruft onBack', async () => { + const api = fakeApi(); let zurueck = 0 + const ui = await start(api, { stufe: 3, onBack: () => { zurueck += 1 } }) + try { + await ui.click('Register zuklappen') + assert.equal(byClass(ui.tree(), 'hk-buch').length, 0); assert.equal(byClass(ui.tree(), 'hk-kl-leer').length, 0); assert.equal(byClass(ui.tree(), 'hk-kl-szene').length, 1) + await ui.click('Register aufklappen') + assert.equal(byClass(ui.tree(), 'hk-kl-leer').length, 1) + await ui.click('Zurück zum Hochhaus'); assert.equal(zurueck, 1) + assert.equal(api.calls.length, 0) + } finally { ui.end() } +}) + +test('Stufe 3: ?bot= (Einzelansicht) ruft nichts ab, zeigt dieselbe ehrliche Auskunft und lässt „Alle anzeigen“ zu', async () => { + const api = fakeApi(); let leer = 0 + const ui = await start(api, { stufe: 3, bot: 'p1', onClearBot: () => { leer += 1 } }) + try { + assert.equal(api.calls.length, 0) + assert.ok(ui.text().includes(OHNE_DIENST_TITEL) && ui.text().includes(OHNE_DIENST_TEXT)) + assert.ok(!ui.text().includes('Wird geladen') && !ui.text().includes('nicht (mehr) in der Liste')) + assert.equal(ui.errors().length, 0); assert.equal(buttons(ui.tree(), 'Erneut versuchen').length, 0) + await ui.click('Alle anzeigen'); assert.equal(leer, 1) + ui.setProps({ bot: '' }); await settle() + assert.equal(api.calls.length, 0); assert.ok(ui.text().includes(OHNE_DIENST_TEXT)) + } finally { ui.end() } +}) + +test('Stufe 3: die Zahl im Schild kommt weiter aus den Kennzahlen (kpis.botLeads); die Szene zeigt Arne nach seinem Status', async () => { + const api = fakeApi() + const ui = await start(api, { stufe: 3, kpis: { botLeads: 0 }, workers: { arne: { status: 'arbeitet', task: '', lastTs: null, connected: true, counters: {} } } }) + try { + assert.equal(textOf(byClass(ui.tree(), 'hk-kl-zahl')[0]), '0') + assert.equal(api.calls.length, 0) + } finally { ui.end() } +}) + +test('Stufe 6 (und höher): Abruf wie bisher, der Live-Leer-Text bleibt der alte (nicht der der Stufe 3), Lade- und Fehlerzustand sind zurück', async () => { + for (const stufe of [6, 9]) { + const api = fakeApi() + const ui = await start(api, { stufe }) + try { + assert.equal(api.calls.length, 1, `Stufe ${stufe}`); assert.ok(ui.text().includes('Wird geladen')) + await ui.answer(0, { items: [], total: 0 }) + assert.ok(ui.text().includes('Noch keine Bot-Leads.')); assert.ok(ui.text().includes('Betriebe, die nicht angeschrieben werden, erscheinen hier mit ihrem Grund.')) + assert.ok(!ui.text().includes(OHNE_DIENST_TEXT)); assert.equal(byClass(ui.tree(), 'hk-kl-reiter-zahl').length, 1) + await ui.pick(idx('handwerk')) + assert.equal(api.calls.length, 2); await ui.fail(1, new Error('Der Dienst antwortet nicht.')) + assert.deepEqual(ui.errors().map((e) => e.text), ['Der Dienst antwortet nicht.']); assert.equal(buttons(ui.tree(), 'Erneut versuchen').length, 1) + } finally { ui.end() } + } +}) + +test('Ändert sich die Stufe von 3 auf 6 (im Betrieb nur über einen neuen Bau, hier als Prop): ab dann Abruf, das Register füllt sich', async () => { + const api = fakeApi() + const ui = await start(api, { stufe: 3 }) + try { + assert.equal(api.calls.length, 0) + ui.setProps({ stufe: 6 }); await settle() + assert.equal(api.calls.length, 1); assert.ok(ui.text().includes('Wird geladen')) + await ui.answer(0, { items: [ITEM], total: 1 }) + assert.equal(ui.rows().length, 1); assert.ok(!ui.text().includes(OHNE_DIENST_TEXT)) + } finally { ui.end() } +}) diff --git a/tests/unit/helpers/kellerRunner.mjs b/tests/unit/helpers/kellerRunner.mjs index cafcd76..99407b2 100644 --- a/tests/unit/helpers/kellerRunner.mjs +++ b/tests/unit/helpers/kellerRunner.mjs @@ -128,7 +128,8 @@ export async function start(api, initial = {}) { setTimeout: (fn, ms) => { const id = nextTimer++; timers.set(id, { fn, ms }); return id }, clearTimeout: (id) => { timers.delete(id) }, } - let props = { api, ...initial } + // stufe 6: Live-Daten (Stufen-Sperre, hustlerStufe.js: unterhalb von Stufe 6 ruft der Keller nichts ab und zeigt sein leeres Register); die Tests der Stufe 3 geben sie ausdrücklich an + let props = { api, stufe: 6, ...initial } const mounted = __mount(jsx(() => jsx(BotLeadList, props), {})) const view = { timers, diff --git a/tests/unit/workersRoute.test.mjs b/tests/unit/workersRoute.test.mjs index b2e0fe4..123557a 100644 --- a/tests/unit/workersRoute.test.mjs +++ b/tests/unit/workersRoute.test.mjs @@ -19,7 +19,7 @@ test('WorkersPage: Tab-Titel Hustler, drei Bildschirme in fester Reihenfolge, ke assert.match(s, /id: 'hochhaus', label: 'Hochhaus'/); assert.match(s, /id: 'freigabe', label: 'Freigabe'/); assert.match(s, /id: 'botleads', label: 'Bot-Leads'/) assert.ok(s.indexOf("id: 'hochhaus'") < s.indexOf("id: 'freigabe'") && s.indexOf("id: 'freigabe'") < s.indexOf("id: 'botleads'")) assert.doesNotMatch(s, /title="Hustler"/); assert.doesNotMatch(s, /PageHeader/); assert.doesNotMatch(s, / 1/) - assert.match(s, /const api = workersApi/); assert.match(s, /pickTab\(WORKERS_TABS\.filter\(\(t\) => tabErlaubt\(t\.id, stufe\)\), params\.get\('tab'\)\)/); assert.match(s, /useAuth\(\)/) // Stufen-Sperre: der Keller ist nur ab Stufe 6 ein Bildschirm (hustlerStufe.test.mjs) + assert.match(s, /const api = workersApi/); assert.match(s, /pickTab\(WORKERS_TABS\.filter\(\(t\) => tabErlaubt\(t\.id, stufe\)\), params\.get\('tab'\)\)/); assert.match(s, /useAuth\(\)/) // Stufen-Sperre: der Keller ist ab Stufe 3 ein Bildschirm, die Bot-Leads ruft er erst ab Stufe 6 ab (hustlerStufe.test.mjs, botleadList.test.mjs) assert.match(s, /]*\bapi=\{api\}/); assert.match(s, /]*isAdmin=\{isAdmin\}/) assert.match(s, /
/) // Vollhöhen-Rahmen, data-tab schaltet den Übergang für Pult und Keller assert.match(s, /import Hochhaus from '\.\.\/components\/workers\/hochhaus\/Hochhaus'/) @@ -39,7 +39,7 @@ test('WorkersPage: freigabe zeigt die PultList, botleads die BotLeadList (api, b assert.match(s, /import PultList from '\.\.\/components\/workers\/PultList'/); assert.match(s, /import BotLeadList from '\.\.\/components\/workers\/BotLeadList'/) assert.match(s, /active\.id === 'freigabe' && \([\s\S]*?]*\bapi=\{api\}/); assert.match(s, /]*me=\{\{ id: user\?\.\$id, name: user\?\.name \}\}/) assert.match(s, /active\.id === 'botleads' && \([\s\S]*?]*\bapi=\{api\}/) - assert.match(s, /params\.get\('bot'\)/); assert.match(s, /]*\bbot=\{bot\}/); assert.match(s, /]*onClearBot=\{\(\) => setParams\(\{ tab: 'botleads' \}\)\}/) + assert.match(s, /params\.get\('bot'\)/); assert.match(s, /]*\bstufe=\{stufe\}/); assert.match(s, /]*\bbot=\{bot\}/); assert.match(s, /]*onClearBot=\{\(\) => setParams\(\{ tab: 'botleads' \}\)\}/) assert.match(s, /onOpen=\{\(tab\) => \{ if \(tabErlaubt\(tab, stufe\)\) setParams\(\{ tab \}\) \}\}/) // setParams ersetzt alle Parameter: bot fällt beim Wechsel weg; ein gesperrter Bildschirm öffnet sich nie assert.equal(s.match(/onBack=\{\(\) => setParams\(\{ tab: 'hochhaus' \}\)\}/g).length, 2) // „◂ Hochhaus“ in Pult und Keller for (const name of ['PultList', 'BotLeadList']) assert.match(s, new RegExp(`<${name}[\\s\\S]*?kpis=\\{state\\?\\.kpis\\}[\\s\\S]*?workers=\\{workers\\}[\\s\\S]*?paused=\\{paused\\}[\\s\\S]*?/>`)) // Pause (R3) geht an Pult und Keller