Files
datatools-dev/tests/gui/test_chrome.py
Michael 35d46a0c1a 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>
2026-05-13 16:13:40 +00:00

182 lines
7.0 KiB
Python

"""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}"