Update accessbility tests to fix false positives

This commit is contained in:
Josh at WLTechBlog
2025-12-08 13:03:17 -07:00
parent ac71ce8797
commit ae0a3a789e
2 changed files with 184 additions and 24 deletions

View File

@@ -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)