f3cce51fb3
Complete color theme system (light/dark/auto) with dynamic UI: Features: - Color theme switcher with auto/light/dark modes - Dynamic button colors on hover (purple/yellow/blue per theme) - localStorage persistence across sessions - Proper button positioning (desktop and mobile) - Mobile: 5-button layout with theme before info button Fixes: - CSP updated to allow jsDelivr CDN for iconify icons - Button repositioning: Download PDF and Print Friendly at top - Hover-only colors (not persistent) - Mobile button order corrected Files: - static/css/color-theme.css - Theme system with CSS variables - static/js/color-theme.js - Theme switching logic - templates/partials/color-theme-switcher.html - Button component - internal/middleware/security.go - CSP fix for jsDelivr - tests/mjs/13-color-theme-switcher.test.mjs - Comprehensive test - tests/TEST-SUMMARY.md - Updated test documentation
216 lines
7.2 KiB
JavaScript
Executable File
216 lines
7.2 KiB
JavaScript
Executable File
#!/usr/bin/env bun
|
|
/**
|
|
* COLOR THEME SWITCHER TEST
|
|
* ==========================
|
|
* Tests color theme switcher functionality with dynamic colors
|
|
* - Verifies button positioning (Download PDF and Print Friendly at top)
|
|
* - Tests theme cycling (auto → light → dark → auto)
|
|
* - Validates dynamic button colors per theme mode
|
|
* - Confirms icon changes per theme
|
|
* - Tests localStorage persistence
|
|
*/
|
|
|
|
import { chromium } from 'playwright';
|
|
|
|
const URL = "http://localhost:1999";
|
|
|
|
async function testColorThemeSwitcher() {
|
|
console.log('🧪 COLOR THEME SWITCHER TEST\n');
|
|
console.log('='.repeat(70));
|
|
|
|
const browser = await chromium.launch({ headless: false });
|
|
const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } });
|
|
|
|
const errors = [];
|
|
const testResults = [];
|
|
|
|
page.on('console', msg => {
|
|
const text = msg.text();
|
|
if (msg.type() === 'error') {
|
|
errors.push(text);
|
|
console.log(`❌ ERROR: ${text}`);
|
|
}
|
|
});
|
|
|
|
page.on('pageerror', err => {
|
|
errors.push(err.message);
|
|
console.log(`❌ PAGE ERROR: ${err.message}`);
|
|
});
|
|
|
|
console.log("\n1️⃣ Loading page...");
|
|
await page.goto(URL);
|
|
await page.waitForTimeout(2000);
|
|
|
|
// ========================================================================
|
|
// TEST 1: Button Positioning
|
|
// ========================================================================
|
|
console.log("\n2️⃣ Testing button positioning...");
|
|
|
|
const themeBtn = page.locator('#color-theme-switcher');
|
|
const downloadBtn = page.locator('.download-btn');
|
|
const printBtn = page.locator('.print-friendly-btn');
|
|
const shortcutsBtn = page.locator('.shortcuts-btn');
|
|
const backToTopBtn = page.locator('.back-to-top');
|
|
|
|
const positions = {};
|
|
const buttons = [
|
|
{ name: 'Download PDF', locator: downloadBtn },
|
|
{ name: 'Print Friendly', locator: printBtn },
|
|
{ name: 'Theme Switcher', locator: themeBtn },
|
|
{ name: 'Shortcuts', locator: shortcutsBtn },
|
|
{ name: 'Back to Top', locator: backToTopBtn }
|
|
];
|
|
|
|
for (const btn of buttons) {
|
|
const box = await btn.locator.boundingBox();
|
|
if (box) {
|
|
positions[btn.name] = box.y;
|
|
}
|
|
}
|
|
|
|
// Verify order: smaller bottom value = higher on screen
|
|
const expectedOrder = ['Download PDF', 'Print Friendly', 'Theme Switcher', 'Shortcuts', 'Back to Top'];
|
|
let positionsCorrect = true;
|
|
|
|
for (let i = 0; i < expectedOrder.length - 1; i++) {
|
|
const current = positions[expectedOrder[i]];
|
|
const next = positions[expectedOrder[i + 1]];
|
|
if (current && next) {
|
|
const isCorrect = current < next;
|
|
if (!isCorrect) {
|
|
positionsCorrect = false;
|
|
console.log(` ❌ ${expectedOrder[i]} should be above ${expectedOrder[i + 1]}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
testResults.push({
|
|
test: 'Button Positioning',
|
|
passed: positionsCorrect,
|
|
message: positionsCorrect ? 'Buttons in correct order (top to bottom)' : 'Button order incorrect'
|
|
});
|
|
|
|
console.log(` ${positionsCorrect ? '✅' : '❌'} Button positioning: ${positionsCorrect ? 'CORRECT' : 'INCORRECT'}`);
|
|
|
|
// ========================================================================
|
|
// TEST 2: Theme Cycling and Dynamic Colors
|
|
// ========================================================================
|
|
console.log("\n3️⃣ Testing theme cycling and dynamic colors...");
|
|
|
|
const themes = [
|
|
{ name: 'auto', color: 'rgb(155, 89, 182)', icon: 'mdi:theme-light-dark' },
|
|
{ name: 'light', color: 'rgb(243, 156, 18)', icon: 'mdi:white-balance-sunny' },
|
|
{ name: 'dark', color: 'rgb(52, 152, 219)', icon: 'mdi:moon-waning-crescent' },
|
|
{ name: 'auto', color: 'rgb(155, 89, 182)', icon: 'mdi:theme-light-dark' } // Cycle back
|
|
];
|
|
|
|
let themeTestsPassed = true;
|
|
|
|
for (let i = 0; i < themes.length; i++) {
|
|
const theme = themes[i];
|
|
|
|
if (i > 0) {
|
|
await themeBtn.click();
|
|
await page.waitForTimeout(500);
|
|
}
|
|
|
|
// Get current theme mode from document
|
|
const currentMode = await page.evaluate(() =>
|
|
document.documentElement.getAttribute('data-color-theme')
|
|
);
|
|
|
|
// Get button background color
|
|
const bgColor = await themeBtn.evaluate(el =>
|
|
window.getComputedStyle(el).backgroundColor
|
|
);
|
|
|
|
// Get button data attribute
|
|
const dataMode = await themeBtn.getAttribute('data-theme-mode');
|
|
|
|
// Get icon
|
|
const iconName = await page.locator('#themeIcon').getAttribute('icon');
|
|
|
|
const modeMatch = currentMode === theme.name;
|
|
const colorMatch = bgColor === theme.color;
|
|
const dataMatch = dataMode === theme.name;
|
|
const iconMatch = iconName === theme.icon;
|
|
|
|
const allMatch = modeMatch && colorMatch && dataMatch && iconMatch;
|
|
|
|
console.log(` ${i + 1}. Theme: ${theme.name.toUpperCase()}`);
|
|
console.log(` Document mode: ${currentMode} ${modeMatch ? '✓' : '✗'}`);
|
|
console.log(` Button data: ${dataMode} ${dataMatch ? '✓' : '✗'}`);
|
|
console.log(` Icon: ${iconName} ${iconMatch ? '✓' : '✗'}`);
|
|
console.log(` Color: ${bgColor} ${colorMatch ? '✓' : '✗'}`);
|
|
console.log(` Status: ${allMatch ? '✅ PASS' : '❌ FAIL'}`);
|
|
|
|
if (!allMatch) themeTestsPassed = false;
|
|
}
|
|
|
|
testResults.push({
|
|
test: 'Theme Cycling',
|
|
passed: themeTestsPassed,
|
|
message: themeTestsPassed ? 'All theme modes cycle correctly' : 'Theme cycling failed'
|
|
});
|
|
|
|
testResults.push({
|
|
test: 'Dynamic Colors',
|
|
passed: themeTestsPassed,
|
|
message: themeTestsPassed ? 'Button colors change per theme mode' : 'Dynamic colors failed'
|
|
});
|
|
|
|
// ========================================================================
|
|
// TEST 3: localStorage Persistence
|
|
// ========================================================================
|
|
console.log("\n4️⃣ Testing localStorage persistence...");
|
|
|
|
// Set to light mode
|
|
await themeBtn.click();
|
|
await page.waitForTimeout(300);
|
|
|
|
const storedTheme = await page.evaluate(() =>
|
|
localStorage.getItem('color-theme-mode')
|
|
);
|
|
|
|
const persistenceWorks = storedTheme === 'light';
|
|
|
|
testResults.push({
|
|
test: 'localStorage Persistence',
|
|
passed: persistenceWorks,
|
|
message: persistenceWorks ? `Theme '${storedTheme}' saved to localStorage` : 'localStorage not saving theme'
|
|
});
|
|
|
|
console.log(` ${persistenceWorks ? '✅' : '❌'} localStorage: ${persistenceWorks ? `'${storedTheme}' saved correctly` : 'FAILED'}`);
|
|
|
|
// ========================================================================
|
|
// TEST SUMMARY
|
|
// ========================================================================
|
|
console.log('\n' + '='.repeat(70));
|
|
console.log('📊 TEST RESULTS');
|
|
console.log('='.repeat(70) + '\n');
|
|
|
|
const allPassed = testResults.every(r => r.passed);
|
|
|
|
testResults.forEach(r => {
|
|
console.log(` ${r.passed ? '✅' : '❌'} ${r.test}: ${r.message}`);
|
|
});
|
|
|
|
console.log('\n' + '='.repeat(70));
|
|
|
|
if (allPassed && errors.length === 0) {
|
|
console.log('\n🎉 ALL COLOR THEME TESTS PASSED!\n');
|
|
await browser.close();
|
|
process.exit(0);
|
|
} else {
|
|
console.log('\n❌ SOME TESTS FAILED\n');
|
|
if (errors.length > 0) {
|
|
console.log(`Browser errors: ${errors.length}`);
|
|
}
|
|
console.log("\nBrowser will stay open for manual inspection.");
|
|
console.log("Press Ctrl+C when done.\n");
|
|
await new Promise(() => {}); // Keep browser open
|
|
}
|
|
}
|
|
|
|
await testColorThemeSwitcher();
|