Corriger la revue stage1 de la refonte : fiche de dalle, thème du fond, cadre d'export, tokens de couleur

Retour/Échap depuis la fiche détaillée ne retouche plus la hauteur du volet
téléphone (openTab remplacé par un rétablissement direct de l'onglet) ; le
fond OSM suit le thème sauf choix explicite (migration du localStorage et des
défauts serveur en v3, plus de dark:true hérité de l'ex-webapp) et part du
bon thème dès sa création (plus de flash sombre) ; l'onglet Export mémorisé ne
recadre plus la vue au premier rendu (lien partagé préservé) et referme son
cadre pendant que la fiche détaillée est ouverte ; la qualité affichée suit le
nom réel de la dalle rendue ; un second clic referme la fiche au lieu de
déplacer son contour derrière elle ; couleurs en dur restantes tokenisées
(--tile-sel, --gps, .gen-log) et lisibles en thème clair ; localStorage
corrompu (lidar-panel, lidar-print) n'y casse plus l'interface.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Antoine Jacquin
2026-09-27 18:52:30 +02:00
parent 56c9fa0ccd
commit 72b10cd228
5 changed files with 249 additions and 25 deletions

View File

@ -268,6 +268,30 @@ def test_map_tile_quality_absent_is_none(tmp_path, monkeypatch):
assert mapserve.map_tile_info(lat=lat, lng=lon)["quality"] is None
def test_map_tile_quality_uses_real_basename(tmp_path, monkeypatch):
"""La qualité lue est celle du nom réel de la dalle rendue (best["basename"]),
pas celle du nom nominal calculé depuis col/row — ils peuvent différer."""
import lidar_pipeline.mapserve as mapserve
from lidar_pipeline import index as index_mod
from lidar_pipeline.quality import write_quality, QUALITY_VERSION
_setup(tmp_path, monkeypatch, layers=("relief_oriente",))
nominal = "LHD_FXX_1054_6882_PTS_LAMB93_IGN69"
real = nominal + "_r0p2"
write_quality(tmp_path, real, {"version": QUALITY_VERSION, "ground_density": 42.0,
"density_grid": [[42.0]], "empty_fraction": 0.0,
"acq_start": "2023-01-01", "acq_end": "2023-01-01",
"acq_source": "gps"})
fake_tile = {"col": 1054, "row": 6882, "resolution": 0.2, "basename": real, "viz": {}}
monkeypatch.setattr(index_mod, "scan_tiles", lambda d: [fake_tile])
monkeypatch.setattr(index_mod, "_collect_tile_metadata",
lambda t, dtm_dir: {"method": None, "generated": None, "viz": {}})
lat, lon = _center()
d = mapserve.map_tile_info(lat=lat, lng=lon)
assert d["found"] is True
assert d["name"] == real
assert d["quality"]["ground_density"] == 42.0
def test_healthz_and_assets(tmp_path, monkeypatch):
"""Sonde de santé et assets de l'interface (revalidés, jamais immuables)."""
from fastapi import HTTPException
@ -414,6 +438,43 @@ def test_defaults_sanitised(tmp_path, monkeypatch):
assert data["main"] is None # la précision n'est pas une principale
def test_defaults_dark_none_is_not_a_choice(tmp_path, monkeypatch):
"""base.dark omis/None n'est pas un choix : il reste None (le fond suit le
thème côté carte), distinct d'un choix explicite (True/False persistent)."""
import lidar_pipeline.mapserve as mapserve
_setup(tmp_path, monkeypatch, layers=("relief_oriente",))
monkeypatch.setattr(mapserve, "DEFAULTS_FILE", tmp_path / ".map-defaults.json")
req = mapserve.DefaultsRequest(main="relief_oriente",
base={"on": True, "opacity": 0.4})
saved = mapserve.set_defaults(req)["defaults"]
assert saved["base"]["dark"] is None
assert saved["v"] == 3
assert mapserve.map_meta()["default_base"]["dark"] is None
# Choix explicite : conservé tel quel
req = mapserve.DefaultsRequest(main="relief_oriente",
base={"on": True, "opacity": 0.4, "dark": True})
mapserve.set_defaults(req)
assert mapserve.map_meta()["default_base"]["dark"] is True
def test_defaults_legacy_dark_true_becomes_unset(tmp_path, monkeypatch):
"""Fichier d'avant v3 (ex-webapp, base.dark:true systématique) : servi avec
dark None (« non choisi »), jamais imposé comme choix de l'utilisateur."""
import json
import lidar_pipeline.mapserve as mapserve
_setup(tmp_path, monkeypatch, layers=("relief_oriente",))
defaults = tmp_path / ".map-defaults.json"
monkeypatch.setattr(mapserve, "DEFAULTS_FILE", defaults)
defaults.write_text(json.dumps({
"main": "relief_oriente", "mode": "relief",
"base": {"on": True, "opacity": 0.85, "dark": True}}), encoding="utf-8")
meta = mapserve.map_meta()
assert meta["default_base"]["dark"] is None
assert meta["default_base"]["opacity"] == 0.85
def test_ui_applies_server_defaults():
"""L'interface part des défauts du serveur et sait les (re)définir."""
from lidar_pipeline.mapui import _MAP_HTML, _MAP_JS
@ -426,6 +487,90 @@ def test_ui_applies_server_defaults():
assert "lsDel(LS_KEY)" in _MAP_JS
def test_ui_base_dark_follows_theme_unless_chosen():
"""base.dark ne vaut jamais un choix implicite : None = suit le thème.
Client : defaultState garde None (au lieu de forcer un booléen) ; loadState
ignore le base.dark d'un ancien lidarMapView_v2 (avant v3, où l'ex-webapp
persistait dark:true à chaque sauvegarde) ; saveState écrit v: 3. La classe
CSS du fond OSM part du thème courant, pas d'un « base-dark » figé."""
from lidar_pipeline.mapui import _MAP_JS
assert "dark: b.dark == null ? null : !!b.dark" in _MAP_JS
assert "if (s.v !== 3) delete sBase.dark;" in _MAP_JS
assert "lsSet(LS_KEY, Object.assign({ v: 3 }, STATE))" in _MAP_JS
assert "className: themeIsDark() ? 'base-dark' : ''" in _MAP_JS
def test_ui_phone_return_and_escape_keep_sheet_height():
"""« ← Retour » et Échap depuis la fiche de dalle rétablissent l'onglet
précédent SANS repasser par openTab (qui retoucherait la hauteur du volet
en re-touchant l'onglet déjà actif) : la fiche ferme via un rétablissement
direct de PANEL.tab/want, partagé par closeTileViews et showTilePopup."""
from lidar_pipeline.mapui import _MAP_JS
assert "function restoreTabAfterDetails()" in _MAP_JS
restore = _MAP_JS[_MAP_JS.index("function restoreTabAfterDetails("):]
assert "PANEL.tab = PANEL.want = tileReturnTab || 'view';" in restore[:300]
assert "openTab(" not in restore[:300]
close_tiles = _MAP_JS[_MAP_JS.index("function closeTileViews("):]
assert "restoreTabAfterDetails()" in close_tiles[:250]
# Second clic sur la carte pendant que la fiche est ouverte : referme la
# fiche plutôt que de bouger le contour derrière elle (M5)
popup_fn = _MAP_JS[_MAP_JS.index("function showTilePopup("):]
assert "restoreTabAfterDetails()" in popup_fn[:400]
def test_ui_saved_print_tab_does_not_rezoom_shared_link():
"""L'onglet Export mémorisé ne recadre pas la vue au tout premier rendu du
panneau (sinon il écrase le zoom d'un lien partagé ou de l'URL initiale) :
BOOTSTRAPPING passe à faux juste après ce premier rendu."""
from lidar_pipeline.mapui import _MAP_JS
assert "let BOOTSTRAPPING = true;" in _MAP_JS
assert "printSetOpen(e.detail.visible === 'print', !BOOTSTRAPPING);" in _MAP_JS
boot = _MAP_JS[_MAP_JS.index("panelRender();\n BOOTSTRAPPING = false;"):]
assert boot.startswith("panelRender();\n BOOTSTRAPPING = false;")
def test_ui_details_view_hides_print_frame():
"""Ouvrir la fiche détaillée pendant que l'onglet Export est affiché
referme le cadre d'export (sinon cadre + poignée restent visibles
derrière la fiche)."""
from lidar_pipeline.mapui import _MAP_JS
open_details = _MAP_JS[_MAP_JS.index("function openTileDetails("):]
assert "printSetOpen(false);" in open_details[:600]
def test_ui_colors_are_tokenised():
"""Plus de couleur en dur pour le contour de sélection, le marqueur GPS et
le journal de génération (AGENTS.md « aucune couleur en dur dans les
composants ») ; #1b1b1b reste en dur sur la poignée d'export (contraste
fixe voulu sur --print), documenté en commentaire."""
from lidar_pipeline.mapui import _MAP_CSS, _MAP_JS
assert "color: cssVar('--tile-sel')" in _MAP_JS
assert "'#ffd24a'" not in _MAP_JS
assert "cssVar('--gps')" in _MAP_JS
assert "'#39d98a'" not in _MAP_JS
assert "--tile-sel:" in _MAP_CSS and "--gps:" in _MAP_CSS
assert ".gen-log { font:" in _MAP_CSS
gen_log = _MAP_CSS[_MAP_CSS.index(".gen-log {"):]
assert "rgba(0,0,0,0.35)" not in gen_log[:250]
assert "var(--surface-2)" in gen_log[:250] and "var(--text)" in gen_log[:250]
assert "#1b1b1b" in _MAP_CSS # volontaire, commenté juste au-dessus
def test_ui_guards_corrupt_local_storage():
"""Une valeur corrompue dans lidar-panel/lidar-print (autre appli sur la
même origine, quota dépassé en écriture…) ne casse pas l'interface :
whitelist de type/valeur plutôt qu'un usage direct de lsGet."""
from lidar_pipeline.mapui import _MAP_JS
assert "function _sanitizedPanelLS()" in _MAP_JS
panel_ls = _MAP_JS[_MAP_JS.index("function _sanitizedPanelLS("):]
assert "typeof s.tab === 'string'" in panel_ls[:700]
assert "typeof s.collapsed === 'boolean'" in panel_ls[:700]
assert "['closed', 'half', 'full'].includes(s.sheet)" in panel_ls[:700]
print_load = _MAP_JS[_MAP_JS.index("function printLoad("):]
assert "raw && typeof raw === 'object' && !Array.isArray(raw)" in print_load[:400]
def test_ui_uses_standard_tilelayer():
"""L'interface s'appuie sur le LOD natif de Leaflet, pas sur un palier maison."""
from lidar_pipeline.mapui import _MAP_JS, render_html, ui_version