Files
padelnomics/padelnomics/tests/test_planner_routes.py
Deeman 696581d57b fix(planner): charts, wizard footer layout, tooltip translations & summary label
- Charts: augment_d() now emits full Chart.js 4.x config objects {type, data,
  options} for all 7 charts. Previously raw data dicts were passed directly to
  new Chart() which requires a proper config, causing silent render failures.

- Wizard footer: HTMX outerHTML OOB swap for #wizPreview was stripping
  class="wizard-preview" on every recalc, collapsing the flex layout and
  stacking CAPEX / Monatl. CF / IRR vertically. Added class back to the OOB
  element in calculate_response.html.

- Wizard nav buttons: showWizStep() was generating wiz-btn--prev and
  wiz-btn--calc classes that had no CSS. Changed to wiz-btn--back and
  wiz-btn--next which are defined in planner.css.

- Tooltip translations: added 60 tip_* keys (EN + DE) to i18n.py and replaced
  all hardcoded English strings in planner.html slider calls with t.tip_* refs.
  German users now see German tooltip text on all "i" info spans.

- Summary label: added wiz_summary_label ("Live Summary" / "Aktuelle Werte")
  as a full-width caption in the wizard preview bar so users understand the
  three values reflect current slider state. Added flex-wrap + caption CSS.

- Tests: 384 new tests across test_planner_charts.py, test_i18n_tips.py,
  test_planner_routes.py covering all fixed bugs. Full suite: 1013 passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 19:11:24 +01:00

126 lines
5.3 KiB
Python

"""
Tests for planner route responses.
Regression for:
1. OOB swap for #wizPreview stripping class="wizard-preview", causing the
flex layout to break and CAPEX/CF/IRR values to stack vertically.
Fix: calculate_response.html OOB element must include class="wizard-preview".
2. Charts: /calculate must embed valid Chart.js JSON (not raw data dicts).
"""
import json
import pytest
class TestCalculateEndpoint:
async def test_returns_200(self, client):
resp = await client.post("/en/planner/calculate", form={"activeTab": "capex"})
assert resp.status_code == 200
async def test_returns_html(self, client):
resp = await client.post("/en/planner/calculate", form={"activeTab": "capex"})
assert resp.content_type.startswith("text/html")
async def test_all_tabs_render(self, client):
for tab in ("capex", "operating", "cashflow", "returns", "metrics"):
resp = await client.post(
"/en/planner/calculate", form={"activeTab": tab}
)
assert resp.status_code == 200, f"Tab {tab} returned {resp.status_code}"
async def test_german_endpoint_works(self, client):
resp = await client.post("/de/planner/calculate", form={"activeTab": "capex"})
assert resp.status_code == 200
class TestWizPreviewOOBSwap:
"""
Regression: HTMX outerHTML OOB swap replaces the entire #wizPreview element.
If the response element lacks class="wizard-preview", the flex layout is lost
and the three preview values stack vertically on every recalculation.
"""
async def test_oob_element_has_wizard_preview_class(self, client):
resp = await client.post("/en/planner/calculate", form={"activeTab": "capex"})
body = (await resp.get_data()).decode()
# The OOB swap element must carry class="wizard-preview" so the flex
# box layout survives the outerHTML replacement.
assert 'class="wizard-preview"' in body, (
"OOB #wizPreview element must include class='wizard-preview' "
"to preserve flex layout after HTMX outerHTML swap"
)
async def test_oob_element_has_correct_id(self, client):
resp = await client.post("/en/planner/calculate", form={"activeTab": "capex"})
body = (await resp.get_data()).decode()
assert 'id="wizPreview"' in body
async def test_oob_element_has_hx_swap_oob(self, client):
resp = await client.post("/en/planner/calculate", form={"activeTab": "capex"})
body = (await resp.get_data()).decode()
assert 'hx-swap-oob="true"' in body
class TestChartJSONInResponse:
"""
Regression: augment_d() was embedding raw data dicts instead of full
Chart.js configs. initCharts() passes the embedded JSON directly to
new Chart(canvas, config) — which requires {type, data, options}.
"""
async def _get_chart_json(self, client, chart_id: str, tab: str) -> dict:
resp = await client.post(
"/en/planner/calculate", form={"activeTab": tab}
)
body = (await resp.get_data()).decode()
# Charts are embedded as: <script type="application/json" id="chartX-data">...</script>
marker = f'id="{chart_id}-data">'
start = body.find(marker)
assert start != -1, f"Chart script tag '{chart_id}-data' not found in response"
start += len(marker)
end = body.find("</script>", start)
return json.loads(body[start:end])
async def test_capex_chart_is_valid_chartjs_config(self, client):
config = await self._get_chart_json(client, "chartCapex", "capex")
assert config["type"] == "doughnut"
assert "datasets" in config["data"]
assert "options" in config
async def test_cf_chart_is_valid_chartjs_config(self, client):
config = await self._get_chart_json(client, "chartCF", "cashflow")
assert config["type"] == "bar"
assert "datasets" in config["data"]
assert config["options"]["responsive"] is True
assert config["options"]["maintainAspectRatio"] is False
async def test_dscr_chart_is_valid_chartjs_config(self, client):
config = await self._get_chart_json(client, "chartDSCR", "returns")
assert config["type"] == "bar"
assert len(config["data"]["datasets"][0]["data"]) == 5
async def test_ramp_chart_is_valid_chartjs_config(self, client):
config = await self._get_chart_json(client, "chartRevRamp", "operating")
assert config["type"] == "line"
assert len(config["data"]["datasets"]) == 2
async def test_pl_chart_is_horizontal_bar(self, client):
config = await self._get_chart_json(client, "chartPL", "operating")
assert config["type"] == "bar"
assert config["options"]["indexAxis"] == "y"
class TestWizSummaryLabel:
"""The wizard preview must include the summary caption."""
async def test_summary_caption_in_response(self, client):
resp = await client.post("/en/planner/calculate", form={"activeTab": "capex"})
body = (await resp.get_data()).decode()
assert "Live Summary" in body
async def test_german_summary_caption_in_response(self, client):
resp = await client.post("/de/planner/calculate", form={"activeTab": "capex"})
body = (await resp.get_data()).decode()
assert "Aktuelle Werte" in body