Spaces:
Sleeping
Sleeping
File size: 13,396 Bytes
2d6c41d 9011070 2d6c41d b80bb93 1e8b84c b80bb93 1e8b84c 2d6c41d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 | """Tests unitaires des helpers de rendu mutualisΓ©s.
Couvre :func:`color_traffic_light`, :func:`color_single_gradient`,
:func:`color_diverging`, :func:`text_color_for_bg`, :func:`build_grid_svg`.
"""
from __future__ import annotations
from picarones.reports._helpers.render_helpers import (
DIVERGING_NEGATIVE_RGB,
DIVERGING_POSITIVE_RGB,
GRADIENT_GREEN_RGB,
GRADIENT_RED_RGB,
GRADIENT_YELLOW_RGB,
build_grid_svg,
color_diverging,
color_single_gradient,
color_traffic_light,
text_color_for_bg,
)
def _hex_to_rgb(hex_str: str) -> tuple[int, int, int]:
s = hex_str.lstrip("#")
return int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# color_traffic_light
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class TestColorTrafficLight:
def test_value_zero_high_is_good_returns_red(self) -> None:
assert _hex_to_rgb(color_traffic_light(0.0)) == GRADIENT_RED_RGB
def test_value_max_high_is_good_returns_green(self) -> None:
assert _hex_to_rgb(color_traffic_light(1.0)) == GRADIENT_GREEN_RGB
def test_value_mid_high_is_good_returns_yellow(self) -> None:
assert _hex_to_rgb(color_traffic_light(0.5)) == GRADIENT_YELLOW_RGB
def test_value_zero_low_is_good_returns_green(self) -> None:
assert _hex_to_rgb(color_traffic_light(0.0, low_is_good=True)) == GRADIENT_GREEN_RGB
def test_value_max_low_is_good_returns_red(self) -> None:
assert _hex_to_rgb(color_traffic_light(1.0, low_is_good=True)) == GRADIENT_RED_RGB
def test_clamping_above_max(self) -> None:
c = color_traffic_light(2.0)
assert _hex_to_rgb(c) == GRADIENT_GREEN_RGB
def test_clamping_below_zero(self) -> None:
c = color_traffic_light(-1.0)
assert _hex_to_rgb(c) == GRADIENT_RED_RGB
def test_custom_scale_max(self) -> None:
# CER 0.30 = max β vert (avec low_is_good=True β vert au max)
# On vΓ©rifie plutΓ΄t high_is_good : 0.30 β vert
assert _hex_to_rgb(color_traffic_light(0.30, scale_max=0.30)) == GRADIENT_GREEN_RGB
# 0.15 β milieu β jaune
assert _hex_to_rgb(color_traffic_light(0.15, scale_max=0.30)) == GRADIENT_YELLOW_RGB
def test_custom_scale_min_and_max(self) -> None:
# Plage [10, 20] : 10 β rouge, 20 β vert, 15 β jaune
assert _hex_to_rgb(color_traffic_light(10.0, scale_min=10, scale_max=20)) == GRADIENT_RED_RGB
assert _hex_to_rgb(color_traffic_light(20.0, scale_min=10, scale_max=20)) == GRADIENT_GREEN_RGB
assert _hex_to_rgb(color_traffic_light(15.0, scale_min=10, scale_max=20)) == GRADIENT_YELLOW_RGB
def test_zero_span_returns_yellow(self) -> None:
# scale_min == scale_max β milieu β jaune
assert _hex_to_rgb(color_traffic_light(5.0, scale_min=10, scale_max=10)) == GRADIENT_YELLOW_RGB
def test_format_is_hex_lowercase(self) -> None:
c = color_traffic_light(0.7)
assert c.startswith("#")
assert len(c) == 7
assert c == c.lower()
def test_nan_propagates_to_green(self) -> None:
# VΓ©rification IEEE 754 : ``min(1.0, NaN)`` retourne ``1.0`` en
# Python (la comparaison avec NaN est False, donc Python retient
# le second argument). ``max(0.0, 1.0) = 1.0``. Donc f=1.0,
# branche ``f > 0.5`` β vert max. Comportement dΓ©terministe et
# testable (pas un test placebo "ne crash pas").
assert _hex_to_rgb(color_traffic_light(float("nan"))) == GRADIENT_GREEN_RGB
# Avec low_is_good=True, l'inversion donne f=0 β rouge.
assert _hex_to_rgb(
color_traffic_light(float("nan"), low_is_good=True)
) == GRADIENT_RED_RGB
def test_inf_clamped_to_max(self) -> None:
# +inf > scale_max β clamp Γ scale_max β vert (high_is_good)
assert _hex_to_rgb(color_traffic_light(float("inf"))) == GRADIENT_GREEN_RGB
# -inf < 0 β clamp Γ 0 β rouge
assert _hex_to_rgb(color_traffic_light(float("-inf"))) == GRADIENT_RED_RGB
def test_inverted_scale_returns_yellow_neutral(self) -> None:
# scale_min > scale_max β span <= 0 β branche "zero span" β f=0.5
# β frontiΓ¨re exacte rouge/jaune/vert β jaune neutre.
assert _hex_to_rgb(
color_traffic_light(5.0, scale_min=10, scale_max=5)
) == GRADIENT_YELLOW_RGB
# Idem pour scale_min == scale_max (dΓ©jΓ couvert par
# test_zero_span_returns_yellow plus haut, mais la cohΓ©rence
# de comportement est explicite ici).
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# color_single_gradient
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class TestColorSingleGradient:
def test_zero_returns_start(self) -> None:
assert color_single_gradient(0.0, end_rgb=(30, 58, 138)) == "#ffffff"
def test_max_returns_end(self) -> None:
assert color_single_gradient(1.0, end_rgb=(30, 58, 138)) == "#1e3a8a"
def test_half_is_midpoint(self) -> None:
c = color_single_gradient(0.5, end_rgb=(30, 58, 138))
# ((255+30)/2, (255+58)/2, (255+138)/2) β (142, 156, 196) β ~#8e9cc4
r, g, b = _hex_to_rgb(c)
assert abs(r - 142) <= 1
assert abs(g - 156) <= 1
assert abs(b - 196) <= 1
def test_above_max_clamped(self) -> None:
assert color_single_gradient(2.0, end_rgb=(30, 58, 138)) == "#1e3a8a"
def test_below_zero_clamped(self) -> None:
assert color_single_gradient(-1.0, end_rgb=(30, 58, 138)) == "#ffffff"
def test_custom_max_value(self) -> None:
# value = 50, max = 100 β 0.5 β milieu
c = color_single_gradient(50.0, end_rgb=(30, 58, 138), max_value=100.0)
r, g, b = _hex_to_rgb(c)
assert abs(r - 142) <= 1
def test_custom_start_rgb(self) -> None:
c = color_single_gradient(0.0, end_rgb=(30, 58, 138), start_rgb=(0, 0, 0))
assert c == "#000000"
def test_zero_max_value_returns_start(self) -> None:
# Garde-fou contre division par zΓ©ro
assert color_single_gradient(5.0, end_rgb=(30, 58, 138), max_value=0) == "#ffffff"
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# color_diverging
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class TestColorDiverging:
def test_zero_returns_neutral(self) -> None:
c = color_diverging(0.0)
# Neutre = vert par dΓ©faut
assert _hex_to_rgb(c) == (130, 200, 130)
def test_positive_max_returns_positive_color(self) -> None:
c = color_diverging(1.0, max_abs=1.0)
assert _hex_to_rgb(c) == DIVERGING_POSITIVE_RGB
def test_negative_max_returns_negative_color(self) -> None:
c = color_diverging(-1.0, max_abs=1.0)
assert _hex_to_rgb(c) == DIVERGING_NEGATIVE_RGB
def test_clamping_above_max_abs(self) -> None:
assert _hex_to_rgb(color_diverging(5.0, max_abs=1.0)) == DIVERGING_POSITIVE_RGB
def test_clamping_below_negative_max_abs(self) -> None:
assert _hex_to_rgb(color_diverging(-5.0, max_abs=1.0)) == DIVERGING_NEGATIVE_RGB
def test_zero_max_abs_returns_neutral(self) -> None:
c = color_diverging(5.0, max_abs=0)
assert _hex_to_rgb(c) == (130, 200, 130)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# text_color_for_bg
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class TestTextColorForBg:
def test_low_intensity_dark_text(self) -> None:
assert text_color_for_bg(0.2) == "#222"
def test_high_intensity_white_text(self) -> None:
assert text_color_for_bg(0.8) == "#fff"
def test_threshold_boundary(self) -> None:
assert text_color_for_bg(0.55) == "#222"
assert text_color_for_bg(0.56) == "#fff"
def test_custom_threshold(self) -> None:
assert text_color_for_bg(0.4, threshold=0.3) == "#fff"
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# build_grid_svg
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class TestBuildGridSvg:
def test_empty_grid_returns_empty_string(self) -> None:
svg = build_grid_svg(
n_rows=0,
n_cols=0,
row_label_fn=lambda i: "",
col_label_fn=lambda j: "",
cell_color_fn=lambda i, j: "#fff",
)
assert svg == ""
def test_zero_rows_returns_empty(self) -> None:
svg = build_grid_svg(
n_rows=0,
n_cols=3,
row_label_fn=lambda i: "",
col_label_fn=lambda j: str(j),
cell_color_fn=lambda i, j: "#fff",
)
assert svg == ""
def test_minimal_grid_renders(self) -> None:
svg = build_grid_svg(
n_rows=1,
n_cols=1,
row_label_fn=lambda i: "row0",
col_label_fn=lambda j: "col0",
cell_color_fn=lambda i, j: "#abc123",
)
assert "<svg" in svg
assert "row0" in svg
assert "col0" in svg
assert "#abc123" in svg
def test_cell_text_displayed_when_provided(self) -> None:
svg = build_grid_svg(
n_rows=1,
n_cols=1,
row_label_fn=lambda i: "r",
col_label_fn=lambda j: "c",
cell_color_fn=lambda i, j: "#fff",
cell_text_fn=lambda i, j: "0.42",
)
assert "0.42" in svg
def test_cell_text_omitted_when_none(self) -> None:
svg = build_grid_svg(
n_rows=1,
n_cols=1,
row_label_fn=lambda i: "r",
col_label_fn=lambda j: "c",
cell_color_fn=lambda i, j: "#fff",
cell_text_fn=lambda i, j: None,
)
# Pas de chiffre dans la cellule
assert ">0.<" not in svg
def test_rotate_col_labels(self) -> None:
svg = build_grid_svg(
n_rows=1,
n_cols=1,
row_label_fn=lambda i: "r",
col_label_fn=lambda j: "long_label",
cell_color_fn=lambda i, j: "#fff",
rotate_col_labels=True,
)
assert "rotate(-45" in svg
def test_x_axis_title(self) -> None:
svg = build_grid_svg(
n_rows=1,
n_cols=1,
row_label_fn=lambda i: "r",
col_label_fn=lambda j: "c",
cell_color_fn=lambda i, j: "#fff",
x_axis_title="Position dans le document",
)
assert "Position dans le document" in svg
def test_html_escape_in_labels(self) -> None:
svg = build_grid_svg(
n_rows=1,
n_cols=1,
row_label_fn=lambda i: "<script>alert(1)</script>",
col_label_fn=lambda j: "<b>bold</b>",
cell_color_fn=lambda i, j: "#fff",
cell_text_fn=lambda i, j: "<i>italic</i>",
)
assert "<script>" not in svg
assert "<script>" in svg
assert "<b>" not in svg
assert "<b>" in svg
assert "<i>" not in svg
def test_grid_dimensions(self) -> None:
svg = build_grid_svg(
n_rows=3,
n_cols=4,
row_label_fn=lambda i: f"r{i}",
col_label_fn=lambda j: f"c{j}",
cell_color_fn=lambda i, j: "#fff",
)
# 12 cellules attendues (3Γ4)
assert svg.count("<rect") == 12
# 3 Γ©tiquettes de ligne + 4 de colonne
# On compte les <text> qui ne sont pas dans une cellule (donc qui sont des labels) :
# 3 labels lignes + 4 labels colonnes = 7. Pas de cell_text_fn donc 0.
assert svg.count("<text") == 7
def test_aria_label_present(self) -> None:
svg = build_grid_svg(
n_rows=1,
n_cols=1,
row_label_fn=lambda i: "r",
col_label_fn=lambda j: "c",
cell_color_fn=lambda i, j: "#fff",
aria_label="Test heatmap",
)
assert 'aria-label="Test heatmap"' in svg
|