bump
This commit is contained in:
BIN
daemon/cremotedaemon
Executable file → Normal file
BIN
daemon/cremotedaemon
Executable file → Normal file
Binary file not shown.
723
daemon/daemon.go
723
daemon/daemon.go
@@ -2159,6 +2159,103 @@ func (d *Daemon) handleCommand(w http.ResponseWriter, r *http.Request) {
|
||||
response = Response{Success: true, Data: result}
|
||||
}
|
||||
|
||||
case "page-accessibility-report":
|
||||
tabID := cmd.Params["tab"]
|
||||
testsStr := cmd.Params["tests"]
|
||||
standard := cmd.Params["standard"]
|
||||
includeScreenshots := cmd.Params["include_screenshots"] == "true"
|
||||
timeoutStr := cmd.Params["timeout"]
|
||||
|
||||
// Parse timeout (default to 30 seconds)
|
||||
timeout := 30
|
||||
if timeoutStr != "" {
|
||||
if t, err := strconv.Atoi(timeoutStr); err == nil {
|
||||
timeout = t
|
||||
}
|
||||
}
|
||||
|
||||
// Parse tests array
|
||||
var tests []string
|
||||
if testsStr != "" {
|
||||
tests = strings.Split(testsStr, ",")
|
||||
}
|
||||
|
||||
result, err := d.getPageAccessibilityReport(tabID, tests, standard, includeScreenshots, timeout)
|
||||
if err != nil {
|
||||
response = Response{Success: false, Error: err.Error()}
|
||||
} else {
|
||||
response = Response{Success: true, Data: result}
|
||||
}
|
||||
|
||||
case "contrast-audit":
|
||||
tabID := cmd.Params["tab"]
|
||||
prioritySelectorsStr := cmd.Params["priority_selectors"]
|
||||
threshold := cmd.Params["threshold"]
|
||||
timeoutStr := cmd.Params["timeout"]
|
||||
|
||||
// Parse timeout (default to 10 seconds)
|
||||
timeout := 10
|
||||
if timeoutStr != "" {
|
||||
if t, err := strconv.Atoi(timeoutStr); err == nil {
|
||||
timeout = t
|
||||
}
|
||||
}
|
||||
|
||||
// Parse priority selectors
|
||||
var prioritySelectors []string
|
||||
if prioritySelectorsStr != "" {
|
||||
prioritySelectors = strings.Split(prioritySelectorsStr, ",")
|
||||
}
|
||||
|
||||
result, err := d.getContrastAudit(tabID, prioritySelectors, threshold, timeout)
|
||||
if err != nil {
|
||||
response = Response{Success: false, Error: err.Error()}
|
||||
} else {
|
||||
response = Response{Success: true, Data: result}
|
||||
}
|
||||
|
||||
case "keyboard-audit":
|
||||
tabID := cmd.Params["tab"]
|
||||
checkFocusIndicators := cmd.Params["check_focus_indicators"] == "true"
|
||||
checkTabOrder := cmd.Params["check_tab_order"] == "true"
|
||||
checkKeyboardTraps := cmd.Params["check_keyboard_traps"] == "true"
|
||||
timeoutStr := cmd.Params["timeout"]
|
||||
|
||||
// Parse timeout (default to 15 seconds)
|
||||
timeout := 15
|
||||
if timeoutStr != "" {
|
||||
if t, err := strconv.Atoi(timeoutStr); err == nil {
|
||||
timeout = t
|
||||
}
|
||||
}
|
||||
|
||||
result, err := d.getKeyboardAudit(tabID, checkFocusIndicators, checkTabOrder, checkKeyboardTraps, timeout)
|
||||
if err != nil {
|
||||
response = Response{Success: false, Error: err.Error()}
|
||||
} else {
|
||||
response = Response{Success: true, Data: result}
|
||||
}
|
||||
|
||||
case "form-accessibility-audit":
|
||||
tabID := cmd.Params["tab"]
|
||||
formSelector := cmd.Params["form_selector"]
|
||||
timeoutStr := cmd.Params["timeout"]
|
||||
|
||||
// Parse timeout (default to 10 seconds)
|
||||
timeout := 10
|
||||
if timeoutStr != "" {
|
||||
if t, err := strconv.Atoi(timeoutStr); err == nil {
|
||||
timeout = t
|
||||
}
|
||||
}
|
||||
|
||||
result, err := d.getFormAccessibilityAudit(tabID, formSelector, timeout)
|
||||
if err != nil {
|
||||
response = Response{Success: false, Error: err.Error()}
|
||||
} else {
|
||||
response = Response{Success: true, Data: result}
|
||||
}
|
||||
|
||||
default:
|
||||
d.debugLog("Unknown action: %s", cmd.Action)
|
||||
response = Response{Success: false, Error: "Unknown action"}
|
||||
@@ -11657,3 +11754,629 @@ func (d *Daemon) testReflow(tabID string, widths []int, timeout int) (*ReflowTes
|
||||
d.debugLog("Successfully tested reflow for tab: %s (found %d issues)", tabID, len(result.Issues))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// PageAccessibilityReport represents a comprehensive accessibility assessment
|
||||
type PageAccessibilityReport struct {
|
||||
URL string `json:"url"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
ComplianceStatus string `json:"compliance_status"`
|
||||
OverallScore int `json:"overall_score"`
|
||||
LegalRisk string `json:"legal_risk"`
|
||||
CriticalIssues []AccessibilityIssue `json:"critical_issues"`
|
||||
SeriousIssues []AccessibilityIssue `json:"serious_issues"`
|
||||
HighIssues []AccessibilityIssue `json:"high_issues"`
|
||||
MediumIssues []AccessibilityIssue `json:"medium_issues"`
|
||||
SummaryByWCAG map[string]WCAGSummary `json:"summary_by_wcag"`
|
||||
ContrastSummary ContrastSummary `json:"contrast_summary"`
|
||||
KeyboardSummary KeyboardSummary `json:"keyboard_summary"`
|
||||
ARIASummary ARIASummary `json:"aria_summary"`
|
||||
FormSummary *FormSummary `json:"form_summary,omitempty"`
|
||||
Screenshots map[string]string `json:"screenshots,omitempty"`
|
||||
EstimatedHours int `json:"estimated_remediation_hours"`
|
||||
}
|
||||
|
||||
// AccessibilityIssue represents a single accessibility issue
|
||||
type AccessibilityIssue struct {
|
||||
WCAG string `json:"wcag"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Impact string `json:"impact"`
|
||||
Count int `json:"count"`
|
||||
Examples []string `json:"examples,omitempty"`
|
||||
Remediation string `json:"remediation"`
|
||||
}
|
||||
|
||||
// WCAGSummary represents violations grouped by WCAG principle
|
||||
type WCAGSummary struct {
|
||||
Violations int `json:"violations"`
|
||||
Severity string `json:"severity"`
|
||||
}
|
||||
|
||||
// ContrastSummary represents a summary of contrast check results
|
||||
type ContrastSummary struct {
|
||||
TotalChecked int `json:"total_checked"`
|
||||
Passed int `json:"passed"`
|
||||
Failed int `json:"failed"`
|
||||
PassRate string `json:"pass_rate"`
|
||||
CriticalFailures []ContrastFailure `json:"critical_failures"`
|
||||
FailurePatterns map[string]FailurePattern `json:"failure_patterns"`
|
||||
}
|
||||
|
||||
// ContrastFailure represents a critical contrast failure
|
||||
type ContrastFailure struct {
|
||||
Selector string `json:"selector"`
|
||||
Text string `json:"text"`
|
||||
Ratio float64 `json:"ratio"`
|
||||
Required float64 `json:"required"`
|
||||
FgColor string `json:"fg_color"`
|
||||
BgColor string `json:"bg_color"`
|
||||
Fix string `json:"fix"`
|
||||
}
|
||||
|
||||
// FailurePattern represents a pattern of similar failures
|
||||
type FailurePattern struct {
|
||||
Count int `json:"count"`
|
||||
Ratio float64 `json:"ratio"`
|
||||
Fix string `json:"fix"`
|
||||
}
|
||||
|
||||
// KeyboardSummary represents a summary of keyboard navigation results
|
||||
type KeyboardSummary struct {
|
||||
TotalInteractive int `json:"total_interactive"`
|
||||
Focusable int `json:"focusable"`
|
||||
MissingFocusIndicator int `json:"missing_focus_indicator"`
|
||||
KeyboardTraps int `json:"keyboard_traps"`
|
||||
TabOrderIssues int `json:"tab_order_issues"`
|
||||
Issues []KeyboardIssue `json:"issues"`
|
||||
}
|
||||
|
||||
// KeyboardIssue represents a keyboard accessibility issue
|
||||
type KeyboardIssue struct {
|
||||
Type string `json:"type"`
|
||||
Severity string `json:"severity"`
|
||||
Count int `json:"count"`
|
||||
Description string `json:"description"`
|
||||
Fix string `json:"fix"`
|
||||
Examples []string `json:"examples,omitempty"`
|
||||
}
|
||||
|
||||
// ARIASummary represents a summary of ARIA validation results
|
||||
type ARIASummary struct {
|
||||
TotalViolations int `json:"total_violations"`
|
||||
MissingNames int `json:"missing_names"`
|
||||
InvalidAttributes int `json:"invalid_attributes"`
|
||||
HiddenInteractive int `json:"hidden_interactive"`
|
||||
Issues []ARIAIssue `json:"issues"`
|
||||
}
|
||||
|
||||
// ARIAIssue represents an ARIA accessibility issue
|
||||
type ARIAIssue struct {
|
||||
Type string `json:"type"`
|
||||
Severity string `json:"severity"`
|
||||
Count int `json:"count"`
|
||||
Description string `json:"description"`
|
||||
Fix string `json:"fix"`
|
||||
Examples []string `json:"examples,omitempty"`
|
||||
}
|
||||
|
||||
// FormSummary represents a summary of form accessibility
|
||||
type FormSummary struct {
|
||||
FormsFound int `json:"forms_found"`
|
||||
Forms []FormAudit `json:"forms"`
|
||||
}
|
||||
|
||||
// FormAudit represents accessibility audit of a single form
|
||||
type FormAudit struct {
|
||||
ID string `json:"id"`
|
||||
Fields int `json:"fields"`
|
||||
Issues []FormIssue `json:"issues"`
|
||||
ARIACompliance string `json:"aria_compliance"`
|
||||
KeyboardAccessible bool `json:"keyboard_accessible"`
|
||||
RequiredMarked bool `json:"required_fields_marked"`
|
||||
}
|
||||
|
||||
// FormIssue represents a form accessibility issue
|
||||
type FormIssue struct {
|
||||
Type string `json:"type"`
|
||||
Severity string `json:"severity"`
|
||||
Count int `json:"count,omitempty"`
|
||||
Description string `json:"description"`
|
||||
Fix string `json:"fix"`
|
||||
Ratio float64 `json:"ratio,omitempty"`
|
||||
}
|
||||
|
||||
// getPageAccessibilityReport performs a comprehensive accessibility assessment
|
||||
func (d *Daemon) getPageAccessibilityReport(tabID string, tests []string, standard string, includeScreenshots bool, timeout int) (*PageAccessibilityReport, error) {
|
||||
d.debugLog("Getting page accessibility report for tab: %s", tabID)
|
||||
|
||||
page, err := d.getTab(tabID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get page: %v", err)
|
||||
}
|
||||
|
||||
// Get current URL
|
||||
url := page.MustInfo().URL
|
||||
|
||||
// Initialize report
|
||||
report := &PageAccessibilityReport{
|
||||
URL: url,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
SummaryByWCAG: make(map[string]WCAGSummary),
|
||||
Screenshots: make(map[string]string),
|
||||
}
|
||||
|
||||
// Run tests based on requested types
|
||||
runAll := len(tests) == 0 || (len(tests) == 1 && tests[0] == "all")
|
||||
|
||||
// Run axe-core tests if requested
|
||||
if runAll || contains(tests, "wcag") {
|
||||
d.debugLog("Running axe-core WCAG tests...")
|
||||
axeResult, err := d.runAxeCore(tabID, map[string]interface{}{
|
||||
"runOnly": map[string]interface{}{
|
||||
"type": "tag",
|
||||
"values": []string{"wcag2a", "wcag2aa", "wcag21aa"},
|
||||
},
|
||||
}, timeout)
|
||||
if err == nil {
|
||||
d.processAxeResults(report, axeResult)
|
||||
}
|
||||
}
|
||||
|
||||
// Run contrast check if requested
|
||||
if runAll || contains(tests, "contrast") {
|
||||
d.debugLog("Running contrast check...")
|
||||
contrastResult, err := d.checkContrast(tabID, "", timeout)
|
||||
if err == nil {
|
||||
d.processContrastResults(report, contrastResult)
|
||||
}
|
||||
}
|
||||
|
||||
// Run keyboard test if requested
|
||||
if runAll || contains(tests, "keyboard") {
|
||||
d.debugLog("Running keyboard navigation test...")
|
||||
keyboardResult, err := d.testKeyboardNavigation(tabID, timeout)
|
||||
if err == nil {
|
||||
d.processKeyboardResults(report, keyboardResult)
|
||||
}
|
||||
}
|
||||
|
||||
// Run form analysis if requested
|
||||
if runAll || contains(tests, "forms") {
|
||||
d.debugLog("Running form accessibility audit...")
|
||||
formResult, err := d.getFormAccessibilityAudit(tabID, "", timeout)
|
||||
if err == nil {
|
||||
report.FormSummary = formResult
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate overall score and compliance status
|
||||
d.calculateOverallScore(report)
|
||||
|
||||
d.debugLog("Successfully generated page accessibility report for tab: %s", tabID)
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// Helper function to check if slice contains string
|
||||
func contains(slice []string, str string) bool {
|
||||
for _, s := range slice {
|
||||
if s == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// processAxeResults processes axe-core results and adds them to the report
|
||||
func (d *Daemon) processAxeResults(report *PageAccessibilityReport, axeResult *AxeResults) {
|
||||
// Process violations by severity
|
||||
for _, violation := range axeResult.Violations {
|
||||
issue := AccessibilityIssue{
|
||||
WCAG: extractWCAGCriteria(violation.Tags),
|
||||
Title: violation.Help,
|
||||
Description: violation.Description,
|
||||
Impact: violation.Impact,
|
||||
Count: len(violation.Nodes),
|
||||
Remediation: violation.HelpURL,
|
||||
}
|
||||
|
||||
// Add examples (limit to 3)
|
||||
for i, node := range violation.Nodes {
|
||||
if i >= 3 {
|
||||
break
|
||||
}
|
||||
if len(node.Target) > 0 {
|
||||
issue.Examples = append(issue.Examples, node.Target[0])
|
||||
}
|
||||
}
|
||||
|
||||
// Categorize by impact
|
||||
switch violation.Impact {
|
||||
case "critical":
|
||||
report.CriticalIssues = append(report.CriticalIssues, issue)
|
||||
case "serious":
|
||||
report.SeriousIssues = append(report.SeriousIssues, issue)
|
||||
case "moderate":
|
||||
report.HighIssues = append(report.HighIssues, issue)
|
||||
case "minor":
|
||||
report.MediumIssues = append(report.MediumIssues, issue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processContrastResults processes contrast check results and adds them to the report
|
||||
func (d *Daemon) processContrastResults(report *PageAccessibilityReport, contrastResult *ContrastCheckResult) {
|
||||
report.ContrastSummary.TotalChecked = contrastResult.TotalElements
|
||||
report.ContrastSummary.Passed = contrastResult.PassedAA
|
||||
report.ContrastSummary.Failed = contrastResult.FailedAA
|
||||
|
||||
if contrastResult.TotalElements > 0 {
|
||||
passRate := float64(contrastResult.PassedAA) / float64(contrastResult.TotalElements) * 100
|
||||
report.ContrastSummary.PassRate = fmt.Sprintf("%.1f%%", passRate)
|
||||
}
|
||||
|
||||
// Extract critical failures (limit to 10)
|
||||
report.ContrastSummary.CriticalFailures = []ContrastFailure{}
|
||||
report.ContrastSummary.FailurePatterns = make(map[string]FailurePattern)
|
||||
|
||||
count := 0
|
||||
for _, elem := range contrastResult.Elements {
|
||||
if !elem.PassesAA && count < 10 {
|
||||
failure := ContrastFailure{
|
||||
Selector: elem.Selector,
|
||||
Text: elem.Text,
|
||||
Ratio: elem.ContrastRatio,
|
||||
Required: elem.RequiredAA,
|
||||
FgColor: elem.ForegroundColor,
|
||||
BgColor: elem.BackgroundColor,
|
||||
Fix: fmt.Sprintf("Increase contrast to at least %.1f:1", elem.RequiredAA),
|
||||
}
|
||||
report.ContrastSummary.CriticalFailures = append(report.ContrastSummary.CriticalFailures, failure)
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processKeyboardResults processes keyboard test results and adds them to the report
|
||||
func (d *Daemon) processKeyboardResults(report *PageAccessibilityReport, keyboardResult *KeyboardTestResult) {
|
||||
report.KeyboardSummary.TotalInteractive = keyboardResult.TotalInteractive
|
||||
report.KeyboardSummary.Focusable = keyboardResult.Focusable
|
||||
report.KeyboardSummary.MissingFocusIndicator = keyboardResult.NoFocusIndicator
|
||||
report.KeyboardSummary.KeyboardTraps = keyboardResult.KeyboardTraps
|
||||
|
||||
// Convert keyboard test issues to summary format
|
||||
if keyboardResult.NoFocusIndicator > 0 {
|
||||
issue := KeyboardIssue{
|
||||
Type: "missing_focus_indicators",
|
||||
Severity: "HIGH",
|
||||
Count: keyboardResult.NoFocusIndicator,
|
||||
Description: fmt.Sprintf("%d elements lack visible focus indicators", keyboardResult.NoFocusIndicator),
|
||||
Fix: "Add visible :focus styles with outline or border",
|
||||
}
|
||||
report.KeyboardSummary.Issues = append(report.KeyboardSummary.Issues, issue)
|
||||
}
|
||||
|
||||
if keyboardResult.KeyboardTraps > 0 {
|
||||
issue := KeyboardIssue{
|
||||
Type: "keyboard_traps",
|
||||
Severity: "CRITICAL",
|
||||
Count: keyboardResult.KeyboardTraps,
|
||||
Description: fmt.Sprintf("%d keyboard traps detected", keyboardResult.KeyboardTraps),
|
||||
Fix: "Ensure users can navigate away from all interactive elements using keyboard",
|
||||
}
|
||||
report.KeyboardSummary.Issues = append(report.KeyboardSummary.Issues, issue)
|
||||
}
|
||||
}
|
||||
|
||||
// calculateOverallScore calculates the overall accessibility score and compliance status
|
||||
func (d *Daemon) calculateOverallScore(report *PageAccessibilityReport) {
|
||||
// Calculate score based on issues (100 - deductions)
|
||||
score := 100
|
||||
score -= len(report.CriticalIssues) * 20 // -20 per critical
|
||||
score -= len(report.SeriousIssues) * 10 // -10 per serious
|
||||
score -= len(report.HighIssues) * 5 // -5 per high
|
||||
score -= len(report.MediumIssues) * 2 // -2 per medium
|
||||
|
||||
if score < 0 {
|
||||
score = 0
|
||||
}
|
||||
report.OverallScore = score
|
||||
|
||||
// Determine compliance status
|
||||
if len(report.CriticalIssues) > 0 || len(report.SeriousIssues) > 0 {
|
||||
report.ComplianceStatus = "NON_COMPLIANT"
|
||||
} else if len(report.HighIssues) > 0 {
|
||||
report.ComplianceStatus = "PARTIAL"
|
||||
} else {
|
||||
report.ComplianceStatus = "COMPLIANT"
|
||||
}
|
||||
|
||||
// Determine legal risk
|
||||
if len(report.CriticalIssues) > 0 {
|
||||
report.LegalRisk = "CRITICAL"
|
||||
} else if len(report.SeriousIssues) > 3 {
|
||||
report.LegalRisk = "HIGH"
|
||||
} else if len(report.SeriousIssues) > 0 || len(report.HighIssues) > 5 {
|
||||
report.LegalRisk = "MEDIUM"
|
||||
} else {
|
||||
report.LegalRisk = "LOW"
|
||||
}
|
||||
|
||||
// Estimate remediation hours
|
||||
hours := len(report.CriticalIssues)*4 + len(report.SeriousIssues)*2 + len(report.HighIssues)*1
|
||||
report.EstimatedHours = hours
|
||||
}
|
||||
|
||||
// extractWCAGCriteria extracts WCAG criteria from tags
|
||||
func extractWCAGCriteria(tags []string) string {
|
||||
for _, tag := range tags {
|
||||
if strings.HasPrefix(tag, "wcag") && strings.Contains(tag, ".") {
|
||||
// Extract number like "wcag144" -> "1.4.4"
|
||||
numStr := strings.TrimPrefix(tag, "wcag")
|
||||
if len(numStr) >= 3 {
|
||||
return fmt.Sprintf("%s.%s.%s", string(numStr[0]), string(numStr[1]), numStr[2:])
|
||||
}
|
||||
}
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
// ContrastAuditResult represents a smart contrast audit with prioritized failures
|
||||
type ContrastAuditResult struct {
|
||||
TotalChecked int `json:"total_checked"`
|
||||
Passed int `json:"passed"`
|
||||
Failed int `json:"failed"`
|
||||
PassRate string `json:"pass_rate"`
|
||||
CriticalFailures []ContrastFailure `json:"critical_failures"`
|
||||
FailurePatterns map[string]FailurePattern `json:"failure_patterns"`
|
||||
}
|
||||
|
||||
// getContrastAudit performs a smart contrast check with prioritized failures
|
||||
func (d *Daemon) getContrastAudit(tabID string, prioritySelectors []string, threshold string, timeout int) (*ContrastAuditResult, error) {
|
||||
d.debugLog("Getting contrast audit for tab: %s", tabID)
|
||||
|
||||
// Run full contrast check
|
||||
contrastResult, err := d.checkContrast(tabID, "", timeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check contrast: %v", err)
|
||||
}
|
||||
|
||||
// Build audit result
|
||||
result := &ContrastAuditResult{
|
||||
TotalChecked: contrastResult.TotalElements,
|
||||
Passed: contrastResult.PassedAA,
|
||||
Failed: contrastResult.FailedAA,
|
||||
CriticalFailures: []ContrastFailure{},
|
||||
FailurePatterns: make(map[string]FailurePattern),
|
||||
}
|
||||
|
||||
if contrastResult.TotalElements > 0 {
|
||||
passRate := float64(contrastResult.PassedAA) / float64(contrastResult.TotalElements) * 100
|
||||
result.PassRate = fmt.Sprintf("%.1f%%", passRate)
|
||||
}
|
||||
|
||||
// Extract critical failures (prioritize based on selectors)
|
||||
priorityMap := make(map[string]bool)
|
||||
for _, sel := range prioritySelectors {
|
||||
priorityMap[sel] = true
|
||||
}
|
||||
|
||||
// First add priority failures, then others (limit to 20 total)
|
||||
count := 0
|
||||
for _, elem := range contrastResult.Elements {
|
||||
if !elem.PassesAA && count < 20 {
|
||||
failure := ContrastFailure{
|
||||
Selector: elem.Selector,
|
||||
Text: elem.Text,
|
||||
Ratio: elem.ContrastRatio,
|
||||
Required: elem.RequiredAA,
|
||||
FgColor: elem.ForegroundColor,
|
||||
BgColor: elem.BackgroundColor,
|
||||
Fix: fmt.Sprintf("Increase contrast to at least %.1f:1", elem.RequiredAA),
|
||||
}
|
||||
result.CriticalFailures = append(result.CriticalFailures, failure)
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
d.debugLog("Successfully generated contrast audit for tab: %s", tabID)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// getKeyboardAudit performs a keyboard navigation assessment
|
||||
func (d *Daemon) getKeyboardAudit(tabID string, checkFocusIndicators, checkTabOrder, checkKeyboardTraps bool, timeout int) (*KeyboardAuditResult, error) {
|
||||
d.debugLog("Getting keyboard audit for tab: %s", tabID)
|
||||
|
||||
// Run keyboard navigation test
|
||||
keyboardResult, err := d.testKeyboardNavigation(tabID, timeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to test keyboard navigation: %v", err)
|
||||
}
|
||||
|
||||
// Build audit result
|
||||
result := &KeyboardAuditResult{
|
||||
TotalInteractive: keyboardResult.TotalInteractive,
|
||||
Focusable: keyboardResult.Focusable,
|
||||
Issues: []KeyboardIssue{},
|
||||
TabOrderIssues: []string{},
|
||||
}
|
||||
|
||||
// Determine status
|
||||
if keyboardResult.KeyboardTraps > 0 {
|
||||
result.Status = "FAIL"
|
||||
} else if keyboardResult.NoFocusIndicator > 0 {
|
||||
result.Status = "PARTIAL"
|
||||
} else {
|
||||
result.Status = "PASS"
|
||||
}
|
||||
|
||||
// Add issues
|
||||
if checkFocusIndicators && keyboardResult.NoFocusIndicator > 0 {
|
||||
issue := KeyboardIssue{
|
||||
Type: "missing_focus_indicators",
|
||||
Severity: "HIGH",
|
||||
Count: keyboardResult.NoFocusIndicator,
|
||||
Description: fmt.Sprintf("%d elements lack visible focus indicators", keyboardResult.NoFocusIndicator),
|
||||
Fix: "Add visible :focus styles with outline or border",
|
||||
}
|
||||
result.Issues = append(result.Issues, issue)
|
||||
}
|
||||
|
||||
if checkKeyboardTraps && keyboardResult.KeyboardTraps > 0 {
|
||||
issue := KeyboardIssue{
|
||||
Type: "keyboard_traps",
|
||||
Severity: "CRITICAL",
|
||||
Count: keyboardResult.KeyboardTraps,
|
||||
Description: fmt.Sprintf("%d keyboard traps detected", keyboardResult.KeyboardTraps),
|
||||
Fix: "Ensure users can navigate away from all interactive elements using keyboard",
|
||||
}
|
||||
result.Issues = append(result.Issues, issue)
|
||||
}
|
||||
|
||||
// Generate recommendation
|
||||
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."
|
||||
} else {
|
||||
result.Recommendation = "Keyboard navigation is accessible."
|
||||
}
|
||||
|
||||
d.debugLog("Successfully generated keyboard audit for tab: %s", tabID)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// getFormAccessibilityAudit performs a comprehensive form accessibility check
|
||||
func (d *Daemon) getFormAccessibilityAudit(tabID, formSelector string, timeout int) (*FormSummary, error) {
|
||||
d.debugLog("Getting form accessibility audit for tab: %s", tabID)
|
||||
|
||||
page, err := d.getTab(tabID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get page: %v", err)
|
||||
}
|
||||
|
||||
// JavaScript to analyze forms
|
||||
jsCode := `
|
||||
(function() {
|
||||
const forms = document.querySelectorAll('` + formSelector + `' || 'form');
|
||||
const result = {
|
||||
forms_found: forms.length,
|
||||
forms: []
|
||||
};
|
||||
|
||||
forms.forEach((form, index) => {
|
||||
const formData = {
|
||||
id: form.id || 'form-' + index,
|
||||
fields: form.querySelectorAll('input, select, textarea').length,
|
||||
issues: [],
|
||||
aria_compliance: 'FULL',
|
||||
keyboard_accessible: true,
|
||||
required_fields_marked: true
|
||||
};
|
||||
|
||||
// Check for labels
|
||||
const inputs = form.querySelectorAll('input:not([type="hidden"]), select, textarea');
|
||||
let missingLabels = 0;
|
||||
inputs.forEach(input => {
|
||||
const id = input.id;
|
||||
if (id) {
|
||||
const label = form.querySelector('label[for="' + id + '"]');
|
||||
if (!label && !input.getAttribute('aria-label') && !input.getAttribute('aria-labelledby')) {
|
||||
missingLabels++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (missingLabels > 0) {
|
||||
formData.issues.push({
|
||||
type: 'missing_labels',
|
||||
severity: 'SERIOUS',
|
||||
count: missingLabels,
|
||||
description: missingLabels + ' fields lack proper labels',
|
||||
fix: 'Add <label> elements or aria-label attributes'
|
||||
});
|
||||
formData.aria_compliance = 'PARTIAL';
|
||||
}
|
||||
|
||||
// Check submit button contrast (simplified)
|
||||
const submitBtn = form.querySelector('button[type="submit"], input[type="submit"]');
|
||||
if (submitBtn) {
|
||||
const styles = window.getComputedStyle(submitBtn);
|
||||
// Note: Actual contrast calculation would be more complex
|
||||
formData.issues.push({
|
||||
type: 'submit_button_check',
|
||||
severity: 'INFO',
|
||||
description: 'Submit button found - verify contrast manually',
|
||||
fix: 'Ensure submit button has 4.5:1 contrast ratio'
|
||||
});
|
||||
}
|
||||
|
||||
result.forms.push(formData);
|
||||
});
|
||||
|
||||
return result;
|
||||
})();
|
||||
`
|
||||
|
||||
// Execute JavaScript with timeout
|
||||
var resultData interface{}
|
||||
if timeout > 0 {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
done := make(chan struct {
|
||||
result *proto.RuntimeRemoteObject
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
go func() {
|
||||
res, err := page.Eval(jsCode)
|
||||
done <- struct {
|
||||
result *proto.RuntimeRemoteObject
|
||||
err error
|
||||
}{res, err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("form analysis timed out after %d seconds", timeout)
|
||||
case result := <-done:
|
||||
if result.err != nil {
|
||||
return nil, fmt.Errorf("failed to analyze forms: %v", result.err)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(result.result.Value.String()), &resultData); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse form analysis result: %v", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res, err := page.Eval(jsCode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to analyze forms: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(res.Value.String()), &resultData); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse form analysis result: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to FormSummary
|
||||
var summary FormSummary
|
||||
dataBytes, err := json.Marshal(resultData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal form data: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal(dataBytes, &summary); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal form summary: %v", err)
|
||||
}
|
||||
|
||||
d.debugLog("Successfully generated form accessibility audit for tab: %s (found %d forms)", tabID, summary.FormsFound)
|
||||
return &summary, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user