"""Mobile-first quality tests — automated validation of mobile UX requirements. These tests verify the HTML, CSS, and HTMX attributes that make the dashboard work correctly on phones. No browser / Playwright required: we parse the static assets and server responses directly. Categories: M1xx Viewport & meta tags M2xx Touch target sizing M3xx iOS keyboard & zoom prevention M4xx HTMX robustness (double-submit, sync) M5xx Safe-area / notch support M6xx AirLLM backend interface contract """ import re from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch # ── helpers ─────────────────────────────────────────────────────────────────── def _css() -> str: """Read the main stylesheet.""" css_path = Path(__file__).parent.parent / "static" / "style.css" return css_path.read_text() def _index_html(client) -> str: return client.get("/").text def _timmy_panel_html(client) -> str: """Fetch the Timmy chat panel (loaded dynamically from index via HTMX).""" return client.get("/agents/timmy/panel").text # ── M1xx — Viewport & meta tags ─────────────────────────────────────────────── def test_M101_viewport_meta_present(client): """viewport meta tag must exist for correct mobile scaling.""" html = _index_html(client) assert 'name="viewport"' in html def test_M102_viewport_includes_width_device_width(client): html = _index_html(client) assert "width=device-width" in html def test_M103_viewport_includes_initial_scale_1(client): html = _index_html(client) assert "initial-scale=1" in html def test_M104_viewport_includes_viewport_fit_cover(client): """viewport-fit=cover is required for iPhone notch / Dynamic Island support.""" html = _index_html(client) assert "viewport-fit=cover" in html def test_M105_apple_mobile_web_app_capable(client): """Enables full-screen / standalone mode when added to iPhone home screen.""" html = _index_html(client) assert "apple-mobile-web-app-capable" in html def test_M106_theme_color_meta_present(client): """theme-color sets the browser chrome colour on Android Chrome.""" html = _index_html(client) assert 'name="theme-color"' in html def test_M107_apple_status_bar_style_present(client): html = _index_html(client) assert "apple-mobile-web-app-status-bar-style" in html def test_M108_lang_attribute_on_html(client): """lang attribute aids screen readers and mobile TTS.""" html = _index_html(client) assert ' str: """Read the mobile template source.""" path = Path(__file__).parent.parent / "src" / "dashboard" / "templates" / "mobile.html" return path.read_text() def _swarm_live_html() -> str: """Read the swarm live template source.""" path = Path(__file__).parent.parent / "src" / "dashboard" / "templates" / "swarm_live.html" return path.read_text() def test_M701_mobile_chat_no_raw_message_interpolation(): """mobile.html must not interpolate ${message} directly into innerHTML — XSS risk.""" html = _mobile_html() # The vulnerable pattern is `${message}` inside a template literal assigned to innerHTML # After the fix, message must only appear via textContent assignment assert "textContent = message" in html or "textContent=message" in html, ( "mobile.html still uses innerHTML + ${message} interpolation — XSS vulnerability" ) def test_M702_mobile_chat_user_input_not_in_innerhtml_template_literal(): """${message} must not appear inside a backtick string that is assigned to innerHTML.""" html = _mobile_html() # Find all innerHTML += `...` blocks and verify none contain ${message} blocks = re.findall(r"innerHTML\s*\+=?\s*`([^`]*)`", html, re.DOTALL) for block in blocks: assert "${message}" not in block, ( "innerHTML template literal still contains ${message} — XSS vulnerability" ) def test_M703_swarm_live_agent_name_not_interpolated_in_innerhtml(): """swarm_live.html must not put ${agent.name} inside innerHTML template literals.""" html = _swarm_live_html() blocks = re.findall(r"innerHTML\s*=\s*agents\.map\([^;]+\)\.join\([^)]*\)", html, re.DOTALL) assert len(blocks) == 0, ( "swarm_live.html still uses innerHTML=agents.map(…) with interpolated agent data — XSS vulnerability" ) def test_M704_swarm_live_uses_textcontent_for_agent_data(): """swarm_live.html must use textContent (not innerHTML) to set agent name/description.""" html = _swarm_live_html() assert "textContent" in html, ( "swarm_live.html does not use textContent — agent data may be raw-interpolated into DOM" )