test: fix v3 branding drift, add reconcile CLI + registry coverage
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>
This commit is contained in:
@@ -85,8 +85,8 @@ class TestGatePassesWithTrialLicense:
|
||||
home_app.run()
|
||||
text = collected_text(home_app)
|
||||
# With a valid license, the activation form should NOT be the
|
||||
# primary content; we should see the home title + tool cards.
|
||||
assert "Data Cleaning Mastery" in text
|
||||
# primary content; we should see the home tagline + tool cards.
|
||||
assert "Clean. Normalize. Transform." in text
|
||||
assert "Activate DataTools" not in text # form not shown inline
|
||||
|
||||
def test_sidebar_shows_active_status(self, trial_license, home_app):
|
||||
@@ -150,7 +150,7 @@ class TestActivationFormSubmission:
|
||||
# After activation the page reruns and the activation form
|
||||
# should be gone — we should see the home page proper.
|
||||
text = collected_text(home_app)
|
||||
assert "Data Cleaning Mastery" in text
|
||||
assert "Clean. Normalize. Transform." in text
|
||||
|
||||
def test_trial_button_absent_paid_only(self, no_license_env, home_app):
|
||||
"""v1.6 dropped the user-facing trial flow — paid licenses only.
|
||||
|
||||
@@ -59,7 +59,7 @@ class TestLanguageSwitch:
|
||||
lang = home_app.session_state["ui_lang"] if "ui_lang" in home_app.session_state else "en"
|
||||
assert lang == "en"
|
||||
text = collected_text(home_app)
|
||||
assert "Data Cleaning Mastery" in text
|
||||
assert "Clean. Normalize. Transform." in text
|
||||
|
||||
def test_selecting_spanish_persists_in_session(self, home_app):
|
||||
home_app.run()
|
||||
@@ -72,22 +72,22 @@ class TestLanguageSwitch:
|
||||
selector = home_app.sidebar.selectbox[0]
|
||||
selector.select("es").run()
|
||||
text = collected_text(home_app)
|
||||
assert "Maestría" in text, (
|
||||
"after selecting Spanish, the home title should switch to "
|
||||
f"'🧹 DataTools — Maestría…'; got:\n{text[:300]}"
|
||||
assert "Limpia. Normaliza. Transforma." in text, (
|
||||
"after selecting Spanish, the home tagline should switch to "
|
||||
f"'Limpia. Normaliza. Transforma.'; got:\n{text[:300]}"
|
||||
)
|
||||
|
||||
def test_selecting_back_to_english_reverts(self, home_app):
|
||||
# Start in Spanish, then flip back.
|
||||
with_language(home_app, "es")
|
||||
home_app.run()
|
||||
assert "Maestría" in collected_text(home_app)
|
||||
assert "Limpia. Normaliza. Transforma." in collected_text(home_app)
|
||||
|
||||
selector = home_app.sidebar.selectbox[0]
|
||||
selector.select("en").run()
|
||||
text = collected_text(home_app)
|
||||
assert "Data Cleaning Mastery" in text
|
||||
assert "Maestría" not in text
|
||||
assert "Clean. Normalize. Transform." in text
|
||||
assert "Limpia. Normaliza. Transforma." not in text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -96,26 +96,34 @@ class TestLanguageSwitch:
|
||||
|
||||
class TestLocalizedChrome:
|
||||
"""A spot-check on the parts of the chrome that aren't the selector:
|
||||
the bottom footer caption and the home-page hero text. Other strings
|
||||
are pinned indirectly by ``TestEveryPageRenders.test_expected_*``."""
|
||||
the home-page privacy pill (visible to AppTest) and the upload
|
||||
section heading. The sticky footer caption is rendered via a
|
||||
component-iframe and isn't visible through ``collected_text``."""
|
||||
|
||||
def test_footer_english(self, home_app):
|
||||
def test_privacy_pill_english(self, home_app):
|
||||
home_app.run()
|
||||
text = collected_text(home_app)
|
||||
assert "Your data never leaves" in text
|
||||
assert "Runs 100% locally" in text
|
||||
|
||||
def test_footer_spanish(self, home_app):
|
||||
def test_privacy_pill_spanish(self, home_app):
|
||||
with_language(home_app, "es")
|
||||
home_app.run()
|
||||
text = collected_text(home_app)
|
||||
assert "Tus datos nunca salen" in text
|
||||
assert "Se ejecuta 100% en local" in text
|
||||
|
||||
def test_upload_section_heading_localizes(self, home_app):
|
||||
with_language(home_app, "es")
|
||||
home_app.run()
|
||||
text = collected_text(home_app)
|
||||
# ``📤 Sube uno o más archivos para empezar`` from the es pack.
|
||||
assert "Sube uno o más archivos" in text
|
||||
# The visible "Files" section heading is hard-coded English
|
||||
# in the redesigned home page; what's still localized is the
|
||||
# file_uploader widget's label (``upload.uploader_label_multi``).
|
||||
# AppTest exposes uploaders separately from the text-bearing
|
||||
# widget collections, so we check the uploader's label
|
||||
# attribute directly.
|
||||
labels = [u.label for u in home_app.file_uploader]
|
||||
assert any("Importa archivos" in lbl for lbl in labels), (
|
||||
f"Spanish uploader label missing; got: {labels}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -98,11 +98,19 @@ class TestHeader:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Per-tool grouping → one expander per tool id
|
||||
# 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 TestGrouping:
|
||||
def test_findings_grouped_into_per_tool_expanders(self):
|
||||
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"),
|
||||
@@ -110,96 +118,96 @@ class TestGrouping:
|
||||
]
|
||||
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}"
|
||||
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_expanders) == 1, (
|
||||
f"expected one Standardize Formats expander; 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 = [e.label for e in app.expander]
|
||||
labels = [b.label for b in app.button]
|
||||
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}"
|
||||
f"Spanish tool name missing; buttons: {labels}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Open-tool button localizes
|
||||
# Open-tool button labels — confirm the arrow + name format
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
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)."""
|
||||
"""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()
|
||||
# ``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
|
||||
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()
|
||||
text = collected_text(app)
|
||||
# Pack template: "Abrir {tool} →"
|
||||
assert "Abrir Limpiar texto" in text
|
||||
labels = [b.label for b in app.button]
|
||||
assert "Limpiar texto →" in labels, (
|
||||
f"expected 'Limpiar texto →' button; got: {labels}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Untargeted findings (file-level) go in the "Other" expander
|
||||
# Untargeted findings (file-level) render without an action button
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestUntargetedFindings:
|
||||
def test_untargeted_goes_to_other_expander_en(self):
|
||||
"""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"),
|
||||
_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 = [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}"
|
||||
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_label_spanish(self):
|
||||
findings = [_make_finding(tool="", id="csv_bom_stripped")]
|
||||
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 = [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}"
|
||||
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}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ PAGE_SLUGS = [
|
||||
"7_Multi_File_Merger",
|
||||
"8_Validator_Reporter",
|
||||
"9_Pipeline_Runner",
|
||||
"10_PDF_Extractor",
|
||||
"11_Reconciler",
|
||||
"99_Close",
|
||||
]
|
||||
|
||||
@@ -61,17 +63,28 @@ EXPECTED_SUBSTRINGS: dict[str, dict[str, str]] = {
|
||||
"7_Multi_File_Merger": {"en": "Combine Files", "es": "Combinar archivos"},
|
||||
"8_Validator_Reporter": {"en": "Quality Check", "es": "Verificación de calidad"},
|
||||
"9_Pipeline_Runner": {"en": "Automated", "es": "Flujos automatizados"},
|
||||
# The PDF Extractor and Reconciler pages are English-only today
|
||||
# (translations tracked as a follow-up). The smoke test value is
|
||||
# still that the page *renders at all* in 'es'; the substring is
|
||||
# the same English hero text under both languages.
|
||||
"10_PDF_Extractor": {"en": "PDF to CSV", "es": "PDF to CSV"},
|
||||
"11_Reconciler": {"en": "Reconcile", "es": "Reconcile"},
|
||||
"99_Close": {"en": "Shutting down", "es": "Cerrando"},
|
||||
}
|
||||
|
||||
|
||||
class TestHomePageRenders:
|
||||
"""The home page is the only one with full EN/ES coverage in v1.6.
|
||||
Pin it independently so its translation is non-regressable."""
|
||||
"""Pin the home hero in both languages.
|
||||
|
||||
Since the v3 brand refresh the title is the literal wordmark
|
||||
("UNALOGIX DataTools") in both packs; the localized tagline is
|
||||
what shifts between en and es. We assert against the tagline
|
||||
string, which lives in ``home.caption`` of each pack.
|
||||
"""
|
||||
|
||||
@pytest.mark.parametrize("lang,expected", [
|
||||
("en", "DataTools — Data Cleaning Mastery"),
|
||||
("es", "DataTools — Maestría en limpieza de datos"),
|
||||
("en", "Clean. Normalize. Transform."),
|
||||
("es", "Limpia. Normaliza. Transforma."),
|
||||
])
|
||||
def test_home_renders_in_language(self, home_app, lang, expected):
|
||||
with_language(home_app, lang)
|
||||
@@ -81,11 +94,15 @@ class TestHomePageRenders:
|
||||
)
|
||||
assert expected in collected_text(home_app)
|
||||
|
||||
def test_home_renders_footer_in_es(self, home_app):
|
||||
def test_home_renders_privacy_pill_in_es(self, home_app):
|
||||
# The footer caption is rendered via a component-iframe so
|
||||
# ``collected_text`` can't see it. The privacy pill on the
|
||||
# home header IS visible to AppTest and carries the same
|
||||
# locality story, so we pin that instead.
|
||||
with_language(home_app, "es")
|
||||
home_app.run()
|
||||
text = collected_text(home_app)
|
||||
assert "Tus datos nunca salen" in text or "Se ejecuta localmente" in text
|
||||
assert "Se ejecuta 100% en local" in text
|
||||
|
||||
class TestEveryPageRenders:
|
||||
"""Parametrize over (page, language). Failure tells you exactly which
|
||||
|
||||
@@ -152,6 +152,48 @@ class TestPipelineRunnerWorkflow:
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PDF to CSV — file-uploader-driven so we can't fully exercise the
|
||||
# scan flow through AppTest. Pin the initial render (which carries the
|
||||
# dep-status banner when deps are missing) so a future regression in
|
||||
# the dep guard shows up here.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestPdfExtractorWorkflow:
|
||||
def test_page_renders_without_upload(self, app_factory):
|
||||
app = app_factory("10_PDF_Extractor")
|
||||
app.run()
|
||||
assert not app.exception
|
||||
text = collected_text(app)
|
||||
assert "PDF to CSV" in text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reconcile Two Files — early-exits at ``st.stop()`` without both
|
||||
# uploads. Pin both the no-upload state and the title.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestReconcilerWorkflow:
|
||||
def test_page_renders_without_uploads(self, app_factory):
|
||||
app = app_factory("11_Reconciler")
|
||||
app.run()
|
||||
assert not app.exception
|
||||
text = collected_text(app)
|
||||
assert "Reconcile" in text
|
||||
|
||||
def test_prompts_for_both_uploads_when_empty(self, app_factory):
|
||||
# ``st.info("Upload both files to continue.")`` fires when
|
||||
# either side is missing; that text is the contract we test
|
||||
# against — if the prompt disappears the user has no idea
|
||||
# what to do next.
|
||||
app = app_factory("11_Reconciler")
|
||||
app.run()
|
||||
info_messages = [i.body for i in app.info if hasattr(i, "body")]
|
||||
assert any("Upload both files" in m for m in info_messages), (
|
||||
f"missing 'Upload both files' prompt; got: {info_messages}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Coming-Soon pages still render (just a stub) — pinned so we know if a
|
||||
# Coming-Soon goes from "stub renders" to "import error".
|
||||
|
||||
Reference in New Issue
Block a user