| <!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>
|
|
|