test: cover help_md keys, header smoke, and bilingual ES smoke
Two stale Spanish smoke assertions still expected English page titles
for PDF Extractor and Reconciler — the i18n work landed real
translations ("PDF a CSV", "Reconciliar dos archivos"), so refresh the
expected substrings and the surrounding comment.
Add new coverage for the help-popover feature:
- TestHelpPopoverKeys (test_lang_packs): every tool_id resolves a
non-empty tools.<id>.help_md in BOTH packs; help.button_label and
help.missing_body resolve in both.
- TestDescriptionCopy (test_tools_registry): every Tool.description
non-empty and under 120 chars — pins the post-jargon-scrub copy
so future drift back into multi-clause prose is loud.
- TestRenderToolHeaderSmoke: render_tool_header is callable, listed
in components.__all__, and every i18n key it touches resolves in
both packs. Runs without a Streamlit script context.
Suite: 2427 passed (+9 new), 91 skipped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -63,12 +63,11 @@ EXPECTED_SUBSTRINGS: dict[str, dict[str, str]] = {
|
||||
"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"},
|
||||
# The PDF Extractor and Reconciler pages are English-only today
|
||||
# (translations tracked as a follow-up). The smoke test value is
|
||||
# still that the page *renders at all* in 'es'; the substring is
|
||||
# the same English hero text under both languages.
|
||||
"10_PDF_Extractor": {"en": "PDF to CSV", "es": "PDF to CSV"},
|
||||
"11_Reconciler": {"en": "Reconcile", "es": "Reconcile"},
|
||||
# PDF Extractor + Reconciler page titles are now translated in
|
||||
# both packs (``tools.<id>.page_title``). Their hero copy diverges
|
||||
# by language, so the smoke test pins the localized substring.
|
||||
"10_PDF_Extractor": {"en": "PDF to CSV", "es": "PDF a CSV"},
|
||||
"11_Reconciler": {"en": "Reconcile", "es": "Reconciliar"},
|
||||
"99_Close": {"en": "Shutting down", "es": "Cerrando"},
|
||||
}
|
||||
|
||||
|
||||
@@ -178,3 +178,32 @@ class TestKeyCoverage:
|
||||
for lang in ("en", "es"):
|
||||
value = t(key, lang)
|
||||
assert value and value != key, f"missing {key!r} in {lang}"
|
||||
|
||||
|
||||
class TestHelpPopoverKeys:
|
||||
"""Every tool's inline Help popover (``render_tool_header``) pulls
|
||||
its copy from ``tools.<id>.help_md`` and the two shared labels
|
||||
``help.button_label`` / ``help.missing_body``. A missing key would
|
||||
fall back to the literal lookup key and render that string in the
|
||||
popover instead of helpful content."""
|
||||
|
||||
@pytest.mark.parametrize("lang", ["en", "es"])
|
||||
def test_help_shared_keys_present(self, lang):
|
||||
for key in ("help.button_label", "help.missing_body"):
|
||||
value = t(key, lang)
|
||||
assert value and value != key, f"missing {key!r} in {lang!r}"
|
||||
|
||||
@pytest.mark.parametrize("lang", ["en", "es"])
|
||||
def test_every_tool_has_help_md(self, lang):
|
||||
# Import lazily so this file stays importable without the GUI.
|
||||
from src.gui.tools_registry import TOOLS
|
||||
|
||||
missing: list[str] = []
|
||||
for tool in TOOLS:
|
||||
key = f"tools.{tool.tool_id}.help_md"
|
||||
value = t(key, lang)
|
||||
if not value or value == key or not value.strip():
|
||||
missing.append(tool.tool_id)
|
||||
assert not missing, (
|
||||
f"language {lang!r} is missing help_md for: {missing}"
|
||||
)
|
||||
|
||||
@@ -157,6 +157,78 @@ class TestLocalizedAccessors:
|
||||
assert label and label != f"nav.section_{section}"
|
||||
|
||||
|
||||
class TestDescriptionCopy:
|
||||
"""The post-jargon-strip descriptions are intentionally tight one-
|
||||
liners. Pin them so future drift toward bloated marketing copy
|
||||
(or an accidentally-empty string) is caught by CI."""
|
||||
|
||||
# Roomy upper bound; the tightest description today is ~60 chars
|
||||
# and the longest is just over 90. ~120 leaves headroom for minor
|
||||
# copy tweaks without inviting paragraph-length card bodies.
|
||||
_MAX_DESCRIPTION_CHARS = 120
|
||||
|
||||
def test_every_description_is_non_empty(self):
|
||||
empty = [t.tool_id for t in TOOLS if not t.description.strip()]
|
||||
assert not empty, f"tools with empty descriptions: {empty}"
|
||||
|
||||
def test_every_description_under_max_chars(self):
|
||||
too_long = [
|
||||
(t.tool_id, len(t.description))
|
||||
for t in TOOLS
|
||||
if len(t.description) > self._MAX_DESCRIPTION_CHARS
|
||||
]
|
||||
assert not too_long, (
|
||||
f"tool descriptions exceed {self._MAX_DESCRIPTION_CHARS} chars: "
|
||||
f"{too_long}"
|
||||
)
|
||||
|
||||
|
||||
class TestRenderToolHeaderSmoke:
|
||||
"""``render_tool_header`` is the helper every tool page now calls in
|
||||
place of ``st.title(...) + st.caption(...)``. We can't render it
|
||||
without a Streamlit script context, but we CAN verify it imports
|
||||
cleanly via the public ``src.gui.components`` surface and resolves
|
||||
the expected i18n keys for a known tool id."""
|
||||
|
||||
def test_importable_from_public_components_package(self):
|
||||
from src.gui.components import render_tool_header
|
||||
|
||||
assert callable(render_tool_header)
|
||||
|
||||
def test_listed_in_public_all(self):
|
||||
# The public ``__all__`` is what per-tool builds key off; a
|
||||
# removal here would silently break tool pages that import
|
||||
# from ``src.gui.components`` directly.
|
||||
from src.gui import components as components_pkg
|
||||
|
||||
assert "render_tool_header" in components_pkg.__all__
|
||||
|
||||
def test_resolves_expected_i18n_keys_for_known_tool(self):
|
||||
# The helper reads four pack keys per render:
|
||||
# ``tools.<id>.page_title``, ``tools.<id>.page_caption``,
|
||||
# ``tools.<id>.help_md``, plus shared ``help.button_label`` /
|
||||
# ``help.missing_body``. We don't invoke the helper (no script
|
||||
# context) — we verify the keys it would touch resolve to
|
||||
# non-empty strings in both packs.
|
||||
from src.i18n import t as _t
|
||||
|
||||
tool_id = "02_text_cleaner"
|
||||
for lang in ("en", "es"):
|
||||
for suffix in ("page_title", "page_caption", "help_md"):
|
||||
key = f"tools.{tool_id}.{suffix}"
|
||||
value = _t(key, lang)
|
||||
assert value and value != key, (
|
||||
f"render_tool_header({tool_id!r}) "
|
||||
f"would render the literal key {key!r} in {lang!r}"
|
||||
)
|
||||
for key in ("help.button_label", "help.missing_body"):
|
||||
value = _t(key, lang)
|
||||
assert value and value != key, (
|
||||
f"render_tool_header would render the literal key "
|
||||
f"{key!r} in {lang!r}"
|
||||
)
|
||||
|
||||
|
||||
class TestReconcilerAndPdfArePresent:
|
||||
"""The two newest pages were the most likely to be forgotten in
|
||||
the registry — pin them explicitly so a regression flagging
|
||||
|
||||
Reference in New Issue
Block a user