Axe-Core Accessibility Testing

Comprehensive WCAG compliance testing using axe-core JavaScript library with Playwright

LoadForge can record your browser, graphically build tests, scan your site with a wizard and more. Sign up now to run your first test.

Sign up now


This guide demonstrates automated accessibility testing using axe-core library injected into web pages via Playwright. This provides the most comprehensive and accurate WCAG compliance testing available.

Use Cases

  • Automated WCAG 2.1 AA/AAA compliance scanning
  • Comprehensive accessibility rule validation
  • Real browser accessibility testing with industry-standard tools
  • Detailed violation reporting with specific remediation guidance
  • Large-scale accessibility auditing across entire websites

Simple Axe-Core Accessibility Testing

from locust import task
from locust_plugins.users.playwright import PlaywrightUser, PageWithRetry, pw, event
import json

class SimpleAxeAccessibilityTester(PlaywrightUser):
    
    def on_start(self):
        """Initialize axe-core accessibility testing"""
        self.accessibility_violations = []
        self.pages_tested = 0
        self.discovered_pages = []
        
    @task(5)
    @pw
    async def test_page_accessibility_with_axe(self, page: PageWithRetry):
        """Test page accessibility using axe-core"""
        if self.pages_tested == 0:
            # Start with homepage and discover pages
            await self._test_axe_accessibility(page, '/')
            await self._discover_pages(page)
            self.pages_tested += 1
        elif self.discovered_pages:
            # Test discovered pages
            import random
            page_url = random.choice(self.discovered_pages)
            await self._test_axe_accessibility(page, page_url)

    async def _discover_pages(self, page: PageWithRetry):
        """Discover internal pages to test"""
        if self.discovered_pages:
            return
            
        # Find internal links
        links = await page.locator('a[href^="/"], a[href^="./"], a[href*="' + page.url.split('/')[2] + '"]').all()
        
        for link in links[:20]:  # Limit to 20 pages
            try:
                href = await link.get_attribute('href')
                if href and self._is_valid_page_link(href):
                    self.discovered_pages.append(href)
            except:
                pass
        
        print(f"Discovered {len(self.discovered_pages)} pages for axe testing")

    def _is_valid_page_link(self, href):
        """Check if href is a valid page link to test"""
        if not href or href in self.discovered_pages:
            return False
            
        # Skip resources and external protocols
        skip_patterns = ['#', 'mailto:', 'tel:', 'javascript:', '.pdf', '.jpg', '.jpeg', '.png', '.gif', '.css', '.js', '.zip']
        return not any(pattern in href.lower() for pattern in skip_patterns)

    async def _test_axe_accessibility(self, page: PageWithRetry, page_url: str):
        """Test accessibility using axe-core on a single page"""
        current_violations = []
        
        async with event(self, f"AXE ACCESSIBILITY: {page_url}"):
            await page.goto(page_url)
            
            # Inject axe-core library
            await self._inject_axe_core(page)
            
            # Run axe accessibility scan
            axe_results = await self._run_axe_scan(page)
            
            if axe_results and axe_results.get('violations'):
                violations = axe_results['violations']
                current_violations = self._process_axe_violations(page_url, violations)
                
                # Report violations to LoadForge
                violation_count = len(violations)
                critical_count = len([v for v in violations if v.get('impact') == 'critical'])
                serious_count = len([v for v in violations if v.get('impact') == 'serious'])
                
                failure_msg = f"❌ {violation_count} axe violations ({critical_count} critical, {serious_count} serious)"
                raise Exception(failure_msg)
            else:
                print(f"✅ {page_url} - No accessibility violations found")

    async def _inject_axe_core(self, page: PageWithRetry):
        """Inject axe-core library into the page"""
        # Load axe-core from CDN
        await page.add_script_tag(url="https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.8.2/axe.min.js")
        
        # Wait for axe to be available
        await page.wait_for_function("typeof axe !== 'undefined'")

    async def _run_axe_scan(self, page: PageWithRetry):
        """Run axe accessibility scan on the current page"""
        try:
            # Configure axe to run WCAG 2.1 AA rules
            axe_config = {
                'runOnly': {
                    'type': 'tag',
                    'values': ['wcag2a', 'wcag2aa', 'wcag21aa']
                }
            }
            
            # Run axe scan
            results = await page.evaluate("""
                (config) => {
                    return new Promise((resolve) => {
                        axe.run(config, (err, results) => {
                            if (err) {
                                resolve(null);
                            } else {
                                resolve(results);
                            }
                        });
                    });
                }
            """, axe_config)
            
            return results
            
        except Exception as e:
            print(f"Error running axe scan: {str(e)}")
            return None

    def _process_axe_violations(self, page_url, violations):
        """Process and log axe violations"""
        processed_violations = []
        
        for violation in violations:
            violation_data = {
                'page': page_url,
                'rule_id': violation.get('id', 'unknown'),
                'impact': violation.get('impact', 'unknown'),
                'description': violation.get('description', ''),
                'help': violation.get('help', ''),
                'help_url': violation.get('helpUrl', ''),
                'nodes_count': len(violation.get('nodes', [])),
                'tags': violation.get('tags', [])
            }
            
            processed_violations.append(violation_data)
            self.accessibility_violations.append(violation_data)
            
            # Log violation details
            impact = violation_data['impact'].upper()
            rule_id = violation_data['rule_id']
            description = violation_data['description']
            nodes_count = violation_data['nodes_count']
            
            print(f"AXE VIOLATION [{impact}]: {rule_id} - {description} ({nodes_count} elements) on {page_url}")
        
        return processed_violations

    @task(1)
    @pw
    async def run_comprehensive_axe_scan(self, page: PageWithRetry):
        """Run comprehensive axe scan with all rules"""
        async with event(self, "Comprehensive Axe Scan"):
            await page.goto('/')
            
            # Inject axe-core
            await self._inject_axe_core(page)
            
            # Run comprehensive scan with all rules
            comprehensive_results = await page.evaluate("""
                () => {
                    return new Promise((resolve) => {
                        // Run all axe rules
                        axe.run((err, results) => {
                            if (err) {
                                resolve(null);
                            } else {
                                resolve({
                                    violations: results.violations.length,
                                    passes: results.passes.length,
                                    incomplete: results.incomplete.length,
                                    inapplicable: results.inapplicable.length,
                                    total_rules: results.violations.length + results.passes.length + results.incomplete.length + results.inapplicable.length
                                });
                            }
                        });
                    });
                }
            """)
            
            if comprehensive_results:
                print(f"COMPREHENSIVE AXE SCAN: {comprehensive_results['violations']} violations, "
                      f"{comprehensive_results['passes']} passes, "
                      f"{comprehensive_results['total_rules']} total rules checked")

    def on_stop(self):
        """Final axe accessibility report"""
        print("\n" + "="*50)
        print("AXE ACCESSIBILITY TEST COMPLETE")
        print("="*50)
        print(f"Pages tested: {len(self.discovered_pages) + 1}")
        print(f"Total axe violations: {len(self.accessibility_violations)}")
        
        if self.accessibility_violations:
            # Group violations by impact
            impact_counts = {}
            for violation in self.accessibility_violations:
                impact = violation['impact']
                impact_counts[impact] = impact_counts.get(impact, 0) + 1
            
            print(f"\nVIOLATIONS BY IMPACT:")
            for impact, count in sorted(impact_counts.items()):
                print(f"  {impact.upper()}: {count}")
            
            print(f"\nTOP VIOLATIONS:")
            for violation in self.accessibility_violations[:5]:
                print(f"❌ [{violation['impact'].upper()}] {violation['rule_id']}: {violation['description']}")
                print(f"   Page: {violation['page']} ({violation['nodes_count']} elements)")
        else:
            print("✅ No accessibility violations found!")

