mirror of
https://github.com/AgentDeskAI/browser-tools-mcp.git
synced 2025-06-27 00:41:26 +00:00
199 lines
6.2 KiB
TypeScript
199 lines
6.2 KiB
TypeScript
import { Result as LighthouseResult } from "lighthouse";
|
|
import { AuditCategory, LighthouseReport } from "./types.js";
|
|
import { runLighthouseAudit } from "./index.js";
|
|
|
|
interface AccessibilityAudit {
|
|
id: string; // e.g., "color-contrast"
|
|
title: string; // e.g., "Color contrast is sufficient"
|
|
description: string; // e.g., "Ensures text is readable..."
|
|
score: number | null; // 0-1 (normalized), null for manual/informative
|
|
scoreDisplayMode: string; // e.g., "binary", "numeric", "manual"
|
|
details?: AuditDetails; // Optional, structured details
|
|
weight?: number; // Optional, audit weight for impact calculation
|
|
}
|
|
|
|
type AuditDetails = {
|
|
items?: Array<{
|
|
node?: {
|
|
selector: string; // e.g., ".my-class"
|
|
snippet?: string; // HTML snippet
|
|
nodeLabel?: string; // e.g., "Modify logging size limits / truncation"
|
|
explanation?: string; // Explanation of why the node fails the audit
|
|
};
|
|
value?: string | number; // Specific value (e.g., contrast ratio)
|
|
explanation?: string; // Explanation at the item level
|
|
}>;
|
|
debugData?: string; // Optional, debug information
|
|
[key: string]: any; // Flexible for other detail types (tables, etc.)
|
|
};
|
|
|
|
const FAILED_AUDITS_LIMIT = 5;
|
|
|
|
// Define a maximum number of items to include in the audit details
|
|
const MAX_ITEMS_IN_DETAILS = 3;
|
|
|
|
/**
|
|
* Runs an accessibility audit on the specified URL
|
|
* @param url The URL to audit
|
|
* @param limit Maximum number of issues to return
|
|
* @returns Promise resolving to simplified accessibility audit results
|
|
*/
|
|
export async function runAccessibilityAudit(
|
|
url: string
|
|
): Promise<LighthouseReport> {
|
|
try {
|
|
const lhr = await runLighthouseAudit(url, [AuditCategory.ACCESSIBILITY]);
|
|
const accessibilityReport = extractLhrResult(lhr, url);
|
|
return accessibilityReport;
|
|
} catch (error) {
|
|
throw new Error(
|
|
`Accessibility audit failed: ${
|
|
error instanceof Error ? error.message : String(error)
|
|
}`
|
|
);
|
|
}
|
|
}
|
|
|
|
const extractLhrResult = (
|
|
lhr: LighthouseResult,
|
|
url: string
|
|
): LighthouseReport => {
|
|
const categoryData = lhr.categories[AuditCategory.ACCESSIBILITY];
|
|
|
|
const metadata = {
|
|
url,
|
|
timestamp: lhr.fetchTime
|
|
? new Date(lhr.fetchTime).toISOString()
|
|
: new Date().toISOString(),
|
|
device: "desktop", // TODO: pass device from the request instead of hardcoding
|
|
lighthouseVersion: lhr.lighthouseVersion,
|
|
};
|
|
|
|
if (!categoryData)
|
|
return {
|
|
metadata,
|
|
failedAudits: [],
|
|
overallScore: 0,
|
|
failedAuditsCount: 0,
|
|
passedAuditsCount: 0,
|
|
manualAuditsCount: 0,
|
|
informativeAuditsCount: 0,
|
|
notApplicableAuditsCount: 0,
|
|
};
|
|
|
|
const overallScore = Math.round((categoryData.score || 0) * 100);
|
|
const auditRefs = categoryData.auditRefs || [];
|
|
const audits = lhr.audits || {};
|
|
|
|
const accessibilityAudits: AccessibilityAudit[] = auditRefs.map((ref) => {
|
|
const audit = audits[ref.id];
|
|
|
|
// Create a simplified version of the audit details
|
|
let simplifiedDetails: AuditDetails | undefined;
|
|
|
|
if (audit.details) {
|
|
simplifiedDetails = {};
|
|
|
|
// Only copy the items array if it exists
|
|
if (
|
|
(audit.details as any).items &&
|
|
Array.isArray((audit.details as any).items)
|
|
) {
|
|
// Limit the number of items to MAX_ITEMS_IN_DETAILS
|
|
const limitedItems = (audit.details as any).items.slice(
|
|
0,
|
|
MAX_ITEMS_IN_DETAILS
|
|
);
|
|
|
|
simplifiedDetails.items = limitedItems.map((item: any) => {
|
|
const simplifiedItem: any = {};
|
|
|
|
// Only include node with selector if they exist
|
|
if (item.node) {
|
|
// Include the node with all its properties
|
|
simplifiedItem.node = {
|
|
selector: item.node.selector || null,
|
|
nodeLabel: item.node.nodeLabel || null,
|
|
snippet: item.node.snippet || null,
|
|
};
|
|
// Include explanation if it exists
|
|
if (item.node.explanation) {
|
|
simplifiedItem.node.explanation = item.node.explanation;
|
|
}
|
|
}
|
|
|
|
// Include value if it exists
|
|
if (item.value !== undefined) {
|
|
simplifiedItem.value = item.value;
|
|
}
|
|
|
|
// Include explanation at the item level if it exists
|
|
if (item.explanation) {
|
|
simplifiedItem.explanation = item.explanation;
|
|
}
|
|
|
|
return simplifiedItem;
|
|
});
|
|
}
|
|
|
|
// Copy any other essential properties that might be needed
|
|
if ((audit.details as any).type) {
|
|
simplifiedDetails.type = (audit.details as any).type;
|
|
}
|
|
|
|
// Include debugData if it exists
|
|
if ((audit.details as any).debugData) {
|
|
simplifiedDetails.debugData = (audit.details as any).debugData;
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: ref.id,
|
|
title: audit.title || "Untitled",
|
|
description: audit.description || "No description",
|
|
score: audit.score, // Individual audit score (0-1 or null)
|
|
scoreDisplayMode: audit.scoreDisplayMode || "numeric",
|
|
details: simplifiedDetails,
|
|
weight: ref.weight || 1,
|
|
};
|
|
});
|
|
|
|
const failedAudits = accessibilityAudits
|
|
.filter((audit) => audit.score !== null && audit.score < 1)
|
|
.sort(
|
|
(a, b) =>
|
|
b.weight! * (1 - (b.score || 0)) - a.weight! * (1 - (a.score || 0))
|
|
)
|
|
.slice(0, FAILED_AUDITS_LIMIT);
|
|
|
|
const passedAudits = accessibilityAudits.filter(
|
|
(audit) => audit.score !== null && audit.score >= 1
|
|
);
|
|
const manualAudits = accessibilityAudits.filter(
|
|
(audit) => audit.scoreDisplayMode === "manual"
|
|
);
|
|
const informativeAudits = accessibilityAudits.filter(
|
|
(audit) => audit.scoreDisplayMode === "informative"
|
|
);
|
|
const notApplicableAudits = accessibilityAudits.filter(
|
|
(audit) => audit.scoreDisplayMode === "notApplicable"
|
|
);
|
|
|
|
const result = {
|
|
metadata,
|
|
overallScore,
|
|
failedAuditsCount: failedAudits.length,
|
|
passedAuditsCount: passedAudits.length,
|
|
manualAuditsCount: manualAudits.length,
|
|
informativeAuditsCount: informativeAudits.length,
|
|
notApplicableAuditsCount: notApplicableAudits.length,
|
|
failedAudits,
|
|
} as LighthouseReport;
|
|
|
|
console.log(result);
|
|
failedAudits.forEach((audit) => {
|
|
console.log(JSON.stringify(audit.details, null, 2));
|
|
});
|
|
return result;
|
|
};
|