Skip to main content
Glama
axe-adapter.ts4.09 kB
import { chromium, type Browser } from 'playwright'; import { injectAxe, getViolations } from 'axe-playwright'; import type { AxeResult } from '../types/audit.js'; import { resolveUrl, type ResolvedUrl } from '../utils/url-resolver.js'; const DEFAULT_TIMEOUT = 30000; const DEFAULT_BROWSER = 'chromium'; export interface AxeAdapterOptions { timeout?: number; browser?: 'chromium' | 'firefox' | 'webkit'; tags?: string[]; rules?: Record<string, { enabled: boolean }>; } export class AxeAdapter { private browser: Browser | null = null; private readonly timeout: number; private readonly browserType: 'chromium' | 'firefox' | 'webkit'; constructor(options: AxeAdapterOptions = {}) { this.timeout = options.timeout ?? DEFAULT_TIMEOUT; this.browserType = options.browser ?? DEFAULT_BROWSER; } async audit(urlOrPath: string, options?: AxeAdapterOptions): Promise<AxeResult> { const resolved: ResolvedUrl = await resolveUrl(urlOrPath); const browser = await this.getBrowser(); const context = await browser.newContext(); const page = await context.newPage(); try { await page.goto(resolved.url, { waitUntil: 'networkidle', timeout: this.timeout }); await injectAxe(page); const axeOptions: { tags?: string[]; rules?: Record<string, { enabled: boolean }>; } = {}; if (options?.tags) { axeOptions.tags = options.tags; } if (options?.rules) { axeOptions.rules = options.rules; } const violations = await getViolations(page, axeOptions); const result: AxeResult = { violations: violations.map((v) => ({ id: v.id, impact: v.impact as 'critical' | 'serious' | 'moderate' | 'minor' | null, description: v.description, help: v.help, helpUrl: v.helpUrl, tags: v.tags, nodes: v.nodes.map((node) => ({ html: node.html, target: node.target, any: node.any.map((check) => ({ id: check.id, impact: check.impact as 'critical' | 'serious' | 'moderate' | 'minor' | null, message: check.message, data: check.data, relatedNodes: check.relatedNodes.map((rn) => ({ html: rn.html, target: rn.target, any: [], all: [], none: [], })), })), all: node.all.map((check) => ({ id: check.id, impact: check.impact as 'critical' | 'serious' | 'moderate' | 'minor' | null, message: check.message, data: check.data, relatedNodes: [], })), none: node.none.map((check) => ({ id: check.id, impact: check.impact as 'critical' | 'serious' | 'moderate' | 'minor' | null, message: check.message, data: check.data, relatedNodes: [], })), })), })), passes: [], incomplete: [], inapplicable: [], url: resolved.url, timestamp: new Date().toISOString(), }; return result; } finally { await page.close(); await context.close(); if (resolved.cleanup) { await resolved.cleanup(); } } } private async getBrowser(): Promise<Browser> { if (!this.browser) { switch (this.browserType) { case 'chromium': this.browser = await chromium.launch({ headless: true }); break; case 'firefox': const { firefox } = await import('playwright'); this.browser = await firefox.launch({ headless: true }); break; case 'webkit': const { webkit } = await import('playwright'); this.browser = await webkit.launch({ headless: true }); break; } } return this.browser; } async close(): Promise<void> { if (this.browser) { await this.browser.close(); this.browser = null; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Duds/accessibility-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server