import { chromium } from "playwright"; import type { Integration } from "muddle/contracts"; import { inspectDocument } from "./inspect.ts"; export interface AuditFinding { rule: string; selector: string; severity: "error" | "warning" | "review"; message: string; evidence: Record; } export interface AuditReport { viewport: { width: number; height: number }; findings: AuditFinding[]; score: number; checked: number; method: "static-rendered-heuristic"; } export interface AuditOptions { viewports?: { width: number; height: number }[]; ignore?: string[]; failBelow?: number; failOnError?: boolean; includeDisclosures?: boolean; } export async function analyzeHtml( html: string, kind: "accessible" | "overflow", options: AuditOptions = {}, ): Promise { const browser = await chromium.launch(); try { const reports: AuditReport[] = []; for (const viewport of options.viewports ?? [ { width: 1440, height: 900 }, { width: 390, height: 844 }, ]) { const context = await browser.newContext({ viewport, javaScriptEnabled: false, }); try { const page = await context.newPage(); await page.route("**/*", (route) => route.abort()); await page.setContent(html, { waitUntil: "load" }); const result = await page.evaluate(inspectDocument, { kind, ignore: options.ignore ?? [], disclosures: options.includeDisclosures ?? false, }); const findings = result.findings as AuditFinding[]; const score = Math.max( 0, Math.round( 100 - findings.reduce( (sum, finding) => sum + (finding.severity === "error" ? 10 : finding.severity === "warning" ? 4 : 2), 0, ), ), ); const report: AuditReport = { viewport, findings, score, checked: result.checked, method: "static-rendered-heuristic", }; reports.push(report); if (options.failBelow !== undefined && score < options.failBelow) { throw new Error( `${kind} score ${score} below ${options.failBelow} at ${viewport.width}px`, ); } if ( options.failOnError && findings.some((finding) => finding.severity === "error") ) { throw new Error(`${kind} audit found errors at ${viewport.width}px`); } } finally { await context.close(); } } return reports; } finally { await browser.close(); } } export function accessibleIntegration(options: AuditOptions = {}): Integration { return { name: "accessible", analyze: (page) => analyzeHtml(page.html, "accessible", options), }; } export function overflowIntegration(options: AuditOptions = {}): Integration { return { name: "overflow", analyze: (page) => analyzeHtml(page.html, "overflow", options), }; }