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>
208 lines
8.1 KiB
Python
208 lines
8.1 KiB
Python
"""Happy-path workflow tests for each Ready tool page.
|
||
|
||
These drive the GUI like a user would: pre-stash an upload + a passed
|
||
gate, render the page, click the primary action, assert the result
|
||
landed in session state. They catch wiring bugs that smoke tests
|
||
can't see — e.g., a primary button mis-keyed, a result not stashed in
|
||
session state, a page reading the wrong key.
|
||
|
||
Slow-ish (~0.5–2s per workflow). Sits behind the ``gui`` marker so
|
||
``pytest -m 'not gui'`` skips them.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import pandas as pd
|
||
import pytest
|
||
|
||
from .conftest import collected_text, stash_upload
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Deduplicator
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestDeduplicatorWorkflow:
|
||
"""Upload → click Find Duplicates → result lands in session_state."""
|
||
|
||
def _setup(self, app_factory, small_csv_bytes):
|
||
app = app_factory("1_Deduplicator")
|
||
stash_upload(app, name="messy.csv", data=small_csv_bytes)
|
||
return app
|
||
|
||
def test_upload_renders_preview(self, app_factory, small_csv_bytes):
|
||
app = self._setup(app_factory, small_csv_bytes)
|
||
app.run()
|
||
text = collected_text(app)
|
||
assert "Preview: messy.csv" in text, (
|
||
f"upload preview header missing; got:\n{text[:500]}"
|
||
)
|
||
|
||
def test_find_duplicates_button_present(self, app_factory, small_csv_bytes):
|
||
app = self._setup(app_factory, small_csv_bytes)
|
||
app.run()
|
||
labels = [b.label for b in app.button]
|
||
assert any("Find Duplicates" in lbl for lbl in labels), (
|
||
f"primary action missing; got: {labels}"
|
||
)
|
||
|
||
def test_clicking_find_duplicates_stashes_result(
|
||
self, app_factory, small_csv_bytes,
|
||
):
|
||
app = self._setup(app_factory, small_csv_bytes)
|
||
app.run()
|
||
# Find the Find-Duplicates button and click it. AppTest's
|
||
# button-by-key access is via ``.button(key=...)`` — we don't
|
||
# have the key here, so locate it by label.
|
||
target = next(b for b in app.button if "Find Duplicates" in b.label)
|
||
target.click().run()
|
||
# The page stores the result under ``result`` in session state.
|
||
result = app.session_state["result"]
|
||
assert result is not None, "Find Duplicates didn't stash a result"
|
||
# The sample has Alice twice → one match group.
|
||
assert len(result.match_groups) >= 1
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Text Cleaner
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestTextCleanerWorkflow:
|
||
def _setup(self, app_factory, small_csv_bytes):
|
||
app = app_factory("2_Text_Cleaner")
|
||
stash_upload(app, name="messy.csv", data=small_csv_bytes)
|
||
return app
|
||
|
||
def test_page_renders_with_upload(self, app_factory, small_csv_bytes):
|
||
app = self._setup(app_factory, small_csv_bytes)
|
||
app.run()
|
||
assert not app.exception
|
||
text = collected_text(app)
|
||
assert "Text Cleaner" in text
|
||
|
||
def test_preview_or_clean_button_present(self, app_factory, small_csv_bytes):
|
||
"""The text cleaner ships a primary action (label varies by
|
||
version). We just assert at least one primary-looking button
|
||
exists past the upload."""
|
||
app = self._setup(app_factory, small_csv_bytes)
|
||
app.run()
|
||
# Filter out the gate-redirect button (which would only be
|
||
# present if the gate fired, which our setup prevents).
|
||
gate_buttons = {"Go to Review & Normalize", "Ir a Revisar y Normalizar"}
|
||
non_gate = [b for b in app.button if b.label not in gate_buttons]
|
||
assert non_gate, (
|
||
f"no primary buttons rendered; got: {[b.label for b in app.button]}"
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Format Standardizer
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestFormatStandardizerWorkflow:
|
||
def test_page_renders_with_upload(self, app_factory, small_csv_bytes):
|
||
app = app_factory("3_Format_Standardizer")
|
||
stash_upload(app, name="messy.csv", data=small_csv_bytes)
|
||
app.run()
|
||
assert not app.exception
|
||
text = collected_text(app)
|
||
assert "Format Standardizer" in text
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Missing Value Handler
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestMissingValuesWorkflow:
|
||
def test_page_renders_with_upload(self, app_factory, small_csv_bytes):
|
||
app = app_factory("4_Missing_Values")
|
||
stash_upload(app, name="messy.csv", data=small_csv_bytes)
|
||
app.run()
|
||
assert not app.exception
|
||
text = collected_text(app)
|
||
assert "Missing" in text
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Column Mapper
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestColumnMapperWorkflow:
|
||
def test_page_renders_with_upload(self, app_factory, small_csv_bytes):
|
||
app = app_factory("5_Column_Mapper")
|
||
stash_upload(app, name="messy.csv", data=small_csv_bytes)
|
||
app.run()
|
||
assert not app.exception
|
||
text = collected_text(app)
|
||
assert "Column" in text
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Pipeline Runner
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestPipelineRunnerWorkflow:
|
||
def test_page_renders_with_upload(self, app_factory, small_csv_bytes):
|
||
app = app_factory("9_Pipeline_Runner")
|
||
stash_upload(app, name="messy.csv", data=small_csv_bytes)
|
||
app.run()
|
||
assert not app.exception
|
||
text = collected_text(app)
|
||
assert "Pipeline" in text
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Review page — special: doesn't gate on upload, has its own analyzer flow
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestReviewWorkflow:
|
||
"""The Review page is the gate-fixer. Without an upload it shows a
|
||
'go back to home' message. With an upload it runs the analyzer and
|
||
shows findings."""
|
||
|
||
def test_no_upload_shows_back_to_home(self, app_factory):
|
||
app = app_factory("0_Review")
|
||
app.run()
|
||
text = collected_text(app)
|
||
# Page shows ``No file uploaded`` + ``Back to home``.
|
||
assert "No file uploaded" in text or "uploaded" in text.lower()
|
||
|
||
def test_with_upload_shows_review_content(
|
||
self, app_factory, small_csv_bytes,
|
||
):
|
||
app = app_factory("0_Review")
|
||
# Review page only needs the upload bytes, not a pre-passed gate.
|
||
app.session_state["home_uploaded_bytes"] = small_csv_bytes
|
||
app.session_state["home_uploaded_name"] = "messy.csv"
|
||
app.session_state["home_uploaded_size"] = len(small_csv_bytes)
|
||
app.run()
|
||
assert not app.exception
|
||
text = collected_text(app)
|
||
# Page ran the analyzer — either we get findings or the
|
||
# "already clean" success message. Either way confirms the
|
||
# analyzer pipeline ran end-to-end with the stashed bytes.
|
||
clean_msg = "No findings to review" in text
|
||
encoding_section = "File encoding" in text
|
||
assert clean_msg or encoding_section, (
|
||
f"Review page didn't surface analyzer output; got:\n{text[:400]}"
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Coming-Soon pages still render (just a stub) — pinned so we know if a
|
||
# Coming-Soon goes from "stub renders" to "import error".
|
||
# ---------------------------------------------------------------------------
|
||
|
||
@pytest.mark.parametrize("slug,name", [
|
||
("6_Outlier_Detector", "Outlier"),
|
||
("7_Multi_File_Merger", "Merger"),
|
||
("8_Validator_Reporter", "Validator"),
|
||
])
|
||
class TestComingSoonStubs:
|
||
def test_stub_renders(self, app_factory, slug, name):
|
||
app = app_factory(slug)
|
||
app.run()
|
||
assert not app.exception
|
||
text = collected_text(app)
|
||
assert name in text
|