Files
padelnomics/web/tests/test_planner_charts.py
Deeman 4ae00b35d1 refactor: flatten padelnomics/padelnomics/ → repo root
git mv all tracked files from the nested padelnomics/ workspace
directory to the git repo root. Merged .gitignore files.
No code changes — pure path rename.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 00:44:40 +01:00

251 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Tests for augment_d() chart output.
Regression for: charts not rendering because augment_d() was producing raw
data dicts instead of full Chart.js 4.x config objects. Each chart must have
the shape {type, data: {labels, datasets: [{data, ...}]}, options} so that
initCharts() can pass it directly to new Chart(canvas, config).
"""
import json
import pytest
from padelnomics.planner.calculator import calc, validate_state
from padelnomics.planner.routes import augment_d
def make_result(lang="en", **state_overrides):
"""Return (d, s) with augment_d applied."""
s = validate_state(state_overrides)
d = calc(s)
augment_d(d, s, lang)
return d, s
INDOOR_CHART_KEYS = ["capex_chart", "ramp_chart", "pl_chart", "cf_chart", "cum_chart", "dscr_chart"]
ALL_CHART_KEYS = INDOOR_CHART_KEYS + ["season_chart"]
# ════════════════════════════════════════════════════════════
# Structure: every chart must be a valid Chart.js config
# ════════════════════════════════════════════════════════════
class TestChartStructure:
"""Each chart must have the shape Chart.js 4.x expects: {type, data, options}."""
def test_all_indoor_charts_present(self):
d, _ = make_result()
for key in INDOOR_CHART_KEYS:
assert key in d, f"Missing chart key: {key}"
def test_season_chart_present_for_outdoor(self):
d, _ = make_result(venue="outdoor")
assert "season_chart" in d
@pytest.mark.parametrize("key", INDOOR_CHART_KEYS)
def test_chart_has_type_string(self, key):
d, _ = make_result()
assert "type" in d[key], f"{key} missing 'type'"
assert isinstance(d[key]["type"], str)
@pytest.mark.parametrize("key", INDOOR_CHART_KEYS)
def test_chart_has_data_with_datasets_list(self, key):
d, _ = make_result()
chart = d[key]
assert "data" in chart, f"{key} missing 'data'"
assert "datasets" in chart["data"], f"{key} missing 'data.datasets'"
assert isinstance(chart["data"]["datasets"], list)
assert len(chart["data"]["datasets"]) >= 1
@pytest.mark.parametrize("key", INDOOR_CHART_KEYS)
def test_chart_has_responsive_options(self, key):
d, _ = make_result()
opts = d[key].get("options", {})
assert opts.get("responsive") is True, f"{key} options.responsive must be True"
assert opts.get("maintainAspectRatio") is False, f"{key} options.maintainAspectRatio must be False"
@pytest.mark.parametrize("key", INDOOR_CHART_KEYS)
def test_chart_is_json_serializable(self, key):
"""Charts are embedded as JSON in templates — they must serialise cleanly."""
d, _ = make_result()
json.dumps(d[key]) # must not raise
@pytest.mark.parametrize("key", INDOOR_CHART_KEYS)
def test_datasets_each_have_data_array(self, key):
d, _ = make_result()
for ds in d[key]["data"]["datasets"]:
assert "data" in ds, f"{key} dataset missing 'data'"
assert isinstance(ds["data"], list)
# ════════════════════════════════════════════════════════════
# Per-chart specifics
# ════════════════════════════════════════════════════════════
class TestCapexChart:
def test_type_is_doughnut(self):
d, _ = make_result()
assert d["capex_chart"]["type"] == "doughnut"
def test_labels_data_colors_same_length(self):
d, _ = make_result()
chart = d["capex_chart"]
ds = chart["data"]["datasets"][0]
assert len(chart["data"]["labels"]) == len(ds["data"]) == len(ds["backgroundColor"])
def test_only_nonzero_items_included(self):
"""Zero-amount CAPEX items must be excluded from the chart."""
d, _ = make_result()
for val in d["capex_chart"]["data"]["datasets"][0]["data"]:
assert val > 0, "Chart must only contain items with amount > 0"
def test_cutout_set(self):
d, _ = make_result()
assert d["capex_chart"]["options"].get("cutout") == "60%"
def test_legend_position_right(self):
d, _ = make_result()
legend = d["capex_chart"]["options"]["plugins"]["legend"]
assert legend["position"] == "right"
class TestRampChart:
def test_type_is_line(self):
d, _ = make_result()
assert d["ramp_chart"]["type"] == "line"
def test_two_datasets(self):
d, _ = make_result()
assert len(d["ramp_chart"]["data"]["datasets"]) == 2
def test_24_data_points(self):
d, _ = make_result()
for ds in d["ramp_chart"]["data"]["datasets"]:
assert len(ds["data"]) == 24
def test_labels_translated(self):
d_en, _ = make_result(lang="en")
d_de, _ = make_result(lang="de")
en_label = d_en["ramp_chart"]["data"]["datasets"][0]["label"]
de_label = d_de["ramp_chart"]["data"]["datasets"][0]["label"]
assert en_label != de_label, "Ramp chart labels must be translated"
class TestPLChart:
def test_type_is_bar(self):
d, _ = make_result()
assert d["pl_chart"]["type"] == "bar"
def test_horizontal_axis(self):
"""P&L chart must be horizontal (indexAxis='y')."""
d, _ = make_result()
assert d["pl_chart"]["options"].get("indexAxis") == "y"
def test_five_labels_and_values(self):
d, _ = make_result()
assert len(d["pl_chart"]["data"]["labels"]) == 5
assert len(d["pl_chart"]["data"]["datasets"][0]["data"]) == 5
def test_colors_reflect_sign(self):
"""Positive values → green, negative → red."""
d, _ = make_result()
ds = d["pl_chart"]["data"]["datasets"][0]
for val, color in zip(ds["data"], ds["backgroundColor"]):
if val >= 0:
assert "22,163,74" in color, f"Expected green for positive {val}"
else:
assert "239,68,68" in color, f"Expected red for negative {val}"
class TestCFChart:
def test_type_is_bar(self):
d, _ = make_result()
assert d["cf_chart"]["type"] == "bar"
def test_60_data_points(self):
d, _ = make_result()
assert len(d["cf_chart"]["data"]["datasets"][0]["data"]) == 60
def test_colors_reflect_sign(self):
d, _ = make_result()
ds = d["cf_chart"]["data"]["datasets"][0]
for val, color in zip(ds["data"], ds["backgroundColor"]):
if val >= 0:
assert "22,163,74" in color
else:
assert "239,68,68" in color
class TestCumChart:
def test_type_is_line(self):
d, _ = make_result()
assert d["cum_chart"]["type"] == "line"
def test_60_data_points(self):
d, _ = make_result()
assert len(d["cum_chart"]["data"]["datasets"][0]["data"]) == 60
def test_fill_enabled(self):
d, _ = make_result()
assert d["cum_chart"]["data"]["datasets"][0].get("fill") is True
class TestDSCRChart:
def test_type_is_bar(self):
d, _ = make_result()
assert d["dscr_chart"]["type"] == "bar"
def test_five_entries(self):
d, _ = make_result()
assert len(d["dscr_chart"]["data"]["datasets"][0]["data"]) == 5
def test_colors_reflect_1_2_threshold(self):
d, _ = make_result()
ds = d["dscr_chart"]["data"]["datasets"][0]
for val, color in zip(ds["data"], ds["backgroundColor"]):
if val >= 1.2:
assert "22,163,74" in color, f"Expected green for DSCR {val} >= 1.2"
else:
assert "239,68,68" in color, f"Expected red for DSCR {val} < 1.2"
def test_values_capped_at_10(self):
"""DSCR values above 10 (no-debt scenarios) must be capped."""
d, _ = make_result(loanPct=0)
for val in d["dscr_chart"]["data"]["datasets"][0]["data"]:
assert val <= 10
class TestSeasonChart:
def test_type_is_bar(self):
d, _ = make_result(venue="outdoor")
assert d["season_chart"]["type"] == "bar"
def test_12_points(self):
d, _ = make_result(venue="outdoor")
assert len(d["season_chart"]["data"]["datasets"][0]["data"]) == 12
assert len(d["season_chart"]["data"]["labels"]) == 12
def test_values_are_percentages(self):
"""Season values are fractions (01.5) multiplied by 100."""
d, s = make_result(venue="outdoor")
chart_vals = d["season_chart"]["data"]["datasets"][0]["data"]
expected = [v * 100 for v in s["season"]]
assert chart_vals == expected
def test_labels_translated(self):
d_en, _ = make_result(lang="en", venue="outdoor")
d_de, _ = make_result(lang="de", venue="outdoor")
assert d_en["season_chart"]["data"]["labels"] != d_de["season_chart"]["data"]["labels"]
# ════════════════════════════════════════════════════════════
# All combos: charts always valid
# ════════════════════════════════════════════════════════════
@pytest.mark.parametrize("venue,own", [
("indoor", "rent"), ("indoor", "buy"), ("outdoor", "rent"), ("outdoor", "buy")
])
def test_all_charts_json_serializable_for_all_combos(venue, own):
d, _ = make_result(venue=venue, own=own)
for key in INDOOR_CHART_KEYS:
json.dumps(d[key])