103 lines
3.7 KiB
Python
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}"
|