Files
ui-ux-pro-max/scripts/tests/test_core_data_quality.py

199 lines
8.7 KiB
Python

#!/usr/bin/env python3
"""Semantic quality contracts for the core UI/UX datasets."""
import csv
import sys
import unittest
from pathlib import Path
SCRIPTS_DIR = Path(__file__).resolve().parent.parent
DATA_DIR = SCRIPTS_DIR.parent / "data"
sys.path.insert(0, str(SCRIPTS_DIR))
from core import search # noqa: E402
from validate_data import ( # noqa: E402
CHART_NON_COLOR_GUIDANCE,
CHART_RISKS,
CHART_TEXT_FALLBACK,
COLOR_CONTRAST_PAIRS,
CSS_IMPORT,
ICON_CONTEXTS,
ICON_ROLES,
ICON_USAGE_REQUIREMENTS,
WCAG_GRADE,
_check_chart_contract,
_check_icon_contract,
_check_typography_contract,
_configured_font_names,
_font_families,
_font_names,
contrast_ratio,
)
def read_rows(name):
with (DATA_DIR / name).open(encoding="utf-8", newline="") as handle:
return list(csv.DictReader(handle))
class TestSemanticColors(unittest.TestCase):
def test_declared_text_and_ui_pairs_meet_role_thresholds(self):
for row in read_rows("colors.csv"):
for foreground, (background, role, minimum) in COLOR_CONTRAST_PAIRS.items():
with self.subTest(
product=row["Product Type"], pair=foreground, role=role):
self.assertGreaterEqual(
contrast_ratio(row[foreground], row[background]), minimum)
def test_destructive_tokens_are_not_success_green(self):
for row in read_rows("colors.csv"):
value = row["Destructive"].lstrip("#")
red, green, blue = (int(value[index:index + 2], 16)
for index in (0, 2, 4))
with self.subTest(product=row["Product Type"]):
self.assertFalse(green > red * 1.1 and green > blue * 1.1)
class TestAccessibilityGuidance(unittest.TestCase):
def test_wcag_22_topics_have_explicit_rows_and_are_retrievable(self):
rows = read_rows("ux-guidelines.csv")
issues = {row["Issue"] for row in rows}
expected_platforms = {
"Focus Not Obscured (Minimum)": "Web",
"Focus Not Obscured (Enhanced)": "Web",
"Focus Appearance": "Web",
"Dragging Movements": "All",
"Target Size (Minimum)": "Web",
"Consistent Help": "All",
"Redundant Entry": "All",
"Accessible Authentication (Minimum)": "All",
"Auto-Rotating Content Controls": "All",
}
self.assertTrue(expected_platforms.keys() <= issues)
for issue, expected_platform in expected_platforms.items():
with self.subTest(issue=issue):
row = next(row for row in rows if row["Issue"] == issue)
self.assertEqual(row["Platform"], expected_platform)
self.assertIn(row["Severity"], {"Medium", "High", "Critical"})
self.assertNotEqual(row["Do"], row["Description"])
result = search(issue, domain="ux", max_results=3)
self.assertTrue(any(row.get("Issue") == issue for row in result["results"]))
def test_native_and_web_target_sizes_remain_distinct(self):
native = next(row for row in read_rows("app-interface.csv")
if row["Issue"] == "Touch Target Size")
web = next(row for row in read_rows("ux-guidelines.csv")
if row["Issue"] == "Target Size (Minimum)")
native_text = " ".join(native.values())
web_text = " ".join(web.values())
self.assertIn("44pt", native_text)
self.assertIn("48dp", native_text)
self.assertIn("24 CSS px", web_text)
def test_motion_recipes_offer_reduced_motion_or_user_control(self):
for row in read_rows("motion.csv"):
text = " ".join(row.values()).casefold()
with self.subTest(row=row["No"], category=row["Category"]):
self.assertTrue(
"reduced-motion" in text or "user-controlled" in text,
"every motion recipe needs an explicit opt-out",
)
class TestChartsTypographyAndIcons(unittest.TestCase):
def test_mutated_accessibility_and_import_contracts_fail(self):
mutations = []
chart = dict(read_rows("charts.csv")[0])
chart["A11y Fallback"] = "A visible table is available."
mutations.append((_check_chart_contract, chart, "keyboard"))
typography = dict(read_rows("typography.csv")[0])
typography["CSS Import"] = (
"@import url('https://fonts.googleapis.com/css2?family=Comic+Sans');"
)
mutations.append((_check_typography_contract, typography, "differ"))
missing_weight = dict(read_rows("typography.csv")[0])
missing_weight["Notes"] += " Recommended weight 900."
mutations.append((_check_typography_contract, missing_weight, "weights"))
icon = dict(read_rows("icons.csv")[0])
icon["Import Code"] = "import { IconName } from '@phosphor-icons/react'"
mutations.append((_check_icon_contract, icon, "import"))
for checker, row, expected in mutations:
with self.subTest(checker=checker.__name__):
problems = []
checker([row], problems)
self.assertTrue(any(expected in problem for problem in problems))
def test_chart_risk_is_not_a_conformance_grade(self):
for row in read_rows("charts.csv"):
with self.subTest(data_type=row["Data Type"]):
self.assertIn(row["Accessibility Risk"], CHART_RISKS)
self.assertEqual(
row["Accessibility Grade"],
"deprecated: use Accessibility Risk",
)
text = " ".join((row["Accessibility Notes"], row["A11y Fallback"]))
self.assertIsNone(WCAG_GRADE.search(text))
self.assertIsNotNone(CHART_TEXT_FALLBACK.search(text.casefold()))
self.assertIsNotNone(
CHART_NON_COLOR_GUIDANCE.search(text.casefold())
)
self.assertIn("keyboard", text.casefold())
def test_named_fonts_match_google_import_css_import_and_tailwind_config(self):
for row in read_rows("typography.csv"):
url_families = _font_families(row["Google Fonts URL"])
families = _font_names(url_families)
configured = _configured_font_names(row["Tailwind Config"])
named = {row["Heading Font"], row["Body Font"]}
with self.subTest(pairing=row["Font Pairing Name"]):
self.assertTrue(named <= families)
self.assertTrue(named <= configured)
match = CSS_IMPORT.fullmatch(row["CSS Import"])
self.assertIsNotNone(match)
self.assertEqual(
sorted(url_families),
sorted(_font_families(match.group(2))),
)
def test_icon_semantics_are_explicit_and_imports_are_concrete(self):
for row in read_rows("icons.csv"):
with self.subTest(icon=row["Icon Name"]):
self.assertIn(row["Semantic Role"], ICON_ROLES)
self.assertEqual(set(row["Allowed Contexts"].split("|")), ICON_CONTEXTS)
self.assertNotRegex(row["Usage"], r"[\u3400-\u9fff]")
self.assertNotIn("IconName", row["Import Code"])
for requirement in ICON_USAGE_REQUIREMENTS:
self.assertRegex(row["Usage"].casefold(), requirement)
def test_natural_accessibility_queries_are_retrievable(self):
cases = {
"chart": ("keyboard accessible chart", "keyboard"),
"landing": ("accessible drag interaction", "keyboard controls"),
"icons": ("decorative icon aria hidden", "aria-hidden"),
"gsap": ("stop animation offscreen", "visibility"),
"ux": ("error summary validation", "error summary"),
}
for domain, (query, expected) in cases.items():
with self.subTest(domain=domain, query=query):
result = search(query, domain=domain, max_results=3)
self.assertGreater(result["count"], 0)
self.assertIn(
expected.casefold(),
" ".join(str(value) for value in result["results"][0].values()).casefold(),
)
class TestCurrentReactGuidance(unittest.TestCase):
def test_effect_event_is_scoped_and_community_use_latest_is_removed(self):
rows = read_rows("react-performance.csv")
effect_event = next(row for row in rows if row["Issue"] == "Effect Events")
text = " ".join(effect_event.values()).casefold()
self.assertIn("inside effects", text)
self.assertIn("dependencies", text)
self.assertFalse(any("uselatest" in " ".join(row.values()).casefold()
for row in rows))
if __name__ == "__main__":
unittest.main()