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

326 lines 10.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.RuleEngine = void 0; const axe_core_1 = __importDefault(require("axe-core")); /** * Hybrid rule engine combining axe-core with custom WCAG 2.2 rules * Provides comprehensive accessibility testing with extensible rule system */ class RuleEngine { constructor(config) { this.customRules = new Map(); this.disabledRules = new Set(); this.config = { wcagLevel: config.wcagLevel ?? 'AA', includeAAA: config.includeAAA ?? false, includeARIA: config.includeARIA ?? true, customRules: config.customRules ?? [], disabledRules: config.disabledRules ?? [], axeCoreConfig: config.axeCoreConfig ?? { tags: ['wcag2a', 'wcag2aa'], rules: {}, }, }; this.initializeRules(); } /** * Initialize rule engine with axe-core and custom rules * @private */ initializeRules() { // Setup axe-core configuration this.setupAxeCore(); // Load custom rules this.loadCustomRules(); // Apply disabled rules this.config.disabledRules.forEach(ruleId => { this.disabledRules.add(ruleId); }); } /** * Configure axe-core based on WCAG level and preferences * @private */ setupAxeCore() { const tags = []; // Base WCAG 2.1/2.2 rules if (this.config.wcagLevel === 'A' || this.config.wcagLevel === 'AA' || this.config.wcagLevel === 'AAA') { tags.push('wcag2a'); } if (this.config.wcagLevel === 'AA' || this.config.wcagLevel === 'AAA') { tags.push('wcag2aa'); } if (this.config.wcagLevel === 'AAA' || this.config.includeAAA) { tags.push('wcag2aaa'); } // ARIA rules if (this.config.includeARIA) { tags.push('wcag2a', 'wcag2aa'); // ARIA is part of WCAG } // Additional rule categories tags.push('best-practice'); this.config.axeCoreConfig = { tags, rules: this.config.axeCoreConfig?.rules ?? {}, }; } /** * Load and validate custom WCAG rules * @private */ loadCustomRules() { this.config.customRules.forEach(rule => { if (this.validateRule(rule)) { this.customRules.set(rule.ruleId, rule); } }); } /** * Validate custom rule configuration * @private */ validateRule(rule) { if (!rule.ruleId || !rule.wcagCriterion || !rule.level) { console.warn(`Invalid rule configuration: ${rule.ruleId}`); return false; } if (!['A', 'AA', 'AAA'].includes(rule.level)) { console.warn(`Invalid WCAG level for rule ${rule.ruleId}: ${rule.level}`); return false; } return true; } /** * Execute accessibility scan on a page using hybrid rule engine * @param page - Playwright page instance * @returns Promise<RuleResult[]> */ async executeRules(page) { const results = []; try { // Execute axe-core rules const axeResults = await this.executeAxeCore(page); results.push(...axeResults); // Execute custom WCAG rules const customResults = await this.executeCustomRules(page); results.push(...customResults); // Filter disabled rules return results.filter(result => !this.disabledRules.has(result.ruleId)); } catch (error) { console.error('Error executing accessibility rules:', error); throw new Error(`Rule execution failed: ${error instanceof Error ? error.message : String(error)}`); } } /** * Execute axe-core accessibility rules * @private */ async executeAxeCore(page) { try { // Inject axe-core into the page await page.addScriptTag({ content: axe_core_1.default.source, }); // Configure and run axe const config = this.config.axeCoreConfig; if (!config) { throw new Error('axeCoreConfig is not defined'); } // Ensure rules is always an object if (!config.rules || Array.isArray(config.rules)) { config.rules = {}; } const axeResults = await page.evaluate((configParam) => { return new Promise((resolve, reject) => { try { const win = globalThis; if (typeof win.axe === 'undefined') { reject(new Error('axe-core not loaded')); return; } win.axe.configure({ tags: configParam.tags, rules: configParam.rules, }); win.axe.run((err, results) => { if (err) { reject(err); } else { resolve(results); } }); } catch (error) { reject(error); } }); }, config); return this.transformAxeResults(axeResults); } catch (error) { console.error('Axe-core execution failed:', error); return []; } } /** * Transform axe-core results to standard RuleResult format * @private */ transformAxeResults(axeResults) { const results = []; // Process violations axeResults.violations?.forEach((violation) => { const ruleResult = { ruleId: violation.id, wcagReference: this.extractWcagReference(violation.tags), level: this.determineWcagLevel(violation.tags), impact: violation.impact || 'moderate', nodes: this.transformAxeNodes(violation.nodes), description: violation.description, help: violation.help, helpUrl: violation.helpUrl, }; results.push(ruleResult); }); return results; } /** * Execute custom WCAG 2.2 rules * @private */ async executeCustomRules(page) { const results = []; for (const [ruleId, rule] of this.customRules) { if (this.disabledRules.has(ruleId)) { continue; } try { const ruleResult = await this.executeCustomRule(page, rule); if (ruleResult) { results.push(ruleResult); } } catch (error) { console.warn(`Custom rule ${ruleId} execution failed:`, error); } } return results; } /** * Execute a single custom rule * @private */ async executeCustomRule(page, rule) { try { // Placeholder for custom rule execution // In a real implementation, this would evaluate the custom rule logic // For now, return null (no violations) as this is a framework setup // Future implementation would: // 1. Parse the rule.evaluate function string // 2. Execute it in the page context with proper error handling // 3. Transform results to RuleResult format return null; } catch (error) { console.warn(`Error executing custom rule ${rule.ruleId}:`, error); return null; } } /** * Extract WCAG reference from axe tags * @private */ extractWcagReference(tags) { const wcagTag = tags.find(tag => tag.match(/^wcag\d{3}$/)); if (wcagTag) { const numbers = wcagTag.replace('wcag', ''); return `${numbers[0]}.${numbers[1]}.${numbers[2]}`; } return ''; } /** * Determine WCAG level from tags * @private */ determineWcagLevel(tags) { if (tags.includes('wcag2aaa')) return 'AAA'; if (tags.includes('wcag2aa')) return 'AA'; if (tags.includes('wcag2a')) return 'A'; return 'AA'; // Default } /** * Transform axe node information * @private */ transformAxeNodes(nodes) { return nodes.map(node => ({ target: node.target, html: node.html, any: node.any || [], all: node.all || [], none: node.none || [], failureSummary: node.failureSummary, })); } /** * Add a custom rule to the engine */ addCustomRule(rule) { if (this.validateRule(rule)) { this.customRules.set(rule.ruleId, rule); } } /** * Remove a custom rule from the engine */ removeCustomRule(ruleId) { this.customRules.delete(ruleId); } /** * Enable a previously disabled rule */ enableRule(ruleId) { this.disabledRules.delete(ruleId); } /** * Disable a rule from execution */ disableRule(ruleId) { this.disabledRules.add(ruleId); } /** * Get current rule configuration */ getConfiguration() { return { ...this.config }; } /** * Get list of available custom rules */ getCustomRules() { return Array.from(this.customRules.values()); } /** * Get list of disabled rules */ getDisabledRules() { return Array.from(this.disabledRules); } /** * Update engine configuration */ updateConfiguration(newConfig) { this.config = { ...this.config, ...newConfig }; this.initializeRules(); } } exports.RuleEngine = RuleEngine; //# sourceMappingURL=rule-engine.js.map