enhance contrast issue detection

This commit is contained in:
Josh at WLTechBlog
2025-12-08 13:51:29 -07:00
parent ae0a3a789e
commit fb94daaef3
2 changed files with 741 additions and 30 deletions

View File

@@ -8997,19 +8997,21 @@ type ContrastCheckResult struct {
// ContrastCheckElement represents a single element's contrast check
type ContrastCheckElement struct {
Selector string `json:"selector"`
Text string `json:"text"`
ForegroundColor string `json:"foreground_color"`
BackgroundColor string `json:"background_color"`
ContrastRatio float64 `json:"contrast_ratio"`
FontSize string `json:"font_size"`
FontWeight string `json:"font_weight"`
IsLargeText bool `json:"is_large_text"`
PassesAA bool `json:"passes_aa"`
PassesAAA bool `json:"passes_aaa"`
RequiredAA float64 `json:"required_aa"`
RequiredAAA float64 `json:"required_aaa"`
Error string `json:"error,omitempty"`
Selector string `json:"selector"`
Text string `json:"text"`
ForegroundColor string `json:"foreground_color"`
BackgroundColor string `json:"background_color"`
ContrastRatio float64 `json:"contrast_ratio"`
FontSize string `json:"font_size"`
FontWeight string `json:"font_weight"`
IsLargeText bool `json:"is_large_text"`
PassesAA bool `json:"passes_aa"`
PassesAAA bool `json:"passes_aaa"`
RequiredAA float64 `json:"required_aa"`
RequiredAAA float64 `json:"required_aaa"`
DetectionMethod string `json:"detection_method,omitempty"` // "dom-tree", "stacking-context", "gradient-analysis"
GradientInfo map[string]interface{} `json:"gradient_info,omitempty"` // Additional info for gradient backgrounds
Error string `json:"error,omitempty"`
}
// checkContrast checks color contrast for text elements on the page
@@ -9062,24 +9064,153 @@ func (d *Daemon) checkContrast(tabID string, selector string, timeout int) (*Con
return (lighter + 0.05) / (darker + 0.05);
}
// Helper function to get effective background color
function getEffectiveBackground(element) {
// Helper function to check if color is transparent
function isTransparent(colorStr) {
if (!colorStr || colorStr === 'transparent' || colorStr === 'rgba(0, 0, 0, 0)') {
return true;
}
const parsed = parseColor(colorStr);
return !parsed || parsed.a === 0;
}
// Tier 1: Enhanced DOM tree walking
function getEffectiveBackgroundDOMTree(element) {
let current = element;
while (current && current !== document.body.parentElement) {
while (current && current !== document.documentElement.parentElement) {
const style = window.getComputedStyle(current);
const bgColor = style.backgroundColor;
const parsed = parseColor(bgColor);
if (parsed && parsed.a > 0) {
// Check if it's not transparent
if (!(parsed.r === 0 && parsed.g === 0 && parsed.b === 0 && parsed.a === 0)) {
return bgColor;
}
if (!isTransparent(bgColor)) {
return bgColor;
}
current = current.parentElement;
}
return 'rgb(255, 255, 255)'; // Default to white
return null; // Return null if no background found
}
// Tier 2: Stacking context analysis with multi-point sampling
function getBackgroundFromStackingContext(element) {
try {
const rect = element.getBoundingClientRect();
// Skip if element is not visible
if (rect.width === 0 || rect.height === 0) {
return null;
}
// Sample multiple points (center, corners) for accuracy
const samplePoints = [
{ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }, // center
{ x: rect.left + rect.width * 0.25, y: rect.top + rect.height * 0.25 }, // top-left
{ x: rect.left + rect.width * 0.75, y: rect.top + rect.height * 0.75 }, // bottom-right
{ x: rect.left + rect.width * 0.75, y: rect.top + rect.height * 0.25 }, // top-right
{ x: rect.left + rect.width * 0.25, y: rect.top + rect.height * 0.75 } // bottom-left
];
const backgrounds = [];
for (const point of samplePoints) {
// Make sure point is within viewport
if (point.x < 0 || point.y < 0 || point.x > window.innerWidth || point.y > window.innerHeight) {
continue;
}
// Get all elements at this point (in z-index order)
const elementsAtPoint = document.elementsFromPoint(point.x, point.y);
// Find the first element with a non-transparent background
for (const el of elementsAtPoint) {
if (el === element) continue; // Skip the text element itself
const bgColor = window.getComputedStyle(el).backgroundColor;
if (!isTransparent(bgColor)) {
backgrounds.push(bgColor);
break;
}
}
}
// Return the most common background color found
if (backgrounds.length > 0) {
// Count occurrences
const colorCounts = {};
backgrounds.forEach(color => {
colorCounts[color] = (colorCounts[color] || 0) + 1;
});
// Find most common
let maxCount = 0;
let mostCommon = backgrounds[0];
for (const color in colorCounts) {
if (colorCounts[color] > maxCount) {
maxCount = colorCounts[color];
mostCommon = color;
}
}
return mostCommon;
}
return null;
} catch (e) {
return null;
}
}
// Tier 3: Gradient analysis
function analyzeGradientContrast(element, textColor) {
try {
const style = window.getComputedStyle(element);
const bgImage = style.backgroundImage;
// Check if background is a gradient
if (!bgImage || !bgImage.includes('gradient')) {
return null;
}
// Extract gradient colors using regex
const colorRegex = /rgba?\([^)]+\)|#[0-9a-f]{3,6}/gi;
const matches = bgImage.match(colorRegex) || [];
// Fix incomplete rgb/rgba matches by adding closing paren
const colors = matches.map(c => c.includes('(') && !c.includes(')') ? c + ')' : c);
if (colors.length === 0) {
return null;
}
// Calculate contrast against each color in the gradient
const fg = parseColor(textColor);
if (!fg) return null;
const contrastRatios = [];
const parsedColors = [];
for (const colorStr of colors) {
const bg = parseColor(colorStr);
if (bg) {
const ratio = getContrastRatio(fg, bg);
contrastRatios.push(ratio);
parsedColors.push(colorStr);
}
}
if (contrastRatios.length === 0) {
return null;
}
// Return worst-case (minimum) contrast ratio
const worstCase = Math.min(...contrastRatios);
const bestCase = Math.max(...contrastRatios);
return {
worstCase: worstCase,
bestCase: bestCase,
colors: parsedColors,
isGradient: true
};
} catch (e) {
return null;
}
}
// Helper function to check if text is large
@@ -9104,10 +9235,42 @@ func (d *Daemon) checkContrast(tabID string, selector string, timeout int) (*Con
// Get computed styles
const style = window.getComputedStyle(element);
const fgColor = style.color;
const bgColor = getEffectiveBackground(element);
const fontSize = style.fontSize;
const fontWeight = style.fontWeight;
// Three-tier background detection with progressive fallback
let bgColor = null;
let detectionMethod = null;
let gradientInfo = null;
// Tier 1: DOM tree walking
bgColor = getEffectiveBackgroundDOMTree(element);
if (bgColor) {
detectionMethod = 'dom-tree';
}
// Tier 2: Stacking context analysis (if Tier 1 failed)
if (!bgColor) {
bgColor = getBackgroundFromStackingContext(element);
if (bgColor) {
detectionMethod = 'stacking-context';
}
}
// Tier 3: Check for gradient backgrounds
if (bgColor) {
gradientInfo = analyzeGradientContrast(element, fgColor);
if (gradientInfo) {
detectionMethod = 'gradient-analysis';
}
}
// Final fallback to white if no background detected
if (!bgColor) {
bgColor = 'rgb(255, 255, 255)';
detectionMethod = 'fallback-white';
}
// Parse colors
const fg = parseColor(fgColor);
const bg = parseColor(bgColor);
@@ -9116,20 +9279,27 @@ func (d *Daemon) checkContrast(tabID string, selector string, timeout int) (*Con
results.push({
selector: '%s:nth-of-type(' + (index + 1) + ')',
text: text.substring(0, 100),
error: 'Unable to parse colors'
error: 'Unable to parse colors',
detection_method: detectionMethod
});
return;
}
// Calculate contrast ratio
const ratio = getContrastRatio(fg, bg);
// Calculate contrast ratio (use worst case for gradients)
let ratio;
if (gradientInfo) {
ratio = gradientInfo.worstCase;
} else {
ratio = getContrastRatio(fg, bg);
}
const large = isLargeText(fontSize, fontWeight);
// WCAG requirements
const requiredAA = large ? 3.0 : 4.5;
const requiredAAA = large ? 4.5 : 7.0;
results.push({
const result = {
selector: '%s:nth-of-type(' + (index + 1) + ')',
text: text.substring(0, 100),
foreground_color: fgColor,
@@ -9141,8 +9311,21 @@ func (d *Daemon) checkContrast(tabID string, selector string, timeout int) (*Con
passes_aa: ratio >= requiredAA,
passes_aaa: ratio >= requiredAAA,
required_aa: requiredAA,
required_aaa: requiredAAA
});
required_aaa: requiredAAA,
detection_method: detectionMethod
};
// Add gradient info if present
if (gradientInfo) {
result.gradient_info = {
worst_contrast: Math.round(gradientInfo.worstCase * 100) / 100,
best_contrast: Math.round(gradientInfo.bestCase * 100) / 100,
colors: gradientInfo.colors,
is_gradient: true
};
}
results.push(result);
} catch (e) {
results.push({
selector: '%s:nth-of-type(' + (index + 1) + ')',