Modern QA2026Automated AI Accessibility Crawl — tiles
Log inJoin
55 / 74 · 10 Visual & Accessibility Testing · AI Agents for Accessibility Audits← prev⊞ allnext →☰ Read as one page

10.4Automated AI Accessibility Crawl

# scripts/ai_accessibility_audit.py
import asyncio
from playwright.async_api import async_playwright

async def crawl_and_audit(base_url: str, max_pages: int = 50):
    """Crawl an application and run accessibility audits on each page."""
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()

        visited = set()
        to_visit = [base_url]
        all_results = []

        while to_visit and len(visited) < max_pages:
            url = to_visit.pop(0)
            if url in visited:
                continue
            visited.add(url)

            try:
                await page.goto(url, timeout=10000)
                await page.wait_for_load_state('networkidle')

                # 1. Run axe-core for rule-based checks
                axe_results = await page.evaluate("""
                    async () => {
                        await new Promise(r => {
                            const s = document.createElement('script');
                            s.src = 'https://cdn.jsdelivr.net/npm/axe-core@4/axe.min.js';
                            s.onload = r;
                            document.head.appendChild(s);
                        });
                        return await axe.run();
                    }
                """)

                # 2. Get accessibility tree for AI analysis
                a11y_tree = await page.accessibility.snapshot()

                # 3. Take screenshot for AI vision analysis
                screenshot = await page.screenshot(full_page=True)

                # 4. Collect links for crawling
                links = await page.evaluate(f"""
                    () => Array.from(document.querySelectorAll('a[href]'))
                        .map(a => a.href)
                        .filter(href => href.startsWith('{base_url}'))
                """)
                to_visit.extend([l for l in links if l not in visited])

                all_results.append({
                    'url': url,
                    'axe_violations': axe_results.get('violations', []),
                    'a11y_tree': a11y_tree,
                    'screenshot': screenshot,
                })

            except Exception as e:
                all_results.append({
                    'url': url,
                    'error': str(e),
                })

        await browser.close()
        return all_results

async def generate_ai_report(results, ai_client):
    """Send collected data to an AI model for qualitative analysis."""
    for result in results:
        if 'error' in result:
            continue

        # Combine axe-core findings with accessibility tree
        context = {
            'url': result['url'],
            'automated_violations': [
                {'rule': v['id'], 'impact': v['impact'], 'count': len(v['nodes'])}
                for v in result['axe_violations']
            ],
            'accessibility_tree_summary': summarize_tree(result['a11y_tree']),
        }

        # Send to AI for qualitative review
        response = ai_client.messages.create(
            model="claude-opus-4-8",  # Claude Opus 4.8, current as of July 2026 -- pin your provider's latest id
            max_tokens=2000,
            messages=[{
                "role": "user",
                "content": [
                    {
                        "type": "image",
                        "source": {
                            "type": "base64",
                            "media_type": "image/png",
                            "data": base64.b64encode(result['screenshot']).decode(),
                        },
                    },
                    {
                        "type": "text",
                        "text": f"""Review this page for accessibility issues.
                        URL: {context['url']}
                        Automated findings: {context['automated_violations']}
                        Accessibility tree: {context['accessibility_tree_summary']}

                        Focus on qualitative issues that axe-core cannot detect.""",
                    },
                ],
            }],
        )

        result['ai_review'] = response.content[0].text

def summarize_tree(tree, depth=0, max_depth=3):
    """Summarize an accessibility tree for AI consumption."""
    if not tree or depth > max_depth:
        return ""
    summary = f"{'  ' * depth}{tree.get('role', 'unknown')}"
    if tree.get('name'):
        summary += f": {tree['name']}"
    children = tree.get('children', [])
    child_summaries = [summarize_tree(c, depth + 1, max_depth) for c in children]
    return summary + '\n' + '\n'.join(filter(None, child_summaries))