GUI/lang-pack tests were asserting against pre-v3 strings ("Data
Cleaning Mastery", "Maestría en limpieza…") that the brand refresh
replaced with "UNALOGIX DataTools" + "Clean. Normalize. Transform."
Updated assertions to the current copy and switched the findings
panel tests to the redesigned flat-list layout (per-finding "Open
Tool →" buttons instead of per-tool expanders).
New coverage:
- tests/test_cli_reconcile.py (13) — preview/apply, tolerance flags,
sign inversion, key flags, error paths, Excel input.
- tests/test_tools_registry.py (27) — unique tool_ids, page_slug →
real file, valid sections/tiers, localized accessor fallbacks,
explicit pins for PDF Extractor + Reconciler entries.
- tests/test_reconcile.py — one-side-empty, key-pass tagging,
additional validation cases, input-DataFrame immutability.
- tests/gui/test_smoke.py — PAGE_SLUGS now includes 10_PDF_Extractor
and 11_Reconciler in both en/es.
- tests/gui/test_workflows.py — TestPdfExtractorWorkflow and
TestReconcilerWorkflow render checks.
Net: 2317 passed → 2418 passed, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
236 lines
8.8 KiB
Python
236 lines
8.8 KiB
Python
"""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
|