From 6eb4dd3c51944fa26efaa24ed009afcf5a1fa2f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 12:21:59 +0000 Subject: [PATCH] Hustler-Stufe: Keller begehbar ab Stufe 3, Bot-Leads abrufbar erst ab Stufe 6 Die Stufen-Sperre trennt jetzt zwei Dinge: KELLER_BEGEHBAR_AB = 3 (Keller-Etage als Link, ?tab=botleads gueltig, Spur-Links in den Keller) und BOTLEADS_AB = 6 (Abruf von GET /botleads). kellerOffen/STUFE_KELLER entfallen, dafuer kellerBegehbar() und botleadsAbrufbar(). workersApi.botleads bleibt unter Stufe 6 hart gesperrt, der Text heisst nun BOTLEADS_GESPERRT_TEXT. Die Keller-Ansicht selbst (leeres Register ohne Abruf) folgt im naechsten Commit. Co-Authored-By: Claude Sonnet 5.5 --- src/components/workers/hochhaus/Hochhaus.jsx | 2 +- .../workers/hochhaus/HochhausWelt.jsx | 10 ++-- .../workers/hochhaus/HustlerDialog.jsx | 2 +- src/lib/hustlerStufe.js | 29 ++++++---- src/lib/workersApi.js | 10 ++-- src/pages/WorkersPage.jsx | 6 ++- tests/unit/hustlerDialog.test.mjs | 8 +-- tests/unit/hustlerHochhausWelt.test.mjs | 22 ++++---- tests/unit/hustlerSeite.test.mjs | 53 +++++++++++++------ tests/unit/hustlerStufe.test.mjs | 50 +++++++++-------- tests/unit/workersApi.test.mjs | 11 ++-- 11 files changed, 121 insertions(+), 82 deletions(-) diff --git a/src/components/workers/hochhaus/Hochhaus.jsx b/src/components/workers/hochhaus/Hochhaus.jsx index 9c92238..ee1b486 100644 --- a/src/components/workers/hochhaus/Hochhaus.jsx +++ b/src/components/workers/hochhaus/Hochhaus.jsx @@ -18,7 +18,7 @@ function Ueberschrift() { // (regieRef, die Seite ruft regieRef.current.push(events) bei neuen Ereignissen), der Bildschirmwechsel (onOpen(tab)) und vonTab (der Bildschirm, von dem man // zurückkommt: 'freigabe' | 'botleads' | null) kommen von der Seite. Beim Zurückkommen bekommt die Etage, über die man gegangen war, den Fokus (M2). // Die Kopfzeile steht im DOM vor der Welt, damit die Tab-Reihenfolge oben beginnt (M7); über der Welt liegt sie per z-index. -// stufe (Standard HUSTLER_STUFE, hustlerStufe.js) geht an die Welt (Keller-Etage als Link nur ab Stufe 6) und an die Detailansicht (Spur-Links in den Keller nur ab Stufe 6). +// stufe (Standard HUSTLER_STUFE, hustlerStufe.js) geht an die Welt (Keller-Etage als Link nur ab Stufe 3) und an die Detailansicht (Spur-Links in den Keller nur ab Stufe 3). // Kein CSS-Import und keine Bilder: die Stile hängt WorkersPage ein (Regeln dieser Datei in styles/hustler/hustler-ui.css). export default function Hochhaus({ api, isAdmin, feed, workers, regieRef, onOpen, vonTab = null, stufe = HUSTLER_STUFE }) { const wurzelRef = useRef(null) diff --git a/src/components/workers/hochhaus/HochhausWelt.jsx b/src/components/workers/hochhaus/HochhausWelt.jsx index 2758f32..fc7a16e 100644 --- a/src/components/workers/hochhaus/HochhausWelt.jsx +++ b/src/components/workers/hochhaus/HochhausWelt.jsx @@ -2,7 +2,7 @@ // Himmel, Funkeln, Mond, die 2400 x 1200 große Bühne (per transform verschoben und gezoomt) mit Welt-, Licht-, Haus- und Fensterebenen, Neon, Antenne, Aufzug, // Raumschilder, Bildschirme, Radar, Briefkasten-Zähler, Tresor-/Keller-Text, den Etagen (Knöpfe, Freigabe und Keller sind Links) und den 19 Hustlern; davor der Schnee. // Alles hängt am Dienst-Zustand: Figur und Zusätze nach Status (figurZustand), graue Raum-Ebene samt Papierschild nur für nicht angeschlossene Such-Räume. -// Stufen-Sperre (hustlerStufe.js): unterhalb der Keller-Stufe (6) ist die Keller-Etage kein Link auf die Bot-Leads, sondern ein Knopf wie jede andere Etage (heranzoomen). +// Stufen-Sperre (hustlerStufe.js): unterhalb der Stufe, ab der der Keller begehbar ist (3), ist die Keller-Etage kein Link auf den Keller, sondern ein Knopf wie jede andere Etage (heranzoomen). // Die Kamera (Rad, Ziehen, Pinch, Tasten) steckt in useKamera. `children` (die Regie-Ebene) liegt in der Bühne über den Figuren, in Weltkoordinaten. // Bedienung: Tastaturfokus (:focus-visible) auf Etage oder Figur außerhalb des Bildes holt sie per Kamera herein (beiFokus, kein Schwenk bei Mausklick); das Sichtfenster ist // per tabindex -1 fokussierbar (ein Klick in die Welt gibt ihm den Fokus, damit + - 0 Esc wirken); die Tippflächen der Figuren wachsen mit dem Zoom (--hk-tipp-*, tippRand); @@ -10,7 +10,7 @@ // Kein CSS-Import (die Stile hängt WorkersPage ein: styles/hustler/hochhaus.css, hustler-ui.css, hustler-welt.css), keine Bilder, kein innerHTML. import { forwardRef, useImperativeHandle } from 'react' import { anzeigeName, figurZustand, schilder, tippRand } from '../../../lib/hustlerSicht.js' -import { HUSTLER_STUFE, kellerOffen } from '../../../lib/hustlerStufe.js' +import { HUSTLER_STUFE, kellerBegehbar } from '../../../lib/hustlerStufe.js' import { AUFZUG, ETAGEN, ETAGEN_SPALTE, HAUS_BILD, HUSTLER, RAEUME, SCHILDER } from '../../../lib/hustlerWelt.js' import { FLOOR_TITLES, ROLES, ROOM_TITLES } from '../../../lib/workersRoster.js' import useKamera from './useKamera.js' @@ -21,9 +21,9 @@ const LINK_ETAGEN = Object.freeze({ keller: Object.freeze({ tab: 'botleads', text: 'Keller · Bot-Leads ▸', label: 'Keller · Bot-Leads: Bot-Leads öffnen' }), }) -// Welche Etagen Links sind: der Keller nur, wenn es die Bot-Leads schon gibt; sonst zoomt er heran wie jede andere Etage +// Welche Etagen Links sind: der Keller nur, wenn er begehbar ist (ab Stufe 3, auch wenn sein Register noch leer ist); sonst zoomt er heran wie jede andere Etage const OHNE_KELLER = Object.freeze({ freigabe: LINK_ETAGEN.freigabe }) -const linkEtagen = (stufe) => (kellerOffen(stufe) ? LINK_ETAGEN : OHNE_KELLER) +const linkEtagen = (stufe) => (kellerBegehbar(stufe) ? LINK_ETAGEN : OHNE_KELLER) const HAUS_RECHTECK = Object.freeze({ left: HAUS_BILD.x, top: HAUS_BILD.y, width: HAUS_BILD.w, height: HAUS_BILD.h }) @@ -220,7 +220,7 @@ const HochhausWelt = forwardRef(function HochhausWelt({ state, workers, onZeige, ) : null} - {/* Etagen: Knöpfe zoomen heran, Freigabe (und ab der Keller-Stufe der Keller) sind Links auf die anderen Bildschirme */} + {/* Etagen: Knöpfe zoomen heran, Freigabe (und ab der Stufe, ab der der Keller begehbar ist, der Keller) sind Links auf die anderen Bildschirme */} {ETAGEN.map((e) => { const stil = { left: ETAGEN_SPALTE.x, top: e.oben, width: ETAGEN_SPALTE.w, height: e.unten - e.oben } const link = links[e.id] diff --git a/src/components/workers/hochhaus/HustlerDialog.jsx b/src/components/workers/hochhaus/HustlerDialog.jsx index 7ef30c7..4d09fd2 100644 --- a/src/components/workers/hochhaus/HustlerDialog.jsx +++ b/src/components/workers/hochhaus/HustlerDialog.jsx @@ -27,7 +27,7 @@ const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v) // Einziger Ort des Dialogs, an dem ein Ziel für einen Link entsteht (wie TraceText in WorkerCard): nur was safeLink durchlässt, wird ein Link // (extern in neuem Tab mit noopener, intern über den Router), alles andere bleibt Text. Stufen-Sperre: ein interner Link in den Keller (?tab=botleads) bleibt -// unterhalb der Keller-Stufe (hustlerStufe.js) Text. +// unterhalb der Stufe, ab der der Keller begehbar ist (hustlerStufe.js, Stufe 3), Text. function SpurText({ link, stufe, children }) { const safe = isRecord(link) ? safeLink(link.href) : null if (!safe || !linkErlaubt(safe.href, stufe)) return <>{children} diff --git a/src/lib/hustlerStufe.js b/src/lib/hustlerStufe.js index ed1551f..109a60f 100644 --- a/src/lib/hustlerStufe.js +++ b/src/lib/hustlerStufe.js @@ -1,14 +1,18 @@ // Stufen-Sperre der Hustler-Seite: EINE Quelle der Wahrheit dafür, was die Oberfläche schon zeigen darf. Der Dienst (workers-dienst) wächst Stufe für Stufe // (P1, P3, P4, P6); die Oberfläche ist fertig und würde sonst Knöpfe und Bildschirme zeigen, deren Endpunkte noch 404 liefern. // Stufe < 4: im Pult keine Antwort-Knöpfe (paste „Antwort des Kunden einfügen“, reply „Als Mensch antworten“, approve-preview, start-build). -// Stufe < 6: kein Keller mit den Bot-Leads (kein Bildschirm, kein Link, kein Abruf von /api/workers/botleads); die Keller-Etage zoomt wie jede andere. +// Stufe < 3: kein Keller (keine Etage als Link, kein Bildschirm, keine Spur-Links in den Keller); die Keller-Etage zoomt wie jede andere. +// Stufe >= 3: der Keller ist begehbar (Keller-Etage, ?tab=botleads, Spur-Links): Arne, Regale und Register-Buch stehen da, das Register aber leer, ehrlich beschriftet. +// Stufe < 6: der Keller ruft die Bot-Leads nicht ab (kein GET /api/workers/botleads, auch kein JWT); der Endpunkt gibt es im Dienst erst mit P6. // Kein React, kein DOM, kein Appwrite: unter Node testbar (loadModule). Es gibt keine Überschreibung zur Laufzeit (kein localStorage, keine Adresse): die Stufe ist -// zur Bauzeit festgelegt. Nur der Prüfstand (scripts/workers-harness) baut mit VITE_HUSTLER_STUFE=6, damit seine P4- und Keller-Szenarien weiter laufen. +// zur Bauzeit festgelegt. Nur der Prüfstand (scripts/workers-harness) baut mit VITE_HUSTLER_STUFE=6, damit seine P4- und Keller-Szenarien (mit Live-Daten) weiter laufen. /** Ab dieser Stufe erscheinen die Antwort-Knöpfe im Pult (Dienst P4: paste, reply, approve-preview, start-build). */ export const STUFE_ANTWORTEN = 4 -/** Ab dieser Stufe erscheint der Keller mit den Bot-Leads (Dienst P6: GET /botleads). */ -export const STUFE_KELLER = 6 +/** Ab dieser Stufe ist der Keller begehbar: Keller-Etage als Link, Bildschirm ?tab=botleads, Spur-Links. Schon mit P3; ohne P6 bleibt das Register leer. */ +export const KELLER_BEGEHBAR_AB = 3 +/** Ab dieser Stufe ruft der Keller die Bot-Leads ab (Dienst P6: GET /botleads). Darunter keine Anfrage, auch kein JWT. */ +export const BOTLEADS_AB = 6 const KLEINSTE = 1 const GROESSTE = 9 @@ -33,7 +37,7 @@ function buildWert() { } } -// 3 = Dienst P3 live; ab 4 (P4) erscheinen die Antwort-Knöpfe im Pult, ab 6 (P6) der Keller mit den Bot-Leads; beim jeweiligen Livegang hier hochsetzen +// 3 = Dienst P3 live (der Keller ist begehbar, aber leer); ab 4 (P4) erscheinen die Antwort-Knöpfe im Pult, ab 6 (P6) liefert der Keller die Bot-Leads; beim jeweiligen Livegang hier hochsetzen export const HUSTLER_STUFE = stufeAus(buildWert(), 3) // Eine übergebene Stufe (Parameter der Funktionen und Bausteine, vor allem für Tests); fehlt sie oder ist sie unbrauchbar, gilt HUSTLER_STUFE @@ -42,21 +46,24 @@ const stufeOder = (stufe) => stufeAus(stufe, HUSTLER_STUFE) /** Zeigt das Pult die Antwort-Knöpfe (paste, reply, approve-preview, start-build)? Ab Stufe 4. */ export const antwortenOffen = (stufe = HUSTLER_STUFE) => stufeOder(stufe) >= STUFE_ANTWORTEN -/** Gibt es den Keller (Bildschirm Bot-Leads, Abruf von /botleads)? Ab Stufe 6. */ -export const kellerOffen = (stufe = HUSTLER_STUFE) => stufeOder(stufe) >= STUFE_KELLER +/** Ist der Keller begehbar (Keller-Etage als Link, Bildschirm Bot-Leads, Spur-Links)? Ab Stufe 3. Ob er Daten hat, sagt botleadsAbrufbar. */ +export const kellerBegehbar = (stufe = HUSTLER_STUFE) => stufeOder(stufe) >= KELLER_BEGEHBAR_AB -/** Darf dieser Bildschirm (Wert von ?tab=) auf der Stufe gezeigt werden? Nur der Keller ('botleads') ist gesperrt, alles andere ist frei. */ -export const tabErlaubt = (tabId, stufe = HUSTLER_STUFE) => tabId !== 'botleads' || kellerOffen(stufe) +/** Darf der Keller die Bot-Leads abrufen (GET /botleads, Dienst P6)? Ab Stufe 6. Darunter zeigt er sein leeres Register und fragt nichts an. */ +export const botleadsAbrufbar = (stufe = HUSTLER_STUFE) => stufeOder(stufe) >= BOTLEADS_AB + +/** Darf dieser Bildschirm (Wert von ?tab=) auf der Stufe gezeigt werden? Nur der Keller ('botleads') ist an die Stufe gebunden (ab 3), alles andere ist frei. */ +export const tabErlaubt = (tabId, stufe = HUSTLER_STUFE) => tabId !== 'botleads' || kellerBegehbar(stufe) // Adressen, unter denen die Hustler-Seite liegt: /hustler und die alten /workers und /leads (App.jsx leitet sie weiter, die Query bleibt) const SEITEN_PFADE = ['/hustler', '/workers', '/leads'] /** * Darf ein interner Link (aus der Spur im Dialog) auf der Stufe ein Link sein? Ein Link in den Keller (…/hustler?tab=botleads, auch über /workers und /leads) - * ist unterhalb der Keller-Stufe nur Text; alles andere bleibt ein Link. Externe Adressen berührt das nicht. + * ist unterhalb der Stufe, ab der der Keller begehbar ist (3), nur Text; alles andere bleibt ein Link. Externe Adressen berührt das nicht. */ export function linkErlaubt(href, stufe = HUSTLER_STUFE) { - if (kellerOffen(stufe)) return true + if (kellerBegehbar(stufe)) return true if (typeof href !== 'string' || !/^\/(?![/\\])/.test(href)) return true // nur interne Pfade (ein einzelner / am Anfang), nie //host oder eine ganze Adresse let ziel try { diff --git a/src/lib/workersApi.js b/src/lib/workersApi.js index 30538d6..0fd7823 100644 --- a/src/lib/workersApi.js +++ b/src/lib/workersApi.js @@ -2,15 +2,15 @@ // Rein: kein Appwrite-Import, getJwt und fetch werden injiziert (workersClient.js verdrahtet sie), damit die // deutschen Fehlertexte unter Node testbar sind. Fehler sind Error mit deutscher `message` und `status`. -import { HUSTLER_STUFE, kellerOffen } from './hustlerStufe' +import { HUSTLER_STUFE, botleadsAbrufbar } from './hustlerStufe' export const DOWN_TEXT = 'Der Hustler-Dienst ist gerade nicht erreichbar.' export const BAD_REPLY_TEXT = 'Der Hustler-Dienst antwortet nicht richtig.' export const SESSION_TEXT = 'Anmeldung abgelaufen. Bitte neu anmelden.' export const FORBIDDEN_TEXT = 'Dafür fehlt dir die Berechtigung.' export const INVALID_ID_TEXT = 'Ungültige Kennung.' -// Der Keller (GET /botleads) gehört zu Stufe 6 (hustlerStufe.js); darunter gibt es den Endpunkt im Dienst noch nicht -export const KELLER_GESPERRT_TEXT = 'Der Keller ist noch nicht freigeschaltet.' +// Die Bot-Leads (GET /botleads) gehören zu Stufe 6 (hustlerStufe.js, BOTLEADS_AB); darunter gibt es den Endpunkt im Dienst noch nicht +export const BOTLEADS_GESPERRT_TEXT = 'Die Bot-Leads sind noch nicht freigeschaltet.' // Zeitgrenze je Abruf (JWT holen, senden, Antwort lesen): ohne sie bliebe ein Abruf auf toter Verbindung (Rechner im Ruhezustand, // Netzwechsel) minutenlang offen und der Feed (ein Abruf je Schleife) stünde still, ohne Fehlermeldung. @@ -198,9 +198,9 @@ export function createWorkersApi({ if (!PULT_ACTIONS.includes(action)) throw statusError(`Unbekannte Aktion: ${action}`, 400) return request(`/pult/${pathId(id)}/${action}`, { method: 'POST', body, limitMs: SLOW_PULT_ACTIONS.includes(action) ? actionTimeoutMs : undefined }) }, - // Unterhalb der Keller-Stufe (6) geht gar keine Anfrage hinaus: der Endpunkt fehlt im Dienst noch (die Oberfläche zeigt den Keller dort auch nicht) + // Unterhalb der Stufe 6 (BOTLEADS_AB) geht gar keine Anfrage hinaus: der Endpunkt fehlt im Dienst noch (der Keller ruft ihn dort auch nicht auf, er zeigt sein leeres Register) botleads: async ({ room, q, page, id } = {}) => { - if (!kellerOffen(stufe)) throw statusError(KELLER_GESPERRT_TEXT, 404) + if (!botleadsAbrufbar(stufe)) throw statusError(BOTLEADS_GESPERRT_TEXT, 404) return request(`/botleads${query([['room', room], ['q', q], ['page', page], ['id', id]])}`) }, prospect: async (id) => request(`/prospect/${pathId(id)}`), diff --git a/src/pages/WorkersPage.jsx b/src/pages/WorkersPage.jsx index 19d9ddf..0fbb835 100644 --- a/src/pages/WorkersPage.jsx +++ b/src/pages/WorkersPage.jsx @@ -24,8 +24,9 @@ import '../styles/hustler/hustler-regie.css' // Bildschirme der Seite: das Hochhaus, das Pult (Freigabe von P3) und der Keller (Bot-Leads von P6). Eine Reiterleiste gibt es nicht mehr; // gewechselt wird im Haus (Etage Freigabe, Etage Keller) und über „◂ Hochhaus“ in Pult und Keller. -// Stufen-Sperre (lib/hustlerStufe.js, HUSTLER_STUFE): der Keller ist erst ab Stufe 6 ein Bildschirm. Darunter fallen ?tab=botleads (auch mit &bot=) und jeder -// andere unbekannte Wert auf das Hochhaus zurück (pickTab nimmt den ersten Reiter), BotLeadList wird nie eingehängt und /api/workers/botleads nie abgerufen. +// Stufen-Sperre (lib/hustlerStufe.js, HUSTLER_STUFE): der Keller ist ab Stufe 3 ein Bildschirm (begehbar), die Bot-Leads ruft er aber erst ab Stufe 6 ab; darunter zeigt +// BotLeadList sein leeres Register und fragt /api/workers/botleads nie an. Unterhalb von Stufe 3 fallen ?tab=botleads (auch mit &bot=) und jeder andere unbekannte Wert +// auf das Hochhaus zurück (pickTab nimmt den ersten Reiter), BotLeadList wird dann nie eingehängt. const WORKERS_TABS = [ { id: 'hochhaus', label: 'Hochhaus' }, { id: 'freigabe', label: 'Freigabe' }, @@ -104,6 +105,7 @@ export default function WorkersPage({ stufe = HUSTLER_STUFE }) { {active.id === 'botleads' && ( setParams({ tab: 'botleads' })} kpis={state?.kpis} diff --git a/tests/unit/hustlerDialog.test.mjs b/tests/unit/hustlerDialog.test.mjs index 808990b..8b3b908 100644 --- a/tests/unit/hustlerDialog.test.mjs +++ b/tests/unit/hustlerDialog.test.mjs @@ -385,8 +385,8 @@ test('Spur-Links: unsichere (javascript:, //host, /\\host, data:) bleiben Text; }) }) -// Stufen-Sperre (hustlerStufe.js): unterhalb der Keller-Stufe (6) führt auch kein Spur-Link in den Keller; der Eintrag bleibt als Text stehen -test('Spur-Links in den Keller (?tab=botleads, auch über /workers und /leads) sind auf Stufe 3 Text und ab Stufe 6 Router-Links; alle anderen Links bleiben', async () => { +// Stufen-Sperre (hustlerStufe.js): unterhalb von Stufe 3 (KELLER_BEGEHBAR_AB) führt auch kein Spur-Link in den Keller; der Eintrag bleibt als Text stehen +test('Spur-Links in den Keller (?tab=botleads, auch über /workers und /leads) sind auf Stufe 1 und 2 Text und ab Stufe 3 Router-Links; alle anderen Links bleiben', async () => { const events = [ { id: '1', ts: '2026-10-22T09:00:00Z', text: 'Keller', link: '/hustler?tab=botleads&bot=abc123' }, { id: '2', ts: '2026-10-22T08:59:00Z', text: 'Alt', link: '/workers?tab=botleads&bot=abc123' }, @@ -396,7 +396,7 @@ test('Spur-Links in den Keller (?tab=botleads, auch über /workers und /leads) s { id: '6', ts: '2026-10-22T08:55:00Z', text: 'Extern', link: 'https://example.org/hustler?tab=botleads' }, ] const ziele = (li) => nodes(li, (n) => n.tag === 'a' || n.tag === 'router-link') - for (const stufe of [1, 3, 5]) { + for (const stufe of [1, 2]) { await lauf({ props: { workerId: 'lea', stufe }, trace: { trace: { events }, loading: false, error: null } }, (ui) => { const zeilen = ui.spurZeilen(); const byText = (t) => zeilen.find((z) => z.text === t) assert.equal(zeilen.length, 6) @@ -407,7 +407,7 @@ test('Spur-Links in den Keller (?tab=botleads, auch über /workers und /leads) s assert.ok(!JSON.stringify(ui.find((x) => x.tag === 'a' || x.tag === 'router-link').map((n) => n.props)).includes('botleads&bot')) }) } - for (const stufe of [6, 9]) { + for (const stufe of [3, 5, 6, 9]) { await lauf({ props: { workerId: 'lea', stufe }, trace: { trace: { events }, loading: false, error: null } }, (ui) => { const zeilen = ui.spurZeilen(); const byText = (t) => zeilen.find((z) => z.text === t) assert.deepEqual(ziele(byText('Keller').li).map((n) => n.props.to), ['/hustler?tab=botleads&bot=abc123']) diff --git a/tests/unit/hustlerHochhausWelt.test.mjs b/tests/unit/hustlerHochhausWelt.test.mjs index 13e5c77..1e0eab1 100644 --- a/tests/unit/hustlerHochhausWelt.test.mjs +++ b/tests/unit/hustlerHochhausWelt.test.mjs @@ -121,7 +121,7 @@ async function start(initial = {}) { const state = initial.state === undefined ? makeState() : initial.state const handle = { current: null } // Stufen-Sperre (hustlerStufe.js): die Zusicherungen dieser Datei gelten für das volle Haus (Stufe 6, Keller offen, Verhalten wie vor der Sperre); - // stufe: 3 (Keller gesperrt) steht in den eigenen Tests am Ende des Abschnitts „Etagen“ + // stufe 1 und 2 (Keller zu) und 3 (Keller begehbar, leer) stehen in den eigenen Tests am Ende des Abschnitts „Etagen“ let props = { state, workers: state ? indexWorkers(state) : {}, onZeige() {}, onOpen() {}, stufe: 6, ...initial } const mounted = __mount(jsx(() => jsx(HochhausWelt, { ...props, ref: handle }), {})) await settle() @@ -480,10 +480,10 @@ test('Etagen: ein Etagen-Knopf zoomt heran (Kamera wie kameraEtage), ohne onOpen } finally { ui.end(); restore() } }) -// Stufen-Sperre: unterhalb der Keller-Stufe (6) gibt es keinen Weg in den Keller; die Etage verhält sich wie jede andere (zoomt heran), Arne bleibt klickbar -test('Etagen bei Stufe 3 und 5: nur Freigabe ist ein Link, die Keller-Etage ist ein Knopf wie die anderen (Beschriftung, Text, kein href)', async () => { +// Stufen-Sperre: unterhalb von Stufe 3 (KELLER_BEGEHBAR_AB) gibt es keinen Weg in den Keller; die Etage verhält sich wie jede andere (zoomt heran), Arne bleibt klickbar +test('Etagen bei Stufe 1 und 2: nur Freigabe ist ein Link, die Keller-Etage ist ein Knopf wie die anderen (Beschriftung, Text, kein href)', async () => { const { ETAGEN } = await laden() - for (const stufe of [1, 3, 5]) { + for (const stufe of [1, 2]) { const ui = await start({ stufe }) try { const etagen = byClass(ui.tree(), 'hk-etage') @@ -507,11 +507,11 @@ test('Etagen bei Stufe 3 und 5: nur Freigabe ist ein Link, die Keller-Etage ist } }) -test('Stufe 3: Klick auf die Keller-Etage zoomt heran und öffnet keinen Bildschirm; die Freigabe-Etage öffnet weiter das Pult', async () => { +test('Stufe 1 und 2: Klick auf die Keller-Etage zoomt heran und öffnet keinen Bildschirm; die Freigabe-Etage öffnet weiter das Pult', async () => { const { kameraEtage, ETAGEN } = await laden() const geoeffnet = [] const restore = fensterAttrappe() - const ui = await start({ stufe: 3, onOpen: (tab) => geoeffnet.push(tab) }) + const ui = await start({ stufe: 2, onOpen: (tab) => geoeffnet.push(tab) }) try { ui.anhaengen() const keller = ETAGEN.find((e) => e.id === 'keller') @@ -540,13 +540,17 @@ test('Stufe 3: Arne bleibt im Haus sichtbar und klickbar (Detail-Dialog), auch d } finally { ui.end() } }) -test('Stufe 6 und höher: die Keller-Etage bleibt der Link auf die Bot-Leads (wie vor der Sperre)', async () => { - for (const stufe of [6, 7, 9]) { - const ui = await start({ stufe }) +// Ab Stufe 3 ist der Keller begehbar (auch ohne Bot-Leads): die Etage ist der Link, genau wie mit Live-Daten ab Stufe 6 +test('Ab Stufe 3 (Produktion) und höher: die Keller-Etage ist der Link auf den Keller (wie vor der Sperre), Klick wechselt den Bildschirm', async () => { + for (const stufe of [3, 4, 5, 6, 7, 9]) { + const geoeffnet = [] + const ui = await start({ stufe, onOpen: (tab) => geoeffnet.push(tab) }) try { const keller = ui.etage('Keller · Bot-Leads ▸') assert.equal(keller.tag, 'a', `Stufe ${stufe}`); assert.equal(keller.props.href, '/hustler?tab=botleads') assert.equal(keller.props['aria-label'], 'Keller · Bot-Leads: Bot-Leads öffnen') + const e = ereignis(); keller.props.onClick(e) + assert.equal(e.defaultPrevented, true, `Stufe ${stufe}`); assert.deepEqual(geoeffnet, ['botleads'], `Stufe ${stufe}`) } finally { ui.end() } } }) diff --git a/tests/unit/hustlerSeite.test.mjs b/tests/unit/hustlerSeite.test.mjs index 6979f3c..874ffbf 100644 --- a/tests/unit/hustlerSeite.test.mjs +++ b/tests/unit/hustlerSeite.test.mjs @@ -269,7 +269,7 @@ const loadSeite = () => (seiteBundle ??= esbuild.build({ // Ein Lauf der Seite. query ist der Teil nach dem ?, feed wird vom Test zwischendurch geändert (setFeed) und die Seite neu gerendert. // stufe (Stufen-Sperre, hustlerStufe.js): die Zusicherungen dieser Datei gelten für die volle Seite (Stufe 6, Keller offen, Verhalten wie vor der Sperre); -// die Tests „Stufe 3“ weiter unten bauen die Seite mit stufe: 3 (Stand der Produktion). +// die Tests „Stufe 1 und 2“ und „Stufe 3“ weiter unten bauen die Seite mit stufe: 1/2 (Keller zu) und 3 (Stand der Produktion: Keller begehbar, ohne Bot-Leads). async function seite(query, { admin = false, feed, stufe = 6 } = {}) { const { __mount, jsx, WorkersPage } = await loadSeite() const setParamsCalls = [] @@ -330,13 +330,13 @@ test('Seite: ?tab=freigabe zeigt das Pult, ?tab=botleads den Keller, beide direk } finally { keller.end() } }) -// ---------- Stufen-Sperre (Stufe 3 = Stand der Produktion: Dienst P3, ohne Keller) ---------- +// ---------- Stufen-Sperre (Keller begehbar ab Stufe 3 = Stand der Produktion: Dienst P3, Keller ohne Bot-Leads) ---------- -// Adressen, die auf Stufe 3 in den Keller führen würden: der Reiter, mit Einzelfall, mit Ballast, andere Schreibweisen und erfundene Parameter +// Adressen, die in den Keller führen würden: der Reiter, mit Einzelfall, mit Ballast, andere Schreibweisen und erfundene Parameter const KELLER_ADRESSEN = ['tab=botleads', 'tab=botleads&bot=abc123', 'bot=abc123&tab=botleads', 'tab=botleads&bot=', 'tab=BotLeads', 'ansicht=keller', 'tab=keller', 'tab=botleads&ansicht=keller'] -test('Stufe 3: jede Adresse in den Keller fällt auf das Hochhaus zurück; der Keller wird nie eingehängt, Hochhaus und Seite bleiben ganz', async () => { - for (const stufe of [1, 3, 5]) { +test('Stufe 1 und 2: jede Adresse in den Keller fällt auf das Hochhaus zurück; der Keller wird nie eingehängt, Hochhaus und Seite bleiben ganz', async () => { + for (const stufe of [1, 2]) { for (const query of KELLER_ADRESSEN) { const ui = await seite(query, { stufe, feed: { state: makeState(), error: null, loaded: true, refresh() {} } }) try { @@ -351,8 +351,8 @@ test('Stufe 3: jede Adresse in den Keller fällt auf das Hochhaus zurück; der K } }) -test('Stufe 3: der Wechsel der Adresse zu ?tab=botleads zeigt das Hochhaus, Freigabe bleibt erreichbar; der Rückweg aus dem Pult funktioniert wie sonst', async () => { - const ui = await seite('tab=freigabe', { stufe: 3, feed: { state: makeState(), error: null, loaded: true, refresh() {} } }) +test('Stufe 2: der Wechsel der Adresse zu ?tab=botleads zeigt das Hochhaus, Freigabe bleibt erreichbar; der Rückweg aus dem Pult funktioniert wie sonst', async () => { + const ui = await seite('tab=freigabe', { stufe: 2, feed: { state: makeState(), error: null, loaded: true, refresh() {} } }) try { assert.deepEqual(ui.screen(), ['pult']) ui.go('tab=botleads&bot=x'); assert.deepEqual(ui.screen(), ['hochhaus']); assert.equal(ui.hk.props.hochhaus.vonTab, 'freigabe') @@ -362,32 +362,51 @@ test('Stufe 3: der Wechsel der Adresse zu ?tab=botleads zeigt das Hochhaus, Frei } finally { ui.end() } }) -test('Stufe 3: das Hochhaus öffnet nur noch die Freigabe (onOpen(\'botleads\') tut nichts); Hochhaus und Pult bekommen die Stufe', async () => { - const ui = await seite('tab=hochhaus', { stufe: 3, feed: { state: makeState(), error: null, loaded: true, refresh() {} } }) +test('Stufe 2: das Hochhaus öffnet nur noch die Freigabe (onOpen(\'botleads\') tut nichts); Hochhaus und Pult bekommen die Stufe', async () => { + const ui = await seite('tab=hochhaus', { stufe: 2, feed: { state: makeState(), error: null, loaded: true, refresh() {} } }) try { const p = ui.hk.props.hochhaus p.onOpen('botleads'); p.onOpen('freigabe') assert.deepEqual(ui.setParamsCalls, [{ tab: 'freigabe' }]) ui.go('tab=freigabe') - assert.equal(ui.hk.props.pult.stufe, 3) + assert.equal(ui.hk.props.pult.stufe, 2) ui.hk.props.pult.onBack() assert.deepEqual(ui.setParamsCalls, [{ tab: 'freigabe' }, { tab: 'hochhaus' }]) } finally { ui.end() } }) -test('Stufe 6 und höher: ?tab=botleads zeigt den Keller samt Einzelfall (bot), onOpen(\'botleads\') wechselt; Stufe 4 und 5 haben den Keller noch nicht', async () => { - for (const stufe of [6, 8, 9]) { +// Stufe 3 (Produktion) und höher: der Keller ist ein Bildschirm; die Stufe geht an BotLeadList, die darunter von Stufe 6 entscheidet, ob sie Bot-Leads abruft (botleadList.test.mjs) +test('Stufe 3 bis 9: ?tab=botleads (auch alle Schreibweisen mit tab=botleads) zeigt den Keller samt Einzelfall (bot) und bekommt die Stufe; onOpen(\'botleads\') wechselt; Zurück ins Hochhaus', async () => { + for (const stufe of [3, 4, 5, 6, 8, 9]) { const ui = await seite('tab=botleads&bot=abc123', { stufe, feed: { state: makeState(), error: null, loaded: true, refresh() {} } }) try { - assert.deepEqual(ui.screen(), ['keller'], `Stufe ${stufe}`); assert.equal(ui.hk.props.keller.bot, 'abc123') + assert.deepEqual(ui.screen(), ['keller'], `Stufe ${stufe}`); assert.equal(ui.hk.props.keller.bot, 'abc123'); assert.equal(ui.hk.props.keller.stufe, stufe) + assert.equal(ui.root().props['data-tab'], 'botleads', `Stufe ${stufe}`) + ui.hk.props.keller.onBack(); ui.hk.props.keller.onClearBot() + assert.deepEqual(ui.setParamsCalls, [{ tab: 'hochhaus' }, { tab: 'botleads' }], `Stufe ${stufe}`) ui.go('tab=hochhaus'); ui.hk.props.hochhaus.onOpen('botleads') - assert.deepEqual(ui.setParamsCalls, [{ tab: 'botleads' }], `Stufe ${stufe}`) + assert.deepEqual(ui.setParamsCalls, [{ tab: 'hochhaus' }, { tab: 'botleads' }, { tab: 'botleads' }], `Stufe ${stufe}`) } finally { ui.end() } } - for (const stufe of [4, 5]) { - const ui = await seite('tab=botleads', { stufe }) - try { assert.deepEqual(ui.screen(), ['hochhaus'], `Stufe ${stufe}`) } finally { ui.end() } + for (const query of ['tab=botleads', 'bot=abc123&tab=botleads', 'tab=botleads&bot=']) { + const ui = await seite(query, { stufe: 3, feed: { state: makeState(), error: null, loaded: true, refresh() {} } }) + try { assert.deepEqual(ui.screen(), ['keller'], query) } finally { ui.end() } } + // erfundene Werte bleiben beim Hochhaus, auch auf Stufe 3 + for (const query of ['tab=BotLeads', 'ansicht=keller', 'tab=keller']) { + const ui = await seite(query, { stufe: 3, feed: { state: makeState(), error: null, loaded: true, refresh() {} } }) + try { assert.deepEqual(ui.screen(), ['hochhaus'], query) } finally { ui.end() } + } +}) + +test('Stufe 3: Hochhaus und Keller bekommen die Stufe, der Keller zählt als Bildschirm der Seite (data-tab); Pause und Kennzahlen gehen an ihn', async () => { + const state = makeState({ kpis: { pultOpen: 2, dealsMonth: 1, botLeads: 0 } }) + const ui = await seite('tab=botleads', { stufe: 3, feed: { state, error: null, loaded: true, refresh() {} } }) + try { + assert.equal(ui.hk.props.keller.stufe, 3); assert.equal(ui.hk.props.keller.kpis, state.kpis); assert.equal(ui.hk.props.keller.paused, false) + ui.go('tab=hochhaus') + assert.equal(ui.hk.props.hochhaus.stufe, 3); assert.equal(ui.hk.props.hochhaus.vonTab, 'botleads') + } finally { ui.end() } }) test('Seite: keine Seitenüberschrift, keine Reiterleiste', async () => { diff --git a/tests/unit/hustlerStufe.test.mjs b/tests/unit/hustlerStufe.test.mjs index f62fc11..09e1719 100644 --- a/tests/unit/hustlerStufe.test.mjs +++ b/tests/unit/hustlerStufe.test.mjs @@ -7,15 +7,16 @@ import esbuild from 'esbuild' import { loadModule } from './load.mjs' // Stufen-Sperre der Hustler-Seite (src/lib/hustlerStufe.js): eine Quelle der Wahrheit, was die Oberfläche schon zeigen darf. -// Stufe 3 = Dienst P3 live (Stand der Produktion): kein Keller (Bot-Leads, P6), keine Antwort-Knöpfe im Pult (P4). Ab Stufe 4 kommen die Antwort-Knöpfe, ab 6 der Keller. -// Diese Datei prüft das Modul selbst (Stufe aus dem Bau, Schwellen, Bildschirme, Links). Die Bausteine prüfen ihre Sperre in ihren eigenen Tests, jeweils mit Stufe 3 (gesperrt) -// und Stufe 6 (offen): rowModel in workersPult.test.mjs, der Client in workersApi.test.mjs, Pult in workersPultUi.test.mjs, Haus in hustlerHochhausWelt.test.mjs, Seite in -// hustlerSeite.test.mjs, Spur-Links in hustlerDialog.test.mjs, der Prüfstand in workersHarness.test.mjs. +// Stufe 3 = Dienst P3 live (Stand der Produktion): der Keller ist begehbar (Etage, Bildschirm, Spur-Links), aber leer und ohne Abruf der Bot-Leads (P6), und es gibt keine +// Antwort-Knöpfe im Pult (P4). Ab Stufe 4 kommen die Antwort-Knöpfe, ab 6 liefert der Keller die Bot-Leads. +// Diese Datei prüft das Modul selbst (Stufe aus dem Bau, Schwellen, Bildschirme, Links). Die Bausteine prüfen ihre Sperre in ihren eigenen Tests, jeweils mit Stufe 3 (Keller leer) +// und Stufe 6 (Live-Daten), dazu 1 (Keller zu): rowModel in workersPult.test.mjs, der Client in workersApi.test.mjs, Pult in workersPultUi.test.mjs, Haus in hustlerHochhausWelt.test.mjs, +// Seite in hustlerSeite.test.mjs, Keller in botleadList.test.mjs, Spur-Links in hustlerDialog.test.mjs, der Prüfstand in workersHarness.test.mjs. const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..') const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8') const stufe = await loadModule('src/lib/hustlerStufe.js') -const { HUSTLER_STUFE, STUFE_ANTWORTEN, STUFE_KELLER, stufeAus, antwortenOffen, kellerOffen, tabErlaubt, linkErlaubt } = stufe +const { HUSTLER_STUFE, STUFE_ANTWORTEN, KELLER_BEGEHBAR_AB, BOTLEADS_AB, stufeAus, antwortenOffen, kellerBegehbar, botleadsAbrufbar, tabErlaubt, linkErlaubt } = stufe // ---------- Stufe aus dem Bau ---------- @@ -36,13 +37,13 @@ test('Standard: ohne Wert aus dem Bau gilt die Stufe aus dem Quelltext, einmal u assert.equal(HUSTLER_STUFE, Number(treffer[0][1])) assert.ok(Number.isInteger(HUSTLER_STUFE) && HUSTLER_STUFE >= 1 && HUSTLER_STUFE <= 9) assert.ok(quelltext.includes('beim jeweiligen Livegang hier hochsetzen')) - assert.equal(STUFE_ANTWORTEN, 4); assert.equal(STUFE_KELLER, 6) + assert.equal(STUFE_ANTWORTEN, 4); assert.equal(KELLER_BEGEHBAR_AB, 3); assert.equal(BOTLEADS_AB, 6) }) // Der Wert aus dem Bau (Vite: import.meta.env.VITE_HUSTLER_STUFE, Prüfstand: define von esbuild) wird beim Bauen eingesetzt. Hier derselbe Weg mit esbuild. async function stufeNachBau(define) { const out = await esbuild.build({ - stdin: { contents: "export { HUSTLER_STUFE, antwortenOffen, kellerOffen } from './src/lib/hustlerStufe.js'", resolveDir: ROOT, loader: 'js' }, + stdin: { contents: "export { HUSTLER_STUFE, antwortenOffen, kellerBegehbar, botleadsAbrufbar } from './src/lib/hustlerStufe.js'", resolveDir: ROOT, loader: 'js' }, bundle: true, format: 'esm', platform: 'neutral', write: false, define, }) return import(`data:text/javascript;base64,${Buffer.from(out.outputFiles[0].text).toString('base64')}`) @@ -52,7 +53,7 @@ test('Bau: VITE_HUSTLER_STUFE überschreibt die Stufe zur Bauzeit (Prüfstand); for (const [roh, erwartet] of [['6', 6], ['4', 4], ['9', 9], ['1', 1], [' 6 ', 6], ['3', 3], ['0', HUSTLER_STUFE], ['10', HUSTLER_STUFE], ['abc', HUSTLER_STUFE], ['', HUSTLER_STUFE], ['6.5', HUSTLER_STUFE], ['-2', HUSTLER_STUFE]]) { const m = await stufeNachBau({ 'import.meta.env.VITE_HUSTLER_STUFE': JSON.stringify(roh) }) assert.equal(m.HUSTLER_STUFE, erwartet, `VITE_HUSTLER_STUFE=${JSON.stringify(roh)}`) - assert.equal(m.kellerOffen(), erwartet >= 6); assert.equal(m.antwortenOffen(), erwartet >= 4) + assert.equal(m.kellerBegehbar(), erwartet >= 3); assert.equal(m.botleadsAbrufbar(), erwartet >= 6); assert.equal(m.antwortenOffen(), erwartet >= 4) } // nicht gesetzt (Vite: undefined) und ohne import.meta.env (Node): der Standard assert.equal((await stufeNachBau({ 'import.meta.env.VITE_HUSTLER_STUFE': 'undefined' })).HUSTLER_STUFE, HUSTLER_STUFE) @@ -72,41 +73,48 @@ test('keine Überschreibung zur Laufzeit: kein localStorage, keine Adresse, kein // ---------- Schwellen ---------- -test('Schwellen: Antwort-Knöpfe ab Stufe 4, Keller ab Stufe 6; unbrauchbare Stufe gilt als HUSTLER_STUFE', () => { +test('Schwellen: Keller begehbar ab Stufe 3, Antwort-Knöpfe ab Stufe 4, Bot-Leads abrufbar ab Stufe 6; unbrauchbare Stufe gilt als HUSTLER_STUFE', () => { for (let n = 1; n <= 9; n += 1) { + assert.equal(kellerBegehbar(n), n >= 3, `kellerBegehbar(${n})`) assert.equal(antwortenOffen(n), n >= 4, `antwortenOffen(${n})`) - assert.equal(kellerOffen(n), n >= 6, `kellerOffen(${n})`) + assert.equal(botleadsAbrufbar(n), n >= 6, `botleadsAbrufbar(${n})`) } - assert.deepEqual([antwortenOffen(3), kellerOffen(3)], [false, false]) - assert.deepEqual([antwortenOffen(6), kellerOffen(6)], [true, true]) + // Stufe 3 (Produktion): Keller begehbar, aber weder Antwort-Knöpfe noch Abruf der Bot-Leads; der Abruf setzt die Begehbarkeit voraus + assert.deepEqual([kellerBegehbar(3), antwortenOffen(3), botleadsAbrufbar(3)], [true, false, false]) + assert.deepEqual([kellerBegehbar(2), antwortenOffen(2), botleadsAbrufbar(2)], [false, false, false]) + assert.deepEqual([kellerBegehbar(6), antwortenOffen(6), botleadsAbrufbar(6)], [true, true, true]) + assert.ok(KELLER_BEGEHBAR_AB <= BOTLEADS_AB) for (const kaputt of [undefined, null, 'x', 0, 12, NaN]) { - assert.equal(antwortenOffen(kaputt), antwortenOffen(HUSTLER_STUFE)); assert.equal(kellerOffen(kaputt), kellerOffen(HUSTLER_STUFE)) + assert.equal(antwortenOffen(kaputt), antwortenOffen(HUSTLER_STUFE)); assert.equal(kellerBegehbar(kaputt), kellerBegehbar(HUSTLER_STUFE)); assert.equal(botleadsAbrufbar(kaputt), botleadsAbrufbar(HUSTLER_STUFE)) } - assert.equal(antwortenOffen(), antwortenOffen(HUSTLER_STUFE)); assert.equal(kellerOffen(), kellerOffen(HUSTLER_STUFE)) + assert.equal(antwortenOffen(), antwortenOffen(HUSTLER_STUFE)); assert.equal(kellerBegehbar(), kellerBegehbar(HUSTLER_STUFE)); assert.equal(botleadsAbrufbar(), botleadsAbrufbar(HUSTLER_STUFE)) }) // ---------- Bildschirme und Deeplinks ---------- -test('tabErlaubt: nur der Keller (botleads) ist gesperrt, und nur unterhalb von Stufe 6', () => { - for (const s of [1, 3, 4, 5]) { +test('tabErlaubt: nur der Keller (botleads) hängt an der Stufe, und nur unterhalb von Stufe 3 ist er gesperrt (ab 3 begehbar, auch ohne Bot-Leads)', () => { + for (const s of [1, 2]) { assert.equal(tabErlaubt('botleads', s), false, `Stufe ${s}`) for (const frei of ['hochhaus', 'freigabe', '', 'unbekannt', undefined]) assert.equal(tabErlaubt(frei, s), true, `${frei} auf Stufe ${s}`) } - for (const s of [6, 7, 9]) assert.equal(tabErlaubt('botleads', s), true, `Stufe ${s}`) + for (const s of [3, 4, 5, 6, 7, 9]) { + assert.equal(tabErlaubt('botleads', s), true, `Stufe ${s}`) + for (const frei of ['hochhaus', 'freigabe', '', 'unbekannt', undefined]) assert.equal(tabErlaubt(frei, s), true, `${frei} auf Stufe ${s}`) + } }) -test('linkErlaubt: Links in den Keller sind unterhalb von Stufe 6 kein Link, alles andere bleibt unberührt', () => { +test('linkErlaubt: Links in den Keller sind unterhalb von Stufe 3 kein Link und ab 3 einer, alles andere bleibt unberührt', () => { const keller = [ '/hustler?tab=botleads', '/hustler?tab=botleads&bot=abc123', '/hustler/?tab=botleads', '/Hustler?tab=botleads', '/hustler?bot=x&tab=botleads', '/workers?tab=botleads&bot=x', '/leads?tab=botleads', '/hustler?tab=botleads#oben', ] for (const href of keller) { - assert.equal(linkErlaubt(href, 3), false, `Stufe 3: ${href}`); assert.equal(linkErlaubt(href, 5), false, `Stufe 5: ${href}`) - assert.equal(linkErlaubt(href, 6), true, `Stufe 6: ${href}`) + assert.equal(linkErlaubt(href, 1), false, `Stufe 1: ${href}`); assert.equal(linkErlaubt(href, 2), false, `Stufe 2: ${href}`) + for (const s of [3, 5, 6]) assert.equal(linkErlaubt(href, s), true, `Stufe ${s}: ${href}`) } const frei = [ '/hustler?tab=freigabe', '/hustler', '/hustler?tab=hochhaus', '/hustler?bot=abc123', '/tickets/100001', '/customers/abc', '/other?tab=botleads', '/hustler/anderes?tab=botleads', 'https://beispiel.de/hustler?tab=botleads', 'http://x.example/?tab=botleads', '//evil.example/hustler?tab=botleads', '/\\evil.example/hustler?tab=botleads', '', 'kein-pfad', null, undefined, 42, ] - for (const href of frei) { assert.equal(linkErlaubt(href, 3), true, `Stufe 3: ${String(href)}`); assert.equal(linkErlaubt(href, 6), true, `Stufe 6: ${String(href)}`) } + for (const href of frei) { for (const s of [1, 3, 6]) assert.equal(linkErlaubt(href, s), true, `Stufe ${s}: ${String(href)}`) } }) diff --git a/tests/unit/workersApi.test.mjs b/tests/unit/workersApi.test.mjs index 2d7ec01..5bdbd8d 100644 --- a/tests/unit/workersApi.test.mjs +++ b/tests/unit/workersApi.test.mjs @@ -3,7 +3,7 @@ import assert from 'node:assert/strict' import { loadModule } from './load.mjs' // API-Client der Hustler-Seite: URLs, Methoden, Header, deutsche Fehlertexte, JWT-Wiederholung, Pult-Prüfungen -const { createWorkersApi, REQUEST_TIMEOUT_MS, AUTH_COOLDOWN_MS, ACTION_TIMEOUT_MS, SLOW_PULT_ACTIONS, UNCERTAIN_STATUS, KELLER_GESPERRT_TEXT } = await loadModule('src/lib/workersApi.js') +const { createWorkersApi, REQUEST_TIMEOUT_MS, AUTH_COOLDOWN_MS, ACTION_TIMEOUT_MS, SLOW_PULT_ACTIONS, UNCERTAIN_STATUS, BOTLEADS_GESPERRT_TEXT } = await loadModule('src/lib/workersApi.js') const { actionErrorText, UNCERTAIN_TEXT } = await loadModule('src/lib/workersPult.js') const { createJwtCache } = await loadModule('src/lib/serverpulsAuth.js') @@ -707,15 +707,14 @@ test('createJWT: Übertragungsfehler beim frischen JWT nach einer 401 sperrt nic assert.deepEqual(mints, [false, true, false, true]) // keine Sperre: weder für frische JWTs noch für createJWT überhaupt }) -// ---------- Stufen-Sperre (hustlerStufe.js): kein Abruf des Kellers unterhalb von Stufe 6 ---------- - +// ---------- Stufen-Sperre (hustlerStufe.js): kein Abruf der Bot-Leads unterhalb von Stufe 6 (BOTLEADS_AB; der Keller selbst ist schon ab Stufe 3 begehbar, dann leer) ---------- test('API: auf Stufe 3 geht kein Abruf von /api/workers/botleads hinaus (auch kein JWT), auf Stufe 6 wie bisher', async () => { - for (const s of [1, 3, 5]) { + for (const s of [1, 3, 4, 5]) { const f = fakeFetch(okReplies(8)); let jwtGeholt = 0 const api = createWorkersApi({ getJwt: async () => { jwtGeholt += 1; return 'jwt' }, fetchImpl: f, stufe: s }) for (const params of [undefined, {}, { room: 'handwerk' }, { q: 'Bäcker', page: 2 }, { id: 'abc' }]) { - await assert.rejects(api.botleads(params), (err) => err.status === 404 && err.message === KELLER_GESPERRT_TEXT, `Stufe ${s}`) + await assert.rejects(api.botleads(params), (err) => err.status === 404 && err.message === BOTLEADS_GESPERRT_TEXT, `Stufe ${s}`) } assert.equal(f.calls.length, 0, `Stufe ${s}`); assert.equal(jwtGeholt, 0, `Stufe ${s}`) // die übrigen Abrufe der Stufe 3 laufen @@ -728,5 +727,5 @@ test('API: auf Stufe 3 geht kein Abruf von /api/workers/botleads hinaus (auch ke await api.botleads({ room: 'handwerk', page: 2 }) assert.deepEqual(f.calls.map((c) => [c.init.method || 'GET', c.url]), [['GET', '/api/workers/botleads?room=handwerk&page=2']]) } - assert.match(KELLER_GESPERRT_TEXT, /Keller/) + assert.match(BOTLEADS_GESPERRT_TEXT, /Bot-Leads/) })