Files
datatools-dev/tests/gui/test_smoke.py
Michael 4955fb239b test: cover help_md keys, header smoke, and bilingual ES smoke
Two stale Spanish smoke assertions still expected English page titles
for PDF Extractor and Reconciler — the i18n work landed real
translations ("PDF a CSV", "Reconciliar dos archivos"), so refresh the
expected substrings and the surrounding comment.

Add new coverage for the help-popover feature:
- TestHelpPopoverKeys (test_lang_packs): every tool_id resolves a
  non-empty tools.<id>.help_md in BOTH packs; help.button_label and
  help.missing_body resolve in both.
- TestDescriptionCopy (test_tools_registry): every Tool.description
  non-empty and under 120 chars — pins the post-jargon-scrub copy
  so future drift back into multi-clause prose is loud.
- TestRenderToolHeaderSmoke: render_tool_header is callable, listed
  in components.__all__, and every i18n key it touches resolves in
  both packs. Runs without a Streamlit script context.

Suite: 2427 passed (+9 new), 91 skipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 18:07:19 +00:00

153 lines
6.2 KiB
Python

"""Smoke tests: every page renders without exception in EN and ES.
The cheapest, highest-value GUI tests in the project. They catch:
- Page-level Python errors (import failures, syntax errors that
``ast.parse`` misses because they're runtime, e.g., a missing
attribute on a module).
- i18n pack key drift (a string that used to render in EN now renders
literally as ``"chrome.language_label"`` because someone renamed the
key in en.json but forgot es.json or the call site).
- Streamlit API churn that breaks ``set_page_config`` /
``hide_streamlit_chrome`` on a single page.
What they don't cover: user interactions. Those live in the workflow
tests.
"""
from __future__ import annotations
import pytest
from .conftest import collected_text, with_language
# Every page that ships in the sidebar nav. Slugs match the filenames
# under ``src/gui/pages/`` so failures point at a real file.
PAGE_SLUGS = [
"1_Deduplicator",
"2_Text_Cleaner",
"3_Format_Standardizer",
"4_Missing_Values",
"5_Column_Mapper",
"6_Outlier_Detector",
"7_Multi_File_Merger",
"8_Validator_Reporter",
"9_Pipeline_Runner",
"10_PDF_Extractor",
"11_Reconciler",
"99_Close",
]
# Substrings that must appear on each page for each language.
#
# v1.6 coverage reality (also documented in docs/USER-GUIDE.md §3.4):
# only the home page, the Close page, and the shared chrome /
# components ship Spanish strings. Per-tool page bodies are still
# hard-coded English in both modes — translating them is tracked as a
# follow-up. The substrings below reflect that reality: a page that
# isn't translated yet asserts the same English substring under both
# languages. The fact that the page *renders at all* in 'es' is still
# the value of the smoke test.
#
# When a page gains real Spanish translation, flip its 'es' entry to
# the localized substring — the test surface stays the same.
EXPECTED_SUBSTRINGS: dict[str, dict[str, str]] = {
"1_Deduplicator": {"en": "Find Duplicates", "es": "Buscar duplicados"},
"2_Text_Cleaner": {"en": "Clean Text", "es": "Limpiar texto"},
"3_Format_Standardizer": {"en": "Standardize", "es": "Estandarizar"},
"4_Missing_Values": {"en": "Fix Missing", "es": "Corregir valores"},
"5_Column_Mapper": {"en": "Map Columns", "es": "Mapear columnas"},
"6_Outlier_Detector": {"en": "Unusual", "es": "atípicos"},
"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"},
# PDF Extractor + Reconciler page titles are now translated in
# both packs (``tools.<id>.page_title``). Their hero copy diverges
# by language, so the smoke test pins the localized substring.
"10_PDF_Extractor": {"en": "PDF to CSV", "es": "PDF a CSV"},
"11_Reconciler": {"en": "Reconcile", "es": "Reconciliar"},
"99_Close": {"en": "Shutting down", "es": "Cerrando"},
}
class TestHomePageRenders:
"""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", "Clean. Normalize. Transform."),
("es", "Limpia. Normaliza. Transforma."),
])
def test_home_renders_in_language(self, home_app, lang, expected):
with_language(home_app, lang)
home_app.run()
assert home_app.exception is None or home_app.exception == [], (
f"home page raised: {home_app.exception}"
)
assert expected in collected_text(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 "Se ejecuta 100% en local" in text
class TestEveryPageRenders:
"""Parametrize over (page, language). Failure tells you exactly which
page + which language broke."""
@pytest.mark.parametrize("slug", PAGE_SLUGS)
@pytest.mark.parametrize("lang", ["en", "es"])
def test_renders_without_exception(self, app_factory, slug, lang):
app = app_factory(slug)
with_language(app, lang)
app.run()
# AppTest exposes ``exception`` as a list of element-wrapped
# exceptions (empty when no error fired).
assert not app.exception, (
f"page {slug!r} raised in language {lang!r}: {app.exception}"
)
@pytest.mark.parametrize("slug", PAGE_SLUGS)
@pytest.mark.parametrize("lang", ["en", "es"])
def test_expected_substring_present(self, app_factory, slug, lang):
app = app_factory(slug)
with_language(app, lang)
app.run()
needle = EXPECTED_SUBSTRINGS[slug][lang]
text = collected_text(app)
assert needle in text, (
f"page {slug!r} ({lang!r}) missing expected substring "
f"{needle!r}\nGot:\n{text[:500]}"
)
class TestPageHasLanguageSelector:
"""Every page that calls ``hide_streamlit_chrome`` should mount the
sidebar language selector. This is the only place the picker is
rendered — if the chrome helper stops calling it, the test fails."""
@pytest.mark.parametrize("slug", PAGE_SLUGS)
def test_sidebar_selectbox_present(self, app_factory, slug):
app = app_factory(slug)
app.run()
# The selector is the only sidebar selectbox we ship today; if
# a page adds another the test should be loosened to "at least
# one selectbox with the language label."
assert len(app.sidebar.selectbox) >= 1, (
f"page {slug!r} has no sidebar selectbox — "
f"hide_streamlit_chrome() should have mounted the language "
f"selector."
)