Comprehensive Axe-Core Site Scanning

from locust import task
from locust_plugins.users.playwright import PlaywrightUser, PageWithRetry, pw, event
import json

class ComprehensiveAxeSiteTester(PlaywrightUser):
    
    def on_start(self):
        """Initialize comprehensive axe site testing"""
        self.all_violations = []
        self.pages_scanned = set()
        self.pages_to_scan = []
        self.scan_summary = {
            'total_pages': 0,
            'total_violations': 0,
            'critical_violations': 0,
            'serious_violations': 0,
            'moderate_violations': 0,
            'minor_violations': 0
        }
        
    @task(3)
    @pw
    async def comprehensive_site_scan(self, page: PageWithRetry):
        """Comprehensive axe scan across entire site"""
        if not self.pages_to_scan:
            await self._discover_all_pages(page)
        
        if self.pages_to_scan:
            page_url = self.pages_to_scan.pop(0)
            if page_url not in self.pages_scanned:
                await self._comprehensive_axe_test(page, page_url)
                self.pages_scanned.add(page_url)

    async def _discover_all_pages(self, page: PageWithRetry):
        """Discover all internal pages for comprehensive testing"""
        async with event(self, "Site Discovery"):
            await page.goto('/')
            
            # Get all internal links
            links = await page.locator('a[href]').all()
            
            for link in links[:50]:  # Limit to 50 pages for comprehensive testing
                try:
                    href = await link.get_attribute('href')
                    if href and self._is_internal_page(href, page.url):
                        normalized_url = self._normalize_url(href, page.url)
                        if normalized_url and normalized_url not in self.pages_to_scan:
                            self.pages_to_scan.append(normalized_url)
                except:
                    pass
            
            print(f"Discovered {len(self.pages_to_scan)} pages for comprehensive axe scanning")

    def _is_internal_page(self, href, base_url):
        """Check if URL is internal page"""
        if not href:
            return False
            
        # Skip anchors, external protocols, and resources
        skip_patterns = ['#', 'mailto:', 'tel:', 'javascript:', '.pdf', '.jpg', '.jpeg', '.png', '.gif', '.css', '.js', '.zip', '.doc', '.xml']
        if any(pattern in href.lower() for pattern in skip_patterns):
            return False
            
        # Check if internal
        if href.startswith('/'):
            return True
        elif href.startswith('http'):
            base_domain = base_url.split('/')[2]
            return base_domain in href
        else:
            return True  # Relative links

    def _normalize_url(self, href, base_url):
        """Normalize URL for consistency"""
        if href.startswith('/'):
            return href
        elif href.startswith('http'):
            base_domain = base_url.split('/')[2]
            if base_domain in href:
                return '/' + href.split(base_domain, 1)[1].lstrip('/')
        else:
            # Relative URL
            return '/' + href.lstrip('./')
        return None

    async def _comprehensive_axe_test(self, page: PageWithRetry, page_url: str):
        """Comprehensive axe test on a single page"""
        async with event(self, f"COMPREHENSIVE AXE: {page_url}"):
            await page.goto(page_url)
            
            # Inject axe-core
            await self._inject_axe_core(page)
            
            # Run comprehensive axe scan with all rule sets
            axe_results = await self._run_comprehensive_axe_scan(page)
            
            if axe_results:
                self._process_comprehensive_results(page_url, axe_results)
                
                violations = axe_results.get('violations', [])
                if violations:
                    critical = len([v for v in violations if v.get('impact') == 'critical'])
                    serious = len([v for v in violations if v.get('impact') == 'serious'])
                    
                    failure_msg = f"❌ {len(violations)} axe violations ({critical} critical, {serious} serious)"
                    raise Exception(failure_msg)
                else:
                    print(f"✅ {page_url} - No violations found")

    async def _inject_axe_core(self, page: PageWithRetry):
        """Inject axe-core library"""
        await page.add_script_tag(url="https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.8.2/axe.min.js")
        await page.wait_for_function("typeof axe !== 'undefined'")

    async def _run_comprehensive_axe_scan(self, page: PageWithRetry):
        """Run comprehensive axe scan with all rules"""
        try:
            # Configure comprehensive scan
            axe_config = {
                'runOnly': {
                    'type': 'tag',
                    'values': ['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa', 'best-practice', 'experimental']
                }
            }
            
            results = await page.evaluate("""
                (config) => {
                    return new Promise((resolve) => {
                        axe.run(config, (err, results) => {
                            if (err) {
                                resolve(null);
                            } else {
                                resolve(results);
                            }
                        });
                    });
                }
            """, axe_config)
            
            return results
            
        except Exception as e:
            print(f"Error running comprehensive axe scan: {str(e)}")
            return None

    def _process_comprehensive_results(self, page_url, results):
        """Process comprehensive axe results"""
        violations = results.get('violations', [])
        
        self.scan_summary['total_pages'] += 1
        self.scan_summary['total_violations'] += len(violations)
        
        for violation in violations:
            impact = violation.get('impact', 'unknown')
            
            # Count by impact
            if impact == 'critical':
                self.scan_summary['critical_violations'] += 1
            elif impact == 'serious':
                self.scan_summary['serious_violations'] += 1
            elif impact == 'moderate':
                self.scan_summary['moderate_violations'] += 1
            elif impact == 'minor':
                self.scan_summary['minor_violations'] += 1
            
            # Store detailed violation
            violation_detail = {
                'page': page_url,
                'rule_id': violation.get('id'),
                'impact': impact,
                'description': violation.get('description'),
                'help': violation.get('help'),
                'help_url': violation.get('helpUrl'),
                'tags': violation.get('tags', []),
                'nodes': len(violation.get('nodes', []))
            }
            
            self.all_violations.append(violation_detail)
            
            # Log violation
            print(f"AXE [{impact.upper()}]: {violation_detail['rule_id']} - {violation_detail['description']} "
                  f"({violation_detail['nodes']} elements) on {page_url}")

    @task(1)
    @pw
    async def generate_accessibility_audit_report(self, page: PageWithRetry):
        """Generate comprehensive accessibility audit report"""
        if self.scan_summary['total_pages'] < 3:
            return
            
        async with event(self, "Accessibility Audit Report"):
            # Calculate compliance score
            total_possible_score = self.scan_summary['total_pages'] * 100
            penalty_score = (
                self.scan_summary['critical_violations'] * 10 +
                self.scan_summary['serious_violations'] * 5 +
                self.scan_summary['moderate_violations'] * 2 +
                self.scan_summary['minor_violations'] * 1
            )
            
            compliance_score = max(0, 100 - (penalty_score / total_possible_score * 100))
            
            print(f"\nACCESSIBILITY AUDIT REPORT:")
            print(f"Pages scanned: {self.scan_summary['total_pages']}")
            print(f"Total violations: {self.scan_summary['total_violations']}")
            print(f"Critical: {self.scan_summary['critical_violations']}")
            print(f"Serious: {self.scan_summary['serious_violations']}")
            print(f"Moderate: {self.scan_summary['moderate_violations']}")
            print(f"Minor: {self.scan_summary['minor_violations']}")
            print(f"Compliance Score: {compliance_score:.1f}/100")

    def on_stop(self):
        """Final comprehensive report"""
        print("\n" + "="*60)
        print("COMPREHENSIVE AXE ACCESSIBILITY AUDIT COMPLETE")
        print("="*60)
        print(f"Total pages scanned: {len(self.pages_scanned)}")
        print(f"Total violations found: {len(self.all_violations)}")
        
        if self.all_violations:
            # Top violation types
            rule_counts = {}
            for violation in self.all_violations:
                rule_id = violation['rule_id']
                rule_counts[rule_id] = rule_counts.get(rule_id, 0) + 1
            
            print("\nTOP VIOLATION TYPES:")
            sorted_rules = sorted(rule_counts.items(), key=lambda x: x[1], reverse=True)
            for rule_id, count in sorted_rules[:10]:
                print(f"  {rule_id}: {count} violations")
                
            # Most problematic pages
            page_counts = {}
            for violation in self.all_violations:
                page = violation['page']
                page_counts[page] = page_counts.get(page, 0) + 1
            
            print("\nMOST PROBLEMATIC PAGES:")
            sorted_pages = sorted(page_counts.items(), key=lambda x: x[1], reverse=True)
            for page, count in sorted_pages[:5]:
                print(f"  {page}: {count} violations")
        else:
            print("✅ No accessibility violations found across entire site!")

Key Axe-Core Features

  1. Industry Standard: Uses axe-core, the same engine used by accessibility professionals
  2. Comprehensive WCAG Coverage: Tests WCAG 2.1 AA/AAA compliance automatically
  3. Detailed Violation Reports: Specific rule violations with remediation guidance
  4. Real Browser Testing: Accurate results from actual browser rendering
  5. Scalable Site Auditing: Can scan entire websites automatically
  6. LoadForge Integration: Violations show as failures in LoadForge reports
  7. Professional Reporting: Impact levels (critical, serious, moderate, minor)

This axe-core integration provides enterprise-grade accessibility testing that matches tools used by accessibility consultants and compliance teams.

Ready to run your test?
Start your first test within minutes.