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,6 +10826,21 @@ func (d *Daemon) analyzeEnhancedAccessibility(tabID string, timeout int) (*Enhan
return result, nil 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 // KeyboardTestResult represents the result of keyboard navigation testing
type KeyboardTestResult struct { type KeyboardTestResult struct {
TotalInteractive int `json:"total_interactive"` TotalInteractive int `json:"total_interactive"`
@@ -10835,6 +10850,8 @@ type KeyboardTestResult struct {
KeyboardTraps int `json:"keyboard_traps"` KeyboardTraps int `json:"keyboard_traps"`
TabOrder []KeyboardTestElement `json:"tab_order"` TabOrder []KeyboardTestElement `json:"tab_order"`
Issues []KeyboardTestIssue `json:"issues"` Issues []KeyboardTestIssue `json:"issues"`
AccessibilityFeatures *AccessibilityFeatures `json:"accessibility_features,omitempty"`
Confidence *ConfidenceLevel `json:"confidence,omitempty"`
} }
// KeyboardTestElement represents an interactive element in tab order // KeyboardTestElement represents an interactive element in tab order
@@ -10857,6 +10874,111 @@ type KeyboardTestIssue struct {
Description string `json:"description"` 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 // testKeyboardNavigationWithRealKeys tests keyboard navigation using real Tab key presses
// This properly triggers :focus-within and other CSS pseudo-classes // This properly triggers :focus-within and other CSS pseudo-classes
func (d *Daemon) testKeyboardNavigationWithRealKeys(tabID string, timeout int) (*KeyboardTestResult, error) { 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) 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 // First, get all interactive elements and their info
jsCode := `() => { jsCode := `() => {
const results = { 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", d.debugLog("Completed keyboard navigation test: %d elements in tab order, %d without focus indicators",
len(initialResult.TabOrder), initialResult.NoFocusIndicator) 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 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") { if runAll || contains(tests, "keyboard") {
d.debugLog("Running keyboard navigation test...") d.debugLog("Running keyboard navigation test with real Tab key simulation...")
keyboardResult, err := d.testKeyboardNavigation(tabID, timeout) keyboardResult, err := d.testKeyboardNavigationWithRealKeys(tabID, timeout)
if err == nil { if err == nil {
d.processKeyboardResults(report, keyboardResult) d.processKeyboardResults(report, keyboardResult)
} }
@@ -12349,6 +12489,8 @@ type KeyboardAuditResult struct {
Issues []KeyboardIssue `json:"issues"` Issues []KeyboardIssue `json:"issues"`
TabOrderIssues []string `json:"tab_order_issues"` TabOrderIssues []string `json:"tab_order_issues"`
Recommendation string `json:"recommendation"` Recommendation string `json:"recommendation"`
AccessibilityFeatures *AccessibilityFeatures `json:"accessibility_features,omitempty"`
Confidence *ConfidenceLevel `json:"confidence,omitempty"`
} }
// getKeyboardAudit performs a keyboard navigation assessment // getKeyboardAudit performs a keyboard navigation assessment
@@ -12375,6 +12517,8 @@ func (d *Daemon) getKeyboardAudit(tabID string, checkFocusIndicators, checkTabOr
Focusable: keyboardResult.Focusable, Focusable: keyboardResult.Focusable,
Issues: []KeyboardIssue{}, Issues: []KeyboardIssue{},
TabOrderIssues: []string{}, TabOrderIssues: []string{},
AccessibilityFeatures: keyboardResult.AccessibilityFeatures,
Confidence: keyboardResult.Confidence,
} }
// Determine status // Determine status
@@ -12413,9 +12557,19 @@ func (d *Daemon) getKeyboardAudit(tabID string, checkFocusIndicators, checkTabOr
if result.Status == "FAIL" { if result.Status == "FAIL" {
result.Recommendation = "Critical keyboard accessibility issues found. Fix keyboard traps immediately." result.Recommendation = "Critical keyboard accessibility issues found. Fix keyboard traps immediately."
} else if result.Status == "PARTIAL" { } 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 { } else {
result.Recommendation = "Keyboard navigation is accessible." 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) d.debugLog("Successfully generated keyboard audit for tab: %s", tabID)

View File

@@ -94,6 +94,9 @@ func handleOptionalNavigation(cremoteServer *CremoteServer, params map[string]an
if err != nil { if err != nil {
return "", fmt.Errorf("failed to load URL: %w", err) 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 != "" { } else if clearCache && tab != "" {
// Clear cache even if not navigating // Clear cache even if not navigating
err := cremoteServer.client.ClearCache(tab, timeout) err := cremoteServer.client.ClearCache(tab, timeout)
@@ -242,6 +245,9 @@ func main() {
return nil, fmt.Errorf("failed to load URL: %w", err) 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) message := fmt.Sprintf("Successfully navigated to %s in tab %s", url, tab)
var extractedData string var extractedData string
@@ -4877,7 +4883,7 @@ func main() {
// Register web_keyboard_test tool // Register web_keyboard_test tool
mcpServer.AddTool(mcp.Tool{ mcpServer.AddTool(mcp.Tool{
Name: "web_keyboard_test_cremotemcp", 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{ InputSchema: mcp.ToolInputSchema{
Type: "object", Type: "object",
Properties: map[string]any{ Properties: map[string]any{
@@ -5231,7 +5237,7 @@ func main() {
// Register web_page_accessibility_report tool // Register web_page_accessibility_report tool
mcpServer.AddTool(mcp.Tool{ mcpServer.AddTool(mcp.Tool{
Name: "web_page_accessibility_report_cremotemcp", 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{ InputSchema: mcp.ToolInputSchema{
Type: "object", Type: "object",
Properties: map[string]any{ Properties: map[string]any{
@@ -5500,7 +5506,7 @@ func main() {
// Register web_keyboard_audit tool // Register web_keyboard_audit tool
mcpServer.AddTool(mcp.Tool{ mcpServer.AddTool(mcp.Tool{
Name: "web_keyboard_audit_cremotemcp", 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{ InputSchema: mcp.ToolInputSchema{
Type: "object", Type: "object",
Properties: map[string]any{ Properties: map[string]any{