Files
lifeos-dev/tests/test_mobile_nav.py

103 lines
3.7 KiB
Python

"""Tests that the mobile navigation bar is correctly structured and positioned."""
import re
import pytest
from httpx import AsyncClient, ASGITransport
from tests.registry import app
@pytest.fixture
async def client():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as c:
yield c
@pytest.mark.asyncio
async def test_mob_nav_exists(client):
"""mob-nav element exists in page output."""
resp = await client.get("/")
assert resp.status_code == 200
assert 'class="mob-nav"' in resp.text, "mob-nav not found in HTML"
@pytest.mark.asyncio
async def test_mob_nav_is_direct_body_child(client):
"""mob-nav must be a direct child of body, not nested in any container."""
resp = await client.get("/")
html = resp.text
mob_idx = html.find('id="mobNav"')
body_close = html.find('</body>')
assert mob_idx != -1, "mobNav not found"
assert body_close != -1, "</body> not found"
between = html[mob_idx:body_close]
assert between.count('</main>') == 0, "mob-nav appears to be inside <main>"
assert between.count('</div></div></div>') == 0, "mob-nav appears deeply nested"
@pytest.mark.asyncio
async def test_mob_nav_has_five_items(client):
"""Bottom bar must have exactly 5 navigation items (4 links + 1 button)."""
resp = await client.get("/")
html = resp.text
start = html.find('id="mobNav"')
assert start != -1
# Scope to just the mob-nav element (ends at first </div> after it)
end = html.find('</div>', start)
chunk = html[start:end]
links = len(re.findall(r'<a\b', chunk))
buttons = len(re.findall(r'<button\b', chunk))
assert links == 4, f"Expected 4 link items, found {links}"
assert buttons == 1, f"Expected 1 button item, found {buttons}"
@pytest.mark.asyncio
async def test_mob_nav_has_inline_fixed_position(client):
"""mob-nav must have position:fixed as an inline style for maximum reliability."""
resp = await client.get("/")
assert 'id="mobNav" style="position:fixed' in resp.text, \
"mob-nav missing inline position:fixed style"
@pytest.mark.asyncio
async def test_mob_nav_css_has_fixed_position(client):
"""CSS must include position:fixed for mob-nav."""
css_resp = await client.get("/static/style.css")
css = css_resp.text
assert "position: fixed" in css or "position:fixed" in css, \
"No position:fixed found in CSS"
@pytest.mark.asyncio
async def test_mob_nav_inline_style_in_head(client):
"""Critical mob-nav styles must be inlined in <head> as a fallback."""
resp = await client.get("/")
html = resp.text
head_end = html.find('</head>')
head = html[:head_end]
assert '.mob-nav' in head, "No inline mob-nav styles found in <head>"
assert 'position:fixed' in head, "No position:fixed in inline <head> styles"
@pytest.mark.asyncio
async def test_mob_nav_not_inside_transformed_parent(client):
"""No ancestor of mob-nav should have transform that breaks position:fixed."""
resp = await client.get("/")
html = resp.text
mob_idx = html.find('id="mobNav"')
body_start = html.find('<body')
prefix = html[body_start:mob_idx]
opens = len(re.findall(r'<div\b[^>]*>', prefix))
closes = prefix.count('</div>')
nesting = opens - closes
assert nesting <= 1, \
f"mob-nav is nested {nesting} divs deep - must be 0 or 1 (direct body child)"
@pytest.mark.asyncio
async def test_mob_nav_present_on_all_pages(client):
"""mob-nav should appear on every page, not just dashboard."""
for path in ["/", "/tasks/", "/focus/", "/capture/", "/contacts/"]:
resp = await client.get(path)
assert 'id="mobNav"' in resp.text, f"mob-nav missing on {path}"