arela
Version:
AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.
152 lines • 5.81 kB
JavaScript
export async function analyzeWithRules(page) {
const issues = [];
// 1. Check contrast ratios (WCAG AA: 4.5:1, AAA: 7:1)
const contrastIssues = await checkContrast(page);
issues.push(...contrastIssues);
// 2. Check touch target sizes (minimum 44x44px)
const sizeIssues = await checkTouchTargets(page);
issues.push(...sizeIssues);
// 3. Check for missing alt text
const altIssues = await checkAltText(page);
issues.push(...altIssues);
// 4. Check for proper heading hierarchy
const headingIssues = await checkHeadings(page);
issues.push(...headingIssues);
// Calculate scores
const critical = issues.filter(i => i.severity === 'critical').length;
const warnings = issues.filter(i => i.severity === 'warning').length;
const wcagScore = Math.max(0, 100 - (critical * 20) - (warnings * 5));
const uxScore = Math.max(0, 100 - (critical * 15) - (warnings * 5));
return {
issues,
scores: {
wcag: wcagScore,
ux: uxScore,
performance: 100, // Will be calculated from Playwright metrics
},
};
}
async function checkContrast(page) {
const issues = [];
// Get all text elements
const elements = await page.$$('body *');
for (const element of elements) {
const text = await element.textContent();
if (!text || text.trim().length === 0)
continue;
// Get computed styles
const styles = await element.evaluate((el) => {
const computed = globalThis.window.getComputedStyle(el);
return {
color: computed.color,
backgroundColor: computed.backgroundColor,
fontSize: computed.fontSize,
};
});
// Calculate contrast ratio
const ratio = calculateContrastRatio(styles.color, styles.backgroundColor);
const fontSize = parseFloat(styles.fontSize);
// WCAG AA: 4.5:1 for normal text, 3:1 for large text (18pt+)
const minRatio = fontSize >= 18 ? 3.0 : 4.5;
if (ratio < minRatio) {
const tagName = await element.evaluate(el => el.tagName);
issues.push({
severity: ratio < 3.0 ? 'critical' : 'warning',
category: 'wcag',
message: `Low contrast ratio: ${ratio.toFixed(1)}:1 (needs ${minRatio}:1)`,
suggestion: 'Increase text darkness or background lightness',
element: tagName,
});
}
}
return issues;
}
async function checkTouchTargets(page) {
const issues = [];
// Get all interactive elements
const interactiveElements = await page.$$('button, a, input, select, textarea');
for (const element of interactiveElements) {
const box = await element.boundingBox();
if (!box)
continue;
// Minimum touch target: 44x44px (iOS), 48x48px (Android)
const minSize = 44;
if (box.width < minSize || box.height < minSize) {
const tagName = await element.evaluate(el => el.tagName);
issues.push({
severity: box.width < 32 || box.height < 32 ? 'critical' : 'warning',
category: 'ux',
message: `Touch target too small: ${Math.round(box.width)}x${Math.round(box.height)}px`,
suggestion: `Increase to at least ${minSize}x${minSize}px`,
element: tagName,
});
}
}
return issues;
}
async function checkAltText(page) {
const issues = [];
const images = await page.$$('img');
for (const img of images) {
const alt = await img.getAttribute('alt');
const src = await img.getAttribute('src');
if (alt === null || alt.trim() === '') {
issues.push({
severity: 'warning',
category: 'wcag',
message: 'Image missing alt text',
suggestion: 'Add descriptive alt text for screen readers',
element: src || 'img',
});
}
}
return issues;
}
async function checkHeadings(page) {
const issues = [];
const headings = await page.$$('h1, h2, h3, h4, h5, h6');
const levels = [];
for (const heading of headings) {
const tagName = await heading.evaluate(el => el.tagName);
const level = parseInt(tagName[1]);
levels.push(level);
}
// Check for skipped levels (h1 -> h3 without h2)
for (let i = 1; i < levels.length; i++) {
if (levels[i] - levels[i - 1] > 1) {
issues.push({
severity: 'info',
category: 'wcag',
message: `Heading level skipped: h${levels[i - 1]} to h${levels[i]}`,
suggestion: 'Use sequential heading levels for proper document structure',
});
}
}
return issues;
}
function calculateContrastRatio(color1, color2) {
// Parse RGB values
const rgb1 = parseRGB(color1);
const rgb2 = parseRGB(color2);
// Calculate relative luminance
const l1 = getRelativeLuminance(rgb1);
const l2 = getRelativeLuminance(rgb2);
// Calculate contrast ratio
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
function parseRGB(color) {
const match = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
if (!match)
return [0, 0, 0];
return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])];
}
function getRelativeLuminance([r, g, b]) {
const [rs, gs, bs] = [r, g, b].map(c => {
c = c / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}
//# sourceMappingURL=rules.js.map