various bug fixes Sunday 20260301 5:15pm
This commit is contained in:
@@ -328,3 +328,11 @@ def seed_note(all_seeds):
|
||||
@pytest.fixture(scope="session")
|
||||
def seed_meeting(all_seeds):
|
||||
return {"id": all_seeds["meeting"], "title": "Test Meeting"}
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def seed_list(all_seeds):
|
||||
return {"id": all_seeds["list"], "name": "Test List"}
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def seed_appointment(all_seeds):
|
||||
return {"id": all_seeds["appointment"], "title": "Test Appointment"}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -33,10 +33,13 @@ from tests.form_factory import build_form_data, build_edit_data
|
||||
_CREATE_ROUTES = [r for r in ALL_ROUTES if r.kind == RouteKind.CREATE and not r.has_file_upload]
|
||||
_EDIT_ROUTES = [r for r in ALL_ROUTES if r.kind == RouteKind.EDIT and not r.has_file_upload]
|
||||
_DELETE_ROUTES = [r for r in ALL_ROUTES if r.kind == RouteKind.DELETE]
|
||||
_ACTION_ROUTES = [r for r in ALL_ROUTES if r.kind in (RouteKind.ACTION, RouteKind.TOGGLE)]
|
||||
|
||||
# Destructive actions that wipe data other tests depend on
|
||||
_DESTRUCTIVE_ACTIONS = {"/admin/trash/empty", "/admin/trash/{table}/{item_id}/permanent-delete"}
|
||||
# Admin trash actions are excluded here — covered by TestAdminTrashLifecycle in test_business_logic.py
|
||||
_ADMIN_TRASH_PATHS = {
|
||||
"/admin/trash/empty",
|
||||
"/admin/trash/{table}/{item_id}/permanent-delete",
|
||||
"/admin/trash/{table}/{item_id}/restore",
|
||||
}
|
||||
_ACTION_ROUTES = [r for r in ALL_ROUTES if r.kind in (RouteKind.ACTION, RouteKind.TOGGLE) and r.path not in _ADMIN_TRASH_PATHS]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -97,10 +100,6 @@ async def test_edit_redirects(client: AsyncClient, all_seeds: dict, route):
|
||||
)
|
||||
async def test_action_does_not_crash(client: AsyncClient, all_seeds: dict, route):
|
||||
"""POST action routes should not return 500."""
|
||||
# Skip destructive actions that would wipe seed data
|
||||
if route.path in _DESTRUCTIVE_ACTIONS:
|
||||
pytest.skip(f"Skipping destructive action {route.path}")
|
||||
|
||||
resolved = resolve_path(route.path, all_seeds)
|
||||
if "{" in resolved:
|
||||
pytest.skip(f"No seed data mapping for {route.path}")
|
||||
|
||||
102
tests/test_mobile_nav.py
Normal file
102
tests/test_mobile_nav.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""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}"
|
||||
Reference in New Issue
Block a user