@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
836 lines โข 35.2 kB
JavaScript
;
/**
* ๐ง UNIFIED Page Analysis Event System
*
* โจ MAIN EVENT SYSTEM - Replaces multiple parallel event systems
*
* Event-driven system where analyzers attach to page load events
* and contribute their data to a unified result structure.
*
* ๐ฏ CONSOLIDATES:
* - AccessibilityChecker event callbacks
* - EventDrivenQueue callbacks
* - ParallelTestManager events
* - Direct callback patterns in bin/audit.js
*
* ๐ BACKWARD COMPATIBILITY:
* - Supports all existing callback patterns via adapters
* - Maintains existing APIs while using unified backend
*
* ๐ FEATURES:
* - Parallel analyzer execution
* - Resource monitoring integration
* - Backpressure control integration
* - Progress tracking
* - Error handling & fallbacks
* - State persistence
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.seoAnalyzer = exports.performanceAnalyzer = exports.accessibilityAnalyzer = exports.PageAnalysisEmitter = void 0;
const events_1 = require("events");
const backpressure_controller_1 = require("../backpressure-controller");
const resource_monitor_1 = require("../resource-monitor");
/**
* ๐ฏ UNIFIED PAGE ANALYSIS EMITTER - Main Event System
*
* Replaces multiple parallel event systems with a single, comprehensive solution.
* Maintains backward compatibility while providing enhanced features.
*/
class PageAnalysisEmitter extends events_1.EventEmitter {
constructor(options = {}) {
super();
this.analyzers = new Map();
this.callbacks = {};
this.isInitialized = false;
this.sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
this.options = {
enableResourceMonitoring: options.enableResourceMonitoring ?? true,
enableBackpressure: options.enableBackpressure ?? true,
maxConcurrent: options.maxConcurrent ?? 3,
maxRetries: options.maxRetries ?? 3,
retryDelay: options.retryDelay ?? 2000,
verbose: options.verbose ?? false,
...options
};
this.callbacks = options.callbacks || {};
// Initialize stats
this.stats = {
total: 0,
pending: 0,
inProgress: 0,
completed: 0,
failed: 0,
retrying: 0,
progress: 0,
averageDuration: 0,
estimatedTimeRemaining: 0,
activeWorkers: 0,
memoryUsage: 0,
cpuUsage: 0
};
// Initialize system metrics
this.systemMetrics = {
memoryUsageMB: 0,
heapUsedMB: 0,
cpuUsagePercent: 0,
eventLoopDelayMs: 0,
activeHandles: 0,
gcCount: 0,
uptimeSeconds: 0
};
}
/**
* ๐ Initialize the unified event system with integrated monitoring
*/
async initialize() {
if (this.isInitialized)
return;
try {
// Setup resource monitoring
if (this.options.enableResourceMonitoring) {
this.setupResourceMonitoring();
}
// Setup backpressure control
if (this.options.enableBackpressure) {
this.setupBackpressureControl();
}
this.isInitialized = true;
if (this.options.verbose) {
console.log(`๐ Unified Page Analysis System initialized (${this.sessionId})`);
console.log(` ๐ Resource Monitoring: ${this.options.enableResourceMonitoring ? 'โ
' : 'โ'}`);
console.log(` ๐ Backpressure Control: ${this.options.enableBackpressure ? 'โ
' : 'โ'}`);
console.log(` ๐ Max Concurrent: ${this.options.maxConcurrent}`);
}
}
catch (error) {
console.error(`โ Failed to initialize unified event system: ${error}`);
throw error;
}
}
/**
* ๐ Register an analyzer that will run when a page is loaded
*
* BACKWARD COMPATIBLE: Maintains existing analyzer registration pattern
*/
registerAnalyzer(name, analyzer) {
this.analyzers.set(name, analyzer);
if (this.options.verbose) {
console.log(`๐ Registered analyzer: ${name} (total: ${this.analyzers.size})`);
}
this.emit('analyzer-registered', { name, total: this.analyzers.size });
}
/**
* ๐ฏ Set unified event callbacks
*
* This replaces/consolidates:
* - TestOptions.eventCallbacks
* - EventDrivenQueueOptions.eventCallbacks
* - Direct callback patterns
*/
setEventCallbacks(callbacks) {
this.callbacks = { ...this.callbacks, ...callbacks };
if (this.options.verbose) {
const callbackNames = Object.keys(callbacks).join(', ');
console.log(`๐ฏ Event callbacks configured: ${callbackNames}`);
}
}
/**
* ๐ Setup resource monitoring integration
*/
setupResourceMonitoring() {
try {
this.resourceMonitor = new resource_monitor_1.ResourceMonitor({
enabled: true,
samplingIntervalMs: 2000,
memoryWarningThresholdMB: 1024,
memoryCriticalThresholdMB: 1536
});
// Connect resource events to unified callbacks
this.resourceMonitor.on('memoryWarning', (data) => {
this.callbacks.onResourceWarning?.(data.current, data.threshold, 'memory');
});
this.resourceMonitor.on('memoryCritical', (data) => {
this.callbacks.onResourceCritical?.(data.current, data.max, 'memory');
});
this.resourceMonitor.on('cpuWarning', (data) => {
this.callbacks.onResourceWarning?.(data.current, data.threshold, 'cpu');
});
// Update system metrics
this.resourceMonitor.on('metricsUpdate', (metrics) => {
this.systemMetrics = {
memoryUsageMB: metrics.rssMemoryMB,
heapUsedMB: metrics.heapUsedMB,
cpuUsagePercent: metrics.cpuUsagePercent,
eventLoopDelayMs: metrics.eventLoopDelayMs,
activeHandles: 0, // Would need additional monitoring
gcCount: metrics.gcCount || 0,
uptimeSeconds: metrics.uptimeSeconds
};
this.callbacks.onSystemMetrics?.(this.systemMetrics);
});
this.resourceMonitor.start();
}
catch (error) {
console.warn(`โ ๏ธ Resource monitoring setup failed: ${error}`);
}
}
/**
* ๐ Setup backpressure control integration
*/
setupBackpressureControl() {
try {
this.backpressureController = new backpressure_controller_1.AdaptiveBackpressureController({
enabled: true,
maxMemoryUsageMB: 1536,
maxCpuUsagePercent: 85
});
// Connect backpressure events to unified callbacks
this.backpressureController.on('backpressureActivated', (data) => {
this.callbacks.onBackpressureActivated?.(data.reason || 'Resource pressure detected');
});
this.backpressureController.on('backpressureDeactivated', () => {
this.callbacks.onBackpressureDeactivated?.();
});
this.backpressureController.on('gcTriggered', (data) => {
this.callbacks.onGarbageCollection?.(data.beforeMB, data.afterMB);
});
}
catch (error) {
console.warn(`โ ๏ธ Backpressure control setup failed: ${error}`);
}
}
/**
* ๐ Update and emit progress statistics
*/
updateProgress() {
// Calculate progress percentage
if (this.stats.total > 0) {
this.stats.progress = ((this.stats.completed + this.stats.failed) / this.stats.total) * 100;
}
// Emit progress update
this.callbacks.onProgressUpdate?.(this.stats);
this.emit('progress-update', this.stats);
}
/**
* ๐ฏ Get current progress statistics
*/
getProgressStats() {
return { ...this.stats };
}
/**
* ๐ Get system metrics
*/
getSystemMetrics() {
return { ...this.systemMetrics };
}
/**
* ๐งช Cleanup resources (enhanced version)
*/
async cleanup() {
try {
if (this.resourceMonitor) {
this.resourceMonitor.stop();
}
if (this.backpressureController) {
// Cleanup backpressure controller if it has cleanup method
}
this.emit('cleanup-complete');
if (this.options.verbose) {
console.log(`๐งช Unified event system cleanup completed (${this.sessionId})`);
}
}
catch (error) {
console.error(`โ Error during cleanup: ${error}`);
}
}
/**
* ๐ฏ UNIFIED PAGE ANALYSIS - Enhanced version with all features
*
* BACKWARD COMPATIBLE: Maintains existing analyzePage signature
* ENHANCED: Integrates resource monitoring, backpressure, progress tracking
*/
async analyzePage(url, page, options = {}, contextOptions = {}) {
const startTime = Date.now();
// Initialize result structure
const result = {
url,
title: '',
status: 'success',
duration: 0,
accessibility: {
passed: true,
score: 100,
errors: [],
warnings: [],
issues: [],
basicChecks: {
imagesWithoutAlt: 0,
buttonsWithoutLabel: 0,
headingsCount: 0,
}
}
};
const context = {
url,
page,
options,
result,
startTime,
...contextOptions
};
try {
// Navigate to page
if (options.verbose)
console.log(` ๐ Loading: ${url}`);
await page.goto(url, {
waitUntil: options.waitUntil || 'domcontentloaded',
timeout: options.timeout || 10000,
});
// Get basic page info
result.title = await page.title();
if (options.verbose)
console.log(` ๐ Title: ${result.title}`);
// Emit page-loaded event and run all analyzers
this.emit('page-loaded', context);
// Run all registered analyzers in parallel
const analyzerPromises = Array.from(this.analyzers.entries()).map(async ([name, analyzer]) => {
try {
if (options.verbose)
console.log(` ๐ Running ${name} analysis...`);
await analyzer(context);
}
catch (error) {
console.error(` โ ${name} analysis failed: ${error}`);
// Add error but don't fail the whole analysis
result.accessibility.warnings.push({
code: `${name.toUpperCase()}_ANALYSIS_ERROR`,
message: `${name} analysis failed: ${error}`,
type: 'warning'
});
}
});
await Promise.all(analyzerPromises);
}
catch (error) {
console.error(` ๐ฅ Page loading failed: ${error}`);
result.status = 'crashed';
result.accessibility.passed = false;
result.accessibility.errors.push({
code: 'PAGE_LOAD_ERROR',
message: `Failed to load page: ${error}`,
type: 'error'
});
}
finally {
result.duration = Date.now() - startTime;
// Determine overall status
if (result.accessibility.errors.length > 0) {
result.accessibility.passed = false;
if (result.status === 'success') {
result.status = 'failed';
}
}
}
return result;
}
/**
* Get list of registered analyzers
*/
getRegisteredAnalyzers() {
return Array.from(this.analyzers.keys());
}
}
exports.PageAnalysisEmitter = PageAnalysisEmitter;
/**
* Comprehensive accessibility analysis using WCAG 2.1 principles
* Categorizes Pa11y/Axe issues into structured compliance metrics
*/
async function performComprehensiveAccessibilityAnalysis(page, result) {
// Initialize comprehensive analysis structures
result.accessibility.wcagAnalysis = {
perceivable: {
colorContrast: { violations: 0, score: 100 },
textAlternatives: { violations: 0, score: 100 },
captions: { violations: 0, score: 100 },
adaptable: { violations: 0, score: 100 }
},
operable: {
keyboardAccessible: { violations: 0, score: 100 },
seizures: { violations: 0, score: 100 },
navigable: { violations: 0, score: 100 },
inputModalities: { violations: 0, score: 100 }
},
understandable: {
readable: { violations: 0, score: 100 },
predictable: { violations: 0, score: 100 },
inputAssistance: { violations: 0, score: 100 }
},
robust: {
compatible: { violations: 0, score: 100 },
parsing: { violations: 0, score: 100 }
}
};
result.accessibility.ariaAnalysis = {
totalViolations: 0,
landmarks: { present: [], missing: [], score: 100 },
roles: { correct: 0, incorrect: 0, missing: 0, score: 100 },
properties: { correct: 0, incorrect: 0, missing: 0, score: 100 },
liveRegions: { present: 0, appropriate: 0, score: 100 }
};
result.accessibility.formAnalysis = {
totalElements: 0,
labeling: { proper: 0, missing: 0, inadequate: 0, score: 100 },
validation: { accessible: 0, inaccessible: 0, score: 100 },
focusManagement: { proper: 0, issues: 0, score: 100 }
};
result.accessibility.keyboardAnalysis = {
focusIndicators: { visible: 0, missing: 0, score: 100 },
tabOrder: { logical: 0, problematic: 0, score: 100 },
keyboardTraps: { detected: 0, score: 100 }
};
// Perform comprehensive DOM analysis
await performDOMAccessibilityAnalysis(page, result);
}
/**
* Analyze DOM for comprehensive accessibility metrics
*/
async function performDOMAccessibilityAnalysis(page, result) {
try {
// Get comprehensive accessibility data from the browser
const domAnalysis = await page.evaluate(() => {
const analysis = {
landmarks: [],
forms: { total: 0, labeled: 0, unlabeled: 0 },
images: { total: 0, withAlt: 0, withoutAlt: 0, decorative: 0 },
headings: { h1: 0, h2: 0, h3: 0, h4: 0, h5: 0, h6: 0, structure: [] },
links: { total: 0, withText: 0, withoutText: 0 },
buttons: { total: 0, labeled: 0, unlabeled: 0 },
tables: { total: 0, withHeaders: 0, withoutHeaders: 0 },
ariaElements: { roles: 0, landmarks: 0, liveRegions: 0 },
focusableElements: { total: 0, withTabindex: 0, withFocusIndicator: 0 },
colorContrast: { potentialIssues: 0 }
};
// Analyze landmarks
const landmarkSelectors = ['main', 'nav', 'aside', 'header', 'footer', '[role="banner"]', '[role="navigation"]', '[role="main"]', '[role="contentinfo"]', '[role="complementary"]'];
landmarkSelectors.forEach(selector => {
const elements = document.querySelectorAll(selector);
elements.forEach(el => {
analysis.landmarks.push({
type: el.tagName.toLowerCase() === 'main' ? 'main' : el.getAttribute('role') || el.tagName.toLowerCase(),
hasLabel: !!(el.getAttribute('aria-label') || el.getAttribute('aria-labelledby'))
});
});
});
// Analyze forms
const forms = document.querySelectorAll('input, select, textarea');
analysis.forms.total = forms.length;
forms.forEach(input => {
const hasLabel = !!(input.getAttribute('aria-label') ||
input.getAttribute('aria-labelledby') ||
document.querySelector(`label[for="${input.id}"]`) ||
input.closest('label'));
if (hasLabel)
analysis.forms.labeled++;
else
analysis.forms.unlabeled++;
});
// Analyze images
const images = document.querySelectorAll('img');
analysis.images.total = images.length;
images.forEach(img => {
const alt = img.getAttribute('alt');
if (alt === '')
analysis.images.decorative++;
else if (alt)
analysis.images.withAlt++;
else
analysis.images.withoutAlt++;
});
// Analyze headings
['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].forEach(tag => {
const count = document.querySelectorAll(tag).length;
analysis.headings[tag] = count;
if (count > 0) {
analysis.headings.structure.push({ level: parseInt(tag[1]), count });
}
});
// Analyze links
const links = document.querySelectorAll('a[href]');
analysis.links.total = links.length;
links.forEach(link => {
const text = (link.textContent || '').trim();
const ariaLabel = link.getAttribute('aria-label');
if (text || ariaLabel)
analysis.links.withText++;
else
analysis.links.withoutText++;
});
// Analyze buttons
const buttons = document.querySelectorAll('button, input[type="button"], input[type="submit"], input[type="reset"]');
analysis.buttons.total = buttons.length;
buttons.forEach(button => {
const text = (button.textContent || '').trim();
const ariaLabel = button.getAttribute('aria-label');
const value = button.getAttribute('value');
if (text || ariaLabel || value)
analysis.buttons.labeled++;
else
analysis.buttons.unlabeled++;
});
// Analyze tables
const tables = document.querySelectorAll('table');
analysis.tables.total = tables.length;
tables.forEach(table => {
const hasHeaders = table.querySelectorAll('th, [scope]').length > 0;
if (hasHeaders)
analysis.tables.withHeaders++;
else
analysis.tables.withoutHeaders++;
});
// Analyze ARIA usage
analysis.ariaElements.roles = document.querySelectorAll('[role]').length;
analysis.ariaElements.landmarks = document.querySelectorAll('[role="banner"], [role="navigation"], [role="main"], [role="contentinfo"], [role="complementary"]').length;
analysis.ariaElements.liveRegions = document.querySelectorAll('[aria-live], [role="status"], [role="alert"]').length;
// Analyze focusable elements
const focusableSelectors = 'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
const focusableElements = document.querySelectorAll(focusableSelectors);
analysis.focusableElements.total = focusableElements.length;
focusableElements.forEach(el => {
if (el.getAttribute('tabindex'))
analysis.focusableElements.withTabindex++;
// Note: Focus indicator detection would require CSS analysis
});
// Basic color contrast analysis (simplified)
const textElements = document.querySelectorAll('p, span, div, h1, h2, h3, h4, h5, h6, a, button, label');
let potentialContrastIssues = 0;
textElements.forEach(el => {
const style = window.getComputedStyle(el);
const backgroundColor = style.backgroundColor;
const color = style.color;
// Simplified check - in reality, you'd need proper contrast calculation
if ((backgroundColor === 'rgb(255, 255, 255)' || backgroundColor === 'rgba(0, 0, 0, 0)') &&
(color === 'rgb(128, 128, 128)' || color.includes('rgba'))) {
potentialContrastIssues++;
}
});
analysis.colorContrast.potentialIssues = potentialContrastIssues;
return analysis;
});
// Process DOM analysis results into structured metrics
processAccessibilityMetrics(domAnalysis, result);
}
catch (error) {
console.error('DOM accessibility analysis failed:', error);
}
}
/**
* Process DOM analysis into structured accessibility metrics
*/
function processAccessibilityMetrics(domAnalysis, result) {
const wcag = result.accessibility.wcagAnalysis;
const aria = result.accessibility.ariaAnalysis;
const form = result.accessibility.formAnalysis;
const keyboard = result.accessibility.keyboardAnalysis;
// WCAG Perceivable Principle
wcag.perceivable.textAlternatives.violations = domAnalysis.images.withoutAlt;
wcag.perceivable.textAlternatives.score = calculateScore(domAnalysis.images.withAlt, domAnalysis.images.total);
wcag.perceivable.colorContrast.violations = domAnalysis.colorContrast.potentialIssues;
wcag.perceivable.colorContrast.score = Math.max(0, 100 - (domAnalysis.colorContrast.potentialIssues * 5));
// WCAG Operable Principle
wcag.operable.navigable.violations = domAnalysis.links.withoutText + (domAnalysis.headings.structure.length === 0 ? 1 : 0);
wcag.operable.navigable.score = calculateScore(domAnalysis.links.withText, domAnalysis.links.total);
// ARIA Analysis
aria.totalViolations = domAnalysis.buttons.unlabeled + domAnalysis.forms.unlabeled;
aria.landmarks.present = domAnalysis.landmarks.map((l) => l.type);
aria.landmarks.missing = ['main', 'navigation'].filter(type => !aria.landmarks.present.includes(type));
aria.landmarks.score = aria.landmarks.present.length > 0 ? 100 : 50;
aria.roles.correct = domAnalysis.ariaElements.roles;
aria.roles.score = Math.min(100, domAnalysis.ariaElements.roles * 10);
// Form Analysis
form.totalElements = domAnalysis.forms.total;
form.labeling.proper = domAnalysis.forms.labeled;
form.labeling.missing = domAnalysis.forms.unlabeled;
form.labeling.score = calculateScore(domAnalysis.forms.labeled, domAnalysis.forms.total);
// Keyboard Analysis
keyboard.focusIndicators.visible = domAnalysis.focusableElements.total - domAnalysis.focusableElements.withTabindex;
keyboard.focusIndicators.missing = domAnalysis.focusableElements.withTabindex;
keyboard.focusIndicators.score = calculateScore(keyboard.focusIndicators.visible, domAnalysis.focusableElements.total);
// Update basic checks with enhanced contrast information
result.accessibility.basicChecks.contrastIssues = domAnalysis.colorContrast.potentialIssues;
}
/**
* Calculate accessibility score based on compliant vs total elements
*/
function calculateScore(compliant, total) {
if (total === 0)
return 100;
return Math.round((compliant / total) * 100);
}
// Default analyzer functions
const accessibilityAnalyzer = async (context) => {
const { page, result } = context;
// Enhanced accessibility analysis with comprehensive metrics
await performComprehensiveAccessibilityAnalysis(page, result);
// Legacy basic checks for backward compatibility
result.accessibility.basicChecks.imagesWithoutAlt = await page.locator('img:not([alt])').count();
result.accessibility.basicChecks.buttonsWithoutLabel = await page
.locator('button:not([aria-label])')
.filter({ hasText: '' })
.count();
result.accessibility.basicChecks.headingsCount = await page.locator('h1, h2, h3, h4, h5, h6').count();
// Add warnings for basic issues (legacy support)
if (result.accessibility.basicChecks.imagesWithoutAlt > 0) {
result.accessibility.warnings.push({
code: 'MISSING_ALT_ATTRIBUTE',
message: `${result.accessibility.basicChecks.imagesWithoutAlt} images without alt attribute`,
type: 'warning'
});
}
if (result.accessibility.basicChecks.buttonsWithoutLabel > 0) {
result.accessibility.warnings.push({
code: 'MISSING_BUTTON_LABEL',
message: `${result.accessibility.basicChecks.buttonsWithoutLabel} buttons without aria-label`,
type: 'warning'
});
}
if (result.accessibility.basicChecks.headingsCount === 0) {
result.accessibility.errors.push({
code: 'NO_HEADINGS',
message: 'No headings found',
type: 'error'
});
}
// Run pa11y tests
try {
const pa11y = require('pa11y');
const pa11yResult = await pa11y(context.url, {
timeout: 15000,
wait: 2000,
standard: 'WCAG2AA',
includeNotices: true,
includeWarnings: true,
runners: ['axe'],
chromeLaunchConfig: {
args: [
'--disable-web-security',
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu'
]
}
});
// Add 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,
};
result.accessibility.issues.push(detailedIssue);
if (issue.type === 'error') {
result.accessibility.errors.push({
code: issue.code,
message: issue.message,
type: 'error'
});
}
else if (issue.type === 'warning') {
result.accessibility.warnings.push({
code: issue.code,
message: issue.message,
type: 'warning'
});
}
});
}
// Calculate score
if (pa11yResult.issues && pa11yResult.issues.length > 0) {
const errors = pa11yResult.issues.filter((i) => i.type === 'error').length;
const warnings = pa11yResult.issues.filter((i) => i.type === 'warning').length;
result.accessibility.score = Math.max(10, 100 - (errors * 5) - (warnings * 2));
}
}
catch (error) {
// pa11y failed, use fallback score
let score = 100;
score -= result.accessibility.errors.length * 15;
score -= result.accessibility.warnings.length * 5;
result.accessibility.score = Math.max(0, score);
}
};
exports.accessibilityAnalyzer = accessibilityAnalyzer;
const performanceAnalyzer = async (context) => {
const { page, result, options } = context;
if (!options.enablePerformanceAnalysis)
return;
try {
// Get performance metrics using browser's Performance API
const metrics = await page.evaluate(() => {
const navigation = performance.getEntriesByType('navigation')[0];
const paint = performance.getEntriesByType('paint');
const fcp = paint.find(p => p.name === 'first-contentful-paint')?.startTime || 0;
const lcp = performance.getEntriesByType('largest-contentful-paint')[0]?.startTime || 0;
return {
loadTime: navigation?.loadEventEnd - navigation?.loadEventStart || 0,
domContentLoaded: navigation?.domContentLoadedEventEnd - navigation?.domContentLoadedEventStart || 0,
renderTime: navigation?.loadEventEnd - navigation?.fetchStart || 0,
fcp,
lcp: lcp || fcp * 1.5, // Fallback estimate
cls: 0, // Would need special measurement
inp: 0, // Would need interaction
ttfb: navigation?.responseStart - navigation?.fetchStart || 0,
};
});
// Calculate performance score (simplified)
let score = 100;
if (metrics.lcp > 4000)
score -= 30;
else if (metrics.lcp > 2500)
score -= 15;
if (metrics.fcp > 3000)
score -= 20;
else if (metrics.fcp > 1800)
score -= 10;
if (metrics.ttfb > 600)
score -= 15;
const grade = score >= 90 ? 'A' : score >= 75 ? 'B' : score >= 60 ? 'C' : score >= 50 ? 'D' : 'F';
result.performance = {
score: Math.max(0, score),
grade,
coreWebVitals: {
lcp: Math.round(metrics.lcp),
fcp: Math.round(metrics.fcp),
cls: metrics.cls,
inp: metrics.inp,
ttfb: Math.round(metrics.ttfb),
},
timing: {
loadTime: Math.round(metrics.loadTime),
domContentLoaded: Math.round(metrics.domContentLoaded),
renderTime: Math.round(metrics.renderTime),
}
};
}
catch (error) {
console.error('Performance analysis failed:', error);
}
};
exports.performanceAnalyzer = performanceAnalyzer;
const seoAnalyzer = async (context) => {
const { page, result, options } = context;
if (!options.enableSEOAnalysis)
return;
try {
const seoData = await page.evaluate(() => {
// Meta tags
const title = document.querySelector('title')?.textContent || '';
const description = document.querySelector('meta[name="description"]')?.getAttribute('content') || '';
const keywords = document.querySelector('meta[name="keywords"]')?.getAttribute('content') || '';
// Open Graph
const ogTags = {};
document.querySelectorAll('meta[property^="og:"]').forEach(meta => {
const property = meta.getAttribute('property')?.replace('og:', '');
const content = meta.getAttribute('content');
if (property && content)
ogTags[property] = content;
});
// Twitter Card
const twitterTags = {};
document.querySelectorAll('meta[name^="twitter:"]').forEach(meta => {
const name = meta.getAttribute('name')?.replace('twitter:', '');
const content = meta.getAttribute('content');
if (name && content)
twitterTags[name] = content;
});
// Headings
const h1 = Array.from(document.querySelectorAll('h1')).map(h => h.textContent || '');
const h2 = Array.from(document.querySelectorAll('h2')).map(h => h.textContent || '');
const h3 = Array.from(document.querySelectorAll('h3')).map(h => h.textContent || '');
// Images
const images = document.querySelectorAll('img');
const missingAlt = Array.from(images).filter(img => !img.getAttribute('alt')).length;
const emptyAlt = Array.from(images).filter(img => img.getAttribute('alt') === '').length;
return {
title,
description,
keywords: keywords ? keywords.split(',').map(k => k.trim()) : [],
ogTags,
twitterTags,
h1,
h2,
h3,
totalImages: images.length,
missingAlt,
emptyAlt,
};
});
// Calculate SEO score
let score = 100;
const issues = [];
if (!seoData.title || seoData.title.length < 10) {
score -= 20;
issues.push('Missing or too short title tag');
}
else if (seoData.title.length > 60) {
score -= 10;
issues.push('Title tag too long');
}
if (!seoData.description || seoData.description.length < 120) {
score -= 15;
issues.push('Missing or too short meta description');
}
else if (seoData.description.length > 160) {
score -= 5;
issues.push('Meta description too long');
}
if (seoData.h1.length === 0) {
score -= 15;
issues.push('Missing H1 heading');
}
else if (seoData.h1.length > 1) {
score -= 10;
issues.push('Multiple H1 headings');
}
const grade = score >= 90 ? 'A' : score >= 75 ? 'B' : score >= 60 ? 'C' : score >= 50 ? 'D' : 'F';
result.seo = {
score: Math.max(0, score),
grade,
metaTags: {
title: seoData.title ? {
content: seoData.title,
length: seoData.title.length,
optimal: seoData.title.length >= 10 && seoData.title.length <= 60
} : undefined,
description: seoData.description ? {
content: seoData.description,
length: seoData.description.length,
optimal: seoData.description.length >= 120 && seoData.description.length <= 160
} : undefined,
keywords: seoData.keywords,
openGraph: seoData.ogTags,
twitterCard: seoData.twitterTags
},
headings: {
h1: seoData.h1,
h2: seoData.h2,
h3: seoData.h3,
issues
},
images: {
total: seoData.totalImages,
missingAlt: seoData.missingAlt,
emptyAlt: seoData.emptyAlt
}
};
}
catch (error) {
console.error('SEO analysis failed:', error);
}
};
exports.seoAnalyzer = seoAnalyzer;
//# sourceMappingURL=page-analysis-emitter.js.map