""" 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 (0–1.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])