""" 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: 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("", 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