import lighthouse from "lighthouse"; import type { Result as LighthouseResult, Flags } from "lighthouse"; import { connectToHeadlessBrowser, scheduleBrowserCleanup, } from "../browser-utils.js"; import { LighthouseConfig, AuditCategory, AuditIssue, LighthouseDetails, ImpactLevel, } from "./types.js"; // ===== Type Definitions ===== /** * Details about an HTML element that has accessibility issues */ export interface ElementDetails { selector: string; snippet: string; explanation: string; url: string; size: number; wastedMs: number; wastedBytes: number; } /** * Creates a Lighthouse configuration object * @param categories Array of categories to audit * @returns Lighthouse configuration and flags */ export function createLighthouseConfig( categories: string[] = [AuditCategory.ACCESSIBILITY] ): LighthouseConfig { return { flags: { output: ["json"], onlyCategories: categories, formFactor: "desktop", port: undefined as number | undefined, screenEmulation: { mobile: false, width: 1350, height: 940, deviceScaleFactor: 1, disabled: false, }, }, config: { extends: "lighthouse:default", settings: { onlyCategories: categories, emulatedFormFactor: "desktop", throttling: { cpuSlowdownMultiplier: 1 }, }, }, }; } /** * Runs a Lighthouse audit on the specified URL via CDP * @param url The URL to audit * @param categories Array of categories to audit, defaults to ["accessibility"] * @returns Promise resolving to the Lighthouse result * @throws Error if the URL is invalid or if the audit fails */ export async function runLighthouseOnExistingTab( url: string, categories: string[] = [AuditCategory.ACCESSIBILITY] ): Promise { console.log(`Starting Lighthouse ${categories.join(", ")} audit for: ${url}`); if (!url || url === "about:blank") { console.error("Invalid URL for Lighthouse audit"); throw new Error( "Cannot run audit on an empty page or about:blank. Please navigate to a valid URL first." ); } try { // Always use a dedicated headless browser for audits console.log("Using dedicated headless browser for audit"); // Determine if this is a performance audit - we need to load all resources for performance audits const isPerformanceAudit = categories.includes(AuditCategory.PERFORMANCE); // For performance audits, we want to load all resources // For accessibility or other audits, we can block non-essential resources try { const { port } = await connectToHeadlessBrowser(url, { blockResources: !isPerformanceAudit, // Don't pass an audit type - the blockResources flag is what matters }); console.log(`Connected to browser on port: ${port}`); // Create Lighthouse config const { flags, config } = createLighthouseConfig(categories); flags.port = port; console.log( `Running Lighthouse with categories: ${categories.join(", ")}` ); const runnerResult = await lighthouse(url, flags as Flags, config); console.log("Lighthouse audit completed"); if (!runnerResult?.lhr) { console.error("Lighthouse audit failed to produce results"); throw new Error("Lighthouse audit failed to produce results"); } // Schedule browser cleanup after a delay to allow for subsequent audits scheduleBrowserCleanup(); // Return the result const result = runnerResult.lhr; return result; } catch (browserError) { // Check if the error is related to Chrome/Edge not being available const errorMessage = browserError instanceof Error ? browserError.message : String(browserError); if ( errorMessage.includes("Chrome could not be found") || errorMessage.includes("Failed to launch browser") || errorMessage.includes("spawn ENOENT") ) { throw new Error( "Chrome or Edge browser could not be found. Please ensure that Chrome or Edge is installed on your system to run audits." ); } // Re-throw other errors throw browserError; } } catch (error) { console.error("Lighthouse audit failed:", error); // Schedule browser cleanup even if the audit fails scheduleBrowserCleanup(); throw new Error( `Lighthouse audit failed: ${ error instanceof Error ? error.message : String(error) }` ); } } // Export from specific audit modules export * from "./accessibility.js"; export * from "./performance.js"; export * from "./seo.js"; export * from "./types.js"; /** * Maps Lighthouse audit items to ElementDetails objects * @param items Array of audit items from Lighthouse * @param detailed Whether to include all items or limit them * @returns Array of ElementDetails objects */ export function mapAuditItemsToElements( items: Record[] = [], detailed: boolean = false ): ElementDetails[] { const elements = items.map( (item: Record) => ({ selector: ((item.node as Record)?.selector as string) || (item.selector as string) || "Unknown selector", snippet: ((item.node as Record)?.snippet as string) || (item.snippet as string) || "No snippet available", explanation: ((item.node as Record)?.explanation as string) || (item.explanation as string) || "No explanation available", url: (item.url as string) || ((item.node as Record)?.url as string) || "", size: (item.totalBytes as number) || (item.transferSize as number) || 0, wastedMs: (item.wastedMs as number) || 0, wastedBytes: (item.wastedBytes as number) || 0, } as ElementDetails) ); return detailed ? elements : elements.slice(0, 3); } /** * Creates an AuditIssue object from Lighthouse audit data * @param audit The Lighthouse audit object * @param ref The audit reference object * @param details The audit details object * @param elements Array of ElementDetails objects * @param categoryName The category name * @param impact The impact level (optional) * @returns An AuditIssue object */ export function createAuditIssue( audit: any, ref: any, details: LighthouseDetails, elements: ElementDetails[], categoryName: string, impact?: string ): AuditIssue { return { id: audit.id, title: audit.title, description: audit.description, score: audit.score || 0, details: { type: details?.type || "unknown" }, category: categoryName, wcagReference: ref.relevantAudits || [], impact: impact || ((details?.items?.[0] as Record)?.impact as string) || ImpactLevel.MODERATE, elements: elements, failureSummary: ((details?.items?.[0] as Record) ?.failureSummary as string) || audit.explanation || "No failure summary available", recommendations: [], }; } /** * Creates audit metadata from Lighthouse results * @param lhr The Lighthouse result object * @returns Audit metadata object */ export function createAuditMetadata(lhr: LighthouseResult): any { return { fetchTime: lhr.fetchTime || new Date().toISOString(), url: lhr.finalUrl || "Unknown URL", deviceEmulation: "desktop", categories: Object.keys(lhr.categories), totalAudits: Object.keys(lhr.audits || {}).length, passedAudits: Object.values(lhr.audits || {}).filter( (audit) => audit.score === 1 ).length, failedAudits: Object.values(lhr.audits || {}).filter( (audit) => audit.score !== null && audit.score < 1 ).length, }; }