Files
colibre/tests/test_figures.py
T
Colin Maudry 6e9b451734 feat(tableau): réserver l'espace de la barre de défilement horizontale AG Grid
alwaysShowHorizontalScroll force la piste de scroll à rester visible
en permanence (comportement déjà natif sous Chrome). Sans effet sur
Firefox, dont le curseur en overlay est géré par l'OS/le navigateur
et non contrôlable côté appli (#41).
2026-07-12 20:45:23 +02:00

384 lines
12 KiB
Python

import polars as pl
import pytest
def _make_lff(rows):
return pl.LazyFrame(rows)
def test_compute_considerations_stats_basic():
from src.figures import compute_considerations_stats
lff = _make_lff(
[
# u1 : social oui (Clause), env non (Sans objet)
{
"uid": "u1",
"considerationsSociales": "Clause sociale",
"considerationsEnvironnementales": "Sans objet",
},
# u2 : social non (Sans objet), env oui (Critère)
{
"uid": "u2",
"considerationsSociales": "Sans objet",
"considerationsEnvironnementales": "Critère environnemental",
},
# u3 : social oui (Marché réservé), env null
{
"uid": "u3",
"considerationsSociales": "Marché réservé",
"considerationsEnvironnementales": None,
},
# u4 : social autre valeur (pas "Sans objet"), env null
{
"uid": "u4",
"considerationsSociales": "Pas de considération sociale",
"considerationsEnvironnementales": "Sans objet",
},
]
)
stats = compute_considerations_stats(lff)
# champs_renseignes : basé sur sociales. 4 non-null / 4 total -> (4, 100%).
assert stats["champs_renseignes"] == (4, 100)
# Sociales renseignées : dén=4 non-null, num=3 != "Sans objet" (u1/u3/u4) -> (4, 3, 75%).
assert stats["sociales_renseignees"] == (4, 3, 75)
# Env renseignées : dén=3 non-null (u1/u2/u4), num=1 != "Sans objet" (u2) -> (3, 1, 33%).
assert stats["environnementales_renseignees"] == (3, 1, 33)
def test_compute_considerations_stats_dedup_per_uid():
from src.figures import compute_considerations_stats
lff = _make_lff(
[
# u1 présent 2 fois (2 titulaires) -> compté une seule fois
{
"uid": "u1",
"considerationsSociales": "Clause sociale",
"considerationsEnvironnementales": "Sans objet",
},
{
"uid": "u1",
"considerationsSociales": "Clause sociale",
"considerationsEnvironnementales": "Sans objet",
},
{
"uid": "u2",
"considerationsSociales": "Sans objet",
"considerationsEnvironnementales": "Sans objet",
},
]
)
stats = compute_considerations_stats(lff)
# 2 uid distincts. Social 2 non-null, 1 != "Sans objet" (u1) -> (2, 1, 50%).
assert stats["champs_renseignes"] == (2, 100)
assert stats["sociales_renseignees"] == (2, 1, 50)
# Env 2 non-null, 0 != "Sans objet" -> (2, 0, 0%).
assert stats["environnementales_renseignees"] == (2, 0, 0)
def test_compute_considerations_stats_missing_column():
from src.figures import compute_considerations_stats
lff = _make_lff(
[
{"uid": "u1", "considerationsSociales": "Clause sociale"},
{"uid": "u2", "considerationsSociales": "Sans objet"},
]
)
stats = compute_considerations_stats(lff)
# Colonne env absente -> (0, 0). Social : 2 non-null, 1 != "Sans objet" -> (2, 1, 50%).
assert stats["champs_renseignes"] == (2, 100)
assert stats["sociales_renseignees"] == (2, 1, 50)
assert stats["environnementales_renseignees"] == (0, 0)
def test_compute_considerations_stats_empty():
from src.figures import compute_considerations_stats
lff = pl.LazyFrame(
{
"uid": pl.Series([], dtype=pl.String),
"considerationsSociales": pl.Series([], dtype=pl.String),
"considerationsEnvironnementales": pl.Series([], dtype=pl.String),
}
)
stats = compute_considerations_stats(lff)
assert stats["champs_renseignes"] == (0, 0)
assert stats["sociales_renseignees"] == (0, 0)
assert stats["environnementales_renseignees"] == (0, 0)
def test_get_considerations_card_content_returns_three_progress_bars():
import dash_bootstrap_components as dbc
from dash import html
from src.figures import get_considerations_card_content
lff = pl.LazyFrame(
[
{
"uid": "u1",
"considerationsSociales": "Clause sociale",
"considerationsEnvironnementales": "Sans objet",
},
{
"uid": "u2",
"considerationsSociales": "Sans objet",
"considerationsEnvironnementales": "Critère environnemental",
},
]
)
div = get_considerations_card_content(lff)
assert isinstance(div, html.Div)
def find_progress(component, found):
if isinstance(component, dbc.Progress):
found.append(component)
children = getattr(component, "children", None)
if isinstance(children, (list, tuple)):
for c in children:
find_progress(c, found)
elif children is not None and not isinstance(children, str):
find_progress(children, found)
return found
inner_bars = [b for b in find_progress(div, []) if getattr(b, "bar", False)]
assert len(inner_bars) == 3
bar_ren, bar_social, bar_env = inner_bars
# Bar 1 : champs renseignés (2/2 = 100%, gris)
assert bar_ren.value == 100
assert bar_ren.color == "#6c757d"
# Bar 2 : sociales parmi renseignés (u1 != "Sans objet" -> 1/2 = 50%, rose)
assert bar_social.value == 50
assert bar_social.color == "#CC6677"
# Bar 3 : env parmi renseignés (u2 != "Sans objet" -> 1/2 = 50%, vert)
assert bar_env.value == 50
assert bar_env.color == "#117733"
def test_build_org_markers_groups_and_counts():
from src.figures import build_org_markers
lff = pl.LazyFrame(
[
{
"uid": "u1",
"acheteur_longitude": 2.35,
"acheteur_latitude": 48.85,
"acheteur_nom": "ACHETEUR A",
},
{
"uid": "u2",
"acheteur_longitude": 2.35,
"acheteur_latitude": 48.85,
"acheteur_nom": "ACHETEUR A",
},
{
"uid": "u3",
"acheteur_longitude": -1.68,
"acheteur_latitude": 48.11,
"acheteur_nom": "ACHETEUR B",
},
]
)
markers = build_org_markers(lff, "acheteur")
assert len(markers) == 2
marker_a = next(m for m in markers if m["tooltip"].startswith("ACHETEUR A"))
assert marker_a["tooltip"] == "ACHETEUR A (2 marchés)"
assert marker_a["lat"] == 48.85
assert marker_a["lon"] == 2.35
assert marker_a["marker_color"] == "#E69F00"
def test_build_org_markers_filters_null_coordinates():
from src.figures import build_org_markers
lff = pl.LazyFrame(
[
{
"uid": "u1",
"acheteur_longitude": None,
"acheteur_latitude": None,
"acheteur_nom": "ACHETEUR A",
},
{
"uid": "u2",
"acheteur_longitude": 2.35,
"acheteur_latitude": 48.85,
"acheteur_nom": "ACHETEUR B",
},
]
)
markers = build_org_markers(lff, "acheteur")
assert len(markers) == 1
assert markers[0]["tooltip"] == "ACHETEUR B (1 marchés)"
def test_build_org_markers_missing_columns_returns_empty():
from src.figures import build_org_markers
lff = pl.LazyFrame([{"uid": "u1", "acheteur_nom": "ACHETEUR A"}])
assert build_org_markers(lff, "acheteur") == []
assert build_org_markers(lff, "titulaire") == []
def _org_map_dff():
return pl.DataFrame(
[
{
"uid": "u1",
"acheteur_longitude": 2.35,
"acheteur_latitude": 48.85,
"acheteur_nom": "ACHETEUR A",
"titulaire_longitude": -1.68,
"titulaire_latitude": 48.11,
"titulaire_nom": "TITULAIRE A",
}
]
)
def test_get_org_location_map_centers_on_home_and_counterpart():
import dash_leaflet as dl
from src.figures import get_org_location_map
leaflet_map = get_org_location_map(_org_map_dff(), "acheteur", "test-map")
assert isinstance(leaflet_map, dl.Map)
# Centre = milieu du point acheteur (2.35, 48.85) et titulaire (-1.68, 48.11).
assert leaflet_map.center == pytest.approx([48.48, 0.335])
assert leaflet_map.zoom == 6
geojson_layers = [c for c in leaflet_map.children if isinstance(c, dl.GeoJSON)]
assert {layer.id for layer in geojson_layers} == {
"test-map-acheteur",
"test-map-titulaire",
}
def test_get_org_location_map_marks_home_org_marker_acheteur():
import dash_leaflet as dl
from src.figures import get_org_location_map
leaflet_map = get_org_location_map(_org_map_dff(), "acheteur", "test-map")
geojson_layers = {
layer.id: layer
for layer in leaflet_map.children
if isinstance(layer, dl.GeoJSON)
}
acheteur_features = geojson_layers["test-map-acheteur"].data["features"]
titulaire_features = geojson_layers["test-map-titulaire"].data["features"]
assert all(f["properties"]["is_home"] for f in acheteur_features)
assert all(not f["properties"].get("is_home") for f in titulaire_features)
def test_get_org_location_map_marks_home_org_marker_titulaire():
import dash_leaflet as dl
from src.figures import get_org_location_map
leaflet_map = get_org_location_map(_org_map_dff(), "titulaire", "test-map")
geojson_layers = {
layer.id: layer
for layer in leaflet_map.children
if isinstance(layer, dl.GeoJSON)
}
acheteur_features = geojson_layers["test-map-acheteur"].data["features"]
titulaire_features = geojson_layers["test-map-titulaire"].data["features"]
assert all(f["properties"]["is_home"] for f in titulaire_features)
assert all(not f["properties"].get("is_home") for f in acheteur_features)
def test_get_org_location_map_defaults_to_france_view_without_coordinates():
from src.figures import get_org_location_map
dff = pl.DataFrame(
[{"uid": "u1", "acheteur_nom": "ACHETEUR A", "titulaire_nom": "TITULAIRE A"}]
)
leaflet_map = get_org_location_map(dff, "acheteur", "test-map")
assert leaflet_map.center == [46.6, 2.2]
assert leaflet_map.zoom == 5
def test_bounds_to_center_zoom_single_point_uses_max_zoom():
from src.figures import bounds_to_center_zoom
center, zoom = bounds_to_center_zoom(48.85, 2.35, 48.85, 2.35, max_zoom=12)
assert center == [48.85, 2.35]
assert zoom == 12
def test_bounds_to_center_zoom_wider_bounds_give_smaller_zoom():
from src.figures import bounds_to_center_zoom
_, zoom_narrow = bounds_to_center_zoom(48.11, -1.68, 48.85, 2.35)
_, zoom_wide = bounds_to_center_zoom(16.23, -61.55, 51.0, 8.0)
assert zoom_wide < zoom_narrow
def test_bounds_to_center_zoom_center_is_bbox_midpoint():
from src.figures import bounds_to_center_zoom
center, _ = bounds_to_center_zoom(40.0, 0.0, 50.0, 4.0)
assert center == [45.0, 2.0]
def test_ag_grid_locale_text_translates_common_filter_labels():
from src.figures import ag_grid
grid = ag_grid("tableau_grid", [])
locale_text = grid.dashGridOptions["localeText"]
assert locale_text["equals"] == "Égal à"
assert locale_text["contains"] == "Contient"
assert locale_text["blank"] == "Vide"
assert locale_text["notBlank"] == "Non vide"
def test_ag_grid_keeps_base_appearance():
"""Pas de thème/className custom au Lot 1 (spec de design)."""
from src.figures import ag_grid
grid = ag_grid("tableau_grid", [])
assert getattr(grid, "className", None) in (None, "")
def test_ag_grid_always_shows_horizontal_scroll():
"""Réserve l'espace de la barre de défilement horizontale en permanence
(comportement natif Chrome). Sans effet sur le curseur en overlay de
Firefox, géré par l'OS/le navigateur, non contrôlable par l'appli."""
from src.figures import ag_grid
grid = ag_grid("tableau_grid", [])
assert grid.dashGridOptions["alwaysShowHorizontalScroll"] is True