enhance contrast issue detection
This commit is contained in:
528
contrast_detection_enhancement.md
Normal file
528
contrast_detection_enhancement.md
Normal file
@@ -0,0 +1,528 @@
|
|||||||
|
# Feature Request: Enhanced Background Color Detection for Contrast Analysis
|
||||||
|
|
||||||
|
**Date:** December 8, 2025
|
||||||
|
**Requested By:** Shortcut Solutions (ADA Audit Team)
|
||||||
|
**Priority:** Medium
|
||||||
|
**Estimated ROI:** High (saves 1-2 hours per audit)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
The current `web_contrast_check_cremotemcp_cremotemcp` tool frequently fails to determine background colors when elements overlap or have transparent backgrounds. This results in:
|
||||||
|
|
||||||
|
1. **False "failures"** that require manual verification
|
||||||
|
2. **Increased audit time** (1-2 hours per site for manual verification)
|
||||||
|
3. **Reduced confidence** in automated results
|
||||||
|
4. **Poor client experience** (reports show "requires manual verification" instead of definitive results)
|
||||||
|
|
||||||
|
### Real-World Example
|
||||||
|
|
||||||
|
During the visionleadership.org audit (December 8, 2025):
|
||||||
|
- **78 elements flagged** for contrast issues (27% of 289 elements)
|
||||||
|
- **Actual issues:** Likely 0-5 (most are false positives)
|
||||||
|
- **Root cause:** Tool couldn't determine background color due to:
|
||||||
|
- Overlapping header elements
|
||||||
|
- Transparent navigation backgrounds
|
||||||
|
- Gradient backgrounds on sliders
|
||||||
|
- Complex z-index stacking contexts
|
||||||
|
|
||||||
|
### Current Behavior
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Current approach (simplified)
|
||||||
|
const bgColor = window.getComputedStyle(element).backgroundColor;
|
||||||
|
|
||||||
|
// Problem: Returns 'transparent' or 'rgba(0, 0, 0, 0)' for many elements
|
||||||
|
// Result: Cannot calculate contrast ratio → flagged as "requires verification"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
|
||||||
|
Implement a **three-tier background detection algorithm** with progressive fallback:
|
||||||
|
|
||||||
|
### Tier 1: Enhanced DOM Tree Walking (Priority 1)
|
||||||
|
**Effort:** Low (2-4 hours)
|
||||||
|
**Impact:** Solves 80% of cases
|
||||||
|
|
||||||
|
Walk up the DOM tree to find the first non-transparent background:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function getEffectiveBackgroundColor(element) {
|
||||||
|
let current = element;
|
||||||
|
let bgColor = window.getComputedStyle(current).backgroundColor;
|
||||||
|
|
||||||
|
// Walk up DOM tree until we find a non-transparent background
|
||||||
|
while (current && (bgColor === 'transparent' || bgColor === 'rgba(0, 0, 0, 0)')) {
|
||||||
|
current = current.parentElement;
|
||||||
|
if (current) {
|
||||||
|
bgColor = window.getComputedStyle(current).backgroundColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still transparent, default to white (common body background)
|
||||||
|
if (bgColor === 'transparent' || bgColor === 'rgba(0, 0, 0, 0)') {
|
||||||
|
bgColor = 'rgb(255, 255, 255)';
|
||||||
|
}
|
||||||
|
|
||||||
|
return bgColor;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tier 2: Element Stacking Context Analysis (Priority 2)
|
||||||
|
**Effort:** Medium (4-8 hours)
|
||||||
|
**Impact:** Solves 95% of cases
|
||||||
|
|
||||||
|
Use `document.elementsFromPoint()` to find what's actually behind the text:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function getBackgroundFromStackingContext(element) {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
|
||||||
|
// 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
|
||||||
|
];
|
||||||
|
|
||||||
|
const backgrounds = [];
|
||||||
|
|
||||||
|
for (const point of samplePoints) {
|
||||||
|
// 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 (bgColor !== 'transparent' && bgColor !== 'rgba(0, 0, 0, 0)') {
|
||||||
|
backgrounds.push(bgColor);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the most common background color found
|
||||||
|
return getMostCommonColor(backgrounds) || 'rgb(255, 255, 255)';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tier 3: Gradient and Complex Background Handling (Priority 3)
|
||||||
|
**Effort:** Medium-High (8-12 hours)
|
||||||
|
**Impact:** Solves 99% of cases
|
||||||
|
|
||||||
|
For gradient backgrounds, calculate worst-case contrast:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function analyzeGradientContrast(element) {
|
||||||
|
const bgImage = window.getComputedStyle(element).backgroundImage;
|
||||||
|
|
||||||
|
// Check if background is a gradient
|
||||||
|
if (bgImage.includes('gradient')) {
|
||||||
|
// Extract gradient colors using regex
|
||||||
|
const colors = extractGradientColors(bgImage);
|
||||||
|
|
||||||
|
// Calculate contrast against each color in the gradient
|
||||||
|
const textColor = window.getComputedStyle(element).color;
|
||||||
|
const contrastRatios = colors.map(bgColor =>
|
||||||
|
calculateContrastRatio(textColor, bgColor)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return worst-case (minimum) contrast ratio
|
||||||
|
return {
|
||||||
|
worstCase: Math.min(...contrastRatios),
|
||||||
|
bestCase: Math.max(...contrastRatios),
|
||||||
|
isGradient: true,
|
||||||
|
colors: colors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // Not a gradient
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractGradientColors(gradientString) {
|
||||||
|
// Parse gradient string to extract color stops
|
||||||
|
// Example: "linear-gradient(90deg, rgb(255,0,0) 0%, rgb(0,0,255) 100%)"
|
||||||
|
const colorRegex = /rgba?\([^)]+\)|#[0-9a-f]{3,6}/gi;
|
||||||
|
return gradientString.match(colorRegex) || [];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Core Enhancement (Week 1)
|
||||||
|
**Effort:** 2-4 hours
|
||||||
|
|
||||||
|
1. Implement Tier 1 (DOM tree walking)
|
||||||
|
2. Update `web_contrast_check_cremotemcp_cremotemcp` to use new function
|
||||||
|
3. Add fallback to existing method if new method fails
|
||||||
|
4. Test on 5-10 real-world sites
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
- Updated contrast checking function
|
||||||
|
- Unit tests for DOM tree walking
|
||||||
|
- Documentation of new behavior
|
||||||
|
|
||||||
|
### Phase 2: Stacking Context Analysis (Week 2)
|
||||||
|
**Effort:** 4-8 hours
|
||||||
|
|
||||||
|
1. Implement Tier 2 (elementsFromPoint)
|
||||||
|
2. Add multi-point sampling for accuracy
|
||||||
|
3. Handle edge cases (fixed positioning, transforms)
|
||||||
|
4. Test on complex layouts
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
- Stacking context analysis function
|
||||||
|
- Integration tests with overlapping elements
|
||||||
|
- Performance benchmarks
|
||||||
|
|
||||||
|
### Phase 3: Gradient Support (Week 3-4)
|
||||||
|
**Effort:** 8-12 hours
|
||||||
|
|
||||||
|
1. Implement Tier 3 (gradient parsing)
|
||||||
|
2. Add worst-case contrast calculation
|
||||||
|
3. Handle CSS gradients, background images
|
||||||
|
4. Test on sites with complex backgrounds
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
- Gradient analysis function
|
||||||
|
- Comprehensive test suite
|
||||||
|
- Documentation with examples
|
||||||
|
|
||||||
|
### Phase 4: Integration and Testing (Week 4)
|
||||||
|
**Effort:** 4-6 hours
|
||||||
|
|
||||||
|
1. Integrate all three tiers with progressive fallback
|
||||||
|
2. Add configuration options (enable/disable tiers)
|
||||||
|
3. Performance optimization
|
||||||
|
4. Documentation and examples
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
- Complete integrated solution
|
||||||
|
- Performance benchmarks
|
||||||
|
- User documentation
|
||||||
|
- Migration guide
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Specifications
|
||||||
|
|
||||||
|
### New Function Signature
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* Enhanced contrast checking with improved background detection
|
||||||
|
* @param {string} selector - CSS selector for elements to check
|
||||||
|
* @param {Object} options - Configuration options
|
||||||
|
* @param {boolean} options.useStackingContext - Enable Tier 2 (default: true)
|
||||||
|
* @param {boolean} options.analyzeGradients - Enable Tier 3 (default: true)
|
||||||
|
* @param {number} options.samplePoints - Number of points to sample (default: 3)
|
||||||
|
* @param {string} options.fallbackBg - Fallback background color (default: 'rgb(255, 255, 255)')
|
||||||
|
* @returns {Object} Enhanced contrast analysis results
|
||||||
|
*/
|
||||||
|
async function enhancedContrastCheck(selector, options = {}) {
|
||||||
|
const elements = document.querySelectorAll(selector);
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (const element of elements) {
|
||||||
|
const textColor = window.getComputedStyle(element).color;
|
||||||
|
let bgColor = null;
|
||||||
|
let method = null;
|
||||||
|
|
||||||
|
// Tier 1: DOM tree walking
|
||||||
|
bgColor = getEffectiveBackgroundColor(element);
|
||||||
|
method = 'dom-tree';
|
||||||
|
|
||||||
|
// Tier 2: Stacking context (if enabled and Tier 1 returned transparent)
|
||||||
|
if (options.useStackingContext && (bgColor === 'transparent' || bgColor === 'rgba(0, 0, 0, 0)')) {
|
||||||
|
bgColor = getBackgroundFromStackingContext(element);
|
||||||
|
method = 'stacking-context';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 3: Gradient analysis (if enabled)
|
||||||
|
let gradientInfo = null;
|
||||||
|
if (options.analyzeGradients) {
|
||||||
|
gradientInfo = analyzeGradientContrast(element);
|
||||||
|
if (gradientInfo) {
|
||||||
|
method = 'gradient-analysis';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate contrast ratio
|
||||||
|
const contrastRatio = gradientInfo
|
||||||
|
? gradientInfo.worstCase
|
||||||
|
: calculateContrastRatio(textColor, bgColor);
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
element: element,
|
||||||
|
selector: getElementSelector(element),
|
||||||
|
textColor: textColor,
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
contrastRatio: contrastRatio,
|
||||||
|
detectionMethod: method,
|
||||||
|
gradientInfo: gradientInfo,
|
||||||
|
passes: {
|
||||||
|
AA: contrastRatio >= 4.5,
|
||||||
|
AAA: contrastRatio >= 7.0,
|
||||||
|
AALarge: contrastRatio >= 3.0,
|
||||||
|
AAALarge: contrastRatio >= 4.5
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value Structure
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
element: HTMLElement,
|
||||||
|
selector: "nav > ul > li > a",
|
||||||
|
textColor: "rgb(51, 51, 51)",
|
||||||
|
backgroundColor: "rgb(255, 255, 255)",
|
||||||
|
contrastRatio: 12.63,
|
||||||
|
detectionMethod: "stacking-context", // or "dom-tree", "gradient-analysis"
|
||||||
|
gradientInfo: null, // or { worstCase, bestCase, isGradient, colors }
|
||||||
|
passes: {
|
||||||
|
AA: true,
|
||||||
|
AAA: true,
|
||||||
|
AALarge: true,
|
||||||
|
AAALarge: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Expected Benefits
|
||||||
|
|
||||||
|
### Quantitative Benefits
|
||||||
|
|
||||||
|
1. **Reduced False Positives:** 80-95% reduction in "requires manual verification" flags
|
||||||
|
2. **Time Savings:** 1-2 hours per audit (manual verification eliminated)
|
||||||
|
3. **Accuracy Improvement:** 95%+ accuracy vs current ~70%
|
||||||
|
4. **Client Satisfaction:** Fewer ambiguous results in reports
|
||||||
|
|
||||||
|
### Qualitative Benefits
|
||||||
|
|
||||||
|
1. **Increased Confidence:** Clients trust automated results more
|
||||||
|
2. **Better Reports:** Definitive pass/fail instead of "requires verification"
|
||||||
|
3. **Competitive Advantage:** More accurate than other automated tools
|
||||||
|
4. **Reduced Support:** Fewer questions about flagged items
|
||||||
|
|
||||||
|
### Cost-Benefit Analysis
|
||||||
|
|
||||||
|
**Development Cost:** 18-30 hours ($1,800-$3,000 at $100/hour)
|
||||||
|
**Time Savings:** 1.5 hours per audit × 50 audits/year = 75 hours/year ($7,500/year)
|
||||||
|
**ROI:** 150-250% in first year
|
||||||
|
**Payback Period:** 3-4 months
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
describe('Enhanced Background Detection', () => {
|
||||||
|
test('should find background from parent element', () => {
|
||||||
|
// Test DOM tree walking
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect background from overlapping element', () => {
|
||||||
|
// Test stacking context analysis
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should calculate worst-case contrast for gradients', () => {
|
||||||
|
// Test gradient analysis
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fallback gracefully when detection fails', () => {
|
||||||
|
// Test fallback behavior
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
|
||||||
|
1. **Simple Layouts:** Text on solid backgrounds
|
||||||
|
2. **Overlapping Elements:** Navigation menus, headers
|
||||||
|
3. **Transparent Backgrounds:** Inherited backgrounds
|
||||||
|
4. **Gradient Backgrounds:** Linear and radial gradients
|
||||||
|
5. **Complex Stacking:** Multiple z-index layers
|
||||||
|
6. **Fixed/Absolute Positioning:** Overlays, modals
|
||||||
|
|
||||||
|
### Real-World Testing
|
||||||
|
|
||||||
|
Test on 10-20 production websites including:
|
||||||
|
- E-commerce sites (complex headers)
|
||||||
|
- News sites (overlapping content)
|
||||||
|
- Corporate sites (gradient backgrounds)
|
||||||
|
- Educational sites (accessibility plugins)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backwards Compatibility
|
||||||
|
|
||||||
|
### Migration Strategy
|
||||||
|
|
||||||
|
1. **Default Behavior:** New algorithm enabled by default
|
||||||
|
2. **Opt-Out Option:** Add `legacy_mode: true` to use old algorithm
|
||||||
|
3. **Gradual Rollout:** Test on internal audits first, then production
|
||||||
|
4. **Comparison Mode:** Run both algorithms and compare results
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Enable all enhancements (recommended)
|
||||||
|
web_contrast_check_cremotemcp_cremotemcp({
|
||||||
|
selector: 'body *',
|
||||||
|
enhanced_detection: true,
|
||||||
|
use_stacking_context: true,
|
||||||
|
analyze_gradients: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Legacy mode (old behavior)
|
||||||
|
web_contrast_check_cremotemcp_cremotemcp({
|
||||||
|
selector: 'body *',
|
||||||
|
legacy_mode: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Custom configuration
|
||||||
|
web_contrast_check_cremotemcp_cremotemcp({
|
||||||
|
selector: 'body *',
|
||||||
|
enhanced_detection: true,
|
||||||
|
use_stacking_context: true,
|
||||||
|
analyze_gradients: false, // Skip gradient analysis for speed
|
||||||
|
sample_points: 5,
|
||||||
|
fallback_bg: 'rgb(255, 255, 255)'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### Current Performance
|
||||||
|
- **Time per element:** ~5-10ms
|
||||||
|
- **289 elements:** ~1.5-3 seconds
|
||||||
|
|
||||||
|
### Expected Performance with Enhancements
|
||||||
|
- **Tier 1 (DOM tree):** +1-2ms per element
|
||||||
|
- **Tier 2 (stacking context):** +3-5ms per element
|
||||||
|
- **Tier 3 (gradient):** +5-10ms per element (only when gradients detected)
|
||||||
|
|
||||||
|
**Total:** ~2-5 seconds for 289 elements (acceptable)
|
||||||
|
|
||||||
|
### Optimization Strategies
|
||||||
|
|
||||||
|
1. **Lazy Evaluation:** Only use Tier 2/3 when Tier 1 fails
|
||||||
|
2. **Caching:** Cache computed styles for repeated elements
|
||||||
|
3. **Parallel Processing:** Process elements in batches
|
||||||
|
4. **Early Exit:** Stop at first successful detection method
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation Requirements
|
||||||
|
|
||||||
|
### User Documentation
|
||||||
|
|
||||||
|
1. **Feature Overview:** What's new and why it matters
|
||||||
|
2. **Configuration Guide:** How to enable/disable features
|
||||||
|
3. **Examples:** Before/after comparisons
|
||||||
|
4. **Troubleshooting:** Common issues and solutions
|
||||||
|
|
||||||
|
### Developer Documentation
|
||||||
|
|
||||||
|
1. **Architecture:** How the three tiers work
|
||||||
|
2. **API Reference:** Function signatures and parameters
|
||||||
|
3. **Testing Guide:** How to test the new features
|
||||||
|
4. **Performance Guide:** Optimization tips
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
### Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] Reduces false positives by 80%+ on test suite
|
||||||
|
- [ ] Maintains or improves performance (< 5 seconds for 300 elements)
|
||||||
|
- [ ] Passes all unit and integration tests
|
||||||
|
- [ ] Successfully tested on 10+ real-world sites
|
||||||
|
- [ ] Documentation complete and reviewed
|
||||||
|
- [ ] Backwards compatible with legacy mode
|
||||||
|
|
||||||
|
### Post-Launch Monitoring
|
||||||
|
|
||||||
|
1. **Accuracy Tracking:** Compare automated vs manual verification results
|
||||||
|
2. **Performance Monitoring:** Track execution time per audit
|
||||||
|
3. **User Feedback:** Collect feedback from audit team
|
||||||
|
4. **Error Rates:** Monitor detection failures and edge cases
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risks and Mitigations
|
||||||
|
|
||||||
|
### Risk 1: Performance Degradation
|
||||||
|
**Likelihood:** Low
|
||||||
|
**Impact:** Medium
|
||||||
|
**Mitigation:** Implement lazy evaluation, caching, and performance benchmarks
|
||||||
|
|
||||||
|
### Risk 2: New False Positives
|
||||||
|
**Likelihood:** Low
|
||||||
|
**Impact:** Medium
|
||||||
|
**Mitigation:** Extensive testing, gradual rollout, legacy mode fallback
|
||||||
|
|
||||||
|
### Risk 3: Browser Compatibility
|
||||||
|
**Likelihood:** Low
|
||||||
|
**Impact:** Low
|
||||||
|
**Mitigation:** Test on Chrome, Firefox, Safari; use polyfills if needed
|
||||||
|
|
||||||
|
### Risk 4: Complex Edge Cases
|
||||||
|
**Likelihood:** Medium
|
||||||
|
**Impact:** Low
|
||||||
|
**Mitigation:** Progressive fallback, comprehensive test suite, manual verification option
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### Phase 5: Machine Learning (Future)
|
||||||
|
- Train ML model on manually verified results
|
||||||
|
- Predict contrast issues with higher accuracy
|
||||||
|
- Learn from false positives/negatives
|
||||||
|
|
||||||
|
### Phase 6: Screenshot-Based Analysis (Future)
|
||||||
|
- Capture actual rendered pixels
|
||||||
|
- Use image processing for perfect accuracy
|
||||||
|
- Handle all edge cases (animations, transforms, etc.)
|
||||||
|
|
||||||
|
### Phase 7: Real-Time Validation (Future)
|
||||||
|
- Integrate with browser DevTools
|
||||||
|
- Provide live feedback during development
|
||||||
|
- Suggest color adjustments for compliance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
This enhancement will significantly improve the accuracy and reliability of automated contrast checking, reducing manual verification time and increasing client confidence in audit results. The three-tier approach provides a good balance between accuracy, performance, and complexity.
|
||||||
|
|
||||||
|
**Recommendation:** Proceed with implementation starting with Phase 1 (Tier 1 DOM tree walking) as a quick win, then evaluate results before proceeding to Phases 2-3.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Feature Request Prepared By:** Shortcut Solutions
|
||||||
|
**Date:** December 8, 2025
|
||||||
|
**Status:** Pending Review
|
||||||
|
**Priority:** Medium
|
||||||
|
**Estimated ROI:** High (150-250% first year)
|
||||||
|
|
||||||
243
daemon/daemon.go
243
daemon/daemon.go
@@ -8997,19 +8997,21 @@ type ContrastCheckResult struct {
|
|||||||
|
|
||||||
// ContrastCheckElement represents a single element's contrast check
|
// ContrastCheckElement represents a single element's contrast check
|
||||||
type ContrastCheckElement struct {
|
type ContrastCheckElement struct {
|
||||||
Selector string `json:"selector"`
|
Selector string `json:"selector"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
ForegroundColor string `json:"foreground_color"`
|
ForegroundColor string `json:"foreground_color"`
|
||||||
BackgroundColor string `json:"background_color"`
|
BackgroundColor string `json:"background_color"`
|
||||||
ContrastRatio float64 `json:"contrast_ratio"`
|
ContrastRatio float64 `json:"contrast_ratio"`
|
||||||
FontSize string `json:"font_size"`
|
FontSize string `json:"font_size"`
|
||||||
FontWeight string `json:"font_weight"`
|
FontWeight string `json:"font_weight"`
|
||||||
IsLargeText bool `json:"is_large_text"`
|
IsLargeText bool `json:"is_large_text"`
|
||||||
PassesAA bool `json:"passes_aa"`
|
PassesAA bool `json:"passes_aa"`
|
||||||
PassesAAA bool `json:"passes_aaa"`
|
PassesAAA bool `json:"passes_aaa"`
|
||||||
RequiredAA float64 `json:"required_aa"`
|
RequiredAA float64 `json:"required_aa"`
|
||||||
RequiredAAA float64 `json:"required_aaa"`
|
RequiredAAA float64 `json:"required_aaa"`
|
||||||
Error string `json:"error,omitempty"`
|
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
|
// 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);
|
return (lighter + 0.05) / (darker + 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to get effective background color
|
// Helper function to check if color is transparent
|
||||||
function getEffectiveBackground(element) {
|
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;
|
let current = element;
|
||||||
while (current && current !== document.body.parentElement) {
|
while (current && current !== document.documentElement.parentElement) {
|
||||||
const style = window.getComputedStyle(current);
|
const style = window.getComputedStyle(current);
|
||||||
const bgColor = style.backgroundColor;
|
const bgColor = style.backgroundColor;
|
||||||
const parsed = parseColor(bgColor);
|
|
||||||
|
|
||||||
if (parsed && parsed.a > 0) {
|
if (!isTransparent(bgColor)) {
|
||||||
// Check if it's not transparent
|
return bgColor;
|
||||||
if (!(parsed.r === 0 && parsed.g === 0 && parsed.b === 0 && parsed.a === 0)) {
|
|
||||||
return bgColor;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
current = current.parentElement;
|
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
|
// 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
|
// Get computed styles
|
||||||
const style = window.getComputedStyle(element);
|
const style = window.getComputedStyle(element);
|
||||||
const fgColor = style.color;
|
const fgColor = style.color;
|
||||||
const bgColor = getEffectiveBackground(element);
|
|
||||||
const fontSize = style.fontSize;
|
const fontSize = style.fontSize;
|
||||||
const fontWeight = style.fontWeight;
|
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
|
// Parse colors
|
||||||
const fg = parseColor(fgColor);
|
const fg = parseColor(fgColor);
|
||||||
const bg = parseColor(bgColor);
|
const bg = parseColor(bgColor);
|
||||||
@@ -9116,20 +9279,27 @@ func (d *Daemon) checkContrast(tabID string, selector string, timeout int) (*Con
|
|||||||
results.push({
|
results.push({
|
||||||
selector: '%s:nth-of-type(' + (index + 1) + ')',
|
selector: '%s:nth-of-type(' + (index + 1) + ')',
|
||||||
text: text.substring(0, 100),
|
text: text.substring(0, 100),
|
||||||
error: 'Unable to parse colors'
|
error: 'Unable to parse colors',
|
||||||
|
detection_method: detectionMethod
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate contrast ratio
|
// Calculate contrast ratio (use worst case for gradients)
|
||||||
const ratio = getContrastRatio(fg, bg);
|
let ratio;
|
||||||
|
if (gradientInfo) {
|
||||||
|
ratio = gradientInfo.worstCase;
|
||||||
|
} else {
|
||||||
|
ratio = getContrastRatio(fg, bg);
|
||||||
|
}
|
||||||
|
|
||||||
const large = isLargeText(fontSize, fontWeight);
|
const large = isLargeText(fontSize, fontWeight);
|
||||||
|
|
||||||
// WCAG requirements
|
// WCAG requirements
|
||||||
const requiredAA = large ? 3.0 : 4.5;
|
const requiredAA = large ? 3.0 : 4.5;
|
||||||
const requiredAAA = large ? 4.5 : 7.0;
|
const requiredAAA = large ? 4.5 : 7.0;
|
||||||
|
|
||||||
results.push({
|
const result = {
|
||||||
selector: '%s:nth-of-type(' + (index + 1) + ')',
|
selector: '%s:nth-of-type(' + (index + 1) + ')',
|
||||||
text: text.substring(0, 100),
|
text: text.substring(0, 100),
|
||||||
foreground_color: fgColor,
|
foreground_color: fgColor,
|
||||||
@@ -9141,8 +9311,21 @@ func (d *Daemon) checkContrast(tabID string, selector string, timeout int) (*Con
|
|||||||
passes_aa: ratio >= requiredAA,
|
passes_aa: ratio >= requiredAA,
|
||||||
passes_aaa: ratio >= requiredAAA,
|
passes_aaa: ratio >= requiredAAA,
|
||||||
required_aa: requiredAA,
|
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) {
|
} catch (e) {
|
||||||
results.push({
|
results.push({
|
||||||
selector: '%s:nth-of-type(' + (index + 1) + ')',
|
selector: '%s:nth-of-type(' + (index + 1) + ')',
|
||||||
|
|||||||
Reference in New Issue
Block a user