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>
This commit is contained in:
Deeman
2026-02-22 00:44:40 +01:00
parent 5e471567b9
commit 4ae00b35d1
235 changed files with 45 additions and 42 deletions

View File

@@ -0,0 +1,250 @@
"""
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])