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
263 lines
11 KiB
JavaScript
Executable File
263 lines
11 KiB
JavaScript
Executable File
#!/usr/bin/env bun
|
|
/**
|
|
* SKELETON LOADERS TEST
|
|
* ======================
|
|
* Tests skeleton loader animations during language transitions
|
|
* - Verifies skeleton loaders appear during language switching
|
|
* - Checks component-wrapper structure exists
|
|
* - Validates .loading class is added/removed correctly
|
|
* - Tests skeleton appears on multiple consecutive switches
|
|
* - Ensures no visual glitches or stuck loading states
|
|
*/
|
|
|
|
import { chromium } from 'playwright';
|
|
|
|
const URL = "http://localhost:1999";
|
|
|
|
async function testSkeletonLoaders() {
|
|
console.log('💀 SKELETON LOADERS 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 => {
|
|
if (msg.type() === 'error') {
|
|
errors.push(msg.text());
|
|
console.log(`❌ ERROR: ${msg.text()}`);
|
|
}
|
|
});
|
|
|
|
console.log("\n1️⃣ Loading page (English default)...");
|
|
await page.goto(URL);
|
|
await page.waitForTimeout(2000);
|
|
|
|
// ========================================================================
|
|
// TEST 1: Component wrapper structure exists
|
|
// ========================================================================
|
|
console.log("\n2️⃣ Testing Component Wrapper Structure...");
|
|
const structure = await page.evaluate(() => {
|
|
const wrappers = document.querySelectorAll('.component-wrapper');
|
|
const hasActual = Array.from(wrappers).every(w => w.querySelector('.actual-content'));
|
|
const hasSkeleton = Array.from(wrappers).every(w => w.querySelector('.skeleton-content'));
|
|
|
|
return {
|
|
wrapperCount: wrappers.length,
|
|
allHaveActual: hasActual,
|
|
allHaveSkeleton: hasSkeleton
|
|
};
|
|
});
|
|
|
|
console.log(` Component wrappers found: ${structure.wrapperCount}`);
|
|
console.log(` All have .actual-content: ${structure.allHaveActual ? '✅' : '❌'}`);
|
|
console.log(` All have .skeleton-content: ${structure.allHaveSkeleton ? '✅' : '❌'}`);
|
|
|
|
const structurePassed = structure.wrapperCount > 0 && structure.allHaveActual && structure.allHaveSkeleton;
|
|
console.log(` ${structurePassed ? '✅ PASS' : '❌ FAIL'} - Dual-state structure exists`);
|
|
testResults.push({ test: 'Component Wrapper Structure', passed: structurePassed });
|
|
|
|
// ========================================================================
|
|
// TEST 2: Skeleton CSS exists and is loaded
|
|
// ========================================================================
|
|
console.log("\n3️⃣ Testing Skeleton CSS...");
|
|
const cssCheck = await page.evaluate(() => {
|
|
const skeleton = document.querySelector('.skeleton-content .skeleton');
|
|
if (!skeleton) return { exists: false };
|
|
|
|
const styles = window.getComputedStyle(skeleton);
|
|
return {
|
|
exists: true,
|
|
hasAnimation: styles.animation !== 'none' && styles.animation !== '',
|
|
background: styles.background,
|
|
borderRadius: styles.borderRadius
|
|
};
|
|
});
|
|
|
|
console.log(` Skeleton elements exist: ${cssCheck.exists ? '✅' : '❌'}`);
|
|
console.log(` Has shimmer animation: ${cssCheck.hasAnimation ? '✅' : '❌'}`);
|
|
console.log(` ${cssCheck.exists && cssCheck.hasAnimation ? '✅ PASS' : '❌ FAIL'} - Skeleton CSS loaded`);
|
|
testResults.push({ test: 'Skeleton CSS', passed: cssCheck.exists && cssCheck.hasAnimation });
|
|
|
|
// ========================================================================
|
|
// TEST 3: Monitor parent container .loading class during language switch
|
|
// ========================================================================
|
|
console.log("\n4️⃣ Testing First Language Switch (EN → ES)...");
|
|
|
|
// Set up monitoring
|
|
await page.evaluate(() => {
|
|
window.loadingEvents = [];
|
|
|
|
const containers = [
|
|
document.querySelector('#cv-inner-content-page-1'),
|
|
document.querySelector('#cv-inner-content-page-2')
|
|
];
|
|
|
|
containers.forEach((container, index) => {
|
|
if (container) {
|
|
const observer = new MutationObserver((mutations) => {
|
|
mutations.forEach((mutation) => {
|
|
if (mutation.attributeName === 'class') {
|
|
window.loadingEvents.push({
|
|
time: Date.now(),
|
|
container: index + 1,
|
|
hasLoading: mutation.target.classList.contains('loading')
|
|
});
|
|
}
|
|
});
|
|
});
|
|
observer.observe(container, { attributes: true });
|
|
}
|
|
});
|
|
});
|
|
|
|
// Click Spanish button
|
|
await page.click('.selector-btn[aria-label="Español"]');
|
|
await page.waitForTimeout(800);
|
|
|
|
const switch1 = await page.evaluate(() => window.loadingEvents || []);
|
|
const loadingAdded1 = switch1.filter(e => e.hasLoading).length;
|
|
const loadingRemoved1 = switch1.filter(e => !e.hasLoading).length;
|
|
|
|
console.log(` Parent containers got .loading: ${loadingAdded1 > 0 ? '✅' : '❌'} (${loadingAdded1} events)`);
|
|
console.log(` Parent containers lost .loading: ${loadingRemoved1 > 0 ? '✅' : '❌'} (${loadingRemoved1} events)`);
|
|
|
|
const switch1Passed = loadingAdded1 > 0 && loadingRemoved1 > 0;
|
|
console.log(` ${switch1Passed ? '✅ PASS' : '❌ FAIL'} - Skeleton displayed during transition`);
|
|
testResults.push({ test: 'First Language Switch', passed: switch1Passed });
|
|
|
|
// ========================================================================
|
|
// TEST 4: Second language switch (ES → EN)
|
|
// ========================================================================
|
|
console.log("\n5️⃣ Testing Second Language Switch (ES → EN)...");
|
|
|
|
// Clear events
|
|
await page.evaluate(() => { window.loadingEvents = []; });
|
|
|
|
// Click English button
|
|
await page.click('.selector-btn[aria-label="English"]');
|
|
await page.waitForTimeout(800);
|
|
|
|
const switch2 = await page.evaluate(() => window.loadingEvents || []);
|
|
const loadingAdded2 = switch2.filter(e => e.hasLoading).length;
|
|
const loadingRemoved2 = switch2.filter(e => !e.hasLoading).length;
|
|
|
|
console.log(` Parent containers got .loading: ${loadingAdded2 > 0 ? '✅' : '❌'} (${loadingAdded2} events)`);
|
|
console.log(` Parent containers lost .loading: ${loadingRemoved2 > 0 ? '✅' : '❌'} (${loadingRemoved2} events)`);
|
|
|
|
const switch2Passed = loadingAdded2 > 0 && loadingRemoved2 > 0;
|
|
console.log(` ${switch2Passed ? '✅ PASS' : '❌ FAIL'} - Skeleton still works on second switch`);
|
|
testResults.push({ test: 'Second Language Switch', passed: switch2Passed });
|
|
|
|
// ========================================================================
|
|
// TEST 5: Third language switch (EN → ES) - consistency check
|
|
// ========================================================================
|
|
console.log("\n6️⃣ Testing Third Language Switch (EN → ES)...");
|
|
|
|
await page.evaluate(() => { window.loadingEvents = []; });
|
|
await page.click('.selector-btn[aria-label="Español"]');
|
|
await page.waitForTimeout(800);
|
|
|
|
const switch3 = await page.evaluate(() => window.loadingEvents || []);
|
|
const loadingAdded3 = switch3.filter(e => e.hasLoading).length;
|
|
const loadingRemoved3 = switch3.filter(e => !e.hasLoading).length;
|
|
|
|
console.log(` Parent containers got .loading: ${loadingAdded3 > 0 ? '✅' : '❌'} (${loadingAdded3} events)`);
|
|
console.log(` Parent containers lost .loading: ${loadingRemoved3 > 0 ? '✅' : '❌'} (${loadingRemoved3} events)`);
|
|
|
|
const switch3Passed = loadingAdded3 > 0 && loadingRemoved3 > 0;
|
|
console.log(` ${switch3Passed ? '✅ PASS' : '❌ FAIL'} - Consistent behavior on third switch`);
|
|
testResults.push({ test: 'Third Language Switch', passed: switch3Passed });
|
|
|
|
// ========================================================================
|
|
// TEST 6: No stuck loading states
|
|
// ========================================================================
|
|
console.log("\n7️⃣ Testing for Stuck Loading States...");
|
|
|
|
const finalState = await page.evaluate(() => {
|
|
const page1 = document.querySelector('#cv-inner-content-page-1');
|
|
const page2 = document.querySelector('#cv-inner-content-page-2');
|
|
const wrappers = document.querySelectorAll('.component-wrapper');
|
|
|
|
return {
|
|
page1HasLoading: page1?.classList.contains('loading') || false,
|
|
page2HasLoading: page2?.classList.contains('loading') || false,
|
|
anyWrapperHasLoading: Array.from(wrappers).some(w => w.classList.contains('loading'))
|
|
};
|
|
});
|
|
|
|
console.log(` Page 1 stuck with .loading: ${finalState.page1HasLoading ? '❌ BUG' : '✅ Clean'}`);
|
|
console.log(` Page 2 stuck with .loading: ${finalState.page2HasLoading ? '❌ BUG' : '✅ Clean'}`);
|
|
console.log(` Any wrapper stuck with .loading: ${finalState.anyWrapperHasLoading ? '❌ BUG' : '✅ Clean'}`);
|
|
|
|
const noStuckStates = !finalState.page1HasLoading && !finalState.page2HasLoading && !finalState.anyWrapperHasLoading;
|
|
console.log(` ${noStuckStates ? '✅ PASS' : '❌ FAIL'} - No stuck loading states`);
|
|
testResults.push({ test: 'No Stuck Loading States', passed: noStuckStates });
|
|
|
|
// ========================================================================
|
|
// TEST 7: Hyperscript event delegation works
|
|
// ========================================================================
|
|
console.log("\n8️⃣ Testing Hyperscript Event Delegation...");
|
|
|
|
const hyperscriptCheck = await page.evaluate(() => {
|
|
const body = document.body;
|
|
const hasHyperscript = body.hasAttribute('_');
|
|
const hyperscriptContent = body.getAttribute('_') || '';
|
|
const hasBeforeRequest = hyperscriptContent.includes('htmx:beforeRequest');
|
|
const hasOobAfterSwap = hyperscriptContent.includes('htmx:oobAfterSwap');
|
|
|
|
return {
|
|
hasHyperscript,
|
|
hasBeforeRequest,
|
|
hasOobAfterSwap
|
|
};
|
|
});
|
|
|
|
console.log(` Body has _hyperscript: ${hyperscriptCheck.hasHyperscript ? '✅' : '❌'}`);
|
|
console.log(` Listens for htmx:beforeRequest: ${hyperscriptCheck.hasBeforeRequest ? '✅' : '❌'}`);
|
|
console.log(` Listens for htmx:oobAfterSwap: ${hyperscriptCheck.hasOobAfterSwap ? '✅' : '❌'}`);
|
|
|
|
const hyperscriptPassed = hyperscriptCheck.hasHyperscript && hyperscriptCheck.hasBeforeRequest && hyperscriptCheck.hasOobAfterSwap;
|
|
console.log(` ${hyperscriptPassed ? '✅ PASS' : '❌ FAIL'} - Global event delegation configured`);
|
|
testResults.push({ test: 'Hyperscript Event Delegation', passed: hyperscriptPassed });
|
|
|
|
// ========================================================================
|
|
// FINAL SUMMARY
|
|
// ========================================================================
|
|
console.log("\n" + "=".repeat(70));
|
|
console.log("📊 TEST SUMMARY\n");
|
|
|
|
const totalTests = testResults.length;
|
|
const passedTests = testResults.filter(r => r.passed).length;
|
|
const failedTests = totalTests - passedTests;
|
|
|
|
testResults.forEach(result => {
|
|
console.log(` ${result.passed ? '✅' : '❌'} ${result.test}`);
|
|
});
|
|
|
|
console.log(`\n Total: ${passedTests}/${totalTests} tests passed`);
|
|
|
|
if (errors.length === 0) {
|
|
console.log("\n✅ NO CONSOLE ERRORS");
|
|
} else {
|
|
console.log(`\n⚠️ ${errors.length} CONSOLE ERRORS`);
|
|
}
|
|
|
|
console.log("=".repeat(70) + "\n");
|
|
|
|
if (failedTests === 0) {
|
|
console.log("🎉 SKELETON LOADERS VALIDATED!");
|
|
} else {
|
|
console.log("⚠️ SOME TESTS FAILED - See details above");
|
|
}
|
|
|
|
console.log("\nBrowser will stay open for manual inspection.");
|
|
console.log("Press Ctrl+C when done.\n");
|
|
|
|
await new Promise(() => {}); // Keep browser open
|
|
}
|
|
|
|
await testSkeletonLoaders();
|