UNPKG

@casoon/auditmysite

Version:

Professional website analysis suite with robust accessibility testing, Core Web Vitals performance monitoring, SEO analysis, and content optimization insights. Features isolated browser contexts, retry mechanisms, and comprehensive API endpoints for profe

449 lines 18.5 kB
"use strict"; /** * AccessibilityChecker v2 - Clean Architecture * * This is a complete rewrite of the AccessibilityChecker with proper * separation of concerns, dependency injection, and typed interfaces. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AccessibilityTestError = exports.AccessibilityChecker = void 0; const pa11y_1 = __importDefault(require("pa11y")); const queue_1 = require("../queue"); const analyzer_factory_1 = require("../analyzers/analyzer-factory"); const analysis_orchestrator_1 = require("../analysis/analysis-orchestrator"); const structured_logger_1 = require("../logging/structured-logger"); /** * AccessibilityChecker v2 - Clean, focused implementation * * Responsibilities: * - Core accessibility testing via pa11y * - Basic page analysis (images, buttons, headings) * - Orchestration of comprehensive analysis (via AnalysisOrchestrator) * - Browser pool management coordination */ class AccessibilityChecker { constructor(config) { // Validate required dependencies if (!config.poolManager) { throw new Error('BrowserPoolManager is required'); } // Set up configuration with defaults this.config = { poolManager: config.poolManager, logger: config.logger || (0, structured_logger_1.createLogger)('accessibility-checker'), enableComprehensiveAnalysis: config.enableComprehensiveAnalysis || false, analyzerTypes: config.analyzerTypes || [], qualityAnalysisOptions: config.qualityAnalysisOptions || {} }; this.logger = this.config.logger; // Initialize analyzer factory if comprehensive analysis is enabled if (this.config.enableComprehensiveAnalysis) { const factoryConfig = { logger: this.logger.child ? this.logger.child('factory') : this.logger, qualityAnalysisOptions: this.config.qualityAnalysisOptions, enabledAnalyzers: this.config.analyzerTypes.length > 0 ? this.config.analyzerTypes : undefined }; this.analyzerFactory = new analyzer_factory_1.AnalyzerFactory(factoryConfig); // Initialize analysis orchestrator const orchestratorConfig = { analyzerFactory: this.analyzerFactory, logger: this.logger.child ? this.logger.child('orchestrator') : this.logger, defaultTimeout: 30000, failFast: false }; this.analysisOrchestrator = new analysis_orchestrator_1.AnalysisOrchestrator(orchestratorConfig); } } /** * Initialize the accessibility checker */ async initialize() { this.logger.info('AccessibilityChecker initialized with browser pooling'); } /** * Cleanup resources */ async cleanup() { if (this.analyzerFactory) { await this.analyzerFactory.cleanup(); } this.logger.info('AccessibilityChecker cleaned up'); } /** * Test a single page for accessibility */ async testPage(url, options = {}) { const startTime = Date.now(); const logger = this.logger.child ? this.logger.child('test-page') : this.logger; logger.info(`Testing page: ${url}`); const { browser, context, release } = await this.config.poolManager.acquire(); try { const page = await context.newPage(); try { // Navigate to page await this.configurePage(page, options); const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: options.timeout || 30000 }); // Check for errors and redirects if (!response || response.status() >= 400) { throw new Error(`HTTP ${response?.status() || 'unknown'} error`); } // Run basic accessibility analysis const accessibilityResult = await this.runBasicAccessibilityAnalysis(page, url, options); // Run comprehensive analysis if enabled let comprehensiveAnalysis; if ((options.enableComprehensiveAnalysis ?? this.config.enableComprehensiveAnalysis) && this.analysisOrchestrator) { comprehensiveAnalysis = await this.analysisOrchestrator.runComprehensiveAnalysis(page, url, { timeout: options.timeout }); } const duration = Date.now() - startTime; const result = { url, title: accessibilityResult.title, accessibilityResult, comprehensiveAnalysis, duration, timestamp: new Date() }; logger.info(`Page testing completed`, { url, duration, passed: accessibilityResult.passed }); return result; } finally { await page.close(); } } finally { await release(); } } /** * Test multiple pages in parallel with redirect filtering */ async testMultiplePages(urls, options = {}) { const startTime = Date.now(); const logger = this.logger.child ? this.logger.child('multi-test') : this.logger; const skipRedirects = options.skipRedirects !== false; logger.info(`Testing ${urls.length} pages`, { concurrency: options.maxConcurrent || 3, comprehensive: options.enableComprehensiveAnalysis ?? this.config.enableComprehensiveAnalysis, skipRedirects }); const results = []; const skippedUrls = []; // Configure queue callbacks const callbacks = { onProgressUpdate: (stats) => { if (stats.progress > 0 && stats.progress % 25 === 0) { logger.info(`Progress: ${stats.progress.toFixed(1)}%`, { completed: stats.completed, total: stats.total, workers: stats.activeWorkers }); } }, onItemCompleted: (item, result) => { logger.debug(`Completed: ${item.data}`, { duration: item.duration }); }, onItemFailed: (item, error) => { logger.warn(`Failed: ${item.data}`, { error, attempts: item.attempts }); }, onQueueEmpty: () => { logger.info('All page tests completed'); } }; // Create optimized queue for accessibility testing const queue = queue_1.Queue.forAccessibilityTesting('parallel', { maxConcurrent: options.maxConcurrent || 3, maxRetries: 3, retryDelay: 2000, timeout: options.timeout || 30000, enableProgressReporting: true, progressUpdateInterval: 3000 }, callbacks); try { // Process URLs with queue and handle redirects const result = await queue.processWithProgress(urls, async (url) => { // First check if URL redirects (if redirect skipping is enabled) if (skipRedirects) { const minimalResult = await this.testUrlMinimal(url, 5000); if (minimalResult.skipped && minimalResult.errors.some(e => e.includes('Redirect'))) { skippedUrls.push(url); logger.debug(`Skipped redirect: ${url}`); return null; // Skip this URL } } return await this.testPage(url, options); }, { showProgress: !options.verbose, progressInterval: 3000 }); // Extract successful results result.completed.forEach(item => { if (item.result) { results.push(item.result); } }); // Add failed items as error results result.failed.forEach(failedItem => { results.push({ url: failedItem.data, title: 'Error', accessibilityResult: { url: failedItem.data, title: 'Error', imagesWithoutAlt: 0, buttonsWithoutLabel: 0, headingsCount: 0, errors: [`Test failed: ${failedItem.error}`], warnings: [], passed: false, crashed: true, duration: failedItem.duration || 0 }, duration: failedItem.duration || 0, timestamp: new Date() }); }); const totalDuration = Date.now() - startTime; logger.info(`Multi-page testing completed`, { total: urls.length, tested: results.length, skipped: skippedUrls.length, failed: result.failed.length, totalDuration }); return { results, skippedUrls, totalDuration, timestamp: new Date() }; } catch (error) { logger.error('Multi-page testing failed', error); throw error; } } /** * Test URL for basic connectivity (used by SmartUrlSampler) */ async testUrlMinimal(url, timeout = 5000) { const startTime = Date.now(); const logger = this.logger.child ? this.logger.child('minimal-test') : this.logger; logger.debug(`Minimal test: ${url}`); const { browser, context, release } = await this.config.poolManager.acquire(); try { const page = await context.newPage(); try { page.setDefaultTimeout(timeout); const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout }); const finalUrl = page.url(); const title = await page.title(); const status = response?.status() || 0; // Only treat 3xx status codes as redirects, not URL changes const isHttpRedirect = status >= 300 && status < 400; const urlChanged = finalUrl !== url; const result = { url, title: title || 'Untitled', imagesWithoutAlt: 0, buttonsWithoutLabel: 0, headingsCount: 0, errors: [], warnings: [], passed: status >= 200 && status < 300, // Only 2xx are successful crashed: false, skipped: isHttpRedirect || status === 404, duration: Date.now() - startTime }; // Add status-specific errors if (status === 404) { result.errors.push('HTTP 404 Not Found'); } else if (status >= 300 && status < 400) { result.errors.push(`HTTP ${status} Redirect`); } else if (status >= 400) { result.errors.push(`HTTP ${status} Error`); } // Add redirect info if needed if (isHttpRedirect) { result.redirectInfo = { status, originalUrl: url, finalUrl, type: 'http_redirect' }; } return result; } finally { await page.close(); } } catch (error) { logger.debug(`Minimal test failed: ${url}`, error); return { url, title: 'Error', imagesWithoutAlt: 0, buttonsWithoutLabel: 0, headingsCount: 0, errors: [`Navigation failed: ${error}`], warnings: [], passed: false, crashed: true, duration: Date.now() - startTime }; } finally { await release(); } } /** * Get available analysis types */ getAvailableAnalysisTypes() { if (!this.analysisOrchestrator) { return []; } return this.analysisOrchestrator.getAvailableAnalyzers(); } // Private helper methods async configurePage(page, options) { // Set viewport await page.setViewportSize({ width: 1920, height: 1080 }); // Set user agent await page.setExtraHTTPHeaders({ 'User-Agent': 'auditmysite/2.0.0 (+https://github.com/casoon/AuditMySite)' }); // Configure console/error logging based on verbose setting if (options.verbose) { page.on('console', msg => this.logger.debug(`Browser: ${msg.text()}`)); page.on('pageerror', error => this.logger.warn(`JS Error: ${error.message}`)); } } async runBasicAccessibilityAnalysis(page, url, options) { const startTime = Date.now(); const result = { url, title: await page.title(), imagesWithoutAlt: 0, buttonsWithoutLabel: 0, headingsCount: 0, errors: [], warnings: [], passed: true, duration: 0 }; // Basic element checks result.imagesWithoutAlt = await page.locator('img:not([alt])').count(); if (result.imagesWithoutAlt > 0) { result.warnings.push(`${result.imagesWithoutAlt} images without alt attribute`); } result.buttonsWithoutLabel = await page .locator('button:not([aria-label])') .filter({ hasText: '' }) .count(); if (result.buttonsWithoutLabel > 0) { result.warnings.push(`${result.buttonsWithoutLabel} buttons without aria-label`); } result.headingsCount = await page.locator('h1, h2, h3, h4, h5, h6').count(); if (result.headingsCount === 0) { result.errors.push('No headings found'); } // Run pa11y tests await this.runPa11yTests(result, options, page); result.duration = Date.now() - startTime; result.passed = result.errors.length === 0; return result; } async runPa11yTests(result, options, page) { try { this.logger.debug('Running pa11y accessibility tests'); const pa11yResult = await (0, pa11y_1.default)(result.url, { timeout: options.timeout || 15000, wait: options.wait || 1000, standard: options.pa11yStandard || 'WCAG2AA', hideElements: options.hideElements || 'iframe[src*="google-analytics"], iframe[src*="doubleclick"]', includeNotices: options.includeNotices !== false, includeWarnings: options.includeWarnings !== false, runners: ['axe'] // Always use axe for pooled browsers }); // Process pa11y issues if (pa11yResult.issues) { pa11yResult.issues.forEach((issue) => { const detailedIssue = { code: issue.code, message: issue.message, type: issue.type, selector: issue.selector, context: issue.context, impact: issue.impact, help: issue.help, helpUrl: issue.helpUrl }; result.pa11yIssues = result.pa11yIssues || []; result.pa11yIssues.push(detailedIssue); // Add to appropriate array const message = `${issue.code}: ${issue.message}`; if (issue.type === 'error') { result.errors.push(message); } else if (issue.type === 'warning') { result.warnings.push(message); } else if (issue.type === 'notice') { result.warnings.push(`Notice: ${message}`); } }); // Calculate pa11y score const totalIssues = pa11yResult.issues.length; const errors = pa11yResult.issues.filter((i) => i.type === 'error').length; const warnings = pa11yResult.issues.filter((i) => i.type === 'warning').length; let score = 100; score -= errors * 10; // 10 points per error score -= warnings * 2; // 2 points per warning result.pa11yScore = Math.max(0, score); } else { result.pa11yScore = 100; } } catch (error) { this.logger.warn('pa11y test failed, using fallback scoring', error); // Fallback score calculation let score = 100; score -= result.errors.length * 15; score -= result.warnings.length * 5; score -= result.imagesWithoutAlt * 3; score -= result.buttonsWithoutLabel * 5; if (result.headingsCount === 0) score -= 20; result.pa11yScore = Math.max(0, score); } } } exports.AccessibilityChecker = AccessibilityChecker; /** * Error classes */ class AccessibilityTestError extends Error { constructor(url, message) { super(`Accessibility test failed for ${url}: ${message}`); this.name = 'AccessibilityTestError'; } } exports.AccessibilityTestError = AccessibilityTestError; //# sourceMappingURL=accessibility-checker-v2.js.map