/** * accessmate.ai frontend overlay — axe-core powered, no framework. * * Two modes: * 1. Interactive — slide-in panel for the current page (admin bar button) * 2. Auto-scan — headless iframe scan triggered by PageScanner admin view * (activated when ?aai_scan=1 is in the URL) */ import axe from 'axe-core'; import { tagsForRulesets, labelsForRulesets, RULESET_PRESETS, presetValueForStandards, standardsForPresetValue, DEFAULT_RULESET_PRESET } from './lib/rulesets'; // ── Types ───────────────────────────────────────────────────── interface OverlayConfig { nonce: string; restBase: string; pageUrl: string; defaultRulesets?: string[]; hasProvider?: boolean; settingsUrl?: string; } interface AaiIssueNode { element: string; selector: string; } interface AaiIssue { id: string; rule: string; wcag: string; wcag_level: string; wcag_standard: string; severity: 'critical' | 'serious' | 'warning' | 'notice'; title: string; description: string; element: string; selector: string; nodes: AaiIssueNode[]; count: number; fixed: boolean; ai_fixable: boolean; help_url: string; // axe helpUrl — always a valid Deque University link category?: 'wcag' | 'best-practice'; } interface AaiPassedCheck { id: string; title: string; description: string; help_url: string; count: number; } interface ScanPayload { url: string; page_title: string; score: number; grade: string; passed: number; failed: number; issues: AaiIssue[]; rulesets?: string[]; passed_checks?: AaiPassedCheck[]; } declare global { interface Window { accessmateOverlayConfig: OverlayConfig; accessmateOverlay: { open(): void; close(): void }; } } // ── Bootstrap ───────────────────────────────────────────────── (function () { const cfg: OverlayConfig = window.accessmateOverlayConfig; if (!cfg) return; // Any non-empty aai_scan value means headless iframe mode. // The value is used as a scanId so the parent can match the response // without fragile URL comparison (URLs may differ in trailing slash, etc.). const scanId = new URLSearchParams(window.location.search).get('aai_scan') ?? ''; const autoScan = new URLSearchParams(window.location.search).get('accessmate_scan') ?? ''; if (scanId) { // Headless iframe mode — scan silently, store results, signal parent. runAutoScan(cfg, scanId); } else { // Interactive mode — slide-in panel for the current page. buildOverlay(cfg); // If accessmate_scan param is present, auto-open and start scanning immediately. if (autoScan) { requestAnimationFrame(() => window.accessmateOverlay?.open()); } } })(); // ── axe helpers ─────────────────────────────────────────────── /** * Maps axe-core rule IDs to the custom issue titles used by the backend PHP * scanner (class-scanner.php). Keeps overlay and dashboard titles in sync. */ const AXE_RULE_TITLE_MAP: Record = { // WCAG 1.1.1 — Alt text 'image-alt': 'Images missing alt text attribute', 'input-image-alt': 'Images missing alt text attribute', 'area-alt': 'Images missing alt text attribute', // WCAG 4.1.2 — Form labels 'label': 'Form inputs missing label association', 'label-title-only':'Form inputs missing label association', // WCAG 2.1.1 — Keyboard accessibility 'keyboard': 'Interactive element not keyboard accessible', // WCAG 3.1.1 — Language 'html-has-lang': 'HTML lang attribute missing', 'html-lang-valid': 'HTML lang attribute missing', // WCAG 2.4.7 — Focus visible 'focus-visible': 'Focus indicator removed via CSS', // WCAG 1.4.3 — Colour contrast 'color-contrast': 'Potentially insufficient colour contrast', 'color-contrast-enhanced': 'Potentially insufficient colour contrast', // WCAG 2.4.6 — Headings 'empty-heading': 'Empty heading elements found', 'heading-order': 'Heading levels are skipped', // WCAG 2.4.4 — Link text 'link-name': 'Links with non-descriptive text', // WCAG 4.1.2 — Button accessible name (matches backend's form-labels / 4.1.2 criterion) 'button-name': 'Form inputs missing label association', // WCAG 4.1.2 — ARIA 'aria-allowed-role': 'Invalid ARIA role attributes', 'aria-roles': 'Invalid ARIA role attributes', // WCAG 1.3.1 — Tables 'scope-attr-valid': 'Data tables missing headers or caption', 'td-headers-attr': 'Data tables missing headers or caption', 'th-has-data-cells': 'Data tables missing headers or caption', 'table-duplicate-name': 'Data tables missing headers or caption', // WCAG 1.3.6 / 1.3.1 — Landmarks (titles match backend's sprintf format) 'landmark-one-main': 'Page missing landmark elements:
', 'landmark-main-is-top-level': 'Page missing landmark elements:
', 'landmark-no-duplicate-main': 'Page missing landmark elements:
', 'landmark-banner-is-top-level': 'Page missing landmark elements: