UNPKG

@casoon/auditmysite

Version:

A comprehensive command-line tool for automated accessibility, security, performance, and SEO testing using Playwright and pa11y, based on sitemap URLs

700 lines 32.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AccessibilityChecker = void 0; const pa11y_1 = __importDefault(require("pa11y")); const browser_manager_1 = require("./browser-manager"); const lighthouse_integration_1 = require("./lighthouse-integration"); const simple_queue_1 = require("./simple-queue"); const parallel_test_manager_1 = require("./parallel-test-manager"); const event_driven_queue_1 = require("./event-driven-queue"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); class AccessibilityChecker { constructor() { this.browserManager = null; this.lighthouseIntegration = null; this.testQueue = null; this.parallelTestManager = null; this.eventDrivenQueue = null; } async initialize() { // 🆕 Browser Manager für geteilten Browser this.browserManager = new browser_manager_1.BrowserManager({ headless: true, port: 9222 }); await this.browserManager.initialize(); // 🆕 Lighthouse Integration this.lighthouseIntegration = new lighthouse_integration_1.LighthouseIntegration(this.browserManager); } async cleanup() { if (this.browserManager) { await this.browserManager.cleanup(); } } async testPage(url, options = {}) { if (!this.browserManager) { throw new Error("Browser Manager not initialized"); } const startTime = Date.now(); const page = await this.browserManager.getPage(); const result = { url, title: "", imagesWithoutAlt: 0, buttonsWithoutLabel: 0, headingsCount: 0, errors: [], warnings: [], passed: true, duration: 0, }; try { if (options.verbose) console.log(` 🔧 Configuring page...`); // 🆕 Erweiterte Page-Konfiguration await this.configurePage(page, options); if (options.verbose) console.log(` 🌐 Navigating to page...`); await page.goto(url, { waitUntil: options.waitUntil || "domcontentloaded", timeout: options.timeout || 10000, }); // 🆕 Performance-Metriken sammeln if (options.collectPerformanceMetrics) { if (options.verbose) console.log(` 📊 Collecting performance metrics...`); await this.collectPerformanceMetrics(page, result, options); } // Seitentitel prüfen if (options.verbose) console.log(` 📋 Extracting page title...`); result.title = await page.title(); // Bilder ohne alt-Attribut if (options.verbose) console.log(` 🖼️ Checking images for alt attributes...`); result.imagesWithoutAlt = await page.locator("img:not([alt])").count(); if (result.imagesWithoutAlt > 0) { result.warnings.push(`${result.imagesWithoutAlt} images without alt attribute`); } // Buttons ohne aria-label if (options.verbose) console.log(` 🔘 Checking buttons for aria labels...`); 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`); } // Überschriften-Hierarchie if (options.verbose) console.log(` 📝 Checking heading hierarchy...`); result.headingsCount = await page .locator("h1, h2, h3, h4, h5, h6") .count(); if (result.headingsCount === 0) { result.errors.push("No headings found"); } // 🆕 Erweiterte Accessibility-Tests if (options.testKeyboardNavigation) { if (options.verbose) console.log(` ⌨️ Testing keyboard navigation...`); await this.testKeyboardNavigation(page, result, options); } if (options.testColorContrast) { if (options.verbose) console.log(` 🎨 Testing color contrast...`); await this.testColorContrast(page, result, options); } if (options.testFocusManagement) { if (options.verbose) console.log(` 🎯 Testing focus management...`); await this.testFocusManagement(page, result, options); } // 🆕 Screenshots if (options.captureScreenshots) { if (options.verbose) console.log(` 📸 Capturing screenshots...`); await this.captureScreenshots(page, url, result, options); } // pa11y Accessibility-Tests durchführen if (options.verbose) console.log(` 🔍 Running pa11y accessibility tests...`); try { // 🆕 Optimierte pa11y-Konfiguration für localhost const pa11yResult = await (0, pa11y_1.default)(url, { timeout: options.timeout || 15000, // Erhöht für localhost wait: options.wait || 2000, // Länger warten für localhost standard: options.pa11yStandard || 'WCAG2AA', hideElements: options.hideElements || 'iframe[src*="google-analytics"], iframe[src*="doubleclick"]', includeNotices: options.includeNotices !== false, includeWarnings: options.includeWarnings !== false, runners: options.runners || ['axe', 'htmlcs'], // 🆕 Vereinfachte Chrome-Konfiguration für localhost chromeLaunchConfig: { ...options.chromeLaunchConfig, args: [ '--disable-web-security', '--disable-features=VizDisplayCompositor', '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--disable-background-timer-throttling', '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding' ] }, log: options.verbose ? console : undefined, }); // pa11y-Ergebnisse in unser Format konvertieren pa11yResult.issues.forEach((issue) => { // Detaillierte Issue-Informationen speichern 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); // Für Kompatibilität auch in errors/warnings 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}`); } }); // Zusätzliche pa11y-Metriken if (pa11yResult.documentTitle) { result.title = pa11yResult.documentTitle; } // pa11y Score berechnen if (pa11yResult.issues.length > 0) { const totalIssues = pa11yResult.issues.length; const errorIssues = pa11yResult.issues.filter(issue => issue.type === 'error').length; result.pa11yScore = Math.max(0, 100 - (errorIssues * 10) - (totalIssues - errorIssues) * 2); } else { result.pa11yScore = 100; } } catch (pa11yError) { // 🆕 Bessere Fehlerbehandlung für pa11y const errorMessage = pa11yError instanceof Error ? pa11yError.message : String(pa11yError); // Timeout-Fehler speziell behandeln if (errorMessage.includes('timeout') || errorMessage.includes('Timeout')) { if (options.verbose) { console.log(` ⚠️ pa11y timeout for ${url} - skipping pa11y tests`); } // Timeout-Fehler nicht als Warning hinzufügen, da sie auf localhost normal sind } else { // Andere pa11y-Fehler als Warning hinzufügen result.warnings.push(`pa11y test failed: ${errorMessage}`); } } // 🆕 Lighthouse Tests durchführen (mit geteiltem Browser) if (options.lighthouse) { if (options.verbose) console.log(` �� Running Lighthouse tests...`); try { const lighthouseResult = await this.lighthouseIntegration.runLighthouse(url); // Lighthouse-Ergebnisse speichern result.lighthouseScores = { performance: lighthouseResult.performance, accessibility: lighthouseResult.accessibility, bestPractices: lighthouseResult.bestPractices, seo: lighthouseResult.seo }; result.lighthouseMetrics = lighthouseResult.metrics; if (options.verbose) { console.log(` 📊 Lighthouse Scores: P:${lighthouseResult.performance} A:${lighthouseResult.accessibility} BP:${lighthouseResult.bestPractices} SEO:${lighthouseResult.seo}`); } } catch (lighthouseError) { result.warnings.push(`Lighthouse test failed: ${lighthouseError}`); } } // Prüfe auf kritische Fehler if (result.errors.length > 0) { result.passed = false; } } catch (error) { result.errors.push(`Navigation error: ${error}`); result.passed = false; } finally { await page.close(); result.duration = Date.now() - startTime; } return result; } async testMultiplePages(urls, options = {}) { const results = []; const maxPages = options.maxPages || urls.length; const pagesToTest = urls.slice(0, maxPages); // Einfache Queue erstellen this.testQueue = new simple_queue_1.SimpleQueue({ maxRetries: 3, maxConcurrent: 1, priorityPatterns: [ { pattern: '/home', priority: 1 }, { pattern: '/', priority: 2 }, { pattern: '/about', priority: 3 }, { pattern: '/contact', priority: 3 }, { pattern: '/blog', priority: 4 }, { pattern: '/products', priority: 4 } ] }); // URLs zur Queue hinzufügen this.testQueue.addUrls(pagesToTest); console.log(`🧪 Testing ${pagesToTest.length} pages using queue system...`); this.testQueue.showStats(); let completedCount = 0; const maxAttempts = pagesToTest.length * 3; // Sicherheitsgrenze let attempts = 0; while (completedCount < pagesToTest.length && attempts < maxAttempts) { attempts++; // Nächste URL aus der Queue holen const queuedUrl = this.testQueue.getNextUrl(); if (!queuedUrl) { // Keine URLs mehr in der Queue break; } const startTime = Date.now(); console.log(`\n📄 Testing page ${completedCount + 1}/${pagesToTest.length}: ${queuedUrl.url}`); console.log(` ⏱️ Starting test (attempt ${queuedUrl.attempts})...`); try { const result = await this.testPage(queuedUrl.url, options); const duration = Date.now() - startTime; result.duration = duration; results.push(result); // URL als abgeschlossen markieren this.testQueue.markCompleted(queuedUrl.url, result); completedCount++; console.log(` ✅ Test completed in ${duration}ms`); if (result.passed) { console.log(` 🎯 Result: PASSED (${result.errors.length} errors, ${result.warnings.length} warnings)`); } else { console.log(` 🎯 Result: FAILED (${result.errors.length} errors, ${result.warnings.length} warnings)`); } // Status alle 5 URLs anzeigen if (completedCount % 5 === 0) { this.testQueue.showStats(); } } catch (error) { const duration = Date.now() - startTime; console.error(` 💥 Error testing page after ${duration}ms: ${error}`); // URL als fehlgeschlagen markieren this.testQueue.markFailed(queuedUrl.url, String(error)); // Error-Result erstellen const errorResult = { url: queuedUrl.url, title: "", imagesWithoutAlt: 0, buttonsWithoutLabel: 0, headingsCount: 0, errors: [`Test failed: ${error}`], warnings: [], passed: false, duration, }; results.push(errorResult); completedCount++; } } // Finale Statistiken anzeigen console.log('\n📊 Final Queue Statistics:'); this.testQueue.showStats(); return results; } /** * 🚀 Parallele Accessibility-Tests mit Event-Driven Queue * * Diese Methode verwendet das Event-Driven Queue System für parallele Tests * mit Echtzeit-Status-Reporting und Resource-Monitoring. */ async testMultiplePagesParallel(urls, options = {}) { const maxPages = options.maxPages || urls.length; const pagesToTest = urls.slice(0, maxPages); // Parallele Test-Optionen const parallelOptions = { maxConcurrent: options.maxConcurrent || 3, maxRetries: options.maxRetries || 3, retryDelay: options.retryDelay || 2000, enableProgressBar: options.enableProgressBar !== false, progressUpdateInterval: options.progressUpdateInterval || 1000, enableResourceMonitoring: options.enableResourceMonitoring !== false, maxMemoryUsage: options.maxMemoryUsage || 512, maxCpuUsage: options.maxCpuUsage || 80, testOptions: options, eventCallbacks: { onUrlStarted: (url) => { if (options.verbose) { console.log(`🚀 Starting parallel test: ${url}`); } }, onUrlCompleted: (url, result, duration) => { const status = result.passed ? '✅ PASSED' : '❌ FAILED'; console.log(`${status} ${url} (${duration}ms) - ${result.errors.length} errors, ${result.warnings.length} warnings`); }, onUrlFailed: (url, error, attempts) => { console.error(`💥 Error testing ${url} (attempt ${attempts}): ${error}`); }, onProgressUpdate: (stats) => { if (options.verbose) { console.log(`📊 Progress: ${stats.progress.toFixed(1)}% | Workers: ${stats.activeWorkers}/${options.maxConcurrent || 3} | Memory: ${stats.memoryUsage}MB`); } }, onQueueEmpty: () => { console.log('🎉 All parallel tests completed!'); } } }; // Parallel Test Manager initialisieren this.parallelTestManager = new parallel_test_manager_1.ParallelTestManager(parallelOptions); try { console.log(`🚀 Starting parallel accessibility tests for ${pagesToTest.length} pages with ${parallelOptions.maxConcurrent} workers`); console.log(`⚙️ Configuration: maxRetries=${parallelOptions.maxRetries}, retryDelay=${parallelOptions.retryDelay}ms`); // Manager initialisieren await this.parallelTestManager.initialize(); // Tests ausführen const startTime = Date.now(); const result = await this.parallelTestManager.runTests(pagesToTest); const totalDuration = Date.now() - startTime; // Ergebnisse ausgeben console.log('\n📋 Parallel Test Results Summary:'); console.log('=================================='); console.log(`⏱️ Total Duration: ${totalDuration}ms`); console.log(`📄 URLs Tested: ${result.results.length}`); console.log(`✅ Successful: ${result.results.filter(r => r.passed).length}`); console.log(`❌ Failed: ${result.results.filter(r => !r.passed).length}`); console.log(`💥 Errors: ${result.errors.length}`); // Performance-Metriken const avgTimePerUrl = totalDuration / pagesToTest.length; const speedup = avgTimePerUrl > 0 ? (avgTimePerUrl * pagesToTest.length) / totalDuration : 0; console.log('\n🚀 Performance Metrics:'); console.log('======================'); console.log(`Average time per URL: ${avgTimePerUrl.toFixed(0)}ms`); console.log(`Speedup factor: ${speedup.toFixed(1)}x`); console.log(`Throughput: ${(pagesToTest.length / (totalDuration / 1000)).toFixed(1)} URLs/second`); // Detaillierte Statistiken console.log('\n📊 Queue Statistics:'); console.log('==================='); console.log(`Total: ${result.stats.total}`); console.log(`Completed: ${result.stats.completed}`); console.log(`Failed: ${result.stats.failed}`); console.log(`Retrying: ${result.stats.retrying}`); console.log(`Progress: ${result.stats.progress.toFixed(1)}%`); console.log(`Average Duration: ${result.stats.averageDuration}ms`); console.log(`Memory Usage: ${result.stats.memoryUsage}MB`); console.log(`CPU Usage: ${result.stats.cpuUsage}s`); // Fehler-Details if (result.errors.length > 0) { console.log('\n❌ Failed URLs:'); console.log('==============='); result.errors.forEach((error, index) => { console.log(`${index + 1}. ${error.url} (${error.attempts} attempts): ${error.error}`); }); } return result.results; } catch (error) { console.error('❌ Parallel test execution failed:', error); throw error; } finally { // Cleanup if (this.parallelTestManager) { await this.parallelTestManager.cleanup(); this.parallelTestManager = null; } } } /** * 🚀 Integrierte Queue-Verarbeitung mit kurzen Status-Updates * Diese Methode nutzt die Event-Driven Queue direkt für maximale Effizienz */ async testMultiplePagesWithQueue(urls, options = {}) { console.log(`🚀 Starte integrierte Queue-Verarbeitung für ${urls.length} URLs`); // Initialisiere Browser if (!this.browserManager) { this.browserManager = new browser_manager_1.BrowserManager({ headless: true, port: 9222 }); await this.browserManager.initialize(); } // Erstelle Event-Driven Queue this.eventDrivenQueue = new event_driven_queue_1.EventDrivenQueue({ maxConcurrent: options.maxConcurrent || 3, maxRetries: options.maxRetries || 3, retryDelay: options.retryDelay || 2000, enableShortStatus: true, statusUpdateInterval: 2000, eventCallbacks: { onShortStatus: (status) => { // Überschreibe die aktuelle Zeile mit dem Status process.stdout.write(`\r${status}`); }, onUrlCompleted: (url, result, duration) => { const shortUrl = url.split('/').pop() || url; console.log(`\n✅ ${shortUrl} (${duration}ms)`); }, onUrlFailed: (url, error, attempts) => { const shortUrl = url.split('/').pop() || url; console.log(`\n❌ ${shortUrl} (Versuch ${attempts})`); } } }); // Definiere den Processor für jede URL const processOptions = { processor: async (url) => { return await this.testPage(url, options); }, onResult: (url, result) => { // Optional: Zusätzliche Verarbeitung nach erfolgreichem Test }, onError: (url, error) => { console.error(`\n💥 Fehler bei ${url}: ${error}`); } }; try { // Verarbeite alle URLs mit der Queue const results = await this.eventDrivenQueue.processUrls(urls, processOptions); console.log(`\n🎉 Queue-Verarbeitung abgeschlossen!`); console.log(`📊 Ergebnisse: ${results.length} URLs getestet`); return results; } finally { // Cleanup this.eventDrivenQueue = null; } } // 🆕 Erweiterte Page-Konfiguration async configurePage(page, options) { // Viewport-Konfiguration const viewportSize = options.viewportSize || { width: 1920, height: 1080 }; await page.setViewportSize(viewportSize); // User-Agent setzen (Standard: auditmysite) const userAgent = options.userAgent || 'auditmysite/1.0 (+https://github.com/casoon/AuditMySite)'; await page.setExtraHTTPHeaders({ 'User-Agent': userAgent }); // Network-Interception für Performance if (options.blockImages) { await page.route('**/*.{png,jpg,jpeg,gif,svg,webp}', route => { route.abort(); }); } if (options.blockCSS) { await page.route('**/*.css', route => { route.abort(); }); } // Console-Logging page.on('console', msg => { if (options.verbose) { console.log(`Browser Console: ${msg.text()}`); } }); // Error-Handling page.on('pageerror', error => { if (options.verbose) { console.log(`JavaScript Error: ${error.message}`); } }); } // 🆕 Performance-Metriken sammeln async collectPerformanceMetrics(page, result, options) { try { const metrics = await page.evaluate(() => { const navigation = performance.getEntriesByType('navigation')[0]; return { loadTime: navigation.loadEventEnd - navigation.loadEventStart, domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart, firstPaint: performance.getEntriesByName('first-paint')[0]?.startTime || 0, firstContentfulPaint: performance.getEntriesByName('first-contentful-paint')[0]?.startTime || 0, largestContentfulPaint: performance.getEntriesByName('largest-contentful-paint')[0]?.startTime || 0 }; }); result.performanceMetrics = metrics; // Performance-Warnungen if (metrics.loadTime > 3000) { result.warnings.push(`Slow page load: ${Math.round(metrics.loadTime)}ms`); } } catch (error) { if (options.verbose) { console.log(`Performance metrics collection failed: ${error}`); } } } // 🆕 Keyboard Navigation Test async testKeyboardNavigation(page, result, options) { try { const keyboardNavigation = await page.evaluate(() => { const focusableElements = document.querySelectorAll('button, input, select, textarea, a[href], [tabindex]:not([tabindex="-1"])'); const navigation = []; // Simuliere Tab-Navigation für die ersten 10 Elemente for (let i = 0; i < Math.min(focusableElements.length, 10); i++) { const element = focusableElements[i]; navigation.push(`${element.tagName.toLowerCase()}: ${element.textContent?.trim().substring(0, 50) || element.outerHTML}`); } return navigation; }); result.keyboardNavigation = keyboardNavigation; } catch (error) { if (options.verbose) { console.log(`Keyboard navigation test failed: ${error}`); } } } // 🆕 Color Contrast Test (vereinfacht) async testColorContrast(page, result, options) { try { const contrastIssues = await page.evaluate(() => { const elements = document.querySelectorAll('p, span, div, h1, h2, h3, h4, h5, h6, a, button, input, label'); const issues = []; elements.forEach(el => { const style = window.getComputedStyle(el); const color = style.color; const backgroundColor = style.backgroundColor; // Einfache Kontrast-Prüfung (vereinfacht) if (color && backgroundColor && color !== backgroundColor && color !== 'rgba(0, 0, 0, 0)' && backgroundColor !== 'rgba(0, 0, 0, 0)') { issues.push(`${el.tagName}: ${color} on ${backgroundColor}`); } }); return issues.slice(0, 10); // Limitiere auf 10 Issues }); if (contrastIssues.length > 0) { result.colorContrastIssues = contrastIssues; result.warnings.push(`${contrastIssues.length} potential color contrast issues found`); } } catch (error) { if (options.verbose) { console.log(`Color contrast test failed: ${error}`); } } } // 🆕 Focus Management Test async testFocusManagement(page, result, options) { try { const focusIssues = await page.evaluate(() => { const issues = []; // Prüfe auf focus-visible const focusableElements = document.querySelectorAll('button, input, select, textarea, a[href]'); focusableElements.forEach(el => { const style = window.getComputedStyle(el); if (style.outline === 'none' && style.border === 'none' && !el.classList.contains('focus-visible') && !el.classList.contains('focus')) { issues.push(`Element without focus indicator: ${el.tagName} - ${el.textContent?.trim().substring(0, 30) || 'no text'}`); } }); return issues.slice(0, 10); // Limitiere auf 10 Issues }); if (focusIssues.length > 0) { result.focusManagementIssues = focusIssues; result.warnings.push(`${focusIssues.length} focus management issues found`); } } catch (error) { if (options.verbose) { console.log(`Focus management test failed: ${error}`); } } } // 🆕 Screenshot-Funktionalität async captureScreenshots(page, url, result, options) { try { // Screenshots-Ordner erstellen const screenshotsDir = './screenshots'; if (!fs.existsSync(screenshotsDir)) { fs.mkdirSync(screenshotsDir, { recursive: true }); } const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const domain = new URL(url).hostname.replace(/\./g, '-'); // Desktop Screenshot const desktopPath = path.join(screenshotsDir, `${domain}-desktop-${timestamp}.png`); await page.screenshot({ path: desktopPath, fullPage: true }); result.screenshots = { desktop: desktopPath }; // Mobile Screenshot await page.setViewportSize({ width: 375, height: 667 }); const mobilePath = path.join(screenshotsDir, `${domain}-mobile-${timestamp}.png`); await page.screenshot({ path: mobilePath, fullPage: true }); result.screenshots.mobile = mobilePath; // Reset viewport await page.setViewportSize({ width: 1920, height: 1080 }); } catch (error) { if (options.verbose) { console.log(`Screenshot capture failed: ${error}`); } } } } exports.AccessibilityChecker = AccessibilityChecker; //# sourceMappingURL=accessibility-checker.js.map