File size: 9,837 Bytes
a8acddd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
"""
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)