From ae0a3a789ed6b46a125845eb430f4cd8f7ac7a8e Mon Sep 17 00:00:00 2001 From: Josh at WLTechBlog Date: Mon, 8 Dec 2025 13:03:17 -0700 Subject: [PATCH] Update accessbility tests to fix false positives --- daemon/daemon.go | 196 ++++++++++++++++++++++++++++++++++++++++++----- mcp/main.go | 12 ++- 2 files changed, 184 insertions(+), 24 deletions(-) diff --git a/daemon/daemon.go b/daemon/daemon.go index 8a13e75..cd591a9 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -10826,15 +10826,32 @@ func (d *Daemon) analyzeEnhancedAccessibility(tabID string, timeout int) (*Enhan return result, nil } +// AccessibilityFeatures represents detected accessibility features +type AccessibilityFeatures struct { + PluginsDetected []string `json:"plugins_detected"` + HasFocusVisible bool `json:"has_focus_visible"` + HasUniversalFocus bool `json:"has_universal_focus"` + CSSVariables map[string]string `json:"css_variables"` +} + +// ConfidenceLevel represents the confidence in test results +type ConfidenceLevel struct { + Level string `json:"level"` // LOW, MEDIUM, HIGH + Reason string `json:"reason"` + Recommendation string `json:"recommendation"` +} + // KeyboardTestResult represents the result of keyboard navigation testing type KeyboardTestResult struct { - TotalInteractive int `json:"total_interactive"` - Focusable int `json:"focusable"` - NotFocusable int `json:"not_focusable"` - NoFocusIndicator int `json:"no_focus_indicator"` - KeyboardTraps int `json:"keyboard_traps"` - TabOrder []KeyboardTestElement `json:"tab_order"` - Issues []KeyboardTestIssue `json:"issues"` + TotalInteractive int `json:"total_interactive"` + Focusable int `json:"focusable"` + NotFocusable int `json:"not_focusable"` + NoFocusIndicator int `json:"no_focus_indicator"` + KeyboardTraps int `json:"keyboard_traps"` + TabOrder []KeyboardTestElement `json:"tab_order"` + Issues []KeyboardTestIssue `json:"issues"` + AccessibilityFeatures *AccessibilityFeatures `json:"accessibility_features,omitempty"` + Confidence *ConfidenceLevel `json:"confidence,omitempty"` } // KeyboardTestElement represents an interactive element in tab order @@ -10857,6 +10874,111 @@ type KeyboardTestIssue struct { Description string `json:"description"` } +// detectAccessibilityFeatures detects accessibility plugins and :focus-visible implementations +func (d *Daemon) detectAccessibilityFeatures(page *rod.Page) (*AccessibilityFeatures, error) { + jsCode := `() => { + // Check for common accessibility plugins + const plugins = { + accessifix: document.querySelectorAll('[class*="accessifix"], [id*="accessifix"]').length > 0, + userway: document.querySelectorAll('[class*="userway"], [id*="userway"]').length > 0, + accessibe: document.querySelectorAll('[class*="accessibe"], [id*="accessibe"]').length > 0, + audioeye: document.querySelectorAll('[class*="audioeye"], [id*="audioeye"]').length > 0, + equalweb: document.querySelectorAll('[class*="equalweb"], [id*="equalweb"]').length > 0 + }; + + const pluginsDetected = Object.keys(plugins).filter(k => plugins[k]); + + // Check for :focus-visible rules + let hasFocusVisible = false; + let hasUniversalFocus = false; + + try { + Array.from(document.styleSheets).forEach(sheet => { + try { + Array.from(sheet.cssRules || []).forEach(rule => { + if (rule.selectorText && rule.selectorText.includes(':focus-visible')) { + hasFocusVisible = true; + // Check for universal :focus-visible (*, html body, etc.) + if (rule.selectorText.match(/^(\*|html\s+body)\s*:focus-visible/)) { + hasUniversalFocus = true; + } + } + }); + } catch(e) { + // CORS or other stylesheet access error - ignore + } + }); + } catch(e) { + // Error accessing stylesheets - ignore + } + + // Check for CSS custom properties (Accessifix-specific) + const root = getComputedStyle(document.documentElement); + const cssVariables = {}; + + const focusWidth = root.getPropertyValue('--accessifix-focus-width'); + const focusColor = root.getPropertyValue('--accessifix-focus-color'); + const focusStyle = root.getPropertyValue('--accessifix-focus-style'); + + if (focusWidth) cssVariables.width = focusWidth.trim(); + if (focusColor) cssVariables.color = focusColor.trim(); + if (focusStyle) cssVariables.style = focusStyle.trim(); + + return JSON.stringify({ + plugins_detected: pluginsDetected, + has_focus_visible: hasFocusVisible, + has_universal_focus: hasUniversalFocus, + css_variables: cssVariables + }); + }` + + result, err := page.Eval(jsCode) + if err != nil { + return nil, fmt.Errorf("failed to detect accessibility features: %w", err) + } + + var features AccessibilityFeatures + err = json.Unmarshal([]byte(result.Value.String()), &features) + if err != nil { + return nil, fmt.Errorf("failed to parse accessibility features: %w", err) + } + + return &features, nil +} + +// calculateConfidence calculates confidence level based on detected accessibility features +func (d *Daemon) calculateConfidence(features *AccessibilityFeatures, noFocusIndicator int) *ConfidenceLevel { + // Low confidence: Plugin or universal :focus-visible detected + if len(features.PluginsDetected) > 0 || features.HasUniversalFocus { + pluginList := "accessibility plugin" + if len(features.PluginsDetected) > 0 { + pluginList = strings.Join(features.PluginsDetected, ", ") + } + + return &ConfidenceLevel{ + Level: "LOW", + Reason: fmt.Sprintf("Detected %s or universal :focus-visible implementation", pluginList), + Recommendation: "Manual keyboard testing required for validation. Automated tests may not accurately detect :focus-visible implementations.", + } + } + + // Medium confidence: Partial :focus-visible implementation + if features.HasFocusVisible && !features.HasUniversalFocus { + return &ConfidenceLevel{ + Level: "MEDIUM", + Reason: "Partial :focus-visible implementation detected", + Recommendation: "Manual validation recommended for elements with :focus-visible styles.", + } + } + + // High confidence: No special implementations detected + return &ConfidenceLevel{ + Level: "HIGH", + Reason: "No accessibility plugins or :focus-visible detected", + Recommendation: "Results likely accurate. Standard focus indicators detected.", + } +} + // testKeyboardNavigationWithRealKeys tests keyboard navigation using real Tab key presses // This properly triggers :focus-within and other CSS pseudo-classes func (d *Daemon) testKeyboardNavigationWithRealKeys(tabID string, timeout int) (*KeyboardTestResult, error) { @@ -10867,6 +10989,17 @@ func (d *Daemon) testKeyboardNavigationWithRealKeys(tabID string, timeout int) ( return nil, fmt.Errorf("failed to get page: %v", err) } + // Detect accessibility features first + features, err := d.detectAccessibilityFeatures(page) + if err != nil { + d.debugLog("Warning: failed to detect accessibility features: %v", err) + // Continue with testing even if detection fails + features = &AccessibilityFeatures{ + PluginsDetected: []string{}, + CSSVariables: make(map[string]string), + } + } + // First, get all interactive elements and their info jsCode := `() => { const results = { @@ -11158,6 +11291,13 @@ func (d *Daemon) testKeyboardNavigationWithRealKeys(tabID string, timeout int) ( d.debugLog("Completed keyboard navigation test: %d elements in tab order, %d without focus indicators", len(initialResult.TabOrder), initialResult.NoFocusIndicator) + // Add accessibility features and confidence to result + initialResult.AccessibilityFeatures = features + initialResult.Confidence = d.calculateConfidence(features, initialResult.NoFocusIndicator) + + d.debugLog("Accessibility features detected: plugins=%v, focus-visible=%v, universal=%v, confidence=%s", + features.PluginsDetected, features.HasFocusVisible, features.HasUniversalFocus, initialResult.Confidence.Level) + return &initialResult, nil } @@ -12090,10 +12230,10 @@ func (d *Daemon) getPageAccessibilityReport(tabID string, tests []string, standa } } - // Run keyboard test if requested + // Run keyboard test if requested (use real keys for accurate :focus-visible detection) if runAll || contains(tests, "keyboard") { - d.debugLog("Running keyboard navigation test...") - keyboardResult, err := d.testKeyboardNavigation(tabID, timeout) + d.debugLog("Running keyboard navigation test with real Tab key simulation...") + keyboardResult, err := d.testKeyboardNavigationWithRealKeys(tabID, timeout) if err == nil { d.processKeyboardResults(report, keyboardResult) } @@ -12343,12 +12483,14 @@ func (d *Daemon) getContrastAudit(tabID string, prioritySelectors []string, thre // KeyboardAuditResult represents a keyboard navigation audit type KeyboardAuditResult struct { - Status string `json:"status"` - TotalInteractive int `json:"total_interactive"` - Focusable int `json:"focusable"` - Issues []KeyboardIssue `json:"issues"` - TabOrderIssues []string `json:"tab_order_issues"` - Recommendation string `json:"recommendation"` + Status string `json:"status"` + TotalInteractive int `json:"total_interactive"` + Focusable int `json:"focusable"` + Issues []KeyboardIssue `json:"issues"` + TabOrderIssues []string `json:"tab_order_issues"` + Recommendation string `json:"recommendation"` + AccessibilityFeatures *AccessibilityFeatures `json:"accessibility_features,omitempty"` + Confidence *ConfidenceLevel `json:"confidence,omitempty"` } // getKeyboardAudit performs a keyboard navigation assessment @@ -12371,10 +12513,12 @@ func (d *Daemon) getKeyboardAudit(tabID string, checkFocusIndicators, checkTabOr // Build audit result result := &KeyboardAuditResult{ - TotalInteractive: keyboardResult.TotalInteractive, - Focusable: keyboardResult.Focusable, - Issues: []KeyboardIssue{}, - TabOrderIssues: []string{}, + TotalInteractive: keyboardResult.TotalInteractive, + Focusable: keyboardResult.Focusable, + Issues: []KeyboardIssue{}, + TabOrderIssues: []string{}, + AccessibilityFeatures: keyboardResult.AccessibilityFeatures, + Confidence: keyboardResult.Confidence, } // Determine status @@ -12413,9 +12557,19 @@ func (d *Daemon) getKeyboardAudit(tabID string, checkFocusIndicators, checkTabOr if result.Status == "FAIL" { result.Recommendation = "Critical keyboard accessibility issues found. Fix keyboard traps immediately." } else if result.Status == "PARTIAL" { - result.Recommendation = "Add visible focus indicators to all interactive elements." + baseRec := "Add visible focus indicators to all interactive elements." + // Adjust recommendation based on confidence level + if result.Confidence != nil && result.Confidence.Level == "LOW" { + result.Recommendation = baseRec + " " + result.Confidence.Recommendation + } else { + result.Recommendation = baseRec + } } else { result.Recommendation = "Keyboard navigation is accessible." + // Add confidence-based note even for passing results + if result.Confidence != nil && result.Confidence.Level == "LOW" { + result.Recommendation += " Note: " + result.Confidence.Recommendation + } } d.debugLog("Successfully generated keyboard audit for tab: %s", tabID) diff --git a/mcp/main.go b/mcp/main.go index e5d1ca8..df4eb6b 100644 --- a/mcp/main.go +++ b/mcp/main.go @@ -94,6 +94,9 @@ func handleOptionalNavigation(cremoteServer *CremoteServer, params map[string]an if err != nil { return "", fmt.Errorf("failed to load URL: %w", err) } + + // Wait 3 seconds after page load to ensure JavaScript has executed + time.Sleep(3 * time.Second) } else if clearCache && tab != "" { // Clear cache even if not navigating err := cremoteServer.client.ClearCache(tab, timeout) @@ -242,6 +245,9 @@ func main() { return nil, fmt.Errorf("failed to load URL: %w", err) } + // Wait 3 seconds after page load to ensure JavaScript has executed + time.Sleep(3 * time.Second) + message := fmt.Sprintf("Successfully navigated to %s in tab %s", url, tab) var extractedData string @@ -4877,7 +4883,7 @@ func main() { // Register web_keyboard_test tool mcpServer.AddTool(mcp.Tool{ Name: "web_keyboard_test_cremotemcp", - Description: "Test keyboard navigation and accessibility including tab order, focus indicators, and keyboard traps. Uses real Tab key simulation by default for accurate :focus-within testing.", + Description: "Test keyboard navigation and accessibility including tab order, focus indicators, and keyboard traps. Uses real Tab key simulation by default for accurate :focus-within and :focus-visible testing. Automatically detects accessibility plugins (Accessifix, UserWay, AccessiBe, AudioEye, EqualWeb) and provides confidence scoring for focus indicator findings.", InputSchema: mcp.ToolInputSchema{ Type: "object", Properties: map[string]any{ @@ -5231,7 +5237,7 @@ func main() { // Register web_page_accessibility_report tool mcpServer.AddTool(mcp.Tool{ Name: "web_page_accessibility_report_cremotemcp", - Description: "Perform comprehensive accessibility assessment of a page and return a summarized report with actionable findings. This tool combines multiple accessibility tests (axe-core, contrast, keyboard, forms) and returns only the critical findings in a token-efficient format. Automatically injects axe-core if not already loaded.", + Description: "Perform comprehensive accessibility assessment of a page and return a summarized report with actionable findings. This tool combines multiple accessibility tests (axe-core, contrast, keyboard, forms) and returns only the critical findings in a token-efficient format. Keyboard testing uses real Tab key simulation for accurate :focus-visible detection and includes accessibility plugin detection with confidence scoring. Automatically injects axe-core if not already loaded.", InputSchema: mcp.ToolInputSchema{ Type: "object", Properties: map[string]any{ @@ -5500,7 +5506,7 @@ func main() { // Register web_keyboard_audit tool mcpServer.AddTool(mcp.Tool{ Name: "web_keyboard_audit_cremotemcp", - Description: "Perform keyboard navigation assessment with actionable results. Uses real Tab key simulation by default for accurate :focus-within testing. Returns summary of issues rather than full element lists, reducing token usage by ~80%.", + Description: "Perform keyboard navigation assessment with actionable results. Uses real Tab key simulation by default for accurate :focus-within and :focus-visible testing. Automatically detects accessibility plugins (Accessifix, UserWay, AccessiBe, AudioEye, EqualWeb) and provides confidence scoring for focus indicator findings. Returns summary of issues rather than full element lists, reducing token usage by ~80%.", InputSchema: mcp.ToolInputSchema{ Type: "object", Properties: map[string]any{