"""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-finding row → one "Open Tool" button per targeted finding # --------------------------------------------------------------------------- # # The findings panel was redesigned (mockup-v2): it now renders ONE # severity-sorted flat list rather than per-tool expanders. Each finding # with a known tool id gets a tertiary button labelled # ``"{Tool display name} →"`` that switches pages on click. Findings # with no tool id (file-level CSV-shape warnings, encoding flags, etc.) # render without a button — the description still shows so the user # isn't blind to them. class TestRowsRenderForFindings: def test_one_button_per_targeted_finding(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 = [b.label for b in app.button] # Each targeted finding gets its own "Open Tool" button — three # findings → three buttons (two pointing at Clean Text, one at # Standardize Formats). clean_text_buttons = [l for l in labels if l == "Clean Text →"] format_buttons = [l for l in labels if l == "Standardize Formats →"] assert len(clean_text_buttons) == 2, ( f"expected 2 Clean Text buttons; got: {labels}" ) assert len(format_buttons) == 1, ( f"expected 1 Standardize Formats button; 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 = [b.label for b in app.button] assert any("Limpiar texto" in lbl for lbl in labels), ( f"Spanish tool name missing; buttons: {labels}" ) # --------------------------------------------------------------------------- # Open-tool button labels — confirm the arrow + name format # --------------------------------------------------------------------------- class TestOpenToolButton: """Each finding with a known tool gets a tertiary button labelled ``"{Tool name} →"``. The arrow + spacing is the affordance that distinguishes the row's primary action from the title text.""" def test_open_tool_label_english(self): findings = [_make_finding(tool="02_text_cleaner")] app = _harness(findings) app.run() labels = [b.label for b in app.button] assert "Clean Text →" in labels, ( f"expected 'Clean Text →' button; got: {labels}" ) def test_open_tool_label_spanish(self): findings = [_make_finding(tool="02_text_cleaner")] app = _harness(findings, lang="es") app.run() labels = [b.label for b in app.button] assert "Limpiar texto →" in labels, ( f"expected 'Limpiar texto →' button; got: {labels}" ) # --------------------------------------------------------------------------- # Untargeted findings (file-level) render without an action button # --------------------------------------------------------------------------- class TestUntargetedFindings: """A finding with ``tool=""`` (e.g., CSV BOM stripped at read time) is file-level — no tool page to jump to — and the redesigned panel renders the description without a button. We assert that the row contributes nothing to ``app.button`` while still appearing in the rendered markdown.""" def test_untargeted_renders_no_button_en(self): findings = [ _make_finding(tool="", id="csv_bom_stripped", description="BOM stripped"), _make_finding(tool="02_text_cleaner", id="nbsp_padding"), ] app = _harness(findings) app.run() labels = [b.label for b in app.button] # Only the targeted finding contributed a button. assert "Clean Text →" in labels # The BOM finding's description must still be visible somewhere. all_md = "\n".join( m.body for m in app.markdown if hasattr(m, "body") ) assert "BOM stripped" in all_md, ( "untargeted finding's description should still render" ) def test_untargeted_renders_no_button_es(self): findings = [_make_finding( tool="", id="csv_bom_stripped", description="BOM eliminado", )] app = _harness(findings, lang="es") app.run() labels = [b.label for b in app.button] # No tool id → no tool-jump button at all. assert not any("→" in lbl for lbl in labels), ( f"untargeted finding should not render a tool button; 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