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:
123
web/tests/test_planner_routes.py
Normal file
123
web/tests/test_planner_routes.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user