ui-copilot / tests /test_learning_mode.py
ksri77's picture
ci: add lint step (ruff), enable ruff+httpx in requirements, fix all lint warnings
e9aa55c
Raw
History Blame Contribute Delete
10.5 kB
"""
Module 8 β€” Learning Mode: every issue must carry a non-empty `why` and at least
one `references` entry. Tests run each rule file individually against a synthetic
bad_page so we know exactly which rule_id produced each issue.
"""
from __future__ import annotations
import pytest
from backend.models.issue import Issue
from backend.rules import (
button_rules,
chart_rules,
contrast_rules,
dashboard_rules,
form_rules,
spacing_rules,
table_rules,
typography_rules,
)
from tests.fixtures import bad_page
# ── helpers ───────────────────────────────────────────────────────────────────
@pytest.fixture
def bad() -> dict:
return bad_page()
@pytest.fixture
def thresholds() -> dict:
import json
import pathlib
cfg = pathlib.Path("config/scoring.json").read_text()
return json.loads(cfg)["thresholds"]
def _assert_has_learning(issue: Issue) -> None:
assert issue.why, f"{issue.rule_id} has empty 'why'"
assert issue.references, f"{issue.rule_id} has empty 'references'"
# ── spacing ───────────────────────────────────────────────────────────────────
class TestSpacingLearning:
def test_s1_has_why_and_refs(self, bad, thresholds):
issues = spacing_rules.analyze(bad, thresholds)
s1 = [i for i in issues if i.rule_id == "S1_button_padding"]
assert s1, "S1 should fire on bad_page"
for i in s1:
_assert_has_learning(i)
def test_s2_has_why_and_refs(self, bad, thresholds):
issues = spacing_rules.analyze(bad, thresholds)
s2 = [i for i in issues if i.rule_id == "S2_card_padding"]
assert s2, "S2 should fire on bad_page"
_assert_has_learning(s2[0])
def test_s3_has_why_and_refs(self, bad, thresholds):
issues = spacing_rules.analyze(bad, thresholds)
s3 = [i for i in issues if i.rule_id == "S3_off_grid_spacing"]
assert s3, "S3 should fire on bad_page"
_assert_has_learning(s3[0])
# ── typography ────────────────────────────────────────────────────────────────
class TestTypographyLearning:
def test_t1_has_why_and_refs(self, bad, thresholds):
issues = typography_rules.analyze(bad, thresholds)
t1 = [i for i in issues if i.rule_id == "T1_body_font_size"]
assert t1
_assert_has_learning(t1[0])
def test_t2_has_why_and_refs(self, bad, thresholds):
issues = typography_rules.analyze(bad, thresholds)
t2 = [i for i in issues if i.rule_id == "T2_font_family_count"]
assert t2
_assert_has_learning(t2[0])
def test_t3_has_why_and_refs(self, bad, thresholds):
issues = typography_rules.analyze(bad, thresholds)
t3 = [i for i in issues if i.rule_id == "T3_line_height"]
assert t3
_assert_has_learning(t3[0])
def test_t4_has_why_and_refs(self, bad, thresholds):
issues = typography_rules.analyze(bad, thresholds)
t4 = [i for i in issues if i.rule_id == "T4_heading_hierarchy"]
assert t4
_assert_has_learning(t4[0])
def test_t5_has_why_and_refs(self, thresholds):
page = {"fonts": [], "headings": [{"level": 1, "font_size_px": 14.0, "text": "Title"}],
"body_font_size_px": None, "line_height": None}
issues = typography_rules.analyze(page, thresholds)
t5 = [i for i in issues if i.rule_id == "T5_heading_size"]
assert t5, "T5 should fire when H1 is below min_heading_size_px"
_assert_has_learning(t5[0])
# ── contrast ──────────────────────────────────────────────────────────────────
class TestContrastLearning:
def test_c1_contrast_has_why_and_refs(self, bad, thresholds):
issues = contrast_rules.analyze(bad, thresholds)
c1 = [i for i in issues if i.rule_id == "C1_wcag_aa_contrast"]
assert c1
_assert_has_learning(c1[0])
def test_c1_a11y_has_why_and_refs(self, bad, thresholds):
issues = contrast_rules.analyze(bad, thresholds)
c1a = [i for i in issues if i.rule_id == "C1_wcag_aa_contrast_a11y"]
assert c1a
_assert_has_learning(c1a[0])
# ── buttons ───────────────────────────────────────────────────────────────────
class TestButtonLearning:
def test_b1_has_why_and_refs(self, bad, thresholds):
issues = button_rules.analyze(bad, thresholds)
b1 = [i for i in issues if i.rule_id == "B1_button_style_count"]
assert b1
_assert_has_learning(b1[0])
def test_b2_has_why_and_refs(self, bad, thresholds):
issues = button_rules.analyze(bad, thresholds)
b2 = [i for i in issues if i.rule_id == "B2_border_radius_variance"]
assert b2
_assert_has_learning(b2[0])
def test_b3_has_why_and_refs(self, bad, thresholds):
issues = button_rules.analyze(bad, thresholds)
b3 = [i for i in issues if i.rule_id == "B3_focus_style"]
assert b3
_assert_has_learning(b3[0])
def test_b4_has_why_and_refs(self, bad, thresholds):
issues = button_rules.analyze(bad, thresholds)
b4 = [i for i in issues if i.rule_id == "B4_touch_target"]
assert b4
_assert_has_learning(b4[0])
# ── tables ────────────────────────────────────────────────────────────────────
class TestTableLearning:
def test_tbl1_has_why_and_refs(self, bad, thresholds):
issues = table_rules.analyze(bad, thresholds)
tbl1 = [i for i in issues if i.rule_id == "TBL1_missing_header"]
assert tbl1
_assert_has_learning(tbl1[0])
def test_tbl2_has_why_and_refs(self, bad, thresholds):
issues = table_rules.analyze(bad, thresholds)
tbl2 = [i for i in issues if i.rule_id == "TBL2_no_zebra"]
assert tbl2
_assert_has_learning(tbl2[0])
def test_tbl3_has_why_and_refs(self, bad, thresholds):
issues = table_rules.analyze(bad, thresholds)
tbl3 = [i for i in issues if i.rule_id == "TBL3_cell_padding"]
assert tbl3
_assert_has_learning(tbl3[0])
# ── forms ─────────────────────────────────────────────────────────────────────
class TestFormLearning:
def test_f1_has_why_and_refs(self, bad, thresholds):
issues = form_rules.analyze(bad, thresholds)
f1 = [i for i in issues if i.rule_id == "F1_missing_label"]
assert f1
_assert_has_learning(f1[0])
def test_f2_has_why_and_refs(self, bad, thresholds):
issues = form_rules.analyze(bad, thresholds)
f2 = [i for i in issues if i.rule_id == "F2_placeholder_as_label"]
assert f2
_assert_has_learning(f2[0])
def test_f3_has_why_and_refs(self, bad, thresholds):
issues = form_rules.analyze(bad, thresholds)
f3 = [i for i in issues if i.rule_id == "F3_input_padding"]
assert f3
_assert_has_learning(f3[0])
# ── charts ────────────────────────────────────────────────────────────────────
class TestChartLearning:
def test_ch1_has_why_and_refs(self, bad, thresholds):
issues = chart_rules.analyze(bad, thresholds)
ch1 = [i for i in issues if i.rule_id == "CH1_missing_axis_labels"]
assert ch1
_assert_has_learning(ch1[0])
def test_ch2_has_why_and_refs(self, bad, thresholds):
issues = chart_rules.analyze(bad, thresholds)
ch2 = [i for i in issues if i.rule_id == "CH2_similar_chart_colors"]
assert ch2
_assert_has_learning(ch2[0])
# ── dashboard ─────────────────────────────────────────────────────────────────
class TestDashboardLearning:
def test_d1_has_why_and_refs(self, bad, thresholds):
issues = dashboard_rules.analyze(bad, thresholds)
d1 = [i for i in issues if i.rule_id == "D1_kpi_card_overload"]
assert d1
_assert_has_learning(d1[0])
def test_d2_has_why_and_refs(self, bad, thresholds):
issues = dashboard_rules.analyze(bad, thresholds)
d2 = [i for i in issues if i.rule_id == "D2_low_whitespace"]
assert d2
_assert_has_learning(d2[0])
# ── model defaults ────────────────────────────────────────────────────────────
class TestIssueModelDefaults:
def test_why_defaults_to_empty_string(self):
from backend.models.issue import Category, Severity
i = Issue(
rule_id="X1_test",
category=Category.SPACING,
severity=Severity.LOW,
confidence=1.0,
message="test",
recommendation="test",
evidence="test",
estimated_time="1 minute",
)
assert i.why == ""
assert i.references == []
def test_why_and_references_set_correctly(self):
from backend.models.issue import Category, Severity
i = Issue(
rule_id="X2_test",
category=Category.SPACING,
severity=Severity.LOW,
confidence=1.0,
message="test",
recommendation="test",
evidence="test",
estimated_time="1 minute",
why="Because it matters.",
references=["Stripe", "Linear"],
)
assert i.why == "Because it matters."
assert i.references == ["Stripe", "Linear"]