ui-copilot / tests /test_mobile_analyzer.py
ksri77's picture
feat: Module 15 β€” Mobile Analyzer (6 rules: MB1-MB6, viewport/touch/font/overflow)
e672797
Raw
History Blame Contribute Delete
7.78 kB
"""
Module 15 β€” Mobile Analyzer tests.
All tests are deterministic: no DB, no network.
"""
from __future__ import annotations
from backend.analyzers import mobile_analyzer
from backend.utils.config_loader import load_scoring_config
_CFG = load_scoring_config()["thresholds"]
# ── helpers ───────────────────────────────────────────────────────────────────
def _page(**kwargs) -> dict:
defaults = {
"buttons": [],
"tables": [],
"cards": [],
"body_font_size_px": 16.0,
"has_viewport_meta": True,
"has_horizontal_overflow": False,
}
defaults.update(kwargs)
return defaults
def _btn(pt: float = 10, pb: float = 10, fs: float = 16) -> dict:
return {"padding_top_px": pt, "padding_bottom_px": pb, "font_size_px": fs}
def _table(cols: int) -> dict:
return {"column_count": cols, "row_count": 5, "has_header": True}
def _card(width_px: float | None = None) -> dict:
d: dict = {"padding_px": 16}
if width_px is not None:
d["width_px"] = width_px
return d
def _ids(issues) -> set[str]:
return {i.rule_id for i in issues}
# ── MB1 β€” missing viewport meta ───────────────────────────────────────────────
class TestMB1ViewportMeta:
def test_no_viewport_meta_fires(self):
assert "MB1_missing_viewport_meta" in _ids(
mobile_analyzer.analyze(_page(has_viewport_meta=False), _CFG))
def test_viewport_meta_present_no_issue(self):
assert "MB1_missing_viewport_meta" not in _ids(
mobile_analyzer.analyze(_page(has_viewport_meta=True), _CFG))
def test_viewport_meta_none_no_issue(self):
# None means "not parsed" β€” don't fire without evidence
assert "MB1_missing_viewport_meta" not in _ids(
mobile_analyzer.analyze(_page(has_viewport_meta=None), _CFG))
def test_critical_severity(self):
issues = mobile_analyzer.analyze(_page(has_viewport_meta=False), _CFG)
issue = next(i for i in issues if i.rule_id == "MB1_missing_viewport_meta")
assert issue.severity.value == "critical"
# ── MB2 β€” touch targets ───────────────────────────────────────────────────────
class TestMB2TouchTargets:
def test_large_buttons_no_issue(self):
# pt=12, fs=16, pb=12 β†’ height = 12 + 16*1.2 + 12 = 43.2 β†’ < 44
# use pt=14 β†’ 14 + 19.2 + 14 = 47.2 β‰₯ 44
page = _page(buttons=[_btn(pt=14, pb=14)])
assert "MB2_small_touch_targets" not in _ids(mobile_analyzer.analyze(page, _CFG))
def test_small_button_fires(self):
# pt=2, fs=12, pb=2 β†’ 2 + 14.4 + 2 = 18.4 < 44
page = _page(buttons=[_btn(pt=2, pb=2, fs=12)])
assert "MB2_small_touch_targets" in _ids(mobile_analyzer.analyze(page, _CFG))
def test_mixed_buttons_fires_if_any_small(self):
page = _page(buttons=[_btn(pt=14, pb=14), _btn(pt=2, pb=2, fs=12)])
assert "MB2_small_touch_targets" in _ids(mobile_analyzer.analyze(page, _CFG))
def test_no_buttons_no_issue(self):
assert "MB2_small_touch_targets" not in _ids(
mobile_analyzer.analyze(_page(), _CFG))
# ── MB3 β€” body font size ──────────────────────────────────────────────────────
class TestMB3BodyFont:
def test_16px_no_issue(self):
assert "MB3_small_body_font" not in _ids(
mobile_analyzer.analyze(_page(body_font_size_px=16.0), _CFG))
def test_14px_at_threshold_no_issue(self):
assert "MB3_small_body_font" not in _ids(
mobile_analyzer.analyze(_page(body_font_size_px=14.0), _CFG))
def test_13px_fires(self):
assert "MB3_small_body_font" in _ids(
mobile_analyzer.analyze(_page(body_font_size_px=13.0), _CFG))
def test_none_body_font_no_issue(self):
assert "MB3_small_body_font" not in _ids(
mobile_analyzer.analyze(_page(body_font_size_px=None), _CFG))
# ── MB4 β€” horizontal overflow ─────────────────────────────────────────────────
class TestMB4HorizontalOverflow:
def test_overflow_fires(self):
assert "MB4_horizontal_overflow" in _ids(
mobile_analyzer.analyze(_page(has_horizontal_overflow=True), _CFG))
def test_no_overflow_no_issue(self):
assert "MB4_horizontal_overflow" not in _ids(
mobile_analyzer.analyze(_page(has_horizontal_overflow=False), _CFG))
def test_none_overflow_no_issue(self):
assert "MB4_horizontal_overflow" not in _ids(
mobile_analyzer.analyze(_page(has_horizontal_overflow=None), _CFG))
# ── MB5 β€” table overflow ──────────────────────────────────────────────────────
class TestMB5TableOverflow:
def test_4_col_table_no_issue(self):
assert "MB5_table_overflow_mobile" not in _ids(
mobile_analyzer.analyze(_page(tables=[_table(4)]), _CFG))
def test_5_col_table_fires(self):
assert "MB5_table_overflow_mobile" in _ids(
mobile_analyzer.analyze(_page(tables=[_table(5)]), _CFG))
def test_multiple_wide_tables(self):
issues = mobile_analyzer.analyze(_page(tables=[_table(6), _table(7)]), _CFG)
issue = next(i for i in issues if i.rule_id == "MB5_table_overflow_mobile")
assert "2" in issue.evidence
# ── MB6 β€” fixed width cards ───────────────────────────────────────────────────
class TestMB6FixedWidthCards:
def test_card_without_width_no_issue(self):
assert "MB6_fixed_width_cards" not in _ids(
mobile_analyzer.analyze(_page(cards=[_card()]), _CFG))
def test_narrow_fixed_card_no_issue(self):
assert "MB6_fixed_width_cards" not in _ids(
mobile_analyzer.analyze(_page(cards=[_card(width_px=320)]), _CFG))
def test_wide_fixed_card_fires(self):
assert "MB6_fixed_width_cards" in _ids(
mobile_analyzer.analyze(_page(cards=[_card(width_px=600)]), _CFG))
# ── issue shape ───────────────────────────────────────────────────────────────
class TestIssueShape:
def test_all_issues_have_why_and_references(self):
page = _page(
has_viewport_meta=False,
body_font_size_px=12.0,
has_horizontal_overflow=True,
buttons=[_btn(pt=1, pb=1, fs=10)],
tables=[_table(8)],
cards=[_card(width_px=700)],
)
issues = mobile_analyzer.analyze(page, _CFG)
assert issues
for issue in issues:
assert issue.why, f"{issue.rule_id} missing why"
assert issue.references, f"{issue.rule_id} missing references"
def test_clean_page_no_issues(self):
page = _page(
has_viewport_meta=True,
body_font_size_px=16.0,
has_horizontal_overflow=False,
buttons=[_btn(pt=14, pb=14)],
tables=[_table(4)],
cards=[_card(width_px=320)],
)
assert mobile_analyzer.analyze(page, _CFG) == []