Browser-Use-mcp / features /accessibility.py
diamond-in's picture
Update features/accessibility.py
a8acddd verified
"""
Accessibility audit and testing features
"""
import json
import logging
from datetime import datetime
from browser.driver import get_driver, cleanup_driver
logger = logging.getLogger(__name__)
def accessibility_audit(url: str, use_persistent: bool = False) -> str:
"""Perform basic accessibility audit on a page"""
driver = None
try:
driver = get_driver(url, use_persistent)
# Run accessibility checks
audit_results = driver.execute_script("""
const results = {
images_without_alt: [],
form_inputs_without_labels: [],
low_contrast_elements: [],
missing_landmarks: [],
heading_structure: [],
interactive_elements_without_text: [],
links_without_text: [],
aria_issues: []
};
// Check images without alt text
document.querySelectorAll('img:not([alt])').forEach(img => {
results.images_without_alt.push(img.src || 'no-src');
});
// Check form inputs without labels
document.querySelectorAll('input, select, textarea').forEach(input => {
const id = input.id;
const hasLabel = id && document.querySelector(`label[for="${id}"]`);
const hasAriaLabel = input.getAttribute('aria-label');
const hasAriaLabelledBy = input.getAttribute('aria-labelledby');
if (!hasLabel && !hasAriaLabel && !hasAriaLabelledBy) {
results.form_inputs_without_labels.push({
type: input.type,
name: input.name,
id: id || 'no-id'
});
}
});
// Check heading structure
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
let lastLevel = 0;
headings.forEach(heading => {
const level = parseInt(heading.tagName.substring(1));
results.heading_structure.push({
level: level,
text: heading.textContent.trim().substring(0, 50),
issue: level > lastLevel + 1 ? 'skipped_level' : null
});
lastLevel = level;
});
// Check for landmarks
const landmarks = {
header: document.querySelector('header, [role="banner"]'),
nav: document.querySelector('nav, [role="navigation"]'),
main: document.querySelector('main, [role="main"]'),
footer: document.querySelector('footer, [role="contentinfo"]')
};
for (const [role, element] of Object.entries(landmarks)) {
if (!element) {
results.missing_landmarks.push(role);
}
}
// Check interactive elements without text
document.querySelectorAll('button, a').forEach(elem => {
const text = elem.textContent.trim();
const ariaLabel = elem.getAttribute('aria-label');
const ariaLabelledBy = elem.getAttribute('aria-labelledby');
if (!text && !ariaLabel && !ariaLabelledBy) {
results.interactive_elements_without_text.push({
tag: elem.tagName,
class: elem.className,
href: elem.href || null
});
}
});
// Check links without meaningful text
document.querySelectorAll('a[href]').forEach(link => {
const text = link.textContent.trim().toLowerCase();
if (text === 'click here' || text === 'here' || text === 'read more') {
results.links_without_text.push({
text: text,
href: link.href
});
}
});
// Check ARIA issues
document.querySelectorAll('[aria-hidden="true"]').forEach(elem => {
if (elem.querySelector('a, button, input, select, textarea')) {
results.aria_issues.push({
issue: 'interactive_element_hidden',
element: elem.tagName + (elem.className ? '.' + elem.className : '')
});
}
});
return results;
""")
# Calculate score
issues_count = sum(len(v) if isinstance(v, list) else 0 for v in audit_results.values())
score = max(0, 100 - (issues_count * 5)) # Deduct 5 points per issue
result = {
"url": url,
"accessibility_score": score,
"grade": get_grade(score),
"issues": audit_results,
"total_issues": issues_count,
"timestamp": datetime.now().isoformat(),
"recommendations": get_recommendations(audit_results)
}
return json.dumps(result, indent=2)
except Exception as e:
logger.error(f"Error in accessibility_audit: {e}")
return f"Error: {e}"
finally:
cleanup_driver(driver, use_persistent)
def get_grade(score):
"""Convert score to letter grade"""
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
elif score >= 60:
return "D"
else:
return "F"
def get_recommendations(audit_results):
"""Get recommendations based on audit results"""
recommendations = []
if audit_results["images_without_alt"]:
recommendations.append("Add alt text to all images for screen reader users")
if audit_results["form_inputs_without_labels"]:
recommendations.append("Associate all form inputs with labels using 'for' attribute or aria-label")
if audit_results["missing_landmarks"]:
recommendations.append("Add semantic HTML5 landmarks (header, nav, main, footer) or ARIA roles")
if audit_results["heading_structure"]:
has_issues = any(h["issue"] for h in audit_results["heading_structure"])
if has_issues:
recommendations.append("Fix heading hierarchy - don't skip heading levels")
if audit_results["interactive_elements_without_text"]:
recommendations.append("Add descriptive text or aria-labels to all interactive elements")
return recommendations
def check_color_contrast(url: str, use_persistent: bool = False) -> str:
"""Check color contrast for text elements"""
driver = None
try:
driver = get_driver(url, use_persistent)
# Check color contrast
contrast_results = driver.execute_script("""
function getLuminance(r, g, b) {
const sRGB = [r, g, b].map(val => {
val = val / 255;
return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4);
});
return 0.2126 * sRGB[0] + 0.7152 * sRGB[1] + 0.0722 * sRGB[2];
}
function getContrastRatio(color1, color2) {
const l1 = getLuminance(...color1);
const l2 = getLuminance(...color2);
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
function getRGBValues(colorStr) {
const match = colorStr.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/);
if (match) {
return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])];
}
return null;
}
const results = [];
const textElements = document.querySelectorAll('p, span, div, h1, h2, h3, h4, h5, h6, a, button');
textElements.forEach(elem => {
const style = window.getComputedStyle(elem);
const color = getRGBValues(style.color);
const bgColor = getRGBValues(style.backgroundColor);
if (color && bgColor) {
const ratio = getContrastRatio(color, bgColor);
const fontSize = parseFloat(style.fontSize);
const fontWeight = style.fontWeight;
// WCAG AA standards
const isLargeText = fontSize >= 18 || (fontSize >= 14 && fontWeight >= 700);
const requiredRatio = isLargeText ? 3 : 4.5;
if (ratio < requiredRatio) {
results.push({
element: elem.tagName,
text: elem.textContent.trim().substring(0, 50),
contrastRatio: ratio.toFixed(2),
requiredRatio: requiredRatio,
fontSize: fontSize,
isLargeText: isLargeText
});
}
}
});
return results;
""")
return json.dumps({
"low_contrast_elements": contrast_results,
"total_issues": len(contrast_results)
}, indent=2)
except Exception as e:
logger.error(f"Error in check_color_contrast: {e}")
return f"Error: {e}"
finally:
cleanup_driver(driver, use_persistent)