UNPKG

a11yanalyze

Version:

A command-line tool for developers and QA engineers to test web pages and websites for WCAG 2.2 AA accessibility compliance

162 lines (155 loc) 6.26 kB
import { readdirSync, statSync, writeFileSync, mkdirSync, existsSync, readFileSync } from 'fs'; import { join, extname, basename } from 'path'; import yaml from 'js-yaml'; import { PageScanner } from '../scanner/page-scanner'; import { VpatReporter } from '../output/vpat-reporter'; import { ScanResult } from '../types'; import Handlebars from 'handlebars'; import { simulateKeyboardNavigation, simulateScreenReader } from '../scanner/page-scanner'; export interface HtmlBatchOptions { inputDir?: string; inputList?: string; inputConfig?: string; outputDir: string; format: 'json' | 'markdown' | 'both'; wcagLevel?: string; timeout?: number; template?: string; keyboardNav?: boolean; screenReaderSim?: boolean; } export class HtmlBatchRunner { async run(options: HtmlBatchOptions): Promise<void> { let targets: { name: string; path: string; isUrl: boolean; meta?: any }[] = []; if (options.inputDir) { targets = this.findHtmlFiles(options.inputDir).map(f => ({ name: basename(f, '.html'), path: f, isUrl: false })); } if (options.inputList) { const urls = readFileSync(options.inputList, 'utf8').split(/\r?\n/).filter(Boolean); targets = targets.concat(urls.map(url => ({ name: this.sanitizeName(url), path: url, isUrl: true }))); } if (options.inputConfig) { const config = this.loadConfigFile(options.inputConfig); if (Array.isArray(config)) { targets = targets.concat(config.map((entry: any) => ({ name: entry.name || this.sanitizeName(entry.url || entry.file), path: entry.url || entry.file, isUrl: !!entry.url, meta: entry }))); } } if (!targets.length) throw new Error('No HTML files, URLs, or config entries found.'); if (!existsSync(options.outputDir)) mkdirSync(options.outputDir, { recursive: true }); let customTemplate: string | undefined; if (options.template) { customTemplate = readFileSync(options.template, 'utf8'); } const summary: any[] = []; const errors: any[] = []; for (const target of targets) { try { const pageScanner = new PageScanner( { headless: true, viewport: { width: 1280, height: 720 }, timeout: options.timeout || 30000, }, { wcagLevel: (options.wcagLevel as any) || 'AA', includeAAA: false, includeARIA: true, customRules: [], disabledRules: [], } ); await pageScanner.initialize(); const scanOptions = { wcagLevel: (options.wcagLevel as any) || 'AA', includeAAA: false, includeARIA: true, customRules: [], disabledRules: [], }; // Run the scan and get results const scanResult = await pageScanner.scan(target.path, scanOptions); // Use keyboardNavigation and screenReaderSimulation from scanResult // (No need to call simulateKeyboardNavigation or simulateScreenReader here) const vpatReport = VpatReporter.generateJsonReport(target.name, scanResult); const baseName = this.sanitizeName(target.name); if (options.format === 'json' || options.format === 'both') { const jsonPath = join(options.outputDir, `${baseName}.vpat.json`); if (customTemplate && options.template?.endsWith('.json')) { writeFileSync(jsonPath, VpatReporter.generateCustomReport(vpatReport, customTemplate), 'utf8'); } else { writeFileSync(jsonPath, JSON.stringify(vpatReport, null, 2), 'utf8'); } } if (options.format === 'markdown' || options.format === 'both') { const mdPath = join(options.outputDir, `${baseName}.vpat.md`); if (customTemplate && options.template?.endsWith('.md')) { writeFileSync(mdPath, VpatReporter.generateCustomReport(vpatReport, customTemplate), 'utf8'); } else { const md = VpatReporter.generateMarkdownReport(vpatReport); writeFileSync(mdPath, md, 'utf8'); } } summary.push({ name: target.name, path: target.path, meta: target.meta, status: 'success', supports: vpatReport.summary.supports, partiallySupports: vpatReport.summary.partiallySupports, doesNotSupport: vpatReport.summary.doesNotSupport, notApplicable: vpatReport.summary.notApplicable, notEvaluated: vpatReport.summary.notEvaluated, }); } catch (error) { errors.push({ name: target.name, path: target.path, meta: target.meta, error: error instanceof Error ? error.message : String(error), }); summary.push({ name: target.name, path: target.path, meta: target.meta, status: 'error', error: error instanceof Error ? error.message : String(error), }); console.warn(`Failed to scan ${target.name}: ${error instanceof Error ? error.message : String(error)}`); } } writeFileSync(join(options.outputDir, 'vpat-index.json'), JSON.stringify(summary, null, 2), 'utf8'); if (errors.length > 0) { writeFileSync(join(options.outputDir, 'vpat-errors.json'), JSON.stringify(errors, null, 2), 'utf8'); } } loadConfigFile(path: string): any { const content = readFileSync(path, 'utf8'); if (path.endsWith('.json')) { return JSON.parse(content); } else if (path.endsWith('.yaml') || path.endsWith('.yml')) { return yaml.load(content); } throw new Error('Unsupported config file format: ' + path); } findHtmlFiles(dir: string): string[] { let results: string[] = []; for (const entry of readdirSync(dir)) { const fullPath = join(dir, entry); const stat = statSync(fullPath); if (stat.isDirectory()) { results = results.concat(this.findHtmlFiles(fullPath)); } else if (stat.isFile() && extname(fullPath).toLowerCase() === '.html') { results.push(fullPath); } } return results; } sanitizeName(name: string): string { return name.replace(/[^a-zA-Z0-9_-]/g, '_'); } }