MCP Accessibility Scanner

by JustasMonkev
Verified
import {chromium} from 'playwright'; import {AxeBuilder} from '@axe-core/playwright'; import path from "node:path"; import * as os from "node:os"; export async function scanViolations(url: string, violationsTag: string[]) { const browser = await chromium.launch({ headless: true, args: [ '--disable-blink-features=AutomationControlled', '--disable-dev-shm-usage', '--no-sandbox', '--disable-setuid-sandbox', ] }); const context = await browser.newContext({ viewport: {width: 1920, height: 1080}, userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' }); const page = await context.newPage(); await page.goto(url); await page.addStyleTag({ content: ` .a11y-violation { position: relative !important; outline: 4px solid #FF4444 !important; margin: 2px !important; } .violation-number { position: absolute !important; top: -12px !important; left: -12px !important; background: #FF4444; color: white !important; width: 25px; height: 25px; border-radius: 50%; display: flex !important; align-items: center; justify-content: center; font-weight: bold; font-size: 14px; z-index: 10000; } .a11y-violation-info { position: absolute !important; background: #333333 !important; color: white !important; padding: 12px !important; border-radius: 4px !important; font-size: 14px !important; max-width: 300px !important; z-index: 9999 !important; box-shadow: 0 2px 10px rgba(0,0,0,0.3); } ` }); const axe = new AxeBuilder({page}) .withTags(violationsTag); const results = await axe.analyze(); let violationCounter = 1; for (const violation of results.violations) { for (const node of violation.nodes) { try { const targetSelector = node.target[0]; const selector = Array.isArray(targetSelector) ? targetSelector.join(' ') : targetSelector; await page.evaluate(({selector, violationData, counter}) => { const elements = document.querySelectorAll(selector); elements.forEach(element => { // Create number badge directly on the element const numberBadge = document.createElement('div'); numberBadge.className = 'violation-number'; numberBadge.textContent = counter.toString(); // Add violation styling element.classList.add('a11y-violation'); element.appendChild(numberBadge); // Create info box const listItem = document.createElement('div'); listItem.style.marginBottom = '15px'; listItem.innerHTML = ` <div style="color: #FF4444; font-weight: bold;"> Violation #${counter}: ${violationData.impact!.toUpperCase()} </div> <div style="margin: 5px 0; font-size: 14px;"> ${violationData.description} </div> `; // Position info box relative to element document.body.appendChild(listItem); const rect = element.getBoundingClientRect(); listItem.style.left = `${rect.left + window.scrollX}px`; listItem.style.top = `${rect.bottom + window.scrollY + 10}px`; }); }, { selector: selector, violationData: { impact: violation.impact, description: violation.description }, counter: violationCounter }); violationCounter++; } catch (error) { console.log(`Failed to highlight element: ${error}`); } } } let reportCounter = 1; const report = []; for (const violation of results.violations) { for (const node of violation.nodes) { report.push({ index: reportCounter++, element: node.target[0], impactLevel: violation.impact, description: violation.description, wcagCriteria: violation.tags?.join(', '), } satisfies accessibilityResult); } } const downloadsDir = path.join(os.homedir(), 'Downloads'); const filePath = path.join(downloadsDir, `a11y-report-${Date.now()}.png`); const screenshot = await page.screenshot({ path: filePath, fullPage: true, }); const base64Screenshot = screenshot.toString('base64'); await browser.close(); return {report, base64Screenshot}; } type accessibilityResult = { index: number, element: any, impactLevel: any, description: string, wcagCriteria: string, }