Spaces:
Sleeping
Sleeping
| <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> | |
| /** | |
| * Property-Based Test for Theme Consistency | |
| * Feature: admin-ui-modernization, Property 1: Theme consistency | |
| * Validates: Requirements 1.4, 5.3, 14.3 | |
| */ | |
| const WCAG_AA_NORMAL_TEXT = 4.5; | |
| const WCAG_AA_LARGE_TEXT = 3.0; | |
| // Required CSS custom properties | |
| 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' | |
| ]; | |
| // Text/background combinations to test | |
| 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' } | |
| ]; | |
| /** | |
| * Calculate relative luminance | |
| */ | |
| 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; | |
| } | |
| /** | |
| * Calculate contrast ratio | |
| */ | |
| 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); | |
| } | |
| /** | |
| * Parse color string to RGB | |
| */ | |
| 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; | |
| } | |
| /** | |
| * Get CSS custom property value | |
| */ | |
| 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; | |
| } | |
| /** | |
| * Test 1: Check if all required properties are defined | |
| */ | |
| 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; | |
| } | |
| /** | |
| * Test 2: Check contrast ratios | |
| */ | |
| 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; | |
| } | |
| /** | |
| * Test 3: Property-based test with random theme switches | |
| */ | |
| function testThemeSwitching(iterations = 100) { | |
| const failures = []; | |
| for (let i = 0; i < iterations; i++) { | |
| const theme = i % 2 === 0 ? 'dark' : 'light'; | |
| // Check a random subset of properties | |
| 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; | |
| } | |
| /** | |
| * Render test results | |
| */ | |
| function renderResults() { | |
| const resultsContainer = document.getElementById('test-results'); | |
| let html = ''; | |
| let allPassed = true; | |
| // Test 1: Required Properties | |
| 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>'; | |
| // Test 2: Contrast Ratios | |
| 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>'; | |
| // Test 3: Theme Switching | |
| 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; | |
| // Summary | |
| 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)'; | |
| } | |
| } | |
| // Run tests when page loads | |
| window.addEventListener('DOMContentLoaded', renderResults); | |
| </script> | |
| </body> | |
| </html> | |