"""GUI tests for the Lite tier. A Lite license unlocks Find Duplicates, Clean Text, Standardize Formats. Opening any other tool page (Fix Missing Values, Map Columns, Automated Workflows, etc.) must render an upgrade prompt and short-circuit the page body. The home grid shows a πŸ”’ Locked badge on the cards for tools the user's tier doesn't unlock. """ from __future__ import annotations import pytest from .conftest import collected_text, stash_upload @pytest.fixture def lite_license(monkeypatch, tmp_path): """Activate a Lite license; return nothing β€” the env vars route every page on this test through this license.""" monkeypatch.delenv("DATATOOLS_DEV_MODE", raising=False) monkeypatch.setenv( "DATATOOLS_LICENSE_PATH", str(tmp_path / "license.json"), ) from src.license.manager import reset_singleton_for_tests reset_singleton_for_tests() from src.license import LicenseManager, Tier LicenseManager()._mint( name="Lite User", email="lite@example.com", tier=Tier.LITE, ) yield reset_singleton_for_tests() # --------------------------------------------------------------------------- # Unlocked tools render normally # --------------------------------------------------------------------------- class TestLiteUnlockedPages: @pytest.mark.parametrize("slug,signal", [ ("1_Deduplicator", "Find Duplicates"), ("2_Text_Cleaner", "Clean Text"), ("3_Format_Standardizer", "Standardize Formats"), ]) def test_unlocked_pages_render_body( self, lite_license, app_factory, slug, signal, small_csv_bytes, ): app = app_factory(slug) stash_upload(app, name="messy.csv", data=small_csv_bytes) app.run() text = collected_text(app) assert signal in text # No upgrade prompt. assert "isn't on your" not in text and "no estΓ‘ incluida" not in text # --------------------------------------------------------------------------- # Locked tools render upgrade prompt # --------------------------------------------------------------------------- class TestLiteLockedPages: @pytest.mark.parametrize("slug", [ "4_Missing_Values", "5_Column_Mapper", "9_Pipeline_Runner", "6_Outlier_Detector", "7_Multi_File_Merger", "8_Validator_Reporter", ]) def test_locked_page_shows_upgrade_prompt( self, lite_license, app_factory, slug, small_csv_bytes, ): app = app_factory(slug) stash_upload(app, name="messy.csv", data=small_csv_bytes) app.run() text = collected_text(app) # Upgrade prompt title carries the localized lock + tier label. assert "isn't on your" in text or "no estΓ‘ incluida" in text, ( f"locked page {slug!r} missing upgrade prompt; got:\n" f"{text[:500]}" ) def test_upgrade_button_present( self, lite_license, app_factory, small_csv_bytes, ): app = app_factory("4_Missing_Values") stash_upload(app, name="messy.csv", data=small_csv_bytes) app.run() labels = [b.label for b in app.button] assert any("Manage license" in lbl for lbl in labels), ( f"upgrade prompt missing Manage-license button; got: {labels}" ) # --------------------------------------------------------------------------- # Home grid shows lock badges # --------------------------------------------------------------------------- class TestLiteHomeGridBadges: def test_locked_tool_card_shows_lock_badge( self, lite_license, home_app, ): home_app.run() text = collected_text(home_app) # Fix Missing Values is locked under Lite β€” its card should # have a πŸ”’ Locked badge. # We assert the lock glyph appears alongside the locked tool's # display name. Streamlit renders the markdown verbatim so the # ``πŸ”’ Locked`` text appears in the page markdown stream. assert "πŸ”’" in text or "Locked" in text, ( "home grid missing lock badge for Lite-locked tool" ) def test_unlocked_tool_card_no_lock(self, lite_license, home_app): home_app.run() # Dedup is unlocked under Lite. Its card markdown should NOT # contain a lock glyph adjacent to its name. We can't easily # scope by card without parsing the markdown stream, but we # can confirm both ``Ready`` (unlocked) and ``Locked`` # (locked) badges coexist on the page. text = collected_text(home_app) assert "Ready" in text or "Listo" in text # --------------------------------------------------------------------------- # Sidebar status shows Lite # --------------------------------------------------------------------------- class TestLiteSidebarStatus: def test_sidebar_caption_mentions_lite(self, lite_license, home_app): home_app.run() captions = " ".join(c.value for c in home_app.sidebar.caption) assert "Lite" in captions