ui-copilot / tests /test_performance_rules.py
ksri77's picture
feat: V1.1 — Conversion, UX Quality, Performance, Interactivity rule categories
94278cd
Raw
History Blame Contribute Delete
6.84 kB
"""Tests for backend/rules/performance_rules.py"""
from __future__ import annotations
import pytest
from backend.rules.performance_rules import analyze
from backend.models.issue import Category, Severity
def _run(page_overrides: dict, threshold_overrides: dict | None = None) -> list:
base = {
"web_fonts": [],
"has_font_display": False,
"images": [],
"media_query_breakpoints": [],
}
base.update(page_overrides)
t = {"performance": {"required_breakpoints": [768]}}
if threshold_overrides:
t["performance"].update(threshold_overrides)
return analyze(base, t)
def rule_ids(issues):
return {i.rule_id for i in issues}
def _img(has_alt=True, has_srcset=True, has_width=True, has_height=True):
return {
"src": "img.png", "has_alt": has_alt, "alt_text": "desc",
"has_srcset": has_srcset, "has_width": has_width, "has_height": has_height,
}
class TestP1MissingFontDisplay:
def test_fires_when_web_font_no_display(self):
issues = _run({"web_fonts": ["Inter"], "has_font_display": False})
assert "P1_missing_font_display" in rule_ids(issues)
def test_not_fires_when_font_display_set(self):
issues = _run({"web_fonts": ["Inter"], "has_font_display": True})
assert "P1_missing_font_display" not in rule_ids(issues)
def test_not_fires_when_no_web_fonts(self):
issues = _run({"web_fonts": [], "has_font_display": False})
assert "P1_missing_font_display" not in rule_ids(issues)
def test_severity_is_medium(self):
issues = _run({"web_fonts": ["Roboto"], "has_font_display": False})
p1 = next(i for i in issues if i.rule_id == "P1_missing_font_display")
assert p1.severity == Severity.MEDIUM
def test_category_is_performance(self):
issues = _run({"web_fonts": ["Roboto"], "has_font_display": False})
p1 = next(i for i in issues if i.rule_id == "P1_missing_font_display")
assert p1.category == Category.PERFORMANCE
def test_evidence_contains_font_name(self):
issues = _run({"web_fonts": ["Poppins"], "has_font_display": False})
p1 = next(i for i in issues if i.rule_id == "P1_missing_font_display")
assert "Poppins" in p1.evidence
class TestP2MissingImageAlt:
def test_fires_when_images_missing_alt(self):
issues = _run({"images": [_img(has_alt=False)]})
assert "P2_missing_image_alt" in rule_ids(issues)
def test_not_fires_when_all_have_alt(self):
issues = _run({"images": [_img(has_alt=True), _img(has_alt=True)]})
assert "P2_missing_image_alt" not in rule_ids(issues)
def test_not_fires_when_no_images(self):
issues = _run({"images": []})
assert "P2_missing_image_alt" not in rule_ids(issues)
def test_fires_accessibility_category(self):
issues = _run({"images": [_img(has_alt=False)]})
p2 = next(i for i in issues if i.rule_id == "P2_missing_image_alt")
assert p2.category == Category.ACCESSIBILITY
def test_severity_is_high(self):
issues = _run({"images": [_img(has_alt=False)]})
p2 = next(i for i in issues if i.rule_id == "P2_missing_image_alt")
assert p2.severity == Severity.HIGH
def test_evidence_includes_count(self):
issues = _run({"images": [_img(has_alt=False), _img(has_alt=False)]})
p2 = next(i for i in issues if i.rule_id == "P2_missing_image_alt")
assert "2" in p2.evidence
class TestP3LayoutShiftImages:
def test_fires_when_images_missing_dimensions(self):
issues = _run({"images": [_img(has_width=False, has_height=False)]})
assert "P3_layout_shift_images" in rule_ids(issues)
def test_fires_when_only_width_missing(self):
issues = _run({"images": [_img(has_width=False, has_height=True)]})
assert "P3_layout_shift_images" in rule_ids(issues)
def test_not_fires_when_all_have_dimensions(self):
issues = _run({"images": [_img(has_width=True, has_height=True)]})
assert "P3_layout_shift_images" not in rule_ids(issues)
def test_not_fires_when_no_images(self):
issues = _run({"images": []})
assert "P3_layout_shift_images" not in rule_ids(issues)
def test_severity_is_medium(self):
issues = _run({"images": [_img(has_width=False)]})
p3 = next(i for i in issues if i.rule_id == "P3_layout_shift_images")
assert p3.severity == Severity.MEDIUM
def test_category_is_performance(self):
issues = _run({"images": [_img(has_width=False)]})
p3 = next(i for i in issues if i.rule_id == "P3_layout_shift_images")
assert p3.category == Category.PERFORMANCE
class TestP4MissingSrcset:
def test_fires_when_multiple_images_no_srcset(self):
issues = _run({"images": [_img(has_srcset=False), _img(has_srcset=False)]})
assert "P4_missing_srcset" in rule_ids(issues)
def test_not_fires_when_only_one_image(self):
# Threshold: >= 2 images without srcset
issues = _run({"images": [_img(has_srcset=False)]})
assert "P4_missing_srcset" not in rule_ids(issues)
def test_not_fires_when_images_have_srcset(self):
issues = _run({"images": [_img(has_srcset=True), _img(has_srcset=True)]})
assert "P4_missing_srcset" not in rule_ids(issues)
def test_severity_is_low(self):
issues = _run({"images": [_img(has_srcset=False), _img(has_srcset=False)]})
p4 = next(i for i in issues if i.rule_id == "P4_missing_srcset")
assert p4.severity == Severity.LOW
class TestP5MissingBreakpoints:
def test_fires_when_no_media_queries(self):
issues = _run({"media_query_breakpoints": []})
assert "P5_missing_breakpoints" in rule_ids(issues)
def test_not_fires_when_768_breakpoint_present(self):
issues = _run({"media_query_breakpoints": [768]})
assert "P5_missing_breakpoints" not in rule_ids(issues)
def test_allows_breakpoints_within_64px(self):
# 800px is within 64px of 768px
issues = _run({"media_query_breakpoints": [800]})
assert "P5_missing_breakpoints" not in rule_ids(issues)
def test_fires_on_only_tiny_breakpoints(self):
issues = _run({"media_query_breakpoints": [320]})
assert "P5_missing_breakpoints" in rule_ids(issues)
def test_severity_is_high(self):
issues = _run({"media_query_breakpoints": []})
p5 = next(i for i in issues if i.rule_id == "P5_missing_breakpoints")
assert p5.severity == Severity.HIGH
def test_custom_required_breakpoint(self):
issues = _run({"media_query_breakpoints": []}, {"required_breakpoints": [1024]})
p5 = next((i for i in issues if i.rule_id == "P5_missing_breakpoints"), None)
assert p5 is not None
assert "1024" in p5.evidence