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))