"""Accessibility (a11y) tests — WCAG 2.1 AA static HTML analysis. Every HTML-generating component function in ``shopstack.ui`` is parsed with BeautifulSoup and validated for correct ARIA attributes, roles, labels, and semantic structure. No browser/DOM required — unit-testing the static HTML. Coverage: - shopstack.ui.components.primitives (item_row, stat_card, data_table, ...) - shopstack.ui.components.cards (card, render_decision_card, ...) - shopstack.ui.renderers.image_cards (SVG cards) - shopstack.ui.renderers.decision_cards (market basket, decision panel, ...) """ from __future__ import annotations from bs4 import BeautifulSoup from shopstack.ui.components.primitives import ( confirm_dialog, data_table, empty_state_enhanced, item_row, loading_skeleton, stat_card, toast, ) from shopstack.ui.components.cards import ( badge_html, card, empty_state, render_action_grid, render_action_tile, render_grouped_cards, render_decision_card as render_decision_card_html, render_hero_panel, render_metric, render_rows, render_workflow_rail, ) from shopstack.ui.renderers import ( decision_cards as dc, image_cards as ic, shopping_results as sr, ) from shopstack.services.results import ( CompletionItem, MarkPurchasedResult, PurchaseResultItem, ShoppingCompletionResult, ) from shopstack.schemas.models import DecisionSet, DecisionResult from shopstack.ui.screens._utils import ( render_home_advice, render_list_summary, render_low_stock, render_recent_purchases, ) # ═══════════════════════════════════════════════════════════════════════ # Helpers # ═══════════════════════════════════════════════════════════════════════ def _parse(html: str) -> BeautifulSoup: """Parse HTML string with lxml and return a BeautifulSoup tree.""" return BeautifulSoup(html, "lxml") def _assert_role(soup: BeautifulSoup, role: str, msg: str = "") -> None: """Assert that at least one element has the given role.""" assert soup.find(attrs={"role": role}) is not None, ( msg or f"Expected role='{role}' not found in HTML" ) # ═══════════════════════════════════════════════════════════════════════ # shopstack.ui.components.primitives # ═══════════════════════════════════════════════════════════════════════ class TestItemRow: """WCAG: role='group', aria-label includes name+quantity.""" def test_role_group_present(self): html = item_row("Milk", 2.0, "L") soup = _parse(html) _assert_role(soup, "group", "item_row missing role='group'") def test_aria_label_contains_name_and_quantity(self): html = item_row("Milk", 2.0, "L") soup = _parse(html) el = soup.find(attrs={"role": "group"}) assert el is not None label = el.get("aria-label", "") assert "Milk" in label assert "2.0" in label or "2 L" in label def test_aria_label_with_decision(self): html = item_row("Eggs", 12, "pieces", decision="buy") soup = _parse(html) el = soup.find(attrs={"role": "group"}) assert el is not None assert "Eggs" in el.get("aria-label", "") def test_escaped_content_does_not_break_html(self): """html.escape is applied; HTML injection should be escaped.""" html = item_row('', 1, "unit") assert "<script>" in html assert "', "green") assert "<script>" in html assert "" status = "active" class MockList: items = [MockItem()] html = render_list_summary(MockList()) assert "<script>" in html assert "" total_price = 0.0 html = render_recent_purchases([MockPurchase()]) assert "<script>" in html assert "