diff --git a/README.md b/README.md index ccd4401a..494f6630 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ ## Table of Contents +- [What's new (2026-06-19) — WCAG 2.2 Audit](#whats-new-2026-06-19--wcag-22-audit) - [What's new (2026-06-19) — Memory & Determinism](#whats-new-2026-06-19--memory--determinism) - [What's new (2026-06-19) — Office I/O](#whats-new-2026-06-19--office-io) - [What's new (2026-06-19) — Agent Toolkit](#whats-new-2026-06-19--agent-toolkit) @@ -68,6 +69,13 @@ --- +## What's new (2026-06-19) — WCAG 2.2 Audit + +The accessibility audit gains a WCAG 2.2 / EN 301 549 success-criterion layer, full stack (facade, `AC_*`, MCP, Script Builder). Full reference: [`docs/source/Eng/doc/new_features/v16_features_doc.rst`](docs/source/Eng/doc/new_features/v16_features_doc.rst). + +- **WCAG-tagged conformance audit** — `wcag_audit(level="AA")` (`AC_wcag_audit`, `ac_wcag_audit`): tags every defect with its WCAG success-criterion id/level/impact (4.1.2, 1.4.3, 1.4.10) and returns a conformance report with `by_criterion`/`by_impact` counts, filtered to A/AA/AAA — mappable to EN 301 549 for EAA compliance evidence. +- **Target Size (SC 2.5.8)** — `audit_target_size(elements, min_px=24)`: new WCAG 2.2 rule flagging interactive targets smaller than 24×24 px, computed from element bounds; `tag_issue` adds SC tagging to any existing audit issue. + ## What's new (2026-06-19) — Memory & Determinism Two pure-stdlib tools from the agent/QA research round, full stack (facade, `AC_*`, MCP, Script Builder). Full reference: [`docs/source/Eng/doc/new_features/v15_features_doc.rst`](docs/source/Eng/doc/new_features/v15_features_doc.rst). diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index 61bee8bc..ed27bec2 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -12,6 +12,7 @@ ## 目录 +- [本次更新 (2026-06-19) — WCAG 2.2 审计](#本次更新-2026-06-19--wcag-22-审计) - [本次更新 (2026-06-19) — 记忆与确定性](#本次更新-2026-06-19--记忆与确定性) - [本次更新 (2026-06-19) — Office 读写](#本次更新-2026-06-19--office-读写) - [本次更新 (2026-06-19) — Agent 工具组](#本次更新-2026-06-19--agent-工具组) @@ -67,6 +68,13 @@ --- +## 本次更新 (2026-06-19) — WCAG 2.2 审计 + +无障碍审计新增 WCAG 2.2 / EN 301 549 成功准则层,走完整五层(facade、`AC_*`、MCP、Script Builder)。完整参考:[`docs/source/Zh/doc/new_features/v16_features_doc.rst`](../docs/source/Zh/doc/new_features/v16_features_doc.rst)。 + +- **WCAG 标注合规审计** — `wcag_audit(level="AA")`(`AC_wcag_audit`、`ac_wcag_audit`):为每个缺陷标注 WCAG 成功准则编号/等级/影响(4.1.2、1.4.3、1.4.10),返回含 `by_criterion`/`by_impact` 计数的合规报告,按 A/AA/AAA 过滤——可对应 EN 301 549 作为 EAA 合规证据。 +- **目标尺寸(SC 2.5.8)** — `audit_target_size(elements, min_px=24)`:WCAG 2.2 新规则,由元素 bounds 标记小于 24×24 px 的交互目标;`tag_issue` 可为任何既有审计问题加上 SC 标注。 + ## 本次更新 (2026-06-19) — 记忆与确定性 由 agent/QA 研究轮找出的两项纯标准库工具,走完整五层(facade、`AC_*`、MCP、Script Builder)。完整参考:[`docs/source/Zh/doc/new_features/v15_features_doc.rst`](../docs/source/Zh/doc/new_features/v15_features_doc.rst)。 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index f1acb529..272bb2c3 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -12,6 +12,7 @@ ## 目錄 +- [本次更新 (2026-06-19) — WCAG 2.2 稽核](#本次更新-2026-06-19--wcag-22-稽核) - [本次更新 (2026-06-19) — 記憶與決定性](#本次更新-2026-06-19--記憶與決定性) - [本次更新 (2026-06-19) — Office 讀寫](#本次更新-2026-06-19--office-讀寫) - [本次更新 (2026-06-19) — Agent 工具組](#本次更新-2026-06-19--agent-工具組) @@ -67,6 +68,13 @@ --- +## 本次更新 (2026-06-19) — WCAG 2.2 稽核 + +無障礙稽核新增 WCAG 2.2 / EN 301 549 成功準則層,走完整五層(facade、`AC_*`、MCP、Script Builder)。完整參考:[`docs/source/Zh/doc/new_features/v16_features_doc.rst`](../docs/source/Zh/doc/new_features/v16_features_doc.rst)。 + +- **WCAG 標註符合度稽核** — `wcag_audit(level="AA")`(`AC_wcag_audit`、`ac_wcag_audit`):為每個缺陷標註 WCAG 成功準則編號/等級/影響(4.1.2、1.4.3、1.4.10),回傳含 `by_criterion`/`by_impact` 計數的符合度報告,依 A/AA/AAA 過濾——可對應 EN 301 549 作為 EAA 合規證據。 +- **目標尺寸(SC 2.5.8)** — `audit_target_size(elements, min_px=24)`:WCAG 2.2 新規則,由元素 bounds 標記小於 24×24 px 的互動目標;`tag_issue` 可為任何既有稽核問題加上 SC 標註。 + ## 本次更新 (2026-06-19) — 記憶與決定性 由 agent/QA 研究輪找出的兩項純標準庫工具,走完整五層(facade、`AC_*`、MCP、Script Builder)。完整參考:[`docs/source/Zh/doc/new_features/v15_features_doc.rst`](../docs/source/Zh/doc/new_features/v15_features_doc.rst)。 diff --git a/docs/source/Eng/doc/new_features/v16_features_doc.rst b/docs/source/Eng/doc/new_features/v16_features_doc.rst new file mode 100644 index 00000000..b36a731c --- /dev/null +++ b/docs/source/Eng/doc/new_features/v16_features_doc.rst @@ -0,0 +1,52 @@ +================================================== +New Features (2026-06-19) — WCAG 2.2 Audit Engine +================================================== + +The accessibility audit gains a **WCAG 2.2 / EN 301 549 success-criterion +layer**: each defect is tagged with the WCAG criterion it violates (id + +name + conformance level + impact), and a new WCAG 2.2 rule — **Target +Size (Minimum), SC 2.5.8** — is computed from element bounds. The result is +a conformance-style report you can map to EN 301 549 for accessibility +compliance evidence (the European Accessibility Act is enforceable since +June 2025). Pure standard library; wired through the full stack. + +.. contents:: + :local: + :depth: 2 + + +Conformance audit +================ + +:: + + from je_auto_control import wcag_audit + + report = wcag_audit(level="AA") # live a11y tree + report = wcag_audit(elements=els, # or supply elements/colours/text + contrast_pairs=pairs, texts=ocr_texts, level="AA") + + report["conformant"] # True when no findings at the requested level + report["by_criterion"] # {"1.4.3 Contrast (Minimum)": 2, ...} + report["findings"] # each tagged {sc, criterion, level, impact, ...} + +Findings are filtered to the requested conformance ``level`` (``A`` / +``AA`` / ``AAA``). Mapped success criteria: + +* **1.1.1 / 4.1.2** — interactive element with no accessible name. +* **1.4.3 Contrast (Minimum)** — foreground/background below the ratio. +* **1.4.10 Reflow** — clipped / truncated text. +* **2.5.8 Target Size (Minimum)** — *new in 2.2*: pointer targets smaller + than 24x24 px. + + +Target Size rule +=============== + +``audit_target_size(elements, min_px=24)`` flags interactive elements whose +bounds are smaller than ``min_px`` on either side (elements with unknown +size are skipped). ``tag_issue(issue)`` annotates any base ``AuditIssue`` +with its success criterion, so existing audits gain SC tagging too. + +Exposed as ``AC_wcag_audit`` / ``ac_wcag_audit`` (and ``wcag_audit`` / +``audit_target_size`` on the package facade). diff --git a/docs/source/Eng/eng_index.rst b/docs/source/Eng/eng_index.rst index a494e4d6..f06e6062 100644 --- a/docs/source/Eng/eng_index.rst +++ b/docs/source/Eng/eng_index.rst @@ -38,6 +38,7 @@ Comprehensive guides for all AutoControl features. doc/new_features/v13_features_doc doc/new_features/v14_features_doc doc/new_features/v15_features_doc + doc/new_features/v16_features_doc doc/ocr_backends/ocr_backends_doc doc/observability/observability_doc doc/operations_layer/operations_layer_doc diff --git a/docs/source/Zh/doc/new_features/v16_features_doc.rst b/docs/source/Zh/doc/new_features/v16_features_doc.rst new file mode 100644 index 00000000..19158cc3 --- /dev/null +++ b/docs/source/Zh/doc/new_features/v16_features_doc.rst @@ -0,0 +1,47 @@ +========================================== +新功能 (2026-06-19) — WCAG 2.2 稽核引擎 +========================================== + +無障礙稽核新增 **WCAG 2.2 / EN 301 549 成功準則層**:每個缺陷都會標註其 +違反的 WCAG 準則(編號 + 名稱 + 符合等級 + 影響程度),並新增一條 WCAG +2.2 規則——**目標尺寸(最小),SC 2.5.8**——由元素 bounds 計算。產出的 +符合度報告可對應 EN 301 549,作為無障礙合規證據(歐洲無障礙法 +EAA 自 2025 年 6 月起強制)。純標準庫;走完整五層。 + +.. contents:: + :local: + :depth: 2 + + +符合度稽核 +========== + +:: + + from je_auto_control import wcag_audit + + report = wcag_audit(level="AA") # 即時 a11y 樹 + report = wcag_audit(elements=els, # 或提供 元素/顏色/文字 + contrast_pairs=pairs, texts=ocr_texts, level="AA") + + report["conformant"] # 在要求等級下無任何發現時為 True + report["by_criterion"] # {"1.4.3 Contrast (Minimum)": 2, ...} + report["findings"] # 每筆標註 {sc, criterion, level, impact, ...} + +發現會依要求的符合等級(``A`` / ``AA`` / ``AAA``)過濾。對應的成功準則: + +* **1.1.1 / 4.1.2** — 互動元素沒有可存取名稱。 +* **1.4.3 Contrast (Minimum)** — 前景/背景對比低於門檻。 +* **1.4.10 Reflow** — 文字被裁切 / 截斷。 +* **2.5.8 Target Size (Minimum)** — *2.2 新增*:指標目標小於 24x24 px。 + + +目標尺寸規則 +============ + +``audit_target_size(elements, min_px=24)`` 會標記 bounds 任一邊小於 +``min_px`` 的互動元素(尺寸未知者略過)。``tag_issue(issue)`` 會把任何 +基礎 ``AuditIssue`` 標註其成功準則,因此既有稽核也能取得 SC 標註。 + +對應 ``AC_wcag_audit`` / ``ac_wcag_audit``(以及 facade 上的 ``wcag_audit`` +/ ``audit_target_size``)。 diff --git a/docs/source/Zh/zh_index.rst b/docs/source/Zh/zh_index.rst index 5d74187b..20318bf3 100644 --- a/docs/source/Zh/zh_index.rst +++ b/docs/source/Zh/zh_index.rst @@ -38,6 +38,7 @@ AutoControl 所有功能的完整使用指南。 doc/new_features/v13_features_doc doc/new_features/v14_features_doc doc/new_features/v15_features_doc + doc/new_features/v16_features_doc doc/ocr_backends/ocr_backends_doc doc/observability/observability_doc doc/operations_layer/operations_layer_doc diff --git a/je_auto_control/__init__.py b/je_auto_control/__init__.py index a351f890..9c88171b 100644 --- a/je_auto_control/__init__.py +++ b/je_auto_control/__init__.py @@ -240,7 +240,8 @@ # Accessibility / i18n audit (missing labels, WCAG contrast, truncation) from je_auto_control.utils.a11y_audit import ( AuditIssue, AuditReport, audit_contrast, audit_missing_labels, - contrast_ratio, detect_truncation, run_audit, + audit_target_size, contrast_ratio, detect_truncation, run_audit, + wcag_audit, ) # Mobile device matrix (parallel script execution across devices) from je_auto_control.utils.device_matrix import ( @@ -676,7 +677,8 @@ def start_autocontrol_gui(*args, **kwargs): "auto_quarantine_from_flakiness", "default_quarantine_store", # Accessibility / i18n audit "AuditIssue", "AuditReport", "audit_contrast", "audit_missing_labels", - "contrast_ratio", "detect_truncation", "run_audit", + "audit_target_size", "contrast_ratio", "detect_truncation", "run_audit", + "wcag_audit", # Mobile device matrix "DeviceResult", "MatrixReport", "run_on_devices", # Media assertions diff --git a/je_auto_control/gui/script_builder/command_schema.py b/je_auto_control/gui/script_builder/command_schema.py index 5b44d1ed..14f53fae 100644 --- a/je_auto_control/gui/script_builder/command_schema.py +++ b/je_auto_control/gui/script_builder/command_schema.py @@ -658,6 +658,17 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None: _add_agent_specs(specs) _add_office_specs(specs) _add_memory_specs(specs) + specs.append(CommandSpec( + "AC_wcag_audit", "Accessibility", "WCAG 2.2 Conformance Audit", + fields=( + FieldSpec("app_name", FieldType.STRING, optional=True), + FieldSpec("level", FieldType.ENUM, choices=("A", "AA", "AAA"), + optional=True, default="AA"), + FieldSpec("min_target_px", FieldType.INT, optional=True, + default=24), + ), + description="WCAG 2.2 audit: SC-tagged findings + Target Size 2.5.8.", + )) def _add_memory_specs(specs: List[CommandSpec]) -> None: diff --git a/je_auto_control/utils/a11y_audit/__init__.py b/je_auto_control/utils/a11y_audit/__init__.py index d80398dd..bdc16ea0 100644 --- a/je_auto_control/utils/a11y_audit/__init__.py +++ b/je_auto_control/utils/a11y_audit/__init__.py @@ -18,6 +18,11 @@ relative_luminance, run_audit, ) +from je_auto_control.utils.a11y_audit.wcag import ( + audit_target_size, + tag_issue, + wcag_audit, +) __all__ = [ @@ -25,9 +30,12 @@ "AuditReport", "audit_contrast", "audit_missing_labels", + "audit_target_size", "contrast_ratio", "detect_truncation", "is_interactive", "relative_luminance", "run_audit", + "tag_issue", + "wcag_audit", ] diff --git a/je_auto_control/utils/a11y_audit/wcag.py b/je_auto_control/utils/a11y_audit/wcag.py new file mode 100644 index 00000000..9b9e6666 --- /dev/null +++ b/je_auto_control/utils/a11y_audit/wcag.py @@ -0,0 +1,129 @@ +"""WCAG 2.2 / EN 301 549 success-criterion tagging for a11y audits. + +The base :mod:`je_auto_control.utils.a11y_audit` checks find *defects*; this +layer tags each defect with the WCAG **success criterion** it violates +(id + name + conformance level + impact) and adds a new WCAG 2.2 rule — +**Target Size (Minimum), SC 2.5.8** — computable from element bounds. The +result is a conformance-style report you can map to EN 301 549 for +accessibility compliance evidence (EAA enforceable since June 2025). + +Pure standard library; imports no ``PySide6``. Reuses the base audit's +pure functions, so it is fully unit-testable with supplied elements. +""" +from typing import Any, Dict, Iterable, List, Optional + +from je_auto_control.utils.a11y_audit.audit import ( + AuditIssue, SEVERITY_ERROR, SEVERITY_WARNING, WCAG_AA_NORMAL, + audit_contrast, audit_missing_labels, detect_truncation, is_interactive, +) + +# kind -> (SC id, criterion name, conformance level) +_SC_BY_KIND = { + "missing_label": ("4.1.2", "Name, Role, Value", "A"), + "contrast": ("1.4.3", "Contrast (Minimum)", "AA"), + "truncation": ("1.4.10", "Reflow", "AA"), + "target_size": ("2.5.8", "Target Size (Minimum)", "AA"), +} +_IMPACT_BY_SEVERITY = {SEVERITY_ERROR: "serious", SEVERITY_WARNING: "moderate"} +_LEVEL_ORDER = {"A": 1, "AA": 2, "AAA": 3} +_MIN_TARGET_PX = 24 + + +def audit_target_size(elements: Iterable[Any], + min_px: int = _MIN_TARGET_PX) -> List[AuditIssue]: + """Flag interactive elements smaller than ``min_px`` on either side. + + WCAG 2.2 SC 2.5.8 (Target Size, Minimum) — pointer targets should be at + least 24x24 CSS px. Elements with unknown (zero) size are skipped. + """ + issues: List[AuditIssue] = [] + for element in elements: + role = getattr(element, "role", "") or "" + bounds = list(getattr(element, "bounds", []) or []) + if not is_interactive(role) or len(bounds) < 4: + continue + width, height = int(bounds[2]), int(bounds[3]) + if width <= 0 or height <= 0 or min(width, height) >= int(min_px): + continue + issues.append(AuditIssue( + kind="target_size", severity=SEVERITY_WARNING, + message=f"target {width}x{height}px below {min_px}px minimum", + target=role, + detail={"width": width, "height": height, "min_px": int(min_px)})) + return issues + + +def tag_issue(issue: AuditIssue) -> Dict[str, Any]: + """Return an issue annotated with its WCAG success criterion.""" + sc, criterion, level = _SC_BY_KIND.get(issue.kind, ("", "", "")) + return { + "sc": sc, "criterion": criterion, "level": level, + "impact": _IMPACT_BY_SEVERITY.get(issue.severity, "minor"), + "kind": issue.kind, "severity": issue.severity, + "message": issue.message, "target": issue.target, + "detail": issue.detail, + } + + +def _level_ok(found_level: str, target_level: str) -> bool: + return (_LEVEL_ORDER.get(found_level, 99) + <= _LEVEL_ORDER.get(target_level, 2)) + + +def _fetch_elements(app_name: Optional[str], elements: Optional[Iterable[Any]], + max_results: int) -> List[Any]: + if elements is not None: + return list(elements) + from je_auto_control.utils.accessibility.accessibility_api import ( + list_accessibility_elements) + return list_accessibility_elements(app_name=app_name, + max_results=int(max_results)) + + +def _collect_issues(app_name: Optional[str], elements: Optional[Iterable[Any]], + contrast_pairs: Optional[Iterable[Dict[str, Any]]], + texts: Optional[Iterable[str]], min_ratio: float, + min_target_px: int, max_results: int) -> List[AuditIssue]: + els = _fetch_elements(app_name, elements, max_results) + issues = list(audit_missing_labels(els)) + issues.extend(audit_target_size(els, min_target_px)) + if contrast_pairs is not None: + issues.extend(audit_contrast(contrast_pairs, min_ratio)) + if texts is not None: + issues.extend(detect_truncation(texts)) + return issues + + +def _summary(findings: List[Dict[str, Any]], level: str) -> Dict[str, Any]: + by_criterion: Dict[str, int] = {} + by_impact: Dict[str, int] = {} + for finding in findings: + key = f"{finding['sc']} {finding['criterion']}".strip() + by_criterion[key] = by_criterion.get(key, 0) + 1 + by_impact[finding["impact"]] = by_impact.get(finding["impact"], 0) + 1 + return { + "level": level, "total": len(findings), + "conformant": len(findings) == 0, + "by_criterion": by_criterion, "by_impact": by_impact, + "findings": findings, + } + + +def wcag_audit(*, app_name: Optional[str] = None, + elements: Optional[Iterable[Any]] = None, + contrast_pairs: Optional[Iterable[Dict[str, Any]]] = None, + texts: Optional[Iterable[str]] = None, + min_ratio: float = WCAG_AA_NORMAL, + min_target_px: int = _MIN_TARGET_PX, + level: str = "AA", max_results: int = 500) -> Dict[str, Any]: + """Run WCAG-tagged audits and return a conformance report. + + Findings are tagged with WCAG SC id / level / impact and filtered to the + requested conformance ``level`` (A, AA, or AAA). When ``elements`` is + omitted the live accessibility tree is queried. + """ + issues = _collect_issues(app_name, elements, contrast_pairs, texts, + min_ratio, min_target_px, max_results) + findings = [tag_issue(issue) for issue in issues] + findings = [f for f in findings if _level_ok(f["level"], level)] + return _summary(findings, level) diff --git a/je_auto_control/utils/executor/action_executor.py b/je_auto_control/utils/executor/action_executor.py index 7a1b1c84..26e2d46e 100644 --- a/je_auto_control/utils/executor/action_executor.py +++ b/je_auto_control/utils/executor/action_executor.py @@ -891,6 +891,19 @@ def _audit_contrast(foreground: List[int], background: List[int], } +def _wcag_audit(app_name: Optional[str] = None, + contrast_pairs: Optional[List[Dict[str, Any]]] = None, + texts: Optional[List[str]] = None, level: str = "AA", + min_target_px: int = 24, max_results: int = 500 + ) -> Dict[str, Any]: + """Executor adapter: WCAG-tagged conformance audit (SC ids + levels).""" + from je_auto_control.utils.a11y_audit import wcag_audit + return wcag_audit( + app_name=app_name, contrast_pairs=contrast_pairs, texts=texts, + level=str(level), min_target_px=int(min_target_px), + max_results=int(max_results)) + + def _run_device_matrix(actions: List[Any], devices: List[Dict[str, Any]], max_parallel: int = 4, var_name: str = "device") -> Dict[str, Any]: @@ -2804,6 +2817,7 @@ def __init__(self): # Accessibility / i18n audit (missing labels, contrast, truncation) "AC_audit_accessibility": _audit_accessibility, "AC_audit_contrast": _audit_contrast, + "AC_wcag_audit": _wcag_audit, # Mobile device matrix (parallel script across devices) "AC_run_device_matrix": _run_device_matrix, diff --git a/je_auto_control/utils/mcp_server/tools/_factories.py b/je_auto_control/utils/mcp_server/tools/_factories.py index 74d76090..a8595606 100644 --- a/je_auto_control/utils/mcp_server/tools/_factories.py +++ b/je_auto_control/utils/mcp_server/tools/_factories.py @@ -3028,6 +3028,25 @@ def a11y_audit_tools() -> List[MCPTool]: handler=h.audit_contrast, annotations=READ_ONLY, ), + MCPTool( + name="ac_wcag_audit", + description=("WCAG 2.2 conformance audit: tags each defect with its " + "success-criterion id/level/impact and adds the 2.2 " + "Target Size (2.5.8) rule from element bounds. Filters " + "to 'level' (A/AA/AAA). Returns a conformance report " + "with by_criterion / by_impact counts and findings."), + input_schema=schema({ + "app_name": {"type": "string"}, + "contrast_pairs": {"type": "array", + "items": {"type": "object"}}, + "texts": {"type": "array", "items": {"type": "string"}}, + "level": {"type": "string", "enum": ["A", "AA", "AAA"]}, + "min_target_px": {"type": "integer"}, + "max_results": {"type": "integer"}, + }), + handler=h.wcag_audit, + annotations=READ_ONLY, + ), ] diff --git a/je_auto_control/utils/mcp_server/tools/_handlers.py b/je_auto_control/utils/mcp_server/tools/_handlers.py index 68843e23..c96972e9 100644 --- a/je_auto_control/utils/mcp_server/tools/_handlers.py +++ b/je_auto_control/utils/mcp_server/tools/_handlers.py @@ -2257,6 +2257,18 @@ def audit_contrast(foreground: List[int], background: List[int], } +def wcag_audit(app_name: Optional[str] = None, + contrast_pairs: Optional[List[Dict[str, Any]]] = None, + texts: Optional[List[str]] = None, level: str = "AA", + min_target_px: int = 24, + max_results: int = 500) -> Dict[str, Any]: + from je_auto_control.utils.a11y_audit import wcag_audit as _wcag + return _wcag( + app_name=app_name, contrast_pairs=contrast_pairs, texts=texts, + level=str(level), min_target_px=int(min_target_px), + max_results=int(max_results)) + + # --- Mobile device matrix -------------------------------------------------- def run_device_matrix(actions: List[Any], devices: List[Dict[str, Any]], diff --git a/test/unit_test/headless/test_wcag_batch.py b/test/unit_test/headless/test_wcag_batch.py new file mode 100644 index 00000000..a2f39afb --- /dev/null +++ b/test/unit_test/headless/test_wcag_batch.py @@ -0,0 +1,87 @@ +"""Headless tests for the WCAG 2.2 SC-tagged accessibility rule engine. +Pure stdlib; no Qt imports (elements are supplied as lightweight fakes).""" +from types import SimpleNamespace + +import je_auto_control as ac +from je_auto_control.utils.a11y_audit import ( + audit_target_size, tag_issue, wcag_audit) +from je_auto_control.utils.a11y_audit.audit import AuditIssue + + +def _el(role, bounds, name="x"): + return SimpleNamespace(role=role, name=name, bounds=bounds) + + +# --- target size (SC 2.5.8) ---------------------------------------------- + +def test_target_size_flags_small_interactive(): + elements = [ + _el("button", (0, 0, 20, 20)), # too small -> flagged + _el("button", (0, 0, 40, 40)), # ok + _el("text", (0, 0, 5, 5)), # not interactive -> ignored + _el("button", (0, 0, 0, 0)), # unknown size -> skipped + ] + issues = audit_target_size(elements) + assert len(issues) == 1 + assert issues[0].kind == "target_size" + assert issues[0].detail == {"width": 20, "height": 20, "min_px": 24} + + +def test_tag_issue_carries_success_criterion(): + tagged = tag_issue(AuditIssue(kind="contrast", severity="error", + message="low")) + assert tagged["sc"] == "1.4.3" + assert tagged["level"] == "AA" + assert tagged["impact"] == "serious" + + +# --- conformance report --------------------------------------------------- + +def test_wcag_audit_tags_and_filters_by_level(): + elements = [_el("button", (0, 0, 10, 10), name="")] # small + unlabeled + report = wcag_audit( + elements=elements, + contrast_pairs=[{"foreground": [120, 120, 120], + "background": [130, 130, 130], "label": "lbl"}], + texts=["clipped text…"], level="AA") + assert report["level"] == "AA" + assert report["conformant"] is False + scs = {f["sc"] for f in report["findings"]} + assert {"4.1.2", "1.4.3", "1.4.10", "2.5.8"} <= scs + assert report["total"] == len(report["findings"]) + assert sum(report["by_impact"].values()) == report["total"] + + +def test_wcag_audit_level_a_excludes_aa(): + report = wcag_audit( + elements=[], + contrast_pairs=[{"foreground": [0, 0, 0], "background": [0, 0, 0]}], + level="A") + # contrast (AA) is excluded at level A + assert all(f["level"] == "A" for f in report["findings"]) + assert "1.4.3" not in {f["sc"] for f in report["findings"]} + + +def test_clean_scope_is_conformant(): + report = wcag_audit(elements=[_el("button", (0, 0, 48, 48))]) + assert report["conformant"] is True + assert report["total"] == 0 + + +# --- wiring --------------------------------------------------------------- + +def test_wiring(): + # Registration only — executing AC_wcag_audit needs a live a11y backend, + # which varies by platform; the functional path is covered above. + assert "AC_wcag_audit" in ac.executor.known_commands() + from je_auto_control.utils.mcp_server.tools import ( + build_default_tool_registry) + assert "ac_wcag_audit" in {t.name for t in build_default_tool_registry()} + from je_auto_control.gui.script_builder.command_schema import _build_specs + assert "AC_wcag_audit" in {s.command for s in _build_specs()} + + +def test_facade_exports(): + for attr in ("wcag_audit", "audit_target_size"): + assert hasattr(ac, attr) + assert attr in ac.__all__