test(gui): add Streamlit AppTest layer (139 tests)
Until now every test ran against core or the CLI; the Streamlit GUI was verified by hand. This commit adds tests/gui/ — 139 AppTest- driven tests behind a 'gui' marker so the quick loop (``pytest -m 'not gui'``) stays at 1777 tests / ~10s while ``pytest`` runs everything (1916 / ~14s). Coverage: - test_smoke.py (59): every page renders in EN and ES, expected substring present, sidebar selector mounted. - test_chrome.py (18): language selector flips session state and re-renders; quit button + farewell strings localize; tool-card names use the active language. - test_gate.py (9): require_normalization_gate no-op / warning / short-circuit / hash-mismatch invariants; warning + button localized. - test_workflows.py (14): happy path per Ready tool — stash upload, render, find primary action, verify result lands in session state. - test_dedup_review.py (8): Accept All / Reject All / Clear Decisions wire through to review_decisions; apply_review_decisions semantics (keep-all, merge, column override). - test_advanced_panels.py (15): config_panel widget defaults and options (algorithm, threshold, survivor rule, merge, multiselects, config save/load). - test_errors.py (4): garbage / empty / single-column uploads don't crash; duplicate-target mapping raises InputValidationError. - test_findings_panel.py (12): driven via a small standalone harness page so we test the component without faking a file_uploader. EN + ES strings, per-tool grouping, open-tool button label, untargeted expander, severity summary. Shared infrastructure in tests/gui/conftest.py: - ``stash_upload`` / ``stash_upload_without_gate`` — populate session_state to pre-pass or block the gate. - ``with_language`` — set ``ui_lang`` before run(). - ``collected_text`` — flatten title/caption/markdown/etc. into one string for substring assertions. - Auto-marking: every test in tests/gui/ gets ``@pytest.mark.gui`` via ``pytest_collection_modifyitems``, so the marker isn't per-test boilerplate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
181
tests/gui/test_chrome.py
Normal file
181
tests/gui/test_chrome.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""Chrome tests — language selector, hide_streamlit_chrome, quit flow.
|
||||
|
||||
These verify the GUI plumbing that every page depends on. Failures here
|
||||
cascade into every other page, so they run cheap and run first
|
||||
(alphabetical name ordering after smoke).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from .conftest import collected_text, with_language
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# hide_streamlit_chrome mounts the selector
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestHideChromeMountsSelector:
|
||||
"""``hide_streamlit_chrome()`` is the one place the language selector
|
||||
is mounted. Every page that hides chrome (= every page) must get
|
||||
exactly one sidebar selectbox with the i18n label."""
|
||||
|
||||
def test_home_has_one_sidebar_selectbox(self, home_app):
|
||||
home_app.run()
|
||||
# Only one selectbox in the sidebar today; if a page adds
|
||||
# another, this becomes a weaker bound.
|
||||
assert len(home_app.sidebar.selectbox) == 1, (
|
||||
"expected exactly one sidebar selectbox (the language picker); "
|
||||
f"got {len(home_app.sidebar.selectbox)}"
|
||||
)
|
||||
|
||||
def test_selector_label_is_localized(self, home_app):
|
||||
with_language(home_app, "es")
|
||||
home_app.run()
|
||||
labels = [sb.label for sb in home_app.sidebar.selectbox]
|
||||
assert "Idioma" in labels, (
|
||||
f"Spanish selector should be labelled 'Idioma'; got {labels}"
|
||||
)
|
||||
|
||||
def test_selector_label_english_default(self, home_app):
|
||||
home_app.run() # no with_language → default = en
|
||||
labels = [sb.label for sb in home_app.sidebar.selectbox]
|
||||
assert "Language" in labels
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Language selector switches session state
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestLanguageSwitch:
|
||||
"""Picking 'es' in the selector flips ``st.session_state['ui_lang']``
|
||||
and re-renders the page with Spanish strings on the next run."""
|
||||
|
||||
def test_default_language_is_english(self, home_app):
|
||||
home_app.run()
|
||||
# AppTest's session_state proxy doesn't implement .get(); use
|
||||
# membership check + attribute access. Absence == default ("en").
|
||||
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
|
||||
|
||||
def test_selecting_spanish_persists_in_session(self, home_app):
|
||||
home_app.run()
|
||||
selector = home_app.sidebar.selectbox[0]
|
||||
selector.select("es").run()
|
||||
assert home_app.session_state["ui_lang"] == "es"
|
||||
|
||||
def test_selecting_spanish_re_renders_in_spanish(self, home_app):
|
||||
home_app.run()
|
||||
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]}"
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Footer + page_title localization
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
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_*``."""
|
||||
|
||||
def test_footer_english(self, home_app):
|
||||
home_app.run()
|
||||
text = collected_text(home_app)
|
||||
assert "Your data never leaves" in text
|
||||
|
||||
def test_footer_spanish(self, home_app):
|
||||
with_language(home_app, "es")
|
||||
home_app.run()
|
||||
text = collected_text(home_app)
|
||||
assert "Tus datos nunca salen" 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 un archivo para empezar`` from the es pack.
|
||||
assert "Sube un archivo" in text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Quit / Close page
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestQuitButtonRenders:
|
||||
"""The Close page must show the localized title, body, and the
|
||||
Close-the-app button. We don't actually click the button — that
|
||||
would call ``os._exit(0)`` and kill the test process. We only
|
||||
assert the button is present and its label is localized."""
|
||||
|
||||
def test_close_page_english(self, app_factory):
|
||||
app = app_factory("99_Close")
|
||||
app.run()
|
||||
text = collected_text(app)
|
||||
assert "Close DataTools" in text
|
||||
labels = [b.label for b in app.button]
|
||||
assert any("Close the app" in lbl for lbl in labels), (
|
||||
f"Close-the-app button missing; buttons: {labels}"
|
||||
)
|
||||
|
||||
def test_close_page_spanish(self, app_factory):
|
||||
app = app_factory("99_Close")
|
||||
with_language(app, "es")
|
||||
app.run()
|
||||
text = collected_text(app)
|
||||
assert "Cerrar DataTools" in text
|
||||
labels = [b.label for b in app.button]
|
||||
assert any("Cerrar la app" in lbl for lbl in labels), (
|
||||
f"Spanish Close button missing; buttons: {labels}"
|
||||
)
|
||||
|
||||
def test_close_body_describes_unsaved_work_warning_es(self, app_factory):
|
||||
app = app_factory("99_Close")
|
||||
with_language(app, "es")
|
||||
app.run()
|
||||
text = collected_text(app)
|
||||
assert "trabajo sin guardar" in text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool cards use localized names on the home grid
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestHomeToolGridLocalization:
|
||||
"""The home grid pulls tool display names through ``tool_name()`` in
|
||||
``tools_registry``. The Spanish pack provides translations for every
|
||||
tool id; a regression in that wiring would make Spanish users see
|
||||
English names. Pin a few representative ones."""
|
||||
|
||||
@pytest.mark.parametrize("needle", [
|
||||
"Eliminador de duplicados",
|
||||
"Limpiador de texto",
|
||||
"Estandarizador de formatos",
|
||||
"Gestor de valores faltantes",
|
||||
"Mapeador de columnas",
|
||||
])
|
||||
def test_es_tool_name_on_home_grid(self, home_app, needle):
|
||||
with_language(home_app, "es")
|
||||
home_app.run()
|
||||
text = collected_text(home_app)
|
||||
assert needle in text, f"missing localized tool name {needle!r}"
|
||||
Reference in New Issue
Block a user