Two unrelated UX issues addressed in one sweep across all nine tool
pages because they share the same edit surface.
(1) Sticky footer replaces the top + bottom back-link buttons.
Reported: a big white empty footer space at the bottom of every page;
the Back to Home button at the top scrolled out of view on long pages.
New ``render_sticky_footer()`` helper in ``components/_legacy.py``
injects a fixed-position bar at ``bottom: 0`` of the viewport with:
- A border-top so it visually reads as a non-movable bar.
- A semi-transparent background (rgba 0.96 + ``backdrop-filter: blur``)
so content underneath shows through faintly when the user scrolls.
- A styled ``<a href="home">`` anchor (not an ``st.button``) because
Streamlit widgets can't be CSS-positioned reliably — Streamlit owns
the widget's DOM container and re-mounts it on every rerun. A real
anchor sits exactly where the CSS puts it and triggers Streamlit's
URL routing to the home page.
- ``padding-bottom: 3.5rem`` on the main container so the last widget
isn't hidden behind the bar.
Called once per tool page, immediately after ``hide_streamlit_chrome()``
so it renders even on pages that ``st.stop()`` early before any other
content runs. The old top-and-bottom ``back_to_home_link()`` calls are
removed from every tool page; their entry/exit points were dropping
the button when the script short-circuited.
(2) Tool-page headers now localize.
Reported: switching the sidebar language picker to Spanish left the
tool page's title + caption in English. Root cause: every page had
hard-coded ``st.title("✂️ Clean Text")`` / ``st.caption("Trim
whitespace...")`` strings.
Added per-tool ``tools.<id>.page_title`` and
``tools.<id>.page_caption`` keys to ``en.json`` and ``es.json`` for
all nine tools. Routed each page's title/caption call through ``t()``.
Verified: with ``ui_lang=es`` set, the Clean Text page now renders
"✂️ Limpiar texto" + the Spanish caption.
Updated ``tests/gui/test_smoke.py::EXPECTED_SUBSTRINGS`` so the
``es`` column for each tool page asserts the actual Spanish string
(was a duplicate of the English string back when the page bodies
were English-only).
2220 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
137 lines
5.5 KiB
Python
137 lines
5.5 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",
|
|
"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"},
|
|
"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."""
|
|
|
|
@pytest.mark.parametrize("lang,expected", [
|
|
("en", "DataTools — Data Cleaning Mastery"),
|
|
("es", "DataTools — Maestría en limpieza de datos"),
|
|
])
|
|
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_footer_in_es(self, home_app):
|
|
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
|
|
|
|
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."
|
|
)
|