|
|
<!DOCTYPE html>
|
|
|
<html lang="en">
|
|
|
<head>
|
|
|
<meta charset="UTF-8">
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
<title>Property Test: Theme Consistency</title>
|
|
|
<link rel="stylesheet" href="../static/css/design-tokens.css">
|
|
|
<style>
|
|
|
body {
|
|
|
font-family: 'Inter', sans-serif;
|
|
|
background: #0f172a;
|
|
|
color: #f1f5f9;
|
|
|
padding: 2rem;
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
.test-container {
|
|
|
max-width: 1200px;
|
|
|
margin: 0 auto;
|
|
|
}
|
|
|
.test-header {
|
|
|
border-bottom: 2px solid rgba(99, 102, 241, 0.3);
|
|
|
padding-bottom: 1rem;
|
|
|
margin-bottom: 2rem;
|
|
|
}
|
|
|
.test-section {
|
|
|
background: rgba(255, 255, 255, 0.05);
|
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
border-radius: 1rem;
|
|
|
padding: 1.5rem;
|
|
|
margin-bottom: 1.5rem;
|
|
|
}
|
|
|
.test-result {
|
|
|
padding: 1rem;
|
|
|
border-radius: 0.5rem;
|
|
|
margin: 0.5rem 0;
|
|
|
}
|
|
|
.test-pass {
|
|
|
background: rgba(16, 185, 129, 0.1);
|
|
|
border-left: 4px solid #10b981;
|
|
|
color: #34d399;
|
|
|
}
|
|
|
.test-fail {
|
|
|
background: rgba(239, 68, 68, 0.1);
|
|
|
border-left: 4px solid #ef4444;
|
|
|
color: #f87171;
|
|
|
}
|
|
|
.test-info {
|
|
|
background: rgba(59, 130, 246, 0.1);
|
|
|
border-left: 4px solid #3b82f6;
|
|
|
color: #60a5fa;
|
|
|
}
|
|
|
.property-list {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
|
gap: 0.5rem;
|
|
|
margin-top: 1rem;
|
|
|
}
|
|
|
.property-item {
|
|
|
background: rgba(255, 255, 255, 0.03);
|
|
|
padding: 0.5rem;
|
|
|
border-radius: 0.25rem;
|
|
|
font-family: monospace;
|
|
|
font-size: 0.875rem;
|
|
|
}
|
|
|
.contrast-test {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 1rem;
|
|
|
padding: 1rem;
|
|
|
margin: 0.5rem 0;
|
|
|
border-radius: 0.5rem;
|
|
|
}
|
|
|
.color-swatch {
|
|
|
width: 60px;
|
|
|
height: 60px;
|
|
|
border-radius: 0.5rem;
|
|
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
|
|
}
|
|
|
.summary {
|
|
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(139, 92, 246, 0.2));
|
|
|
border: 2px solid rgba(99, 102, 241, 0.3);
|
|
|
border-radius: 1rem;
|
|
|
padding: 2rem;
|
|
|
text-align: center;
|
|
|
margin-top: 2rem;
|
|
|
}
|
|
|
.summary h2 {
|
|
|
margin: 0 0 1rem 0;
|
|
|
font-size: 2rem;
|
|
|
}
|
|
|
code {
|
|
|
background: rgba(0, 0, 0, 0.3);
|
|
|
padding: 0.2rem 0.4rem;
|
|
|
border-radius: 0.25rem;
|
|
|
font-family: monospace;
|
|
|
}
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
<div class="test-container">
|
|
|
<div class="test-header">
|
|
|
<h1>π§ͺ Property-Based Test: Theme Consistency</h1>
|
|
|
<p><strong>Feature:</strong> admin-ui-modernization, Property 1</p>
|
|
|
<p><strong>Validates:</strong> Requirements 1.4, 5.3, 14.3</p>
|
|
|
<p><strong>Property:</strong> For any theme mode (light/dark), all CSS custom properties should be defined and color contrast ratios should meet WCAG AA standards (4.5:1 for normal text, 3:1 for large text)</p>
|
|
|
</div>
|
|
|
|
|
|
<div id="test-results"></div>
|
|
|
|
|
|
<div class="summary" id="summary"></div>
|
|
|
</div>
|
|
|
|
|
|
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const WCAG_AA_NORMAL_TEXT = 4.5;
|
|
|
const WCAG_AA_LARGE_TEXT = 3.0;
|
|
|
|
|
|
|
|
|
const REQUIRED_PROPERTIES = [
|
|
|
'color-primary', 'color-accent', 'color-success', 'color-warning', 'color-error',
|
|
|
'bg-primary', 'bg-secondary', 'text-primary', 'text-secondary',
|
|
|
'glass-bg', 'glass-border', 'border-color',
|
|
|
'gradient-primary', 'gradient-glass',
|
|
|
'font-family-primary', 'font-size-base', 'font-weight-normal',
|
|
|
'line-height-normal', 'letter-spacing-normal',
|
|
|
'spacing-xs', 'spacing-sm', 'spacing-md', 'spacing-lg', 'spacing-xl',
|
|
|
'shadow-sm', 'shadow-md', 'shadow-lg',
|
|
|
'blur-sm', 'blur-md', 'blur-lg',
|
|
|
'transition-fast', 'transition-base', 'ease-in-out'
|
|
|
];
|
|
|
|
|
|
|
|
|
const CONTRAST_TESTS = [
|
|
|
{ text: 'text-primary', bg: 'bg-primary', name: 'Primary Text on Primary Background' },
|
|
|
{ text: 'text-secondary', bg: 'bg-primary', name: 'Secondary Text on Primary Background' },
|
|
|
{ text: 'text-primary', bg: 'bg-secondary', name: 'Primary Text on Secondary Background' }
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getLuminance(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;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getContrastRatio(color1, color2) {
|
|
|
const lum1 = getLuminance(color1.r, color1.g, color1.b);
|
|
|
const lum2 = getLuminance(color2.r, color2.g, color2.b);
|
|
|
const lighter = Math.max(lum1, lum2);
|
|
|
const darker = Math.min(lum1, lum2);
|
|
|
return (lighter + 0.05) / (darker + 0.05);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function parseColor(colorStr) {
|
|
|
const div = document.createElement('div');
|
|
|
div.style.color = colorStr;
|
|
|
document.body.appendChild(div);
|
|
|
const computed = window.getComputedStyle(div).color;
|
|
|
document.body.removeChild(div);
|
|
|
|
|
|
const match = computed.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
|
if (match) {
|
|
|
return {
|
|
|
r: parseInt(match[1]),
|
|
|
g: parseInt(match[2]),
|
|
|
b: parseInt(match[3])
|
|
|
};
|
|
|
}
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getCSSProperty(propertyName, theme = 'dark') {
|
|
|
const testElement = document.createElement('div');
|
|
|
testElement.setAttribute('data-theme', theme);
|
|
|
document.body.appendChild(testElement);
|
|
|
|
|
|
const value = window.getComputedStyle(testElement).getPropertyValue(`--${propertyName}`).trim();
|
|
|
|
|
|
document.body.removeChild(testElement);
|
|
|
return value;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function testRequiredProperties() {
|
|
|
const results = {
|
|
|
dark: { defined: [], missing: [] },
|
|
|
light: { defined: [], missing: [] }
|
|
|
};
|
|
|
|
|
|
['dark', 'light'].forEach(theme => {
|
|
|
REQUIRED_PROPERTIES.forEach(prop => {
|
|
|
const value = getCSSProperty(prop, theme);
|
|
|
if (value && value !== '') {
|
|
|
results[theme].defined.push(prop);
|
|
|
} else {
|
|
|
results[theme].missing.push(prop);
|
|
|
}
|
|
|
});
|
|
|
});
|
|
|
|
|
|
return results;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function testContrastRatios() {
|
|
|
const results = {
|
|
|
dark: [],
|
|
|
light: []
|
|
|
};
|
|
|
|
|
|
['dark', 'light'].forEach(theme => {
|
|
|
CONTRAST_TESTS.forEach(test => {
|
|
|
const textColor = getCSSProperty(test.text, theme);
|
|
|
const bgColor = getCSSProperty(test.bg, theme);
|
|
|
|
|
|
if (textColor && bgColor) {
|
|
|
const textRgb = parseColor(textColor);
|
|
|
const bgRgb = parseColor(bgColor);
|
|
|
|
|
|
if (textRgb && bgRgb) {
|
|
|
const ratio = getContrastRatio(textRgb, bgRgb);
|
|
|
const passes = ratio >= WCAG_AA_NORMAL_TEXT;
|
|
|
|
|
|
results[theme].push({
|
|
|
name: test.name,
|
|
|
textColor,
|
|
|
bgColor,
|
|
|
textRgb,
|
|
|
bgRgb,
|
|
|
ratio: ratio.toFixed(2),
|
|
|
passes,
|
|
|
required: WCAG_AA_NORMAL_TEXT
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
});
|
|
|
|
|
|
return results;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function testThemeSwitching(iterations = 100) {
|
|
|
const failures = [];
|
|
|
|
|
|
for (let i = 0; i < iterations; i++) {
|
|
|
const theme = i % 2 === 0 ? 'dark' : 'light';
|
|
|
|
|
|
|
|
|
const propsToCheck = REQUIRED_PROPERTIES.slice(0, 5 + Math.floor(Math.random() * 5));
|
|
|
|
|
|
for (const prop of propsToCheck) {
|
|
|
const value = getCSSProperty(prop, theme);
|
|
|
if (!value || value === '') {
|
|
|
failures.push({
|
|
|
iteration: i + 1,
|
|
|
theme,
|
|
|
property: prop
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return failures;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function renderResults() {
|
|
|
const resultsContainer = document.getElementById('test-results');
|
|
|
let html = '';
|
|
|
let allPassed = true;
|
|
|
|
|
|
|
|
|
html += '<div class="test-section">';
|
|
|
html += '<h2>Test 1: Required CSS Custom Properties</h2>';
|
|
|
|
|
|
const propResults = testRequiredProperties();
|
|
|
|
|
|
['dark', 'light'].forEach(theme => {
|
|
|
const themeName = theme.charAt(0).toUpperCase() + theme.slice(1);
|
|
|
const passed = propResults[theme].missing.length === 0;
|
|
|
|
|
|
if (!passed) allPassed = false;
|
|
|
|
|
|
html += `<div class="test-result ${passed ? 'test-pass' : 'test-fail'}">`;
|
|
|
html += `<strong>${themeName} Theme:</strong> `;
|
|
|
|
|
|
if (passed) {
|
|
|
html += `β All ${propResults[theme].defined.length} required properties defined`;
|
|
|
} else {
|
|
|
html += `β Missing ${propResults[theme].missing.length} properties: `;
|
|
|
html += `<code>${propResults[theme].missing.join(', ')}</code>`;
|
|
|
}
|
|
|
|
|
|
html += '</div>';
|
|
|
});
|
|
|
|
|
|
html += '</div>';
|
|
|
|
|
|
|
|
|
html += '<div class="test-section">';
|
|
|
html += '<h2>Test 2: WCAG AA Contrast Ratios</h2>';
|
|
|
|
|
|
const contrastResults = testContrastRatios();
|
|
|
|
|
|
['dark', 'light'].forEach(theme => {
|
|
|
const themeName = theme.charAt(0).toUpperCase() + theme.slice(1);
|
|
|
html += `<h3>${themeName} Theme</h3>`;
|
|
|
|
|
|
contrastResults[theme].forEach(result => {
|
|
|
if (!result.passes) allPassed = false;
|
|
|
|
|
|
html += `<div class="contrast-test" style="background: ${result.bgColor}; color: ${result.textColor};">`;
|
|
|
html += `<div class="color-swatch" style="background: ${result.textColor};"></div>`;
|
|
|
html += `<div class="color-swatch" style="background: ${result.bgColor};"></div>`;
|
|
|
html += '<div>';
|
|
|
html += `<strong>${result.name}</strong><br>`;
|
|
|
html += `Ratio: <strong>${result.ratio}:1</strong> `;
|
|
|
html += result.passes
|
|
|
? '<span style="color: #34d399;">β PASS</span>'
|
|
|
: `<span style="color: #f87171;">β FAIL (required: ${result.required}:1)</span>`;
|
|
|
html += `<br><small>Text: ${result.textColor} | Background: ${result.bgColor}</small>`;
|
|
|
html += '</div>';
|
|
|
html += '</div>';
|
|
|
});
|
|
|
});
|
|
|
|
|
|
html += '</div>';
|
|
|
|
|
|
|
|
|
html += '<div class="test-section">';
|
|
|
html += '<h2>Test 3: Property-Based Theme Switching (100 iterations)</h2>';
|
|
|
|
|
|
const switchingFailures = testThemeSwitching(100);
|
|
|
const switchingPassed = switchingFailures.length === 0;
|
|
|
|
|
|
if (!switchingPassed) allPassed = false;
|
|
|
|
|
|
html += `<div class="test-result ${switchingPassed ? 'test-pass' : 'test-fail'}">`;
|
|
|
|
|
|
if (switchingPassed) {
|
|
|
html += 'β All 100 random theme switches maintained property consistency';
|
|
|
} else {
|
|
|
html += `β Found ${switchingFailures.length} failures across 100 iterations<br>`;
|
|
|
html += '<small>First 5 failures:</small><br>';
|
|
|
switchingFailures.slice(0, 5).forEach(failure => {
|
|
|
html += `<small>Iteration ${failure.iteration} (${failure.theme}): Missing property <code>${failure.property}</code></small><br>`;
|
|
|
});
|
|
|
}
|
|
|
|
|
|
html += '</div>';
|
|
|
html += '</div>';
|
|
|
|
|
|
resultsContainer.innerHTML = html;
|
|
|
|
|
|
|
|
|
const summary = document.getElementById('summary');
|
|
|
if (allPassed) {
|
|
|
summary.innerHTML = `
|
|
|
<h2 style="color: #34d399;">β ALL TESTS PASSED</h2>
|
|
|
<p>Theme consistency property is satisfied.</p>
|
|
|
<p>All CSS custom properties are properly defined and contrast ratios meet WCAG AA standards.</p>
|
|
|
`;
|
|
|
summary.style.background = 'linear-gradient(135deg, rgba(16, 185, 129, 0.2), rgba(6, 182, 212, 0.2))';
|
|
|
summary.style.borderColor = 'rgba(16, 185, 129, 0.3)';
|
|
|
} else {
|
|
|
summary.innerHTML = `
|
|
|
<h2 style="color: #f87171;">β SOME TESTS FAILED</h2>
|
|
|
<p>Theme consistency property is NOT satisfied.</p>
|
|
|
<p>Please review the failures above and update the design tokens accordingly.</p>
|
|
|
`;
|
|
|
summary.style.background = 'linear-gradient(135deg, rgba(239, 68, 68, 0.2), rgba(236, 72, 153, 0.2))';
|
|
|
summary.style.borderColor = 'rgba(239, 68, 68, 0.3)';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
window.addEventListener('DOMContentLoaded', renderResults);
|
|
|
</script>
|
|
|
</body>
|
|
|
</html>
|
|
|
|