@casoon/auditmysite
Version:
A comprehensive command-line tool for automated accessibility, security, performance, and SEO testing using Playwright and pa11y, based on sitemap URLs
365 lines (321 loc) • 14.8 kB
text/typescript
import { SitemapParser } from '../parsers';
import { AccessibilityChecker } from './accessibility-checker';
import { OutputGenerator } from '../generators';
import { DetailedReportGenerator, PerformanceReportGenerator, SeoReportGenerator, SecurityReportGenerator } from '../reports';
import { HtmlReportGenerator } from '../reports/html-report-generator';
import { SecurityScanner } from './security-scanner';
import { TestOptions, TestSummary, AccessibilityResult } from '../types';
import * as path from 'path';
export interface StandardPipelineOptions {
sitemapUrl: string;
maxPages?: number;
timeout?: number;
pa11yStandard?: 'WCAG2A' | 'WCAG2AA' | 'WCAG2AAA' | 'Section508';
outputDir?: string;
includeDetails?: boolean;
includePa11yIssues?: boolean;
generateDetailedReport?: boolean;
generatePerformanceReport?: boolean;
generateSeoReport?: boolean;
generateSecurityReport?: boolean;
skipCspForLocalhost?: boolean;
hideElements?: string;
includeNotices?: boolean;
includeWarnings?: boolean;
wait?: number;
// 🆕 Neue Playwright-Optionen
collectPerformanceMetrics?: boolean;
captureScreenshots?: boolean;
testKeyboardNavigation?: boolean;
testColorContrast?: boolean;
testFocusManagement?: boolean;
blockImages?: boolean;
blockCSS?: boolean;
mobileEmulation?: boolean;
viewportSize?: { width: number; height: number };
userAgent?: string;
// 🚀 Parallele Test-Optionen (Queue ist jetzt Standard)
maxConcurrent?: number;
maxRetries?: number;
retryDelay?: number;
enableProgressBar?: boolean;
progressUpdateInterval?: number;
enableResourceMonitoring?: boolean;
maxMemoryUsage?: number;
maxCpuUsage?: number;
// 🆕 Legacy-Option für sequenzielle Tests (nur für Kompatibilität)
useSequentialTesting?: boolean;
// 🆕 Output-Format-Option
outputFormat?: 'markdown' | 'html';
includeCopyButtons?: boolean;
}
export class StandardPipeline {
/**
* Führt die Standard-Pipeline aus und erstellt KI-freundliche Output-Dateien
*/
async run(options: StandardPipelineOptions): Promise<{
summary: TestSummary;
outputFiles: string[];
}> {
const outputDir = options.outputDir || './reports';
const dateOnly = new Date().toISOString().split('T')[0]; // Nur Datum, kein Timestamp
// Parser initialisieren
const parser = new SitemapParser();
// Sitemap parsen
const urls = await parser.parseSitemap(options.sitemapUrl);
console.log(`📄 Sitemap geladen: ${urls.length} URLs gefunden`);
// URLs filtern
const filterPatterns = ['[...slug]', '[category]', '/demo/'];
const filteredUrls = parser.filterUrls(urls, { filterPatterns });
console.log(`🔍 URLs gefiltert: ${filteredUrls.length} URLs zum Testen`);
// URLs zu lokalen URLs konvertieren
const baseUrl = new URL(options.sitemapUrl).origin;
const localUrls = parser.convertToLocalUrls(filteredUrls, baseUrl);
// 🆕 WICHTIG: URLs auf maxPages begrenzen
const maxPages = options.maxPages || 20;
const limitedUrls = localUrls.slice(0, maxPages);
console.log(`📋 URLs auf ${maxPages} begrenzt: ${limitedUrls.length} URLs werden getestet`);
// Accessibility-Checker initialisieren
const checker = new AccessibilityChecker();
await checker.initialize();
console.log('🧪 Führe Accessibility-Tests aus...');
console.log(` 📊 Performance-Metriken sammeln: ${options.collectPerformanceMetrics ? 'Ja' : 'Nein'}`);
console.log(` 📸 Screenshots aufnehmen: ${options.captureScreenshots ? 'Ja' : 'Nein'}`);
console.log(` ⌨️ Tastaturnavigation testen: ${options.testKeyboardNavigation ? 'Ja' : 'Nein'}`);
console.log(` 🎨 Farbkontrast testen: ${options.testColorContrast ? 'Ja' : 'Nein'}`);
console.log(` 🎯 Fokus-Management testen: ${options.testFocusManagement ? 'Ja' : 'Nein'}`);
console.log(` 🚀 Parallele Tests: ${options.useSequentialTesting ? 'Nein' : 'Ja'}`);
if (options.useSequentialTesting) {
console.log(` 📋 Verwende sequenzielle Tests (Legacy-Modus)...`);
} else {
console.log(` 🔧 Parallele Worker: ${options.maxConcurrent || 3}`);
console.log(` 🔄 Max. Wiederholungen: ${options.maxRetries || 3}`);
console.log(` ⏱️ Wiederholungsverzögerung: ${options.retryDelay || 2000}ms`);
}
// Tests ausführen
const testOptions: TestOptions = {
maxPages: maxPages,
timeout: options.timeout || 10000,
waitUntil: 'domcontentloaded',
pa11yStandard: options.pa11yStandard || 'WCAG2AA',
hideElements: options.hideElements,
includeNotices: options.includeNotices,
includeWarnings: options.includeWarnings,
wait: options.wait,
// 🆕 Neue Playwright-Optionen
collectPerformanceMetrics: options.collectPerformanceMetrics,
captureScreenshots: options.captureScreenshots,
testKeyboardNavigation: options.testKeyboardNavigation,
testColorContrast: options.testColorContrast,
testFocusManagement: options.testFocusManagement,
blockImages: options.blockImages,
blockCSS: options.blockCSS,
mobileEmulation: options.mobileEmulation,
viewportSize: options.viewportSize,
userAgent: options.userAgent,
// 🚀 Parallele Test-Optionen
useParallelTesting: !options.useSequentialTesting, // Keep this for compatibility, but it's now the default
maxConcurrent: options.maxConcurrent,
maxRetries: options.maxRetries,
retryDelay: options.retryDelay,
enableProgressBar: options.enableProgressBar,
progressUpdateInterval: options.progressUpdateInterval,
enableResourceMonitoring: options.enableResourceMonitoring,
maxMemoryUsage: options.maxMemoryUsage,
maxCpuUsage: options.maxCpuUsage
};
// Wähle zwischen Queue (Standard) und sequenzieller Verarbeitung
let results: AccessibilityResult[];
if (options.useSequentialTesting) {
console.log('📋 Verwende sequenzielle Tests (Legacy-Modus)...');
results = await checker.testMultiplePages(
limitedUrls.map(url => url.loc),
testOptions
);
} else {
console.log('🚀 Verwende integrierte Queue-Verarbeitung mit kurzen Status-Updates (Standard)...');
results = await checker.testMultiplePagesWithQueue(
limitedUrls.map(url => url.loc),
testOptions
);
}
console.log('\n📋 Erstelle Test-Zusammenfassung...');
// Zusammenfassung erstellen
const summary: TestSummary = {
totalPages: limitedUrls.length, // 🆕 Verwende limitedUrls statt localUrls
testedPages: results.length,
passedPages: results.filter(r => r.passed).length,
failedPages: results.filter(r => !r.passed).length,
totalErrors: results.reduce((sum, r) => sum + r.errors.length, 0),
totalWarnings: results.reduce((sum, r) => sum + r.warnings.length, 0),
totalDuration: results.reduce((sum, r) => sum + r.duration, 0),
results
};
await checker.cleanup();
console.log('📄 Generiere Ausgabedateien...');
// Output-Dateien generieren
const outputGenerator = new OutputGenerator();
const outputFiles: string[] = [];
// Wähle zwischen Markdown und HTML Output
if (options.outputFormat === 'html') {
console.log(' 🌐 Generiere HTML-Bericht...');
const htmlReportGenerator = new HtmlReportGenerator();
const htmlReportPath = await htmlReportGenerator.generateHtmlReport(summary, {
outputDir: options.outputDir,
includeCopyButtons: options.includeCopyButtons !== false
});
outputFiles.push(htmlReportPath);
} else {
// Markdown für Menschen (Standard) - ohne Domain im Dateinamen
const mdOutputPath = path.join(outputDir, `accessibility-report-${dateOnly}.md`);
await outputGenerator.generateOutput(summary, {
format: 'markdown',
outputFile: mdOutputPath,
includeDetails: options.includeDetails || true,
includePa11yIssues: options.includePa11yIssues || false,
summaryOnly: false
});
outputFiles.push(mdOutputPath);
}
// Detailed-Report generieren (falls gewünscht)
if (options.generateDetailedReport !== false && (summary.totalErrors > 0 || summary.totalWarnings > 0)) {
console.log(' 📋 Generiere detaillierten Fehlerbericht...');
const detailedReportGenerator = new DetailedReportGenerator();
const detailedReportPath = await detailedReportGenerator.generateDetailedReport(summary, {
outputDir: options.outputDir,
includeContext: true,
includeRecommendations: true,
groupByType: true,
includeCodeExamples: true
});
outputFiles.push(detailedReportPath);
}
// Performance-Report generieren (falls gewünscht)
if (options.generatePerformanceReport !== false && options.collectPerformanceMetrics) {
console.log(' 📊 Generiere Performance-Bericht...');
const performanceReportGenerator = new PerformanceReportGenerator();
const performanceReportPath = await performanceReportGenerator.generatePerformanceReport(summary, {
outputDir: options.outputDir,
includeRecommendations: true,
includePageDetails: true,
includeCoreWebVitals: true,
includeResourceAnalysis: true
});
outputFiles.push(performanceReportPath);
}
// SEO-Report generieren (falls gewünscht)
if (options.generateSeoReport !== false) {
console.log(' 🔍 Generiere SEO-Bericht...');
const seoReportGenerator = new SeoReportGenerator();
const seoReportPath = await seoReportGenerator.generateSeoReport(summary, {
outputDir: options.outputDir,
includeRecommendations: true,
includePageDetails: true,
includeTechnicalAnalysis: true,
includeContentAnalysis: true
});
outputFiles.push(seoReportPath);
}
// Security-Report generieren (falls gewünscht)
if (options.generateSecurityReport !== false) {
console.log(' 🔒 Führe Security-Scan durch und generiere Security-Bericht...');
// Security Scanner initialisieren
const securityScanner = new SecurityScanner();
// Security-Scan für die erste URL durchführen (als Beispiel)
if (limitedUrls.length > 0) {
const firstUrl = limitedUrls[0].loc;
console.log(` 🔍 Scanne ${firstUrl} auf Sicherheitslücken...`);
// Browser-Instanz für Security-Scan erstellen
const { chromium } = require('playwright');
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
try {
const securityScanResult = await securityScanner.scanPage(page, firstUrl, {
skipCspForLocalhost: options.skipCspForLocalhost || false
});
// Security-Report generieren - ohne Domain im Dateinamen
const securityReportGenerator = new SecurityReportGenerator();
if (options.outputFormat === 'html') {
const securityHtmlPath = path.join(outputDir, `security-report-${dateOnly}.html`);
await securityReportGenerator.generateHtmlReport(securityScanResult, securityHtmlPath);
outputFiles.push(securityHtmlPath);
} else {
const securityMdPath = path.join(outputDir, `security-report-${dateOnly}.md`);
await securityReportGenerator.generateMarkdownReport(securityScanResult, securityMdPath);
outputFiles.push(securityMdPath);
}
console.log(` ✅ Security-Scan abgeschlossen mit Score: ${securityScanResult.overallScore}/100`);
} catch (error) {
console.error(' ❌ Security-Scan fehlgeschlagen:', error);
} finally {
await browser.close();
}
}
}
return { summary, outputFiles };
}
/**
* Führt eine schnelle Pipeline nur mit Markdown-Output aus
*/
async runQuick(options: StandardPipelineOptions): Promise<{
summary: TestSummary;
markdownFile: string;
}> {
const outputDir = options.outputDir || './reports';
const dateOnly = new Date().toISOString().split('T')[0]; // Nur Datum, kein Timestamp
// Parser initialisieren
const parser = new SitemapParser();
// Sitemap parsen
const urls = await parser.parseSitemap(options.sitemapUrl);
// URLs filtern
const filterPatterns = ['[...slug]', '[category]', '/demo/'];
const filteredUrls = parser.filterUrls(urls, { filterPatterns });
// URLs zu lokalen URLs konvertieren
const baseUrl = new URL(options.sitemapUrl).origin;
const localUrls = parser.convertToLocalUrls(filteredUrls, baseUrl);
// 🆕 WICHTIG: URLs auf maxPages begrenzen
const maxPages = options.maxPages || 20;
const limitedUrls = localUrls.slice(0, maxPages);
// Accessibility-Checker initialisieren
const checker = new AccessibilityChecker();
await checker.initialize();
// Tests ausführen
const testOptions: TestOptions = {
maxPages: maxPages,
timeout: options.timeout || 10000,
waitUntil: 'domcontentloaded',
pa11yStandard: options.pa11yStandard || 'WCAG2AA',
hideElements: options.hideElements,
includeNotices: options.includeNotices,
includeWarnings: options.includeWarnings,
wait: options.wait
};
const results = await checker.testMultiplePages(
limitedUrls.map(url => url.loc),
testOptions
);
// Zusammenfassung erstellen
const summary: TestSummary = {
totalPages: limitedUrls.length,
testedPages: results.length,
passedPages: results.filter(r => r.passed).length,
failedPages: results.filter(r => !r.passed).length,
totalErrors: results.reduce((sum, r) => sum + r.errors.length, 0),
totalWarnings: results.reduce((sum, r) => sum + r.warnings.length, 0),
totalDuration: results.reduce((sum, r) => sum + r.duration, 0),
results
};
await checker.cleanup();
// Markdown-Output generieren - ohne Domain im Dateinamen
const outputGenerator = new OutputGenerator();
const markdownFile = path.join(outputDir, `accessibility-report-${dateOnly}.md`);
await outputGenerator.generateOutput(summary, {
format: 'markdown',
outputFile: markdownFile,
includeDetails: options.includeDetails || true,
includePa11yIssues: options.includePa11yIssues || false,
summaryOnly: false
});
return { summary, markdownFile };
}
}