diff --git a/tests/gui/test_smoke.py b/tests/gui/test_smoke.py index 44a0ed5..ca2ed54 100644 --- a/tests/gui/test_smoke.py +++ b/tests/gui/test_smoke.py @@ -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..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"}, } diff --git a/tests/test_lang_packs.py b/tests/test_lang_packs.py index 5c8e26e..bec8c88 100644 --- a/tests/test_lang_packs.py +++ b/tests/test_lang_packs.py @@ -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..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}" + ) diff --git a/tests/test_tools_registry.py b/tests/test_tools_registry.py index 68bcf75..87e2c0b 100644 --- a/tests/test_tools_registry.py +++ b/tests/test_tools_registry.py @@ -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..page_title``, ``tools..page_caption``, + # ``tools..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