"""BUG-118 — barre d'apps commune en mobile (Fusion ACC-1, #215). Retours de Bruno (2026-10-21) sur téléphone : la barre débordait (pastilles domaines et onglets entiers affichés) et son bouton ☰ ne faisait rien — il cliquait le bouton bureau ``sidebar-toggle-btn`` au lieu du vrai hamburger mobile ``hamburger-btn``. Correctifs verrouillés ici : CSS commune v0.1.2 (palier 700 px resserré selon la spec) et rail contextuel dans index.html (mobile ≤ 768 px → hamburger-btn). """ from pathlib import Path FRONTEND = Path(__file__).resolve().parent.parent / "frontend" def _css() -> str: return (FRONTEND / "draco-barre.css").read_text(encoding="utf-8") def _index() -> str: return (FRONTEND / "index.html").read_text(encoding="utf-8") def test_palier_mobile_resserre_v012(): css = _css() assert "@media (max-width: 900px)" in css # Seul l'onglet de l'app courante reste visible en mobile. assert '.draco-bar[data-current="obsigate"] .draco-apptab[data-app="obsigate"]' in css # Domaines et boutons icônes (rail, menu) masqués en mobile. bloc = css.split("@media (max-width: 900px)", 1)[1] assert ".draco-domaines" in bloc assert "display: none" in bloc def test_menu_des_applications_par_la_marque(): # v0.1.3 : la marque ouvre le menu des applications # (changement d'app en mobile). v0.1.10 : la marque n'est plus # le glyphe ✦ à 24 px mais le logo Spark en img 26 px. # v0.1.11 : la marque n'est plus le logo rouge mais l'étoile # Spark monochrome (brand-spark.png) ; l'onglet agent, lui, # garde le logo rouge. html = _index() assert 'data-act="apps"' in html js = (FRONTEND / "draco-barre.js").read_text(encoding="utf-8") assert "draco-apps-menu" in js assert '")[0] assert f'data-app="shaarit" href="{jeton}"' in nav assert 'data-icon="shaarit">/admin à visibilité conditionnelle. Le JS # vendangé la construit (data-hub-link="admin"), masquée tant # que l'hôte n'affirme pas le statut d'administrateur # (cfg.admin / DRACO_BAR.setAdmin) ; le CSS porte la règle # [hidden] qui la masque malgré le display:flex des items. # Branchement hôte : index.html affirme le statut depuis le # rôle global de /api/auth/me — la définition même de # l'administrateur de suite (garde /admin/* du Hub). js = (FRONTEND / "draco-barre.js").read_text(encoding="utf-8") assert 'data-hub-link", "admin"' in js assert '"Administration"' in js assert "setAdmin" in js assert "compteAdmin = cfg.admin === true" in js css = _css() assert ".draco-menu-item[hidden]" in css html = _index() assert 'window.DRACO_BAR.setAdmin(u.role === "admin")' in html # Le fetch d'amorçage d'index.html tourne avant toute # connexion : le branchement qui couvre la connexion par le # formulaire vit dans renderUserSection() (js/auth.js — le # frontend servi est en modules ES ; le app.js monolithique # à la racine n'est plus chargé), appelée par showApp() pour # toutes les sessions établies, au même endroit que la # rangée d'administration native. auth_js = (FRONTEND / "js" / "auth.js").read_text(encoding="utf-8") assert "window.DRACO_BAR.setAdmin(user.role === \"admin\")" in auth_js assert "window.DRACO_BAR.setAdmin(false)" in auth_js def test_panneau_spark_integre_218(): # #218 : le bouton Spark de la barre ouvre le panneau de l'agent # commun (mêmes assets partagés que TikTak / Flowdeck / Kanbante). html = _index() assert (FRONTEND / "draco-panneau.js").is_file() assert (FRONTEND / "draco-panneau.css").is_file() assert '' in html assert '' in html # L'action spark bascule sur le panneau — plus le message d'attente. assert "window.DRACO_PANNEAU.toggle()" in html assert "arrive en phase 4 du programme Fusion" not in html panneau = (FRONTEND / "draco-panneau.js").read_text(encoding="utf-8") assert "https://agent.dracodev.net" in panneau def test_panneau_bouton_console_221(): # #221 : le panneau Spark (asset vendangé, octet-identique dans # les 4 apps) porte un bouton « Console » qui mène à la console # web du service, sur la discussion courante (?disc=) et avec # l'app d'origine (?app= déduite du sous-domaine : og → obsigate). # Révisé par #222 : l'ouverture en nouvel onglet a disparu (la # console s'ouvre en surimpression, voir le test #222) ; le lien # profond, lui, subsiste via urlConsole(). panneau = (FRONTEND / "draco-panneau.js").read_text(encoding="utf-8") assert 'data-disc="console"' in panneau assert "ouvrirConsole" in panneau assert "urlConsole" in panneau assert '"disc=" + encodeURIComponent(id)' in panneau assert '"app=" + encodeURIComponent(app)' in panneau assert 'og: "obsigate"' in panneau assert 'window.open(' not in panneau def test_panneau_console_surimpression_222(): # #222 : le bouton « Console ⤢ » ouvre la console dans la même # page, en surimpression née à la largeur du panneau et étendue # à toute la fenêtre (transition sur la largeur), avec deux # sorties : « Mode sidebar » et « Fermer l'agent ». L'URL de # l'iframe garde le lien profond de #221 (urlConsole()). panneau = (FRONTEND / "draco-panneau.js").read_text(encoding="utf-8") assert "Console ⤢" in panneau assert "draco-console-sur" in panneau assert "assurerSurimpression" in panneau assert "draco-console-styles" in panneau assert 'var url = urlConsole()' in panneau assert 'surCadre.src = url' in panneau assert 'surEl.style.width = "100%"' in panneau assert 'data-dc="sidebar"' in panneau assert "Mode sidebar" in panneau assert "modeSidebarConsole" in panneau assert 'data-dc="fermer"' in panneau assert "Fermer l'agent" in panneau assert "fermerAgentConsole" in panneau def test_csp_ouvre_le_service_d_agents_218(): # Le panneau parle au service d'agents : la CSP doit nommer son # hôte en connect-src, sans ouvrir de générique supplémentaire. # #223 : l'origine n'est plus un littéral dans main.py — elle est # dérivée de la configuration (backend.draco_config.agent_origine, # testée via la config dans tests/test_security_headers.py). main = (Path(__file__).resolve().parent.parent / "backend" / "main.py").read_text(encoding="utf-8") ligne = next(l for l in main.splitlines() if "connect-src" in l and not l.strip().startswith("#")) assert "agent_origine" in main assert "{agent}" in ligne def test_urls_ecosysteme_via_injection_hote_223(): # #223 : panneau Spark, liens TikTak et onglets des apps lisent # window.DRACO_URLS (injecté par l'hôte/backend depuis sa config) ; # les littéraux restants ne sont que les défauts officiels en repli. panneau = (FRONTEND / "draco-panneau.js").read_text(encoding="utf-8") assert "window.DRACO_URLS" in panneau assert "DRACO_URLS.agent" in panneau liens = (FRONTEND / "draco-liens.js").read_text(encoding="utf-8") assert "window.DRACO_URLS" in liens assert "baseTikTak()" in liens html = _index() assert "window.DRACO_URLS" in html assert "DRACO_URLS[a.dataset.app]" in html def test_barre_deconnexion_branchee(): # BUG-122 (fusion #141) : la barre d'ObsiGate elle-même branche # l'action « deconnexion » sur son point d'entrée de déconnexion # globale (GET /sso/logout, retour à la racine) — avant, le clic # ne faisait rien ici non plus. html = _index() assert 'act === "deconnexion"' in html assert '"/sso/logout?return_to="' in html def test_rail_mobile_vers_le_vrai_hamburger(): html = _index() assert 'matchMedia("(max-width: 768px)")' in html assert '"hamburger-btn"' in html assert '"sidebar-toggle-btn"' in html def test_panneau_version_service_affichee_225(): # #225 : le pied du panneau Spark (mode sidebar) affiche la # version du service d'agents, comme la console pleine page. # La version est lue en direct dans la santé du service # (GET /healthz de l'origine injectée par l'hôte), à l'ouverture # du panneau ; jamais un littéral de version dans l'asset, et # source injoignable = ligne laissée masquée (rien d'affiché). panneau = (FRONTEND / "draco-panneau.js").read_text(encoding="utf-8") assert "draco-panneau-version" in panneau assert "chargerVersion" in panneau assert 'API + "/healthz"' in panneau assert '"Service d\'agents v" + versionConnue' in panneau # La ligne naît masquée et n'est démasquée qu'avec une version lue. assert 'class="draco-panneau-version"' in panneau assert "versionEl.hidden = false" in panneau def test_barre_v040_icones_monoline_menu_compte(): # v0.4.0 (HUB-11, fusion #125) : les items du menu compte # portent des icônes SVG monoline construites en DOM, dans le # style des icônes du rail de la console — aucun emoji, aucune # icône pleine. La vendange reste verbatim commons. js = (FRONTEND / "draco-barre.js").read_text(encoding="utf-8") assert "MENU_ICONES" in js for nom in ("telecharger", "a-propos", "aide", "profil", "deconnexion"): assert nom in js assert "draco-menu-icone" in js assert "stroke: currentColor" not in js # attributs posés en DOM assert 'setAttribute("stroke", "currentColor")' in js assert 'setAttribute("fill", "none")' in js css = _css() assert ".draco-menu-icone" in css bloc = css.split(".draco-menu-icone {", 1)[1].split("}", 1)[0] assert "width: 18px" in bloc assert "var(--ink-2)" in bloc def test_barre_v041_correctif_icones_espace_de_noms_svg(): # v0.4.1 (correctif fusion #125) : les icônes du menu compte # sont créées via createElementNS (espace de noms SVG). En # v0.4.0, createElement() les créait dans l'espace de noms # HTML : jamais dessinées par le navigateur, libellés # décalés d'un cran d'icône sans aucun glyphe. js = (FRONTEND / "draco-barre.js").read_text(encoding="utf-8") assert 'createElementNS(SVG_NS, "svg")' in js assert "createElementNS(SVG_NS, spec[0])" in js assert 'createElement("svg")' not in js assert "createElement(spec[0])" not in js def test_barre_v050_theme_commun_hub18(): # v0.5.0 (HUB-18, fusion #132) : la barre apprend la préférence # de thème du Hub et l'applique à l'ouverture. La vendange reste # verbatim commons ; l'initialisation propre à ObsiGate vit dans # app.js (écouteur draco:theme → applyTheme). js = (FRONTEND / "draco-barre.js").read_text(encoding="utf-8") assert "api/profil/theme" in js assert 'credentials: "include"' in js assert "draco-theme-hub" in js assert "THEME_CACHE_TTL_MS" in js assert 'CustomEvent("draco:theme"' in js assert "themePref" in js and "themeMode" in js # Aucun nom de Hub figé de plus que les défauts/dérivations # documentés : l'URL du Hub reste la résolue de la barre. assert "resolvedUrls.hub" in js app = (FRONTEND / "app.js").read_text(encoding="utf-8") assert 'addEventListener("draco:theme"' in app assert "barre.themeMode" in app