Spaces:
Running
Running
| """ | |
| 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 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def bad() -> dict: | |
| return bad_page() | |
| 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"] | |