Update accessbility tests to fix false positives
This commit is contained in:
196
daemon/daemon.go
196
daemon/daemon.go
@@ -10826,15 +10826,32 @@ 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"`
|
||||||
Focusable int `json:"focusable"`
|
Focusable int `json:"focusable"`
|
||||||
NotFocusable int `json:"not_focusable"`
|
NotFocusable int `json:"not_focusable"`
|
||||||
NoFocusIndicator int `json:"no_focus_indicator"`
|
NoFocusIndicator int `json:"no_focus_indicator"`
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -12343,12 +12483,14 @@ func (d *Daemon) getContrastAudit(tabID string, prioritySelectors []string, thre
|
|||||||
|
|
||||||
// KeyboardAuditResult represents a keyboard navigation audit
|
// KeyboardAuditResult represents a keyboard navigation audit
|
||||||
type KeyboardAuditResult struct {
|
type KeyboardAuditResult struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
TotalInteractive int `json:"total_interactive"`
|
TotalInteractive int `json:"total_interactive"`
|
||||||
Focusable int `json:"focusable"`
|
Focusable int `json:"focusable"`
|
||||||
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
|
||||||
@@ -12371,10 +12513,12 @@ func (d *Daemon) getKeyboardAudit(tabID string, checkFocusIndicators, checkTabOr
|
|||||||
|
|
||||||
// Build audit result
|
// Build audit result
|
||||||
result := &KeyboardAuditResult{
|
result := &KeyboardAuditResult{
|
||||||
TotalInteractive: keyboardResult.TotalInteractive,
|
TotalInteractive: keyboardResult.TotalInteractive,
|
||||||
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)
|
||||||
|
|||||||
12
mcp/main.go
12
mcp/main.go
@@ -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{
|
||||||
|
|||||||
Reference in New Issue
Block a user