Spaces:
Running
Running
| """ | |
| 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) == [] | |