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
JavaScript
"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