"""Findings panel rendering tests. ``render_findings_panel`` is the central widget on the home page and the Review page; failures here cascade into the user's first impression. We drive it via a tiny test harness page (``_findings_panel_harness.py``) so the test can inject findings directly into session state — no file_uploader simulation needed. We verify: - Empty findings list → localized "no issues" success message. - Findings with tool ids → one expander per tool, labeled in the active language. - Header + severity summary render at the top. - Untargeted findings land in the "Other / file-level" expander. Pack-key parity is already pinned by ``test_lang_packs.py``; this file pins the call sites instead. """ from __future__ import annotations from pathlib import Path import pandas as pd import pytest from streamlit.testing.v1 import AppTest from .conftest import PROJECT_ROOT, collected_text, with_language HARNESS_PATH = Path(__file__).resolve().parent / "_findings_panel_harness.py" def _harness(findings, lang: str = "en") -> AppTest: """Build an AppTest of the harness page with ``findings`` pre-stashed.""" app = AppTest.from_file(str(HARNESS_PATH)) app.session_state["test_findings"] = findings if lang != "en": app.session_state["ui_lang"] = lang return app def _make_finding(tool: str = "", **overrides): """Build a minimal :class:`Finding` object. ``Finding`` is a frozen dataclass; constructor signature is well-pinned by core tests, so we use it directly here rather than building dicts.""" from src.core.analyze import Finding kwargs = dict( id="test_finding", severity="warn", tool=tool, count=1, description="A test finding.", column=None, samples=[], confidence="medium", fix_action="", ) kwargs.update(overrides) return Finding(**kwargs) # --------------------------------------------------------------------------- # Empty findings → success message # --------------------------------------------------------------------------- class TestEmptyFindings: def test_empty_renders_no_issues_english(self): app = _harness([]) app.run() text = collected_text(app) assert "No issues detected" in text def test_empty_renders_no_issues_spanish(self): app = _harness([], lang="es") app.run() text = collected_text(app) assert "No se detectaron problemas" in text # --------------------------------------------------------------------------- # Header text # --------------------------------------------------------------------------- class TestHeader: def test_header_english(self): app = _harness([_make_finding(tool="02_text_cleaner")]) app.run() text = collected_text(app) assert "Detected issues" in text def test_header_spanish(self): app = _harness([_make_finding(tool="02_text_cleaner")], lang="es") app.run() text = collected_text(app) assert "Problemas detectados" in text # --------------------------------------------------------------------------- # Per-tool grouping → one expander per tool id # --------------------------------------------------------------------------- class TestGrouping: def test_findings_grouped_into_per_tool_expanders(self): findings = [ _make_finding(tool="02_text_cleaner", id="whitespace_padding"), _make_finding(tool="02_text_cleaner", id="nbsp_padding"), _make_finding(tool="03_format_standardizer", id="mixed_case_email"), ] app = _harness(findings) app.run() labels = [e.label for e in app.expander] # Two unique tools → two expanders. Each label carries the # tool's display name + finding count. text_cleaner_expanders = [lbl for lbl in labels if "Clean Text" in lbl] format_expanders = [lbl for lbl in labels if "Standardize Formats" in lbl] assert len(text_cleaner_expanders) == 1, ( f"expected one Clean Text expander; got: {labels}" ) assert len(format_expanders) == 1, ( f"expected one Standardize Formats expander; got: {labels}" ) def test_tool_names_localize_in_spanish(self): findings = [_make_finding(tool="02_text_cleaner")] app = _harness(findings, lang="es") app.run() labels = [e.label for e in app.expander] assert any("Limpiar texto" in lbl for lbl in labels), ( f"Spanish tool name missing; expanders: {labels}" ) def test_finding_count_in_expander_label(self): findings = [ _make_finding(tool="02_text_cleaner", id=f"f{i}") for i in range(3) ] app = _harness(findings) app.run() labels = [e.label for e in app.expander] # Pack template: "{tool} — {n} finding(s)" text_cleaner_label = next(l for l in labels if "Clean Text" in l) assert "3" in text_cleaner_label, ( f"expected count '3' in expander label; got {text_cleaner_label!r}" ) # --------------------------------------------------------------------------- # Open-tool button localizes # --------------------------------------------------------------------------- class TestOpenToolButton: """Each tool section has an ``st.page_link`` to jump to that tool's page. AppTest exposes page_links as ``app.button`` entries with label ``"Open {tool} →"`` (English) / ``"Abrir {tool} →"`` (Spanish).""" def test_open_tool_label_english(self): findings = [_make_finding(tool="02_text_cleaner")] app = _harness(findings) app.run() # ``st.page_link`` may show up under ``app.button`` or in the # raw markdown. We probe both. text = collected_text(app) # Pack template: "Open {tool} →" assert "Open Clean Text" in text def test_open_tool_label_spanish(self): findings = [_make_finding(tool="02_text_cleaner")] app = _harness(findings, lang="es") app.run() text = collected_text(app) # Pack template: "Abrir {tool} →" assert "Abrir Limpiar texto" in text # --------------------------------------------------------------------------- # Untargeted findings (file-level) go in the "Other" expander # --------------------------------------------------------------------------- class TestUntargetedFindings: def test_untargeted_goes_to_other_expander_en(self): findings = [ _make_finding(tool="", id="csv_bom_stripped"), _make_finding(tool="02_text_cleaner", id="nbsp_padding"), ] app = _harness(findings) app.run() labels = [e.label for e in app.expander] # Pack template: "Other / file-level — {n} finding(s)" assert any("Other / file-level" in lbl for lbl in labels), ( f"untargeted expander missing; got: {labels}" ) def test_untargeted_label_spanish(self): findings = [_make_finding(tool="", id="csv_bom_stripped")] app = _harness(findings, lang="es") app.run() labels = [e.label for e in app.expander] # Spanish pack: "Otros / a nivel de archivo — {n} hallazgo(s)" assert any("Otros / a nivel de archivo" in lbl for lbl in labels), ( f"Spanish 'Other' expander missing; got: {labels}" ) # --------------------------------------------------------------------------- # Severity summary # --------------------------------------------------------------------------- class TestSeveritySummary: """The panel renders a per-severity summary caption like ``⚠️ 2 warn · ℹ️ 1 info``. We pin the icon + count rendering.""" def test_severity_icons_render(self): findings = [ _make_finding(tool="02_text_cleaner", severity="warn"), _make_finding(tool="02_text_cleaner", severity="warn"), _make_finding(tool="03_format_standardizer", severity="info"), ] app = _harness(findings) app.run() text = collected_text(app) # Icons live in the per-language pack ("findings.severity_*"). # The summary template is shared between languages. assert "⚠️" in text or "warn" in text # Counts present. assert "2 warn" in text or "2 warn" in text