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:
147
tests/gui/test_smoke.py
Normal file
147
tests/gui/test_smoke.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""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 = [
|
||||
"0_Review",
|
||||
"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]] = {
|
||||
"0_Review": {"en": "Review", "es": "Review"},
|
||||
"1_Deduplicator": {"en": "Deduplicator", "es": "Deduplicator"},
|
||||
"2_Text_Cleaner": {"en": "Text Cleaner", "es": "Text Cleaner"},
|
||||
"3_Format_Standardizer": {"en": "Format", "es": "Format"},
|
||||
"4_Missing_Values": {"en": "Missing", "es": "Missing"},
|
||||
"5_Column_Mapper": {"en": "Column", "es": "Column"},
|
||||
"6_Outlier_Detector": {"en": "Outlier", "es": "Outlier"},
|
||||
"7_Multi_File_Merger": {"en": "Merger", "es": "Merger"},
|
||||
"8_Validator_Reporter": {"en": "Validator", "es": "Validator"},
|
||||
"9_Pipeline_Runner": {"en": "Pipeline", "es": "Pipeline"},
|
||||
"99_Close": {"en": "Close DataTools", "es": "Cerrar DataTools"},
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
def test_home_tool_card_uses_es_name(self, home_app):
|
||||
"""When the home grid renders in Spanish, the dedup card title
|
||||
must use the Spanish display name, not the English fallback."""
|
||||
with_language(home_app, "es")
|
||||
home_app.run()
|
||||
text = collected_text(home_app)
|
||||
assert "Eliminador de duplicados" 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."
|
||||
)
|
||||
Reference in New Issue
Block a user