"""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")