Files
padelnomics/web/tests/test_planner_routes.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

124 lines
5.2 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
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