Spaces:
Running
Running
| """Tests for backend/rules/interactivity_rules.py""" | |
| from __future__ import annotations | |
| import pytest | |
| from backend.rules.interactivity_rules import analyze | |
| from backend.models.issue import Category, Severity | |
| def _run(page_overrides: dict) -> list: | |
| base = { | |
| "buttons": [], | |
| "inputs": [], | |
| "cta_texts": [], | |
| "has_hover_styles": False, | |
| "has_transitions": False, | |
| "has_focus_outline_removed": False, | |
| } | |
| base.update(page_overrides) | |
| return analyze(base, {}) | |
| def rule_ids(issues): | |
| return {i.rule_id for i in issues} | |
| _MOCK_BUTTON = { | |
| "text": "Buy Now", "height_px": 44, "background_color": "#007bff", | |
| "border_radius_px": 4, "has_focus_style": True, | |
| "padding_top_px": 8, "padding_right_px": 16, | |
| "padding_bottom_px": 8, "padding_left_px": 16, | |
| "color": "#fff", | |
| } | |
| _MOCK_INPUT = { | |
| "has_label": True, "label_text": "Email", "placeholder": None, | |
| "padding_px": 10, "has_focus_style": True, "input_type": "email", | |
| } | |
| class TestINT1MissingHoverStates: | |
| def test_fires_when_no_hover_on_interactive_page(self): | |
| issues = _run({ | |
| "buttons": [_MOCK_BUTTON, _MOCK_BUTTON], | |
| "has_hover_styles": False, | |
| }) | |
| assert "INT1_missing_hover_states" in rule_ids(issues) | |
| def test_not_fires_when_hover_styles_present(self): | |
| issues = _run({ | |
| "buttons": [_MOCK_BUTTON, _MOCK_BUTTON], | |
| "has_hover_styles": True, | |
| }) | |
| assert "INT1_missing_hover_states" not in rule_ids(issues) | |
| def test_not_fires_when_no_interactive_elements(self): | |
| issues = _run({"buttons": [], "inputs": [], "cta_texts": [], "has_hover_styles": False}) | |
| assert "INT1_missing_hover_states" not in rule_ids(issues) | |
| def test_not_fires_when_only_one_element(self): | |
| issues = _run({"buttons": [_MOCK_BUTTON], "inputs": [], "cta_texts": [], "has_hover_styles": False}) | |
| assert "INT1_missing_hover_states" not in rule_ids(issues) | |
| def test_severity_is_medium(self): | |
| issues = _run({"buttons": [_MOCK_BUTTON, _MOCK_BUTTON], "has_hover_styles": False}) | |
| int1 = next(i for i in issues if i.rule_id == "INT1_missing_hover_states") | |
| assert int1.severity == Severity.MEDIUM | |
| def test_category_is_interactivity(self): | |
| issues = _run({"buttons": [_MOCK_BUTTON, _MOCK_BUTTON], "has_hover_styles": False}) | |
| int1 = next(i for i in issues if i.rule_id == "INT1_missing_hover_states") | |
| assert int1.category == Category.INTERACTIVITY | |
| def test_counts_cta_texts_toward_interactive(self): | |
| issues = _run({ | |
| "cta_texts": ["buy now", "learn more"], | |
| "has_hover_styles": False, | |
| }) | |
| assert "INT1_missing_hover_states" in rule_ids(issues) | |
| class TestINT2NoTransitions: | |
| def test_fires_when_no_transitions_on_interactive_page(self): | |
| issues = _run({ | |
| "buttons": [_MOCK_BUTTON, _MOCK_BUTTON], | |
| "has_transitions": False, | |
| }) | |
| assert "INT2_no_transitions" in rule_ids(issues) | |
| def test_not_fires_when_transitions_present(self): | |
| issues = _run({ | |
| "buttons": [_MOCK_BUTTON, _MOCK_BUTTON], | |
| "has_transitions": True, | |
| }) | |
| assert "INT2_no_transitions" not in rule_ids(issues) | |
| def test_not_fires_when_no_interactive_elements(self): | |
| issues = _run({"buttons": [], "inputs": [], "has_transitions": False}) | |
| assert "INT2_no_transitions" not in rule_ids(issues) | |
| def test_severity_is_low(self): | |
| issues = _run({ | |
| "inputs": [_MOCK_INPUT, _MOCK_INPUT], | |
| "has_transitions": False, | |
| }) | |
| int2 = next(i for i in issues if i.rule_id == "INT2_no_transitions") | |
| assert int2.severity == Severity.LOW | |
| def test_category_is_interactivity(self): | |
| issues = _run({ | |
| "buttons": [_MOCK_BUTTON, _MOCK_BUTTON], | |
| "has_transitions": False, | |
| }) | |
| int2 = next(i for i in issues if i.rule_id == "INT2_no_transitions") | |
| assert int2.category == Category.INTERACTIVITY | |
| class TestINT3FocusOutlineRemoved: | |
| def test_fires_when_outline_removed(self): | |
| issues = _run({"has_focus_outline_removed": True}) | |
| assert "INT3_focus_outline_removed" in rule_ids(issues) | |
| def test_not_fires_when_outline_not_removed(self): | |
| issues = _run({"has_focus_outline_removed": False}) | |
| assert "INT3_focus_outline_removed" not in rule_ids(issues) | |
| def test_severity_is_high(self): | |
| issues = _run({"has_focus_outline_removed": True}) | |
| int3 = next(i for i in issues if i.rule_id == "INT3_focus_outline_removed") | |
| assert int3.severity == Severity.HIGH | |
| def test_category_is_accessibility(self): | |
| # This rule crosses into accessibility since it's a WCAG violation | |
| issues = _run({"has_focus_outline_removed": True}) | |
| int3 = next(i for i in issues if i.rule_id == "INT3_focus_outline_removed") | |
| assert int3.category == Category.ACCESSIBILITY | |
| def test_evidence_is_descriptive(self): | |
| issues = _run({"has_focus_outline_removed": True}) | |
| int3 = next(i for i in issues if i.rule_id == "INT3_focus_outline_removed") | |
| assert "outline" in int3.evidence.lower() or "True" in int3.evidence | |
| class TestCssParserHelpers: | |
| """Integration: verify the CSS parser helpers that feed interactivity signals.""" | |
| def test_has_hover_rules_detects_hover(self): | |
| from backend.analyzers.css_parser import has_hover_rules | |
| assert has_hover_rules("button:hover { background: blue; }") | |
| def test_has_hover_rules_false_without_hover(self): | |
| from backend.analyzers.css_parser import has_hover_rules | |
| assert not has_hover_rules("button { background: blue; }") | |
| def test_has_transition_rules_detects_transition(self): | |
| from backend.analyzers.css_parser import has_transition_rules | |
| assert has_transition_rules("button { transition: background 0.2s; }") | |
| def test_has_transition_rules_detects_keyframes(self): | |
| from backend.analyzers.css_parser import has_transition_rules | |
| assert has_transition_rules("@keyframes spin { from { } to { } }") | |
| def test_has_transition_rules_false_without(self): | |
| from backend.analyzers.css_parser import has_transition_rules | |
| assert not has_transition_rules("button { background: blue; }") | |
| def test_focus_outline_removed_detects_none(self): | |
| from backend.analyzers.css_parser import has_focus_outline_removed | |
| assert has_focus_outline_removed("* { outline: none; }") | |
| def test_focus_outline_removed_detects_zero(self): | |
| from backend.analyzers.css_parser import has_focus_outline_removed | |
| assert has_focus_outline_removed("button { outline: 0; }") | |
| def test_focus_outline_not_removed_when_present(self): | |
| from backend.analyzers.css_parser import has_focus_outline_removed | |
| assert not has_focus_outline_removed("button:focus { outline: 2px solid blue; }") | |