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