Use Cases
- Detecting missing alt text and accessibility labels
- Validating form labels and required field indicators
- Checking heading structure and hierarchy
- Finding generic or missing link text
- Testing basic ARIA implementation
Simple Accessibility Checker
Copy
Ask AI
from locust import HttpUser, task, between
import re
import time
class SimpleAccessibilityChecker(HttpUser):
wait_time = between(1, 2)
def on_start(self):
"""Initialize simple accessibility checking"""
self.accessibility_issues = []
self.pages_checked = 0
self.discovered_pages = []
self.current_page_issues = []
print("Starting simple accessibility check...")
@task(5)
def discover_and_check_accessibility(self):
"""Discover pages and check accessibility continuously"""
if self.pages_checked == 0:
# Start with homepage
self._check_page_accessibility('/')
elif len(self.discovered_pages) > 0:
# Keep cycling through discovered pages for continuous testing
self._cycle_through_pages()
def _check_page_accessibility(self, page_url):
"""Check accessibility on a single page"""
self.current_page_issues = [] # Reset issues for this page
with self.client.get(page_url, name=f"ACCESSIBILITY: {page_url}", catch_response=True) as response:
if response.status_code == 200:
self._check_basic_accessibility(page_url, response.text)
# Only increment pages_checked during initial discovery phase
if page_url == '/' and self.pages_checked == 0:
self.pages_checked += 1
self._find_internal_pages(response.text)
elif page_url in self.discovered_pages and self.pages_checked == 1:
# We're in the continuous testing phase, don't increment counter
pass
else:
# Still in discovery mode
self.pages_checked += 1
# Report page result based on issues found
if self.current_page_issues:
high_issues = [i for i in self.current_page_issues if i['severity'] == 'HIGH']
medium_issues = [i for i in self.current_page_issues if i['severity'] == 'MEDIUM']
failure_msg = f"❌ {len(self.current_page_issues)} accessibility issues ({len(high_issues)} high, {len(medium_issues)} medium)"
response.failure(failure_msg)
else:
response.success()
else:
response.failure(f"Could not access page: HTTP {response.status_code}")
def _find_internal_pages(self, html_content):
"""Find internal pages from homepage links"""
# Find internal links
links = re.findall(r'<a[^>]+href=["\']([^"\']+)["\']', html_content, re.IGNORECASE)
for link in links:
if self._is_internal_page_link(link):
normalized_link = self._normalize_link(link)
if normalized_link and normalized_link not in self.discovered_pages and normalized_link != '/':
self.discovered_pages.append(normalized_link)
if len(self.discovered_pages) >= 100: # Discover up to 100 pages
break
print(f"Discovered {len(self.discovered_pages)} internal pages to check")
def _cycle_through_pages(self):
"""Continuously cycle through discovered pages for accessibility testing"""
if not self.discovered_pages:
return
# Pick a random page from discovered pages to check
import random
page_to_check = random.choice(self.discovered_pages)
# Check accessibility on this page (this creates continuous load testing)
self._check_page_accessibility(page_to_check)
def _is_internal_page_link(self, link):
"""Check if link is an internal page (not resource)"""
# Skip anchors, external protocols, and resources
if any(skip in link.lower() for skip in ['#', 'mailto:', 'tel:', 'javascript:']):
return False
# Skip common resource extensions
resource_extensions = ['.css', '.js', '.jpg', '.jpeg', '.png', '.gif', '.pdf', '.zip']
if any(link.lower().endswith(ext) for ext in resource_extensions):
return False
# Must be internal (relative or same domain)
if link.startswith('/') or not link.startswith('http'):
return True
return False
def _normalize_link(self, link):
"""Normalize link for checking"""
try:
if link.startswith('/'):
return link.split('#')[0] # Remove fragment
elif not link.startswith('http'):
return '/' + link.lstrip('./')
return None
except:
return None
def _check_basic_accessibility(self, page_url, html_content):
"""Check basic accessibility issues on a page"""
print(f"Checking accessibility for: {page_url}")
# Check images for alt text
self._check_image_alt_text(page_url, html_content)
# Check form labels
self._check_form_labels(page_url, html_content)
# Check heading structure
self._check_heading_structure(page_url, html_content)
# Check link text
self._check_link_text(page_url, html_content)
def _check_image_alt_text(self, page_url, html_content):
"""Check images for alt text"""
img_tags = re.findall(r'<img[^>]*>', html_content, re.IGNORECASE)
for img_tag in img_tags:
if 'alt=' not in img_tag.lower():
self._log_issue(page_url, 'HIGH', 'Image missing alt attribute', img_tag[:80])
else:
# Check for generic alt text
alt_match = re.search(r'alt=["\']([^"\']*)["\']', img_tag, re.IGNORECASE)
if alt_match:
alt_text = alt_match.group(1).strip()
if alt_text.lower() in ['image', 'picture', 'photo', 'img']:
self._log_issue(page_url, 'MEDIUM', f'Generic alt text: "{alt_text}"', img_tag[:80])
def _check_form_labels(self, page_url, html_content):
"""Check form elements for labels"""
form_elements = re.findall(r'<(input|textarea|select)[^>]*>', html_content, re.IGNORECASE)
for element in form_elements:
# Skip hidden inputs and buttons
if 'type="hidden"' in element.lower() or 'type="submit"' in element.lower():
continue
# Check for id and corresponding label
id_match = re.search(r'id=["\']([^"\']*)["\']', element, re.IGNORECASE)
if id_match:
element_id = id_match.group(1)
label_pattern = f'<label[^>]*for=["\']?{re.escape(element_id)}["\']?[^>]*>'
if not re.search(label_pattern, html_content, re.IGNORECASE):
# Check for aria-label as alternative
if 'aria-label=' not in element.lower():
self._log_issue(page_url, 'HIGH', f'Form element missing label: {element_id}', element[:80])
def _check_heading_structure(self, page_url, html_content):
"""Check heading structure"""
headings = re.findall(r'<(h[1-6])[^>]*>(.*?)</\1>', html_content, re.IGNORECASE | re.DOTALL)
if not headings:
self._log_issue(page_url, 'HIGH', 'Page has no headings', '')
return
# Check if page starts with h1
heading_levels = [int(h[0][1]) for h in headings]
if heading_levels[0] != 1:
self._log_issue(page_url, 'MEDIUM', f'Page does not start with h1 (starts with h{heading_levels[0]})', '')
# Check for empty headings
for heading_tag, heading_text in headings:
clean_text = re.sub(r'<[^>]+>', '', heading_text).strip()
if not clean_text:
self._log_issue(page_url, 'HIGH', f'Empty {heading_tag.upper()} heading', '')
def _check_link_text(self, page_url, html_content):
"""Check link text for accessibility"""
links = re.finditer(r'<a[^>]*>(.*?)</a>', html_content, re.IGNORECASE | re.DOTALL)
for link_match in links:
link_text = link_match.group(1).strip()
clean_link_text = re.sub(r'<[^>]+>', '', link_text).strip()
# Check for empty link text
if not clean_link_text:
self._log_issue(page_url, 'HIGH', 'Link has no text', link_match.group(0)[:80])
elif clean_link_text.lower() in ['click here', 'read more', 'more', 'here', 'link']:
self._log_issue(page_url, 'MEDIUM', f'Generic link text: "{clean_link_text}"', link_match.group(0)[:80])
def _log_issue(self, page_url, severity, description, element):
"""Log accessibility issue"""
issue = {
'page': page_url,
'severity': severity,
'description': description,
'element': element,
'timestamp': time.time()
}
self.accessibility_issues.append(issue)
self.current_page_issues.append(issue) # Track issues for current page
print(f"ACCESSIBILITY ISSUE [{severity}]: {description} on {page_url}")
@task(1)
def report_accessibility_status(self):
"""Report current accessibility status"""
if len(self.discovered_pages) == 0:
return
high_issues = [issue for issue in self.accessibility_issues if issue['severity'] == 'HIGH']
medium_issues = [issue for issue in self.accessibility_issues if issue['severity'] == 'MEDIUM']
total_pages_in_scope = len(self.discovered_pages) + 1 # +1 for homepage
print(f"ACCESSIBILITY LOAD TEST: {len(self.accessibility_issues)} total issues "
f"({len(high_issues)} high, {len(medium_issues)} medium) "
f"found across {total_pages_in_scope} pages (continuously testing)")
def on_stop(self):
"""Final accessibility report"""
print("\n" + "="*50)
print("ACCESSIBILITY LOAD TEST COMPLETE")
print("="*50)
total_pages_in_scope = len(self.discovered_pages) + 1 # +1 for homepage
print(f"Pages in test scope: {total_pages_in_scope}")
print(f"Total accessibility issues found: {len(self.accessibility_issues)}")
if self.accessibility_issues:
print(f"\nTOP ACCESSIBILITY ISSUES:")
for issue in self.accessibility_issues[:5]:
print(f"❌ [{issue['severity']}] {issue['description']}")
print(f" Page: {issue['page']}")
else:
print("✅ No accessibility issues found!")
Comprehensive Accessibility Testing
Copy
Ask AI
from locust import HttpUser, task, between
import json
import time
import re
from urllib.parse import urlparse
from collections import defaultdict, deque
class ComprehensiveAccessibilityTester(HttpUser):
wait_time = between(1, 3)
def on_start(self):
"""Initialize comprehensive accessibility testing"""
self.visited_pages = set()
self.pages_to_check = deque(['/'])
self.accessibility_issues = []
self.accessibility_stats = defaultdict(int)
self.wcag_violations = []
self.base_domain = None
# Initialize crawler
self._initialize_crawler()
def _initialize_crawler(self):
"""Initialize the accessibility crawler"""
response = self.client.get('/', name="INIT: Homepage accessibility check")
if response.status_code == 200:
self.base_domain = urlparse(self.client.base_url).netloc
print(f"Starting comprehensive accessibility testing for: {self.base_domain}")
else:
print(f"Failed to access homepage: {response.status_code}")
@task(5)
def crawl_and_test_accessibility(self):
"""Main crawling task to test accessibility"""
if not self.pages_to_check:
return
current_page = self.pages_to_check.popleft()
if current_page in self.visited_pages:
return
self.visited_pages.add(current_page)
response = self.client.get(current_page, name=f"CRAWL: {current_page}")
if response.status_code == 200:
self._test_page_accessibility(current_page, response.text)
self._find_more_pages(response.text)
else:
print(f"Cannot access page for accessibility testing: {current_page}")
def _test_page_accessibility(self, page_url, html_content):
"""Test comprehensive accessibility on a page"""
self.accessibility_stats['total_pages'] += 1
# Run all accessibility tests
self._test_images(page_url, html_content)
self._test_forms(page_url, html_content)
self._test_headings(page_url, html_content)
self._test_links(page_url, html_content)
self._test_aria(page_url, html_content)
self._test_semantic_html(page_url, html_content)
def _test_images(self, page_url, html_content):
"""Test image accessibility"""
img_tags = re.findall(r'<img[^>]*>', html_content, re.IGNORECASE)
for img_tag in img_tags:
self.accessibility_stats['total_images'] += 1
if 'alt=' not in img_tag.lower():
self._record_issue(page_url, 'HIGH', 'Image missing alt attribute', 'WCAG 1.1.1', img_tag[:100])
else:
alt_match = re.search(r'alt=["\']([^"\']*)["\']', img_tag, re.IGNORECASE)
if alt_match:
alt_text = alt_match.group(1).strip()
if not alt_text and 'role="presentation"' not in img_tag.lower():
self._record_issue(page_url, 'MEDIUM', 'Image has empty alt text', 'WCAG 1.1.1', img_tag[:100])
elif alt_text.lower() in ['image', 'picture', 'photo', 'img']:
self._record_issue(page_url, 'MEDIUM', f'Generic alt text: "{alt_text}"', 'WCAG 1.1.1', img_tag[:100])
def _test_forms(self, page_url, html_content):
"""Test form accessibility"""
form_elements = re.findall(r'<(input|textarea|select)[^>]*>', html_content, re.IGNORECASE)
for element in form_elements:
self.accessibility_stats['total_form_elements'] += 1
# Skip certain input types
if any(t in element.lower() for t in ['type="hidden"', 'type="submit"', 'type="button"']):
continue
id_match = re.search(r'id=["\']([^"\']*)["\']', element, re.IGNORECASE)
if id_match:
element_id = id_match.group(1)
label_pattern = f'<label[^>]*for=["\']?{re.escape(element_id)}["\']?[^>]*>'
if not re.search(label_pattern, html_content, re.IGNORECASE):
if 'aria-label=' not in element.lower() and 'aria-labelledby=' not in element.lower():
self._record_issue(page_url, 'HIGH', f'Form element missing label: {element_id}', 'WCAG 1.3.1', element[:100])
# Check required fields
if 'required' in element.lower() and 'aria-required=' not in element.lower():
self._record_issue(page_url, 'MEDIUM', 'Required field missing aria-required', 'WCAG 3.3.2', element[:100])
def _test_headings(self, page_url, html_content):
"""Test heading structure"""
headings = re.findall(r'<(h[1-6])[^>]*>(.*?)</\1>', html_content, re.IGNORECASE | re.DOTALL)
if not headings:
self._record_issue(page_url, 'HIGH', 'Page has no heading structure', 'WCAG 1.3.1', '')
return
heading_levels = [int(h[0][1]) for h in headings]
# Check if starts with h1
if heading_levels[0] != 1:
self._record_issue(page_url, 'MEDIUM', f'Page does not start with h1', 'WCAG 1.3.1', '')
# Check for heading level skips
for i in range(1, len(heading_levels)):
if heading_levels[i] > heading_levels[i-1] + 1:
self._record_issue(page_url, 'MEDIUM', f'Heading level skip: h{heading_levels[i-1]} to h{heading_levels[i]}', 'WCAG 1.3.1', '')
# Check for empty headings
for heading_tag, heading_text in headings:
clean_text = re.sub(r'<[^>]+>', '', heading_text).strip()
if not clean_text:
self._record_issue(page_url, 'HIGH', f'Empty {heading_tag.upper()} heading', 'WCAG 2.4.6', '')
def _test_links(self, page_url, html_content):
"""Test link accessibility"""
for link_match in re.finditer(r'<a[^>]*>(.*?)</a>', html_content, re.IGNORECASE | re.DOTALL):
link_tag = link_match.group(0)
link_text = link_match.group(1).strip()
clean_link_text = re.sub(r'<[^>]+>', '', link_text).strip()
self.accessibility_stats['total_links'] += 1
if not clean_link_text:
if 'aria-label=' not in link_tag.lower() and 'title=' not in link_tag.lower():
self._record_issue(page_url, 'HIGH', 'Link has no accessible text', 'WCAG 2.4.4', link_tag[:100])
elif clean_link_text.lower() in ['click here', 'read more', 'more', 'link', 'here']:
self._record_issue(page_url, 'MEDIUM', f'Generic link text: "{clean_link_text}"', 'WCAG 2.4.4', link_tag[:100])
def _test_aria(self, page_url, html_content):
"""Test ARIA implementation"""
# Check for main landmark
if 'role="main"' not in html_content.lower() and '<main' not in html_content.lower():
self._record_issue(page_url, 'MEDIUM', 'Page missing main landmark', 'WCAG 1.3.1', '')
# Check for invalid ARIA attributes (basic check)
aria_attributes = re.findall(r'aria-([a-zA-Z-]+)=', html_content, re.IGNORECASE)
valid_aria = ['label', 'labelledby', 'describedby', 'hidden', 'expanded', 'controls', 'live', 'required']
for attr in aria_attributes:
if attr not in valid_aria:
self._record_issue(page_url, 'LOW', f'Unknown ARIA attribute: aria-{attr}', 'WCAG 4.1.2', '')
def _test_semantic_html(self, page_url, html_content):
"""Test semantic HTML usage"""
semantic_elements = ['header', 'nav', 'main', 'article', 'section', 'aside', 'footer']
found_semantic = []
for element in semantic_elements:
if f'<{element}' in html_content.lower():
found_semantic.append(element)
if len(found_semantic) < 2:
self._record_issue(page_url, 'LOW', f'Limited semantic HTML (found: {", ".join(found_semantic)})', 'WCAG 1.3.1', '')
def _find_more_pages(self, html_content):
"""Find more internal pages to test"""
if len(self.pages_to_check) > 20: # Limit crawling
return
links = re.findall(r'<a[^>]+href=["\']([^"\']+)["\']', html_content, re.IGNORECASE)
for link in links:
if self._is_internal_link(link) and link not in self.visited_pages:
normalized_link = self._normalize_url(link)
if normalized_link:
self.pages_to_check.append(normalized_link)
def _is_internal_link(self, link):
"""Check if link is internal"""
if link.startswith('/'):
return True
if link.startswith('http'):
return urlparse(link).netloc == self.base_domain
return not link.startswith(('mailto:', 'tel:', 'javascript:', '#'))
def _normalize_url(self, link):
"""Normalize URL for consistency"""
try:
if link.startswith('/'):
return link.split('#')[0] # Remove fragments
elif link.startswith('http'):
parsed = urlparse(link)
if parsed.netloc == self.base_domain:
return parsed.path
return None
except:
return None
def _record_issue(self, page_url, severity, description, wcag_guideline, element):
"""Record accessibility issue"""
issue = {
'page': page_url,
'severity': severity,
'description': description,
'wcag_guideline': wcag_guideline,
'element': element[:200] if element else '',
'timestamp': time.time()
}
self.accessibility_issues.append(issue)
self.accessibility_stats['total_issues'] += 1
if wcag_guideline:
self.wcag_violations.append(wcag_guideline)
print(f"ACCESSIBILITY ISSUE [{severity}]: {description} ({wcag_guideline}) on {page_url}")
@task(1)
def generate_accessibility_report(self):
"""Generate accessibility report"""
if len(self.visited_pages) < 3:
return
high_issues = [i for i in self.accessibility_issues if i['severity'] == 'HIGH']
medium_issues = [i for i in self.accessibility_issues if i['severity'] == 'MEDIUM']
print(f"ACCESSIBILITY REPORT: {len(self.accessibility_issues)} total issues "
f"({len(high_issues)} high, {len(medium_issues)} medium) "
f"across {len(self.visited_pages)} pages")
def on_stop(self):
"""Final accessibility report"""
print("\n=== COMPREHENSIVE ACCESSIBILITY TEST COMPLETE ===")
print(f"Pages tested: {len(self.visited_pages)}")
print(f"Total accessibility issues: {len(self.accessibility_issues)}")
print(f"WCAG violations: {len(set(self.wcag_violations))}")
if self.accessibility_issues:
print("\nTOP ACCESSIBILITY ISSUES:")
for issue in self.accessibility_issues[:5]:
print(f" - [{issue['severity']}] {issue['description']} ({issue['wcag_guideline']})")
Advanced WCAG Compliance Testing
Copy
Ask AI
from locust import HttpUser, task, between
import json
import time
import re
class AdvancedWCAGTester(HttpUser):
wait_time = between(2, 4)
@task(3)
def test_wcag_aa_compliance(self):
"""Test specific WCAG AA compliance requirements"""
pages = ['/', '/about', '/contact', '/services']
for page in pages:
response = self.client.get(page, name=f"WCAG AA: {page}")
if response.status_code == 200:
self._test_color_contrast_requirements(page, response.text)
self._test_focus_management(page, response.text)
self._test_keyboard_navigation(page, response.text)
def _test_color_contrast_requirements(self, page_url, html_content):
"""Test color contrast indicators"""
# Look for potential low contrast in inline styles
styles = re.findall(r'style=["\']([^"\']*)["\']', html_content, re.IGNORECASE)
for style in styles:
if 'color:' in style.lower() and 'background' in style.lower():
# Basic check for obvious low contrast patterns
if '#fff' in style.lower() and any(c in style.lower() for c in ['#f', '#e']):
print(f"POTENTIAL LOW CONTRAST: {style[:50]} on {page_url}")
def _test_focus_management(self, page_url, html_content):
"""Test focus management elements"""
# Check for skip links
if not re.search(r'<a[^>]*href=["\']#[^"\']*["\'][^>]*>.*?skip.*?</a>', html_content, re.IGNORECASE):
print(f"MISSING SKIP LINK: {page_url}")
# Check for positive tabindex (anti-pattern)
positive_tabindex = re.findall(r'tabindex=["\']([1-9][0-9]*)["\']', html_content)
if positive_tabindex:
print(f"POSITIVE TABINDEX FOUND: {positive_tabindex} on {page_url}")
def _test_keyboard_navigation(self, page_url, html_content):
"""Test keyboard navigation support"""
# Check interactive elements for keyboard support
interactive_elements = re.findall(r'<(button|a|input|select|textarea)[^>]*>', html_content, re.IGNORECASE)
for element in interactive_elements:
if 'onclick=' in element.lower() and element.lower().startswith('<div'):
print(f"NON-SEMANTIC INTERACTIVE ELEMENT: {element[:50]} on {page_url}")
@task(1)
def validate_html_semantics(self):
"""Validate HTML semantic structure"""
response = self.client.get('/', name="HTML Semantics Validation")
if response.status_code == 200:
# Check document structure
html = response.text.lower()
required_elements = ['<title', '<main', '<h1']
for element in required_elements:
if element not in html:
print(f"MISSING REQUIRED ELEMENT: {element}")
# Check for proper landmark structure
landmarks = ['header', 'nav', 'main', 'footer']
found_landmarks = [l for l in landmarks if f'<{l}' in html]
if len(found_landmarks) < 3:
print(f"INSUFFICIENT LANDMARKS: Only found {found_landmarks}")
Key Accessibility Features
- Progressive Complexity: Simple → Comprehensive → Advanced testing levels
- WCAG Compliance: Tests against WCAG 2.1 guidelines with specific rule references
- Core Accessibility Checks: Alt text, form labels, heading structure, link text
- Semantic HTML: HTML5 semantic element validation
- ARIA Support: Basic ARIA attribute and landmark testing
- Focus Management: Skip links and keyboard navigation testing