"""Tests — thème de la page /recherche ObsiGate (défaut 4). Mécanisme audité (ne pas réinventer) : le thème ObsiGate n'est PAS une préférence serveur. La SPA le lit dans localStorage au démarrage — ``frontend/js/ui.js`` ``initTheme`` (mode seul, pour la première peinture) puis ``frontend/js/sync.js`` ``init`` -> ``themes.js`` ``initThemes`` (clé + mode, palette complète en variables inline sur ``:root``). Clés : ``obsigate-theme`` (clé, ex. ``'spark'``, ``'dracula'``, ou historiquement un mode) et ``obsigate-theme-mode`` (``'dark'`` | ``'light'`` | ``'high-contrast'`` | ``'sepia'``). La page /recherche, servie hors SPA avec un ``data-theme="dark"`` en dur et aucun script de thème, restait donc sombre quel que soit le réglage. Correctif verrouillé ici, en deux étapes dans ``frontend/recherche.html`` (aucun fichier partagé touché : ``js/recherche.js`` reste identique sur les 4 hôtes, ``draco-barre.css`` reste le fichier commun vendangé, ``backend/main.py`` et sa CSP strictes sont inchangés) : - étape 1 : script synchrone en tête, avant les feuilles — pose ``data-theme`` = mode persisté avant la première peinture ; - étape 2 : module différé — ``initThemes()`` de ``themes.js`` pose la palette complète (16 thèmes, modes générés, thèmes personnalisés). ``frontend/recherche.css`` décline la barre pour les modes que ``draco-barre.css`` ne connaît pas et branche bordures/accents sur ``var(--border)`` / ``var(--accent)`` (anciennes valeurs en repli). """ from __future__ import annotations import re from pathlib import Path ROOT = Path(__file__).resolve().parent.parent FRONTEND = ROOT / "frontend" MODES = ["dark", "light", "high-contrast", "sepia"] NONCE = "TESTNONCE1234567890" def _html() -> str: return (FRONTEND / "recherche.html").read_text(encoding="utf-8") def _css() -> str: return (FRONTEND / "recherche.css").read_text(encoding="utf-8") def _inline_scripts(html: str) -> list[str]: """Contenu des scripts inline exécutables (sans src).""" return re.findall( r"(.*?)", html, re.DOTALL ) def test_etape1_avant_les_feuilles_et_mode_valide(): html = _html() scripts = _inline_scripts(html) assert scripts, "aucun script inline dans recherche.html" etape1 = scripts[0] assert "obsigate-theme-mode" in etape1 assert "obsigate-theme" in etape1 assert "setAttribute('data-theme'" in etape1 or 'setAttribute("data-theme"' in etape1 for mode in MODES: assert f"'{mode}'" in etape1, f"mode {mode} absent de l'étape 1" # Le script étape 1 précède la première feuille de style. assert html.index("', html, re.DOTALL ) assert len(modules) == 1, "exactement un module inline attendu" etape2 = modules[0] assert "import('/static/js/themes.js')" in etape2 assert "initThemes" in etape2 # Garde historique : 'obsigate-theme' valant encore un mode sans # 'obsigate-theme-mode' -> on garde le mode de l'étape 1. assert "savedMode === null" in etape2 assert "saved === 'dark'" in etape2 and "saved === 'light'" in etape2 # Échec de chargement silencieux : la page garde le mode étape 1. assert ".catch(function () {})" in etape2 # Le module est après recherche.js (différé : il tourne avant # l'init DOMContentLoaded de recherche.js, sans le modifier). assert html.index("/static/js/recherche.js") < html.index( '', 1, ) out = inject_csp_nonce(html, NONCE) assert not re.findall(r"' in html # Aucune police externe ajoutée : le repli système imposé par la # CSP stricte de /recherche (font-src 'self' data:) est inchangé. assert "fonts.googleapis.com" not in html assert "fonts.gstatic.com" not in html # Ordre des feuilles inchangé (recherche.css après la barre). assert html.index("draco-barre.css") < html.index("recherche.css")