""" Sprint 41 Tests: Title auto-generation fix + mobile close button CSS (PR #333). Covers: - streaming.py: sessions titled 'New Chat' trigger auto-title generation - streaming.py: sessions with empty/falsy title trigger auto-title generation - streaming.py: sessions titled 'Untitled' (original guard) still trigger - streaming.py: sessions with a user-set title do NOT trigger auto-title - style.css: .mobile-close-btn is hidden by default (desktop rule present) - style.css: .mobile-close-btn shown in <=900px media query - style.css: #btnCollapseWorkspacePanel hidden in <=900px media query - index.html: both .mobile-close-btn and #btnCollapseWorkspacePanel buttons exist """ import pathlib import re import unittest REPO_ROOT = pathlib.Path(__file__).parent.parent CSS = (REPO_ROOT / "static" / "style.css").read_text() HTML = (REPO_ROOT / "static" / "index.html").read_text() STREAMING_PY = (REPO_ROOT / "api" / "streaming.py").read_text() # ── streaming.py: title auto-generation condition ───────────────────────── class TestTitleAutoGenerationCondition(unittest.TestCase): """Verify the guarded condition in streaming.py covers all default title cases.""" def _titles_that_trigger(self): """Extract the condition from the source so tests stay in sync with code.""" # Find the if-condition that calls title_from m = re.search( r'if\s+(s\.title\s*==.*?):\s*\n\s*s\.title\s*=\s*title_from', STREAMING_PY, re.DOTALL, ) self.assertIsNotNone(m, "Could not find title auto-generation condition in streaming.py") return m.group(1) def test_untitled_in_condition(self): cond = self._titles_that_trigger() self.assertIn("'Untitled'", cond, "Original 'Untitled' guard must be present") def test_new_chat_in_condition(self): cond = self._titles_that_trigger() self.assertIn("'New Chat'", cond, "'New Chat' guard must be present (PR #333)") def test_empty_title_guard_in_condition(self): cond = self._titles_that_trigger() self.assertIn("not s.title", cond, "Empty/falsy title guard must be present (PR #333)") def test_condition_logic_covers_all_defaults(self): """The condition uses OR so any one default title triggers generation.""" cond = self._titles_that_trigger() # All three guards must be joined by 'or' parts = re.split(r'\bor\b', cond) self.assertGreaterEqual(len(parts), 3, "Expected at least 3 OR-joined sub-conditions (Untitled, New Chat, not s.title)") # ── style.css: mobile close button visibility ───────────────────────────── class TestMobileCloseButtonCSS(unittest.TestCase): """Verify CSS rules that control the duplicate close button on mobile.""" def test_mobile_close_btn_hidden_by_default(self): """Desktop default: .mobile-close-btn must be display:none outside any media query.""" # Find the rule before the first @media block that contains mobile-close-btn # We look for the pattern in the desktop (non-media-query) section self.assertIn( ".mobile-close-btn{display:none;}", CSS.replace(" ", ""), ".mobile-close-btn should be hidden by default (desktop) — rule missing or wrong" ) def test_mobile_close_btn_shown_in_900px_query(self): """Inside max-width:900px media query, .mobile-close-btn must be display:flex.""" # Extract the 900px media block m = re.search(r'@media\s*\(max-width\s*:\s*900px\)\s*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}', CSS) self.assertIsNotNone(m, "@media(max-width:900px) block not found in style.css") block = m.group(1).replace(" ", "") self.assertIn(".mobile-close-btn{display:flex;}", block, ".mobile-close-btn must be display:flex inside the 900px media query") def test_desktop_collapse_btn_hidden_in_900px_query(self): """Inside max-width:900px media query, #btnCollapseWorkspacePanel must be display:none.""" m = re.search(r'@media\s*\(max-width\s*:\s*900px\)\s*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}', CSS) self.assertIsNotNone(m, "@media(max-width:900px) block not found in style.css") block = m.group(1).replace(" ", "") self.assertIn("#btnCollapseWorkspacePanel{display:none;}", block, "#btnCollapseWorkspacePanel must be display:none in 900px media query") def test_900px_query_retains_existing_rules(self): """Ensure the PR didn't accidentally drop existing rules from the 900px block.""" m = re.search(r'@media\s*\(max-width\s*:\s*900px\)\s*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}', CSS) self.assertIsNotNone(m) block = m.group(1) self.assertIn("rightpanel", block, ".rightpanel rule missing from 900px block") self.assertIn("mobile-files-btn", block, ".mobile-files-btn rule missing from 900px block") # ── index.html: button presence ─────────────────────────────────────────── class TestWorkspacePanelButtons(unittest.TestCase): """Verify both panel buttons are present in the HTML so CSS rules have targets.""" def test_desktop_collapse_button_exists(self): self.assertIn("btnCollapseWorkspacePanel", HTML, "#btnCollapseWorkspacePanel button must exist in index.html") def test_mobile_close_button_exists(self): self.assertIn("mobile-close-btn", HTML, ".mobile-close-btn button must exist in index.html") def test_mobile_close_button_has_aria_label(self): """Accessibility: mobile close button must have an aria-label.""" m = re.search(r'class="[^"]*mobile-close-btn[^"]*"[^>]*>', HTML) self.assertIsNotNone(m, "Could not find mobile-close-btn element") self.assertIn("aria-label", m.group(0), "mobile-close-btn must have aria-label for accessibility") if __name__ == "__main__": unittest.main()