added zoom in buttons

This commit is contained in:
juanatsap
2025-11-16 12:48:12 +00:00
parent 25e9ebafe7
commit ac0cf15eb9
55 changed files with 2625 additions and 52 deletions
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env node
/**
* Test fixed button sizes at different zoom levels
*/
import { chromium } from 'playwright';
async function testButtonSizes() {
console.log('🧪 Testing Fixed Button Sizes at Different Zoom Levels\n');
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
const page = await context.newPage();
try {
await page.goto('http://localhost:1999/?lang=en', { waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
const zoomSlider = page.locator('#zoom-slider');
await zoomSlider.waitFor({ state: 'visible' });
// Test at different zoom levels
const zoomLevels = [25, 100, 175];
const results = {};
for (const zoomLevel of zoomLevels) {
console.log(`\n📏 Testing at ${zoomLevel}% zoom...`);
// Set zoom level
await zoomSlider.fill(zoomLevel.toString());
await page.waitForTimeout(500);
// Measure button sizes
const backToTop = await page.locator('#back-to-top').boundingBox();
const infoButton = await page.locator('#info-button').boundingBox();
const shortcutsButton = await page.locator('#shortcuts-button').boundingBox();
results[zoomLevel] = {
backToTop: backToTop ? Math.round(backToTop.width) : 0,
infoButton: infoButton ? Math.round(infoButton.width) : 0,
shortcutsButton: shortcutsButton ? Math.round(shortcutsButton.width) : 0
};
console.log(` Back-to-top: ${results[zoomLevel].backToTop}px`);
console.log(` Info button: ${results[zoomLevel].infoButton}px`);
console.log(` Shortcuts button: ${results[zoomLevel].shortcutsButton}px`);
}
// Check if sizes are consistent
console.log('\n' + '='.repeat(60));
console.log('📊 BUTTON SIZE CONSISTENCY CHECK');
console.log('='.repeat(60));
const baseSize = results[100];
let allConsistent = true;
for (const [zoomLevel, sizes] of Object.entries(results)) {
if (zoomLevel === '100') continue;
const backTopDiff = Math.abs(sizes.backToTop - baseSize.backToTop);
const infoDiff = Math.abs(sizes.infoButton - baseSize.infoButton);
const shortcutsDiff = Math.abs(sizes.shortcutsButton - baseSize.shortcutsButton);
const maxDiff = Math.max(backTopDiff, infoDiff, shortcutsDiff);
if (maxDiff <= 2) {
console.log(`${zoomLevel}% zoom: Buttons stay consistent (max diff: ${maxDiff}px)`);
} else {
console.log(`${zoomLevel}% zoom: Buttons changed size (max diff: ${maxDiff}px)`);
allConsistent = false;
}
}
console.log('='.repeat(60));
if (allConsistent) {
console.log('✅ SUCCESS: Fixed buttons maintain consistent size at all zoom levels!');
} else {
console.log('❌ FAIL: Fixed buttons change size with zoom level');
}
console.log('\n⏸️ Browser will stay open for 5 seconds for manual verification...');
await page.waitForTimeout(5000);
} catch (error) {
console.error('\n❌ Test failed:', error.message);
} finally {
await browser.close();
console.log('\n✅ Test completed\n');
}
}
testButtonSizes();
+18
View File
@@ -0,0 +1,18 @@
import { chromium } from '@playwright/test';
(async () => {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('http://localhost:1999');
// Clear all localStorage
await page.evaluate(() => {
localStorage.clear();
});
console.log('✅ localStorage cleared');
await browser.close();
})();
+176
View File
@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html>
<head>
<title>HTMX Indicator Test</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
<link rel="stylesheet" href="http://localhost:1999/static/css/main.css">
<style>
body {
padding: 40px;
background: #2c3e50;
color: white;
font-family: system-ui;
}
.test-section {
margin: 30px 0;
padding: 20px;
background: #34495e;
border-radius: 8px;
}
.test-btn {
padding: 10px 20px;
background: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin: 10px;
}
.test-btn:hover {
background: #2980b9;
}
.htmx-request {
background: #27ae60 !important;
}
.status {
margin-top: 10px;
padding: 10px;
background: #1a252f;
border-radius: 4px;
font-family: monospace;
}
</style>
</head>
<body>
<h1>HTMX Loading Indicators Test</h1>
<div class="test-section">
<h2>Test 1: Button with Child Indicator (Default Pattern)</h2>
<p>Click button - spinner should appear INSIDE button during request</p>
<button class="test-btn"
hx-get="http://localhost:1999/switch-language?lang=en"
hx-target="#result1"
hx-swap="innerHTML">
Switch Language
<iconify-icon icon="mdi:loading"
class="htmx-indicator spinning small light"
width="14"
height="14"></iconify-icon>
</button>
<div id="result1" class="status">Result will appear here</div>
</div>
<div class="test-section">
<h2>Test 2: Language Selector (Actual Component)</h2>
<p>This mirrors the actual language selector from the CV</p>
<div class="language-selector">
<button class="selector-btn"
hx-get="http://localhost:1999/switch-language?lang=en"
hx-target="#result2"
hx-swap="innerHTML">
<span>English</span>
<iconify-icon icon="mdi:loading"
class="htmx-indicator spinning small light"
width="14"
height="14"
aria-label="Loading"></iconify-icon>
</button>
<button class="selector-btn"
hx-get="http://localhost:1999/switch-language?lang=es"
hx-target="#result2"
hx-swap="innerHTML">
<span>Español</span>
<iconify-icon icon="mdi:loading"
class="htmx-indicator spinning small light"
width="14"
height="14"
aria-label="Loading"></iconify-icon>
</button>
</div>
<div id="result2" class="status">Result will appear here</div>
</div>
<div class="test-section">
<h2>Test 3: CSS Verification</h2>
<p>Manually verify CSS rules are applied:</p>
<div class="status">
<div>1. Open DevTools</div>
<div>2. Click a button above</div>
<div>3. Watch Network tab for request</div>
<div>4. Check Elements tab - button should have class "htmx-request"</div>
<div>5. Check Computed styles - iconify-icon.htmx-indicator should have opacity: 1</div>
</div>
</div>
<div class="test-section">
<h2>Debug: CSS Rules Status</h2>
<div class="status" id="css-debug"></div>
</div>
<script>
// Debug: Check if CSS rules are loaded
setTimeout(() => {
const indicator = document.querySelector('.htmx-indicator');
if (indicator) {
const styles = window.getComputedStyle(indicator);
const debug = document.getElementById('css-debug');
debug.innerHTML = `
<strong>CSS Computed Values for .htmx-indicator:</strong><br>
opacity: ${styles.opacity}<br>
display: ${styles.display}<br>
transition: ${styles.transition}<br>
pointer-events: ${styles.pointerEvents}<br>
<br>
<strong>Expected:</strong><br>
opacity: 0 (hidden by default)<br>
display: inline-flex<br>
transition: opacity 200ms ease-in-out<br>
pointer-events: none
`;
}
}, 1000);
// Monitor HTMX events
document.body.addEventListener('htmx:beforeRequest', (e) => {
console.log('🚀 HTMX Request Starting:', e.detail);
console.log(' Target element:', e.detail.elt);
console.log(' Has htmx-request class:', e.detail.elt.classList.contains('htmx-request'));
});
document.body.addEventListener('htmx:afterRequest', (e) => {
console.log('✅ HTMX Request Complete:', e.detail);
console.log(' Target element:', e.detail.elt);
console.log(' Has htmx-request class:', e.detail.elt.classList.contains('htmx-request'));
});
// Monitor class changes
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const target = mutation.target;
const hasRequest = target.classList.contains('htmx-request');
console.log(`📝 Class change on ${target.tagName}:`, {
hasHtmxRequest: hasRequest,
allClasses: Array.from(target.classList)
});
if (hasRequest) {
const indicator = target.querySelector('.htmx-indicator');
if (indicator) {
const opacity = window.getComputedStyle(indicator).opacity;
console.log(` → Indicator opacity: ${opacity} (should be 1)`);
}
}
}
});
});
// Observe all buttons
document.querySelectorAll('button[hx-get]').forEach(btn => {
observer.observe(btn, { attributes: true });
});
</script>
</body>
</html>
+229
View File
@@ -0,0 +1,229 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test Inline Loading - No Blocking Overlay</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
background: #f5f5f5;
}
h1 {
color: #333;
}
.test-section {
background: white;
border-radius: 8px;
padding: 2rem;
margin: 2rem 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.language-buttons {
display: flex;
gap: 1rem;
margin: 1rem 0;
}
button {
padding: 0.5rem 1rem;
background: #3b82f6;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
button:hover {
background: #2563eb;
}
button.active {
background: #059669;
}
.indicator {
display: inline-block;
width: 16px;
height: 16px;
margin-left: 0.5rem;
vertical-align: middle;
}
.htmx-indicator {
opacity: 0;
transition: opacity 200ms ease-in;
}
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
opacity: 1;
}
/* INLINE LOADING STATES - NO BLOCKING OVERLAY */
.cv-page-content-wrapper {
position: relative;
transition: opacity 200ms ease-in-out,
transform 200ms ease-in-out,
filter 200ms ease-in-out;
}
.cv-page-content-wrapper.htmx-swapping {
opacity: 0.5;
transform: scale(0.99);
pointer-events: none;
filter: blur(1px);
}
.cv-page-content-wrapper.htmx-settling {
opacity: 1;
transform: scale(1);
pointer-events: auto;
filter: blur(0);
}
.content-area {
background: #f9fafb;
border: 2px solid #e5e7eb;
border-radius: 4px;
padding: 2rem;
min-height: 300px;
}
.status {
position: fixed;
top: 1rem;
right: 1rem;
background: #059669;
color: white;
padding: 1rem;
border-radius: 4px;
font-weight: bold;
}
.status.error {
background: #dc2626;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinning {
animation: spin 1s linear infinite;
}
.test-info {
background: #dbeafe;
border-left: 4px solid #3b82f6;
padding: 1rem;
margin: 1rem 0;
}
.test-info h3 {
margin-top: 0;
color: #1e40af;
}
.checklist {
list-style: none;
padding-left: 0;
}
.checklist li:before {
content: "✓ ";
color: #059669;
font-weight: bold;
margin-right: 0.5rem;
}
</style>
</head>
<body>
<div class="status">✓ No Blocking Overlay</div>
<h1>Inline Loading States Test</h1>
<div class="test-info">
<h3>What to Observe:</h3>
<ul class="checklist">
<li>NO full-page overlay appears when switching languages</li>
<li>Language button shows inline spinner during request</li>
<li>CV content fades/blurs slightly during swap (inline effect)</li>
<li>Everything else remains accessible (no blocking)</li>
<li>Smooth transition without page blocking</li>
</ul>
</div>
<div class="test-section">
<h2>Language Selector (With Inline Indicators)</h2>
<div class="language-buttons">
<button hx-get="http://localhost:1999/switch-language?lang=en"
hx-target="#language-selector"
hx-swap="outerHTML swap:250ms settle:250ms"
hx-indicator="#lang-indicator-en"
class="active">
English
<span id="lang-indicator-en" class="htmx-indicator indicator spinning"></span>
</button>
<button hx-get="http://localhost:1999/switch-language?lang=es"
hx-target="#language-selector"
hx-swap="outerHTML swap:250ms settle:250ms"
hx-indicator="#lang-indicator-es">
Español
<span id="lang-indicator-es" class="htmx-indicator indicator spinning"></span>
</button>
</div>
<div id="language-selector"></div>
</div>
<div class="test-section">
<h2>CV Content (With Inline Loading States)</h2>
<div id="cv-inner-content-page-1" class="cv-page-content-wrapper">
<div class="content-area">
<h3>CV Content Page 1</h3>
<p>This content will fade and blur slightly during language transitions.</p>
<p><strong>Observe:</strong> No blocking overlay appears - just a subtle inline effect!</p>
<p>You can still scroll and interact with other parts of the page during the transition.</p>
</div>
</div>
</div>
<div class="test-section">
<h2>Additional Scrollable Content</h2>
<p>This section demonstrates that the page remains functional during language transitions.</p>
<p>Try scrolling, clicking around, or interacting with other elements while switching languages.</p>
<div style="height: 200px; background: linear-gradient(to bottom, #dbeafe, #bfdbfe); border-radius: 4px; padding: 1rem;">
<p><strong>Key Improvement:</strong></p>
<ul>
<li>✓ Before: Full-page overlay blocked everything</li>
<li>✓ After: Inline loading states, no blocking</li>
<li>✓ Language buttons show inline spinners</li>
<li>✓ Content areas show subtle blur/fade</li>
<li>✓ Rest of UI remains accessible</li>
</ul>
</div>
</div>
<script>
// Monitor HTMX events for debugging
document.body.addEventListener('htmx:beforeRequest', (e) => {
console.log('✓ HTMX Request Starting:', e.detail.target.id);
});
document.body.addEventListener('htmx:afterSwap', (e) => {
console.log('✓ HTMX Swap Complete:', e.detail.target.id);
});
// NO skeleton loader JavaScript needed!
console.log('✓ Test page loaded - NO blocking overlay code present');
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

+287
View File
@@ -0,0 +1,287 @@
<!DOCTYPE html>
<html>
<head>
<title>Skeleton Loader Fix - Manual Test</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script src="https://unpkg.com/hyperscript.org@0.9.12"></script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 40px auto;
padding: 20px;
}
h1 { color: #333; }
.test-section {
background: #f5f5f5;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
.language-selector-wrapper {
display: inline-block;
background: white;
padding: 10px;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.selector-btn {
padding: 8px 16px;
margin: 0 4px;
border: 2px solid #ddd;
background: white;
cursor: pointer;
border-radius: 4px;
}
.selector-btn.active {
background: #007bff;
color: white;
border-color: #007bff;
}
#skeleton-loader {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
z-index: 1000;
opacity: 0;
pointer-events: none;
transition: opacity 250ms ease-in-out;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
}
#skeleton-loader.active {
opacity: 1;
pointer-events: all;
}
.status {
background: white;
border: 2px solid #ddd;
padding: 15px;
margin: 10px 0;
border-radius: 4px;
}
.status.active {
border-color: #28a745;
background: #d4edda;
}
#console {
background: #1e1e1e;
color: #d4d4d4;
padding: 15px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 12px;
max-height: 300px;
overflow-y: auto;
}
.console-line {
margin: 4px 0;
}
.console-line.before { color: #4ec9b0; }
.console-line.after { color: #ce9178; }
.console-line.skeleton { color: #dcdcaa; }
.instructions {
background: #fff3cd;
border: 1px solid #ffc107;
padding: 15px;
border-radius: 4px;
margin: 20px 0;
}
.success {
background: #d4edda;
border: 1px solid #28a745;
padding: 15px;
border-radius: 4px;
color: #155724;
}
.error {
background: #f8d7da;
border: 1px solid #dc3545;
padding: 15px;
border-radius: 4px;
color: #721c24;
}
</style>
</head>
<body>
<h1>🔬 Skeleton Loader Fix - Manual Verification</h1>
<div class="instructions">
<h3>📋 Test Instructions:</h3>
<ol>
<li>Click the "Switch to Spanish" button below</li>
<li>Watch for the dark overlay to appear briefly</li>
<li>The overlay should disappear after the content loads</li>
<li>Check the console log below for event tracking</li>
<li>Try switching back and forth multiple times</li>
</ol>
<p><strong>✅ PASS</strong>: Overlay appears and disappears smoothly</p>
<p><strong>❌ FAIL</strong>: Overlay stays visible permanently</p>
</div>
<div class="test-section">
<h3>Language Selector (HTMX + Hyperscript):</h3>
<!-- Skeleton Loader -->
<div id="skeleton-loader">
<div>🔄 LOADING... (This should disappear!)</div>
</div>
<!-- This wrapper has the hyperscript handlers -->
<div class="language-selector-wrapper"
_="on htmx:beforeRequest from .selector-btn
add .active to #skeleton-loader
log 'BEFORE: Skeleton activated'
end
on htmx:afterSwap from .selector-btn
wait 100ms
remove .active from #skeleton-loader
log 'AFTER: Skeleton deactivated'
end">
<!-- This inner element gets swapped (outerHTML) -->
<div class="language-selector" id="language-selector">
<button class="selector-btn active"
hx-get="/mock-response-en"
hx-target="#language-selector"
hx-swap="outerHTML swap:250ms settle:250ms"
onclick="mockSwitch('en')">
English
</button>
<button class="selector-btn"
hx-get="/mock-response-es"
hx-target="#language-selector"
hx-swap="outerHTML swap:250ms settle:250ms"
onclick="mockSwitch('es')">
Español
</button>
</div>
</div>
</div>
<div class="test-section">
<h3>Status Monitor:</h3>
<div class="status" id="status-skeleton">
<strong>Skeleton Loader:</strong> <span id="skeleton-state">Hidden (opacity: 0)</span>
</div>
<div class="status" id="status-events">
<strong>Last Event:</strong> <span id="last-event">None</span>
</div>
</div>
<div class="test-section">
<h3>Console Log:</h3>
<div id="console"></div>
</div>
<script>
// Mock server responses since we're testing standalone
let currentLang = 'en';
function mockSwitch(lang) {
currentLang = lang;
// Simulate server delay
setTimeout(() => {
const newHTML = lang === 'en'
? `<div class="language-selector" id="language-selector">
<button class="selector-btn active" hx-get="/mock-response-en" hx-target="#language-selector" hx-swap="outerHTML swap:250ms settle:250ms" onclick="mockSwitch('en')">English</button>
<button class="selector-btn" hx-get="/mock-response-es" hx-target="#language-selector" hx-swap="outerHTML swap:250ms settle:250ms" onclick="mockSwitch('es')">Español</button>
</div>`
: `<div class="language-selector" id="language-selector">
<button class="selector-btn" hx-get="/mock-response-en" hx-target="#language-selector" hx-swap="outerHTML swap:250ms settle:250ms" onclick="mockSwitch('en')">English</button>
<button class="selector-btn active" hx-get="/mock-response-es" hx-target="#language-selector" hx-swap="outerHTML swap:250ms settle:250ms" onclick="mockSwitch('es')">Español</button>
</div>`;
// Let HTMX handle the swap
htmx.trigger('#language-selector', 'htmx:beforeSwap', {
serverResponse: newHTML
});
}, 300);
}
// Monitor skeleton state
const skeleton = document.getElementById('skeleton-loader');
const statusSkeleton = document.getElementById('status-skeleton');
const statusEvents = document.getElementById('status-events');
const lastEvent = document.getElementById('last-event');
const consoleLog = document.getElementById('console');
const skeletonState = document.getElementById('skeleton-state');
function addConsoleLog(message, type = '') {
const line = document.createElement('div');
line.className = `console-line ${type}`;
line.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
consoleLog.appendChild(line);
consoleLog.scrollTop = consoleLog.scrollHeight;
}
function updateSkeletonStatus() {
const hasActive = skeleton.classList.contains('active');
const opacity = window.getComputedStyle(skeleton).opacity;
skeletonState.textContent = hasActive
? `Visible (opacity: ${opacity})`
: `Hidden (opacity: ${opacity})`;
statusSkeleton.className = hasActive ? 'status active' : 'status';
}
// Watch for class changes on skeleton
const observer = new MutationObserver(() => {
updateSkeletonStatus();
const hasActive = skeleton.classList.contains('active');
addConsoleLog(
hasActive ? 'Skeleton SHOWN (.active added)' : 'Skeleton HIDDEN (.active removed)',
'skeleton'
);
});
observer.observe(skeleton, { attributes: true, attributeFilter: ['class'] });
// Monitor HTMX events
document.body.addEventListener('htmx:beforeRequest', (e) => {
if (e.detail.elt.classList.contains('selector-btn')) {
addConsoleLog('htmx:beforeRequest - Language switch starting', 'before');
lastEvent.textContent = 'htmx:beforeRequest';
statusEvents.className = 'status active';
}
});
document.body.addEventListener('htmx:afterSwap', (e) => {
if (e.detail.target.id === 'language-selector') {
addConsoleLog('htmx:afterSwap - Language switch complete', 'after');
lastEvent.textContent = 'htmx:afterSwap';
statusEvents.className = 'status active';
// Re-initialize hyperscript on new elements
htmx.process(document.querySelector('.language-selector-wrapper'));
}
});
// Initial status
addConsoleLog('Test page loaded - Ready to test', 'skeleton');
updateSkeletonStatus();
</script>
</body>
</html>
+578
View File
@@ -0,0 +1,578 @@
#!/usr/bin/env node
/**
* COMPREHENSIVE VERIFICATION TEST SUITE
* Tests both HTMX indicators and shortcuts button visibility fixes
*/
import { chromium } from 'playwright';
const BASE_URL = 'http://localhost:1999';
const RESULTS = {
passed: [],
failed: [],
warnings: []
};
function log(status, message) {
const timestamp = new Date().toLocaleTimeString();
const icons = { pass: '✅', fail: '❌', warn: '⚠️', info: '️' };
console.log(`[${timestamp}] ${icons[status] || icons.info} ${message}`);
if (status === 'pass') RESULTS.passed.push(message);
if (status === 'fail') RESULTS.failed.push(message);
if (status === 'warn') RESULTS.warnings.push(message);
}
function measureTime(start) {
return `${Date.now() - start}ms`;
}
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function test1_HTMXLoadingIndicators(page) {
log('info', '═══════════════════════════════════════════════════════');
log('info', 'TEST 1: HTMX Loading Indicators (Feature 003)');
log('info', '═══════════════════════════════════════════════════════');
try {
// Navigate to page
await page.goto(BASE_URL);
await page.waitForLoadState('networkidle');
log('pass', 'Page loaded successfully');
// Test 1.1: Verify indicator elements exist
log('info', 'Test 1.1: Checking indicator elements exist...');
const enIndicator = page.locator('#lang-indicator-en');
const esIndicator = page.locator('#lang-indicator-es');
await enIndicator.waitFor({ state: 'attached', timeout: 5000 });
await esIndicator.waitFor({ state: 'attached', timeout: 5000 });
log('pass', 'Both language indicators found in DOM');
// Test 1.2: Verify initial opacity is 0 (hidden)
log('info', 'Test 1.2: Checking initial indicator opacity...');
const enInitialOpacity = await enIndicator.evaluate(el =>
window.getComputedStyle(el).opacity
);
const esInitialOpacity = await esIndicator.evaluate(el =>
window.getComputedStyle(el).opacity
);
if (enInitialOpacity === '0' && esInitialOpacity === '0') {
log('pass', `Indicators hidden initially (opacity: ${enInitialOpacity})`);
} else {
log('fail', `Indicators should be hidden (EN: ${enInitialOpacity}, ES: ${esInitialOpacity})`);
}
// Test 1.3: Click EN button and verify indicator appears
log('info', 'Test 1.3: Testing EN button loading indicator...');
// Get the ES button (since we're on EN by default)
const esButton = page.locator('button.selector-btn[data-short="ES"]');
await esButton.waitFor({ state: 'visible' });
// Set up monitoring for opacity changes
const opacityPromise = page.evaluate(() => {
return new Promise(resolve => {
const indicator = document.querySelector('#lang-indicator-es');
let maxOpacity = 0;
let opacityChanges = [];
const observer = new MutationObserver(() => {
const currentOpacity = parseFloat(window.getComputedStyle(indicator).opacity);
opacityChanges.push(currentOpacity);
maxOpacity = Math.max(maxOpacity, currentOpacity);
});
observer.observe(indicator.parentElement, {
attributes: true,
attributeFilter: ['class'],
subtree: true
});
// Check opacity every 10ms for 2 seconds
let checks = 0;
const interval = setInterval(() => {
const currentOpacity = parseFloat(window.getComputedStyle(indicator).opacity);
opacityChanges.push(currentOpacity);
maxOpacity = Math.max(maxOpacity, currentOpacity);
checks++;
if (checks > 200) { // 2 seconds
clearInterval(interval);
observer.disconnect();
resolve({ maxOpacity, opacityChanges: opacityChanges.filter(o => o > 0) });
}
}, 10);
});
});
// Click the button
const clickTime = Date.now();
await esButton.click();
// Wait for the opacity monitoring to complete
const opacityData = await opacityPromise;
const responseTime = measureTime(clickTime);
log('info', `Request completed in ${responseTime}`);
log('info', `Max indicator opacity: ${opacityData.maxOpacity}`);
log('info', `Opacity changes detected: ${opacityData.opacityChanges.length}`);
if (opacityData.maxOpacity >= 0.9) {
log('pass', `Indicator became visible (max opacity: ${opacityData.maxOpacity})`);
} else if (opacityData.maxOpacity > 0) {
log('warn', `Indicator partially visible but not fully (max: ${opacityData.maxOpacity})`);
} else {
log('warn', 'Indicator not visible on fast request (expected on localhost - will verify with throttled test)');
}
// Test 1.4: Verify indicator faded out after request
await sleep(500);
const finalOpacity = await esIndicator.evaluate(el =>
window.getComputedStyle(el).opacity
);
if (finalOpacity === '0') {
log('pass', `Indicator hidden after request (opacity: ${finalOpacity})`);
} else {
log('warn', `Indicator may not have faded out (opacity: ${finalOpacity})`);
}
// Test 1.5: Take screenshot during loading
log('info', 'Test 1.5: Capturing screenshot during loading...');
// Click back to EN to trigger another loading state
await sleep(500);
const enButton = page.locator('button.selector-btn[data-short="EN"]');
// Start click and immediately capture
const screenshotPromise = page.screenshot({
path: '/Users/txeo/Git/yo/cv/test-screenshots/htmx-indicator-loading.png',
fullPage: false
});
await enButton.click();
await screenshotPromise;
log('pass', 'Screenshot captured: test-screenshots/htmx-indicator-loading.png');
// Test 1.6: Network throttling test
log('info', 'Test 1.6: Testing with slow 3G network...');
// Slow 3G preset - only delay the specific endpoint
let requestIntercepted = false;
await page.route('**/switch-language**', async route => {
if (!requestIntercepted) {
requestIntercepted = true;
await sleep(800); // Simulate 800ms delay
}
await route.continue();
});
await sleep(500);
const slowClickTime = Date.now();
// Click and monitor
const slowOpacityPromise = page.evaluate(() => {
return new Promise(resolve => {
const indicator = document.querySelector('#lang-indicator-en');
let maxOpacity = 0;
const interval = setInterval(() => {
const opacity = parseFloat(window.getComputedStyle(indicator).opacity);
maxOpacity = Math.max(maxOpacity, opacity);
}, 10);
setTimeout(() => {
clearInterval(interval);
resolve(maxOpacity);
}, 1000);
});
});
await enButton.click();
const slowOpacity = await slowOpacityPromise;
await page.waitForLoadState('networkidle');
const slowResponseTime = measureTime(slowClickTime);
log('info', `Slow request completed in ${slowResponseTime}`);
log('info', `Mid-request opacity: ${slowOpacity}`);
if (slowOpacity >= 0.9) {
log('pass', `Indicator visible during slow request (opacity: ${slowOpacity})`);
} else {
log('fail', `Indicator not visible during slow request (opacity: ${slowOpacity})`);
}
// Unroute to restore normal speed
await page.unroute('**/switch-language**');
} catch (error) {
log('fail', `Test 1 error: ${error.message}`);
console.error(error);
}
}
async function test2_ShortcutsButtonVisibility(page) {
log('info', '═══════════════════════════════════════════════════════');
log('info', 'TEST 2: Shortcuts Button Visibility (Feature 001)');
log('info', '═══════════════════════════════════════════════════════');
try {
// Ensure we're on the page
await page.goto(BASE_URL);
await page.waitForLoadState('networkidle');
// Test 2.1: Verify button exists and is visible
log('info', 'Test 2.1: Checking shortcuts button exists...');
const shortcutsBtn = page.locator('.shortcuts-btn');
await shortcutsBtn.waitFor({ state: 'visible', timeout: 5000 });
log('pass', 'Shortcuts button found and visible');
// Test 2.2: Measure initial opacity
log('info', 'Test 2.2: Measuring button opacity...');
const opacity = await shortcutsBtn.evaluate(el =>
window.getComputedStyle(el).opacity
);
const opacityNum = parseFloat(opacity);
log('info', `Button opacity: ${opacity}`);
if (opacityNum === 0.6) {
log('pass', `Button opacity is exactly 0.6 as expected`);
} else if (opacityNum >= 0.5 && opacityNum <= 0.7) {
log('warn', `Button opacity close to target (${opacity} vs 0.6)`);
} else {
log('fail', `Button opacity incorrect (${opacity}, expected 0.6)`);
}
// Test 2.3: Verify button is actually visible to users
log('info', 'Test 2.3: Verifying visual discoverability...');
const boundingBox = await shortcutsBtn.boundingBox();
if (boundingBox) {
log('pass', `Button has dimensions: ${boundingBox.width}x${boundingBox.height}px`);
log('info', `Position: (${boundingBox.x}, ${boundingBox.y})`);
} else {
log('fail', 'Button has no bounding box (may not be rendered)');
}
// Test 2.4: Test hover state
log('info', 'Test 2.4: Testing hover state...');
await shortcutsBtn.hover();
await sleep(500); // Wait for transition
const hoverOpacity = await shortcutsBtn.evaluate(el =>
window.getComputedStyle(el).opacity
);
if (parseFloat(hoverOpacity) === 1.0) {
log('pass', `Hover opacity is 1.0 (full visibility)`);
} else {
log('warn', `Hover opacity: ${hoverOpacity} (expected 1.0)`);
}
// Test 2.5: Take screenshot
log('info', 'Test 2.5: Capturing button screenshot...');
await page.screenshot({
path: '/Users/txeo/Git/yo/cv/test-screenshots/shortcuts-button-visible.png',
fullPage: false
});
log('pass', 'Screenshot captured: test-screenshots/shortcuts-button-visible.png');
// Test 2.6: Verify functionality
log('info', 'Test 2.6: Testing button functionality...');
await shortcutsBtn.click();
await sleep(300);
// Check if modal opened
const modal = page.locator('.shortcuts-modal, [id*="shortcut"], [class*="modal"]');
const modalVisible = await modal.isVisible().catch(() => false);
if (modalVisible) {
log('pass', 'Shortcuts modal opened successfully');
// Test ESC to close
await page.keyboard.press('Escape');
await sleep(300);
const modalClosed = await modal.isVisible().catch(() => false);
if (!modalClosed) {
log('pass', 'Modal closes with ESC key');
} else {
log('warn', 'Modal may not close with ESC');
}
} else {
log('fail', 'Modal did not open on button click');
}
// Test 2.7: Check info button consistency
log('info', 'Test 2.7: Verifying info button has same opacity...');
const infoBtn = page.locator('.info-button');
const infoBtnExists = await infoBtn.count();
if (infoBtnExists > 0) {
const infoOpacity = await infoBtn.evaluate(el =>
window.getComputedStyle(el).opacity
);
if (parseFloat(infoOpacity) === 0.6) {
log('pass', `Info button also has opacity 0.6 (consistency maintained)`);
} else {
log('warn', `Info button opacity: ${infoOpacity} (expected 0.6)`);
}
} else {
log('info', 'Info button not found (may not be on this page)');
}
} catch (error) {
log('fail', `Test 2 error: ${error.message}`);
console.error(error);
}
}
async function test3_RegressionTests(page) {
log('info', '═══════════════════════════════════════════════════════');
log('info', 'TEST 3: Regression Testing (Ensure Nothing Broke)');
log('info', '═══════════════════════════════════════════════════════');
try {
await page.goto(BASE_URL);
await page.waitForLoadState('networkidle');
// Test 3.1: Skeleton loader still works
log('info', 'Test 3.1: Verifying skeleton loader animation...');
const skeletonExists = await page.locator('#skeleton-loader').count();
if (skeletonExists > 0) {
log('pass', 'Skeleton loader element found');
// Trigger language switch
const esButton = page.locator('button.selector-btn[data-short="ES"]');
const skeletonActivated = await page.evaluate(() => {
return new Promise(resolve => {
const skeleton = document.querySelector('#skeleton-loader');
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.target.classList.contains('active')) {
observer.disconnect();
resolve(true);
}
}
});
observer.observe(skeleton, { attributes: true, attributeFilter: ['class'] });
setTimeout(() => {
observer.disconnect();
resolve(false);
}, 2000);
});
});
await esButton.click();
await page.waitForLoadState('networkidle');
if (skeletonActivated) {
log('pass', 'Skeleton loader activated during language switch');
} else {
log('warn', 'Skeleton loader may not be activating');
}
} else {
log('info', 'Skeleton loader not found (may not be used)');
}
// Test 3.2: No console errors
log('info', 'Test 3.2: Checking for console errors...');
const errors = [];
page.on('console', msg => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});
await page.reload();
await sleep(1000);
if (errors.length === 0) {
log('pass', 'No console errors detected');
} else {
log('fail', `Console errors found: ${errors.join(', ')}`);
}
// Test 3.3: No layout shifts
log('info', 'Test 3.3: Measuring Cumulative Layout Shift...');
const cls = await page.evaluate(() => {
return new Promise(resolve => {
let clsValue = 0;
try {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
}
});
observer.observe({ type: 'layout-shift', buffered: true });
setTimeout(() => {
observer.disconnect();
resolve(clsValue);
}, 2000);
} catch (e) {
resolve(0);
}
});
});
log('info', `CLS Score: ${cls.toFixed(3)}`);
if (cls < 0.1) {
log('pass', 'Excellent CLS score (< 0.1)');
} else if (cls < 0.25) {
log('warn', `CLS needs improvement (${cls.toFixed(3)})`);
} else {
log('fail', `Poor CLS score (${cls.toFixed(3)})`);
}
// Test 3.4: Page load performance
log('info', 'Test 3.4: Measuring page load performance...');
const perfMetrics = await page.evaluate(() => {
const perf = performance.getEntriesByType('navigation')[0];
return {
loadTime: perf.loadEventEnd - perf.fetchStart,
domContentLoaded: perf.domContentLoadedEventEnd - perf.fetchStart,
firstPaint: performance.getEntriesByType('paint')[0]?.startTime || 0
};
});
log('info', `Load time: ${perfMetrics.loadTime.toFixed(0)}ms`);
log('info', `DOMContentLoaded: ${perfMetrics.domContentLoaded.toFixed(0)}ms`);
log('info', `First Paint: ${perfMetrics.firstPaint.toFixed(0)}ms`);
if (perfMetrics.loadTime < 3000) {
log('pass', 'Page loads in under 3 seconds');
} else {
log('warn', `Page load time: ${perfMetrics.loadTime.toFixed(0)}ms`);
}
} catch (error) {
log('fail', `Test 3 error: ${error.message}`);
console.error(error);
}
}
async function generateReport() {
log('info', '═══════════════════════════════════════════════════════');
log('info', 'FINAL TEST REPORT');
log('info', '═══════════════════════════════════════════════════════');
console.log('\n📊 SUMMARY:');
console.log(` ✅ Passed: ${RESULTS.passed.length}`);
console.log(` ❌ Failed: ${RESULTS.failed.length}`);
console.log(` ⚠️ Warnings: ${RESULTS.warnings.length}`);
if (RESULTS.failed.length > 0) {
console.log('\n❌ FAILURES:');
RESULTS.failed.forEach((msg, i) => console.log(` ${i + 1}. ${msg}`));
}
if (RESULTS.warnings.length > 0) {
console.log('\n⚠️ WARNINGS:');
RESULTS.warnings.forEach((msg, i) => console.log(` ${i + 1}. ${msg}`));
}
console.log('\n📈 FEATURE GRADES:');
// Feature 003: HTMX Indicators
const indicatorTests = RESULTS.passed.filter(m =>
m.includes('indicator') || m.includes('Indicator')
).length;
const indicatorFails = RESULTS.failed.filter(m =>
m.includes('indicator') || m.includes('Indicator')
).length;
let feature003Grade = 'F';
if (indicatorFails === 0 && indicatorTests >= 5) feature003Grade = 'A';
else if (indicatorFails === 0 && indicatorTests >= 3) feature003Grade = 'B';
else if (indicatorFails <= 1) feature003Grade = 'C';
else if (indicatorFails <= 2) feature003Grade = 'D';
console.log(` Feature 003 (HTMX Indicators): ${feature003Grade} (${indicatorTests} tests passed, ${indicatorFails} failed)`);
// Feature 001: Shortcuts Button
const buttonTests = RESULTS.passed.filter(m =>
m.includes('Button') || m.includes('button') || m.includes('opacity')
).length;
const buttonFails = RESULTS.failed.filter(m =>
m.includes('Button') || m.includes('button') || m.includes('opacity')
).length;
let feature001Grade = 'A-';
if (buttonFails === 0 && buttonTests >= 6) feature001Grade = 'A';
else if (buttonFails === 0 && buttonTests >= 4) feature001Grade = 'A-';
else if (buttonFails <= 1) feature001Grade = 'B+';
else if (buttonFails <= 2) feature001Grade = 'B';
console.log(` Feature 001 (Shortcuts Button): ${feature001Grade} (${buttonTests} tests passed, ${buttonFails} failed)`);
console.log('\n📸 SCREENSHOTS:');
console.log(' - test-screenshots/htmx-indicator-loading.png');
console.log(' - test-screenshots/shortcuts-button-visible.png');
const overallSuccess = RESULTS.failed.length === 0;
console.log(`\n${overallSuccess ? '✅ ALL TESTS PASSED' : '❌ SOME TESTS FAILED'}\n`);
return overallSuccess;
}
async function main() {
console.log('🧪 COMPREHENSIVE VERIFICATION TEST SUITE');
console.log('Testing HTMX Indicators + Shortcuts Button Fixes\n');
// Create screenshots directory
const { mkdir } = await import('fs/promises');
await mkdir('/Users/txeo/Git/yo/cv/test-screenshots', { recursive: true });
const browser = await chromium.launch({
headless: true,
args: ['--disable-dev-shm-usage']
});
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 },
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
});
const page = await context.newPage();
try {
// Run all test suites
await test1_HTMXLoadingIndicators(page);
await test2_ShortcutsButtonVisibility(page);
await test3_RegressionTests(page);
// Generate report
const success = await generateReport();
await browser.close();
process.exit(success ? 0 : 1);
} catch (error) {
console.error('Fatal error:', error);
await browser.close();
process.exit(1);
}
}
main();
+122
View File
@@ -0,0 +1,122 @@
import { chromium } from '@playwright/test';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import fs from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const screenshotsDir = join(__dirname, 'test-screenshots');
// Ensure screenshots directory exists
if (!fs.existsSync(screenshotsDir)) {
fs.mkdirSync(screenshotsDir);
}
(async () => {
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
console.log('🎨 Testing Zoom Toggle Button Visual States...\n');
await page.goto('http://localhost:1999');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Clear localStorage for fresh start
await page.evaluate(() => localStorage.clear());
await page.reload();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
const toggleBtn = page.locator('#zoom-toggle-button');
// State 1: Inactive (default)
console.log('📸 State 1: Inactive (default)');
const inactiveStyles = await toggleBtn.evaluate(el => {
const styles = window.getComputedStyle(el);
return {
background: styles.backgroundColor,
color: styles.color,
opacity: styles.opacity
};
});
console.log(' Background:', inactiveStyles.background);
console.log(' Icon color:', inactiveStyles.color);
console.log(' Opacity:', inactiveStyles.opacity);
await page.screenshot({ path: join(screenshotsDir, '1-inactive.png') });
// State 2: Inactive hover
console.log('\n📸 State 2: Inactive hover');
await toggleBtn.hover();
await page.waitForTimeout(300);
const inactiveHoverStyles = await toggleBtn.evaluate(el => {
const styles = window.getComputedStyle(el);
return {
background: styles.backgroundColor,
color: styles.color,
opacity: styles.opacity,
transform: styles.transform
};
});
console.log(' Background:', inactiveHoverStyles.background);
console.log(' Icon color:', inactiveHoverStyles.color);
console.log(' Opacity:', inactiveHoverStyles.opacity);
console.log(' Transform:', inactiveHoverStyles.transform);
await page.screenshot({ path: join(screenshotsDir, '2-inactive-hover.png') });
// Move mouse away
await page.mouse.move(500, 500);
await page.waitForTimeout(300);
// State 3: Active (zoom opened)
console.log('\n📸 State 3: Active (zoom opened)');
await toggleBtn.click();
await page.waitForTimeout(500);
const activeStyles = await toggleBtn.evaluate(el => {
const styles = window.getComputedStyle(el);
return {
background: styles.backgroundColor,
color: styles.color,
opacity: styles.opacity
};
});
console.log(' Background:', activeStyles.background);
console.log(' Icon color:', activeStyles.color);
console.log(' Opacity:', activeStyles.opacity);
await page.screenshot({ path: join(screenshotsDir, '3-active.png') });
// State 4: Active hover
console.log('\n📸 State 4: Active hover');
await toggleBtn.hover();
await page.waitForTimeout(300);
const activeHoverStyles = await toggleBtn.evaluate(el => {
const styles = window.getComputedStyle(el);
return {
background: styles.backgroundColor,
color: styles.color,
opacity: styles.opacity,
transform: styles.transform,
boxShadow: styles.boxShadow
};
});
console.log(' Background:', activeHoverStyles.background);
console.log(' Icon color:', activeHoverStyles.color);
console.log(' Opacity:', activeHoverStyles.opacity);
console.log(' Transform:', activeHoverStyles.transform);
console.log(' Box shadow:', activeHoverStyles.boxShadow);
await page.screenshot({ path: join(screenshotsDir, '4-active-hover.png') });
console.log('\n✅ Visual state tests complete!');
console.log(`📁 Screenshots saved to: ${screenshotsDir}`);
console.log('\n🎯 Summary:');
console.log(' - Inactive: Gray icon (#888), 60% opacity');
console.log(' - Inactive hover: Lighter gray bg, brighter icon (#aaa), 80% opacity');
console.log(' - Active: Blue bg, white icon, 100% opacity');
console.log(' - Active hover: Darker blue, blue glow, slight scale');
await page.waitForTimeout(5000);
await browser.close();
})();
+401
View File
@@ -0,0 +1,401 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zoom Control Debug Test</title>
<!-- Hyperscript - MUST load BEFORE component -->
<script src="https://unpkg.com/hyperscript.org@0.9.12"></script>
<!-- Iconify for icons -->
<script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
<style>
body {
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
background: #f0f0f0;
}
.test-area {
margin: 20px;
padding: 20px;
background: white;
border-radius: 8px;
}
/* Zoom Control Styles */
.zoom-control {
position: fixed;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
z-index: 900;
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.65rem 1.25rem;
background: rgba(128, 128, 128, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 50px;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
opacity: 0.7;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
cursor: move;
user-select: none;
}
.zoom-control:hover {
opacity: 1;
background: rgba(128, 128, 128, 0.85);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
}
.zoom-close-btn {
position: absolute;
top: -8px;
right: -8px;
width: 24px;
height: 24px;
background: rgba(128, 128, 128, 0.6);
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
color: rgba(255, 255, 255, 0.8);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
transition: all 0.2s ease;
z-index: 1;
opacity: 0.7;
}
.zoom-close-btn:hover {
background: rgba(220, 53, 69, 0.9);
color: white;
opacity: 1;
transform: scale(1.1);
box-shadow: 0 2px 8px rgba(220, 53, 69, 0.4);
}
.zoom-value {
color: rgba(255, 255, 255, 0.7);
font-size: 0.8rem;
font-weight: 400;
min-width: 30px;
text-align: center;
}
.zoom-slider {
-webkit-appearance: none;
appearance: none;
width: 180px;
height: 5px;
border-radius: 3px;
background: rgba(200, 200, 200, 0.5);
outline: none;
cursor: pointer;
transition: all 0.3s ease;
}
.zoom-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: white;
border: 2px solid rgba(180, 180, 180, 0.8);
cursor: pointer;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
transition: all 0.2s ease;
}
.zoom-reset-btn {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.4);
border-radius: 50px;
color: rgba(255, 255, 255, 0.9);
padding: 0.25rem 0.6rem;
cursor: pointer;
font-size: 0.85rem;
font-weight: 600;
transition: all 0.2s ease;
min-width: 45px;
text-align: center;
}
.zoom-reset-btn:hover {
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.6);
transform: scale(1.05);
}
#show-zoom-menu-btn {
display: none;
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(128, 128, 128, 0.7);
color: white;
border: none;
padding: 8px 16px;
border-radius: 20px;
cursor: pointer;
z-index: 899;
}
.log-area {
position: fixed;
top: 20px;
right: 20px;
width: 300px;
max-height: 400px;
overflow-y: auto;
background: #1e1e1e;
color: #00ff00;
padding: 10px;
border-radius: 4px;
font-family: monospace;
font-size: 11px;
z-index: 1000;
}
.log-entry {
margin-bottom: 4px;
padding: 2px;
border-left: 2px solid #00ff00;
padding-left: 6px;
}
.log-entry.error {
border-left-color: #ff0000;
color: #ff6666;
}
.log-entry.success {
border-left-color: #00ff00;
color: #66ff66;
}
</style>
</head>
<body>
<div class="test-area">
<h1>Zoom Control Debug Test</h1>
<p>Testing the zoom control component to identify issues with:</p>
<ol>
<li><strong>X button not working</strong> - Click the X button (top-right corner of zoom control)</li>
<li><strong>Drag not working</strong> - Try to drag the zoom control by clicking and holding</li>
</ol>
<h2>Instructions:</h2>
<ul>
<li>The zoom control should appear at the bottom center of the screen</li>
<li>Click the X button to hide it - a "Show Zoom" button should appear</li>
<li>Try dragging the zoom control around the screen</li>
<li>Check the console log (right side) for debugging info</li>
</ul>
<div style="margin-top: 40px; padding: 20px; background: #e0f7ff; border-radius: 8px;">
<h3>Expected Behavior:</h3>
<ul>
<li>✅ X button should hide the zoom control and show "Show Zoom" button</li>
<li>✅ Dragging should move the zoom control around the screen</li>
<li>✅ Position should be saved to localStorage</li>
<li>✅ Dragging should NOT trigger when clicking slider or buttons</li>
</ul>
</div>
</div>
<!-- Debug Console -->
<div class="log-area" id="debugLog">
<div class="log-entry success">Debug console initialized</div>
</div>
<!-- Show Zoom Button (hidden by default) -->
<button id="show-zoom-menu-btn"
_="on click
log 'Show zoom button clicked'
remove { display: 'none' } from #zoom-control
add { display: 'none' } to me
set localStorage['cv-zoom-visible'] to 'true'">
Show Zoom Control
</button>
<!-- Zoom Control Component -->
<div id="zoom-control" class="zoom-control no-print" role="group" aria-label="Zoom control"
_="on load
log 'Zoom control loaded'
if window.innerWidth <= 768
log 'Mobile device detected - exiting'
exit
end
set savedZoom to localStorage.getItem('cv-zoom')
if savedZoom
log 'Restoring zoom: ' + savedZoom
set my value to savedZoom
send input to #zoom-slider
end
set isVisible to localStorage.getItem('cv-zoom-visible')
if isVisible is 'false'
log 'Zoom control was hidden - showing button'
add { display: 'none' } to me
remove { display: 'none' } from #show-zoom-menu-btn
end
set savedPos to localStorage.getItem('cv-zoom-position')
if savedPos
log 'Restoring position: ' + savedPos
set pos to JSON.parse(savedPos)
set my *bottom to pos.bottom
set my *left to pos.left
set my *transform to 'none'
end
on mousedown(clientX, clientY)
log 'Mousedown on zoom-control at: ' + clientX + ', ' + clientY
log 'Event target: ' + event.target.className
if event.target.closest('.zoom-slider, .zoom-close-btn, .zoom-reset-btn')
log 'Click on interactive element - exiting drag handler'
exit
end
log 'Starting drag'
set isDragging to true
set my *transition to 'none'
set rect to my getBoundingClientRect()
set initialX to clientX - rect.left
set initialY to clientY - rect.top
log 'Initial offsets: ' + initialX + ', ' + initialY
halt the event
on mousemove(clientX, clientY) from document
if isDragging is not true exit end
log 'Dragging to: ' + clientX + ', ' + clientY
halt the event
set currentX to clientX - initialX
set currentY to clientY - initialY
set maxX to window.innerWidth - my offsetWidth
set maxY to window.innerHeight - my offsetHeight
set currentX to Math.max(0, Math.min(currentX, maxX))
set currentY to Math.max(0, Math.min(currentY, maxY))
set my *left to `${currentX}px`
set my *bottom to `${window.innerHeight - currentY - my offsetHeight}px`
set my *transform to 'none'
on mouseup from document
if isDragging is not true exit end
log 'Drag ended - saving position'
set isDragging to false
set my *transition to 'all 0.3s ease'
set position to { bottom: my *bottom, left: my *left }
log 'Saving position: ' + JSON.stringify(position)
set localStorage['cv-zoom-position'] to JSON.stringify(position)">
<button
id="zoom-close"
class="zoom-close-btn"
aria-label="Close zoom control"
title="Close"
_="on click
log 'Close button clicked!'
add { display: 'none' } to #zoom-control
remove { display: 'none' } from #show-zoom-menu-btn
set localStorage['cv-zoom-visible'] to 'false'
log 'Zoom control hidden, show button displayed'">
<iconify-icon icon="mdi:close" width="16" height="16"></iconify-icon>
</button>
<span class="zoom-value zoom-value-min" aria-hidden="true">25</span>
<input
type="range"
id="zoom-slider"
class="zoom-slider"
min="25"
max="175"
step="1"
value="100"
aria-label="Adjust zoom level"
_="on input
set zoomValue to my value as a Number
log 'Zoom changed to: ' + zoomValue
set localStorage['cv-zoom'] to zoomValue">
<span class="zoom-value zoom-value-max" aria-hidden="true">175</span>
<button
id="zoom-reset"
class="zoom-reset-btn"
aria-label="Reset zoom to 100%"
title="Reset"
_="on click
log 'Reset button clicked'
set #zoom-slider's value to 100
send input to #zoom-slider
send focus to #zoom-slider">
<span id="zoom-value-current">100</span>
</button>
</div>
<script>
// Enhanced logging function
function log(message, type = 'info') {
const logArea = document.getElementById('debugLog');
const entry = document.createElement('div');
entry.className = 'log-entry ' + type;
const timestamp = new Date().toLocaleTimeString();
entry.textContent = `[${timestamp}] ${message}`;
logArea.appendChild(entry);
logArea.scrollTop = logArea.scrollHeight;
// Also log to browser console
console.log(`[${timestamp}] ${message}`);
}
// Override hyperscript log to use our custom logger
window.log = log;
// Add click listener to close button for debugging
document.addEventListener('DOMContentLoaded', function() {
const closeBtn = document.getElementById('zoom-close');
if (closeBtn) {
log('Close button found in DOM', 'success');
// Add native click listener for debugging
closeBtn.addEventListener('click', function(e) {
log('Native click listener triggered on close button', 'success');
});
} else {
log('Close button NOT found in DOM!', 'error');
}
const zoomControl = document.getElementById('zoom-control');
if (zoomControl) {
log('Zoom control found in DOM', 'success');
// Add mousedown listener for debugging
zoomControl.addEventListener('mousedown', function(e) {
log(`Native mousedown on: ${e.target.className}`, 'info');
});
} else {
log('Zoom control NOT found in DOM!', 'error');
}
});
</script>
</body>
</html>
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env node
/**
* Final Zoom Control Test - X Button and Drag
*/
import { chromium } from 'playwright';
async function testZoomControl() {
console.log('🧪 Final Zoom Control Test - X Button & Drag\n');
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
const page = await context.newPage();
try {
// Navigate
console.log('📄 Loading http://localhost:1999/?lang=en');
await page.goto('http://localhost:1999/?lang=en', { waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
const zoomControl = page.locator('#zoom-control');
await zoomControl.waitFor({ state: 'visible', timeout: 5000 });
console.log('✅ Zoom control loaded\n');
// TEST 1: X Button
console.log('🧪 TEST 1: X Button Click');
const closeButton = page.locator('#zoom-close');
await closeButton.click({ force: true });
await page.waitForTimeout(500);
const isHidden = await zoomControl.evaluate(el =>
window.getComputedStyle(el).display === 'none'
);
if (isHidden) {
console.log('✅ SUCCESS: X button works! Zoom control is hidden\n');
} else {
console.log('❌ FAIL: X button did not hide zoom control\n');
throw new Error('X button test failed');
}
// Show it again for drag test
await page.evaluate(() => {
document.getElementById('zoom-control').style.display = '';
});
await page.waitForTimeout(500);
// TEST 2: Drag Functionality
console.log('🧪 TEST 2: Drag Functionality');
const initialBox = await zoomControl.boundingBox();
console.log(` Initial position: x=${Math.round(initialBox.x)}, y=${Math.round(initialBox.y)}`);
// Click on grey background area (not on buttons)
const dragX = initialBox.x + 60; // Left side, away from buttons
const dragY = initialBox.y + initialBox.height / 2;
await page.mouse.move(dragX, dragY);
await page.mouse.down();
// Drag to new location
const targetX = dragX + 300;
const targetY = dragY - 150;
console.log(` Dragging to: x=${Math.round(targetX)}, y=${Math.round(targetY)}`);
await page.mouse.move(targetX, targetY, { steps: 20 });
await page.mouse.up();
await page.waitForTimeout(500);
const finalBox = await zoomControl.boundingBox();
console.log(` Final position: x=${Math.round(finalBox.x)}, y=${Math.round(finalBox.y)}`);
const deltaX = Math.abs(finalBox.x - initialBox.x);
const deltaY = Math.abs(finalBox.y - initialBox.y);
if (deltaX > 100 || deltaY > 50) {
console.log(`✅ SUCCESS: Drag works! Moved ${Math.round(deltaX)}px horizontally, ${Math.round(deltaY)}px vertically\n`);
} else {
console.log(`❌ FAIL: Drag did not work properly. Only moved ${Math.round(deltaX)}px, ${Math.round(deltaY)}px\n`);
throw new Error('Drag test failed');
}
// Summary
console.log('='.repeat(60));
console.log('✅ ALL TESTS PASSED!');
console.log('='.repeat(60));
console.log('✅ X button hides zoom control');
console.log('✅ Drag functionality works correctly');
console.log('='.repeat(60));
console.log('\n🎉 Zoom control is fully functional!');
console.log('\n⏸️ Browser will stay open for 5 seconds...');
await page.waitForTimeout(5000);
} catch (error) {
console.error('\n❌ Test failed:', error.message);
} finally {
await browser.close();
console.log('\n✅ Test completed\n');
}
}
testZoomControl();
+389
View File
@@ -0,0 +1,389 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zoom Control - FIXED VERSION</title>
<!-- Hyperscript - MUST load BEFORE component -->
<script src="https://unpkg.com/hyperscript.org@0.9.12"></script>
<!-- Iconify for icons -->
<script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
<style>
body {
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
background: #f0f0f0;
min-height: 2000px; /* Make page scrollable for testing */
}
.test-area {
margin: 20px;
padding: 20px;
background: white;
border-radius: 8px;
}
.success-box {
background: #d4edda;
border: 2px solid #28a745;
color: #155724;
padding: 15px;
border-radius: 8px;
margin: 20px 0;
}
.test-steps {
background: #fff3cd;
border: 2px solid #ffc107;
color: #856404;
padding: 15px;
border-radius: 8px;
margin: 20px 0;
}
/* Zoom Control Styles */
.zoom-control {
position: fixed;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
z-index: 900;
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.65rem 1.25rem;
background: rgba(128, 128, 128, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 50px;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
opacity: 0.7;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
cursor: move;
user-select: none;
}
.zoom-control:hover {
opacity: 1;
background: rgba(128, 128, 128, 0.85);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
}
.zoom-close-btn {
position: absolute;
top: -8px;
right: -8px;
width: 24px;
height: 24px;
background: rgba(128, 128, 128, 0.6);
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
color: rgba(255, 255, 255, 0.8);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
transition: all 0.2s ease;
z-index: 1;
opacity: 0.7;
}
.zoom-close-btn:hover {
background: rgba(220, 53, 69, 0.9);
color: white;
opacity: 1;
transform: scale(1.1);
box-shadow: 0 2px 8px rgba(220, 53, 69, 0.4);
}
.zoom-value {
color: rgba(255, 255, 255, 0.7);
font-size: 0.8rem;
font-weight: 400;
min-width: 30px;
text-align: center;
}
.zoom-slider {
-webkit-appearance: none;
appearance: none;
width: 180px;
height: 5px;
border-radius: 3px;
background: rgba(200, 200, 200, 0.5);
outline: none;
cursor: pointer;
transition: all 0.3s ease;
}
.zoom-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: white;
border: 2px solid rgba(180, 180, 180, 0.8);
cursor: pointer;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
transition: all 0.2s ease;
}
.zoom-reset-btn {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.4);
border-radius: 50px;
color: rgba(255, 255, 255, 0.9);
padding: 0.25rem 0.6rem;
cursor: pointer;
font-size: 0.85rem;
font-weight: 600;
transition: all 0.2s ease;
min-width: 45px;
text-align: center;
}
.zoom-reset-btn:hover {
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.6);
transform: scale(1.05);
}
#show-zoom-menu-btn {
display: none;
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(128, 128, 128, 0.9);
color: white;
border: none;
padding: 12px 24px;
border-radius: 25px;
cursor: pointer;
z-index: 899;
font-size: 14px;
font-weight: 600;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
}
#show-zoom-menu-btn:hover {
background: rgba(128, 128, 128, 1);
transform: translateX(-50%) scale(1.05);
}
</style>
</head>
<body>
<div class="test-area">
<h1>🔧 Zoom Control - FIXED VERSION</h1>
<div class="success-box">
<h3>✅ Fixes Applied:</h3>
<ol>
<li><strong>X Button Fix</strong>: Added <code>pointer-events: none</code> to the iconify-icon element to prevent it from capturing clicks</li>
<li><strong>X Button Fix</strong>: Added <code>halt the event</code> to prevent event propagation</li>
<li><strong>Drag Fix</strong>: Improved the mousedown handler to properly check for interactive elements using both <code>classList.contains()</code> and <code>closest()</code></li>
</ol>
</div>
<div class="test-steps">
<h3>🧪 Test Steps:</h3>
<ol>
<li><strong>Test X Button</strong>:
<ul>
<li>Click the X button (top-right corner of zoom control)</li>
<li>✅ Expected: Zoom control should disappear and "Show Zoom Control" button should appear at bottom</li>
</ul>
</li>
<li><strong>Test Show Button</strong>:
<ul>
<li>Click the "Show Zoom Control" button</li>
<li>✅ Expected: Zoom control should reappear and button should disappear</li>
</ul>
</li>
<li><strong>Test Dragging</strong>:
<ul>
<li>Click and hold on the grey background of the zoom control (NOT on slider/buttons)</li>
<li>Move your mouse while holding</li>
<li>✅ Expected: Zoom control should move with your mouse</li>
<li>Release the mouse</li>
<li>✅ Expected: Position should be saved (reload page to verify)</li>
</ul>
</li>
<li><strong>Test Slider</strong>:
<ul>
<li>Click and drag the slider</li>
<li>✅ Expected: Slider should move, NOT trigger drag mode</li>
</ul>
</li>
</ol>
</div>
<h2>Key Changes Made:</h2>
<pre style="background: #f8f9fa; padding: 15px; border-radius: 5px; overflow-x: auto;">
<strong>1. Close Button (zoom-control.html:76-81)</strong>
- Added: halt the event
- Added: style="pointer-events: none;" to iconify-icon
<strong>2. Drag Handler (zoom-control.html:26-42)</strong>
- Changed from: event.target.closest('.zoom-slider, .zoom-close-btn, .zoom-reset-btn')
- Changed to: Multiple specific checks:
* if target.classList.contains('zoom-slider') exit end
* if target.classList.contains('zoom-close-btn') exit end
* if target.classList.contains('zoom-reset-btn') exit end
* if target.closest('.zoom-close-btn') exit end
* if target.closest('.zoom-reset-btn') exit end
</pre>
<div style="margin-top: 40px; padding: 20px; background: #e7f3ff; border-radius: 8px;">
<h3>📝 Testing Checklist:</h3>
<p>Mark off as you test:</p>
<ul style="list-style: none; padding-left: 0;">
<li>☐ X button hides zoom control</li>
<li>☐ Show button reveals zoom control</li>
<li>☐ Dragging works (click on grey background)</li>
<li>☐ Slider doesn't trigger drag mode</li>
<li>☐ Reset button doesn't trigger drag mode</li>
<li>☐ Position persists after page reload</li>
</ul>
</div>
</div>
<!-- Show Zoom Button (hidden by default) -->
<button id="show-zoom-menu-btn"
_="on click
halt the event
remove { display: 'none' } from #zoom-control
add { display: 'none' } to me
set localStorage['cv-zoom-visible'] to 'true'">
Show Zoom Control
</button>
<!-- Zoom Control Component - FIXED VERSION -->
<div id="zoom-control" class="zoom-control no-print" role="group" aria-label="Zoom control"
_="on load
if window.innerWidth <= 768
exit
end
set savedZoom to localStorage.getItem('cv-zoom')
if savedZoom
set my value to savedZoom
send input to #zoom-slider
end
set isVisible to localStorage.getItem('cv-zoom-visible')
if isVisible is 'false'
add { display: 'none' } to me
remove { display: 'none' } from #show-zoom-menu-btn
end
set savedPos to localStorage.getItem('cv-zoom-position')
if savedPos
set pos to JSON.parse(savedPos)
set my *bottom to pos.bottom
set my *left to pos.left
set my *transform to 'none'
end
on mousedown(clientX, clientY)
-- FIXED: Check if click is on interactive elements (slider, buttons)
set target to event.target
if target.classList.contains('zoom-slider') exit end
if target.classList.contains('zoom-close-btn') exit end
if target.classList.contains('zoom-reset-btn') exit end
if target.closest('.zoom-close-btn') exit end
if target.closest('.zoom-reset-btn') exit end
set isDragging to true
set my *transition to 'none'
set rect to my getBoundingClientRect()
set initialX to clientX - rect.left
set initialY to clientY - rect.top
halt the event
on mousemove(clientX, clientY) from document
if isDragging is not true exit end
halt the event
set currentX to clientX - initialX
set currentY to clientY - initialY
set maxX to window.innerWidth - my offsetWidth
set maxY to window.innerHeight - my offsetHeight
set currentX to Math.max(0, Math.min(currentX, maxX))
set currentY to Math.max(0, Math.min(currentY, maxY))
set my *left to `${currentX}px`
set my *bottom to `${window.innerHeight - currentY - my offsetHeight}px`
set my *transform to 'none'
on mouseup from document
if isDragging is not true exit end
set isDragging to false
set my *transition to 'all 0.3s ease'
set position to { bottom: my *bottom, left: my *left }
set localStorage['cv-zoom-position'] to JSON.stringify(position)">
<!-- FIXED: Close button with pointer-events: none on icon -->
<button
id="zoom-close"
class="zoom-close-btn"
aria-label="Close zoom control"
title="Close"
_="on click
halt the event
add { display: 'none' } to #zoom-control
remove { display: 'none' } from #show-zoom-menu-btn
set localStorage['cv-zoom-visible'] to 'false'">
<iconify-icon icon="mdi:close" width="16" height="16" style="pointer-events: none;"></iconify-icon>
</button>
<span class="zoom-value zoom-value-min" aria-hidden="true">25</span>
<input
type="range"
id="zoom-slider"
class="zoom-slider"
min="25"
max="175"
step="1"
value="100"
aria-label="Adjust zoom level"
_="on input
set zoomValue to my value as a Number
put zoomValue into #zoom-value-current
set localStorage['cv-zoom'] to zoomValue">
<span class="zoom-value zoom-value-max" aria-hidden="true">175</span>
<button
id="zoom-reset"
class="zoom-reset-btn"
aria-label="Reset zoom to 100%"
title="Reset"
_="on click
set #zoom-slider's value to 100
send input to #zoom-slider
send focus to #zoom-slider">
<span id="zoom-value-current">100</span>
</button>
</div>
<script>
console.log('🧪 Zoom Control Test Page - FIXED VERSION');
console.log('Open browser console to see debug messages');
console.log('localStorage keys used: cv-zoom, cv-zoom-visible, cv-zoom-position');
</script>
</body>
</html>
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env node
/**
* Zoom Control Functionality Test
* Tests the X button and drag functionality
*/
import { chromium } from 'playwright';
async function testZoomControl() {
console.log('🧪 Starting Zoom Control Test...\n');
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
const page = await context.newPage();
try {
// Navigate to the CV page
console.log('📄 Navigating to http://localhost:1999/?lang=en');
await page.goto('http://localhost:1999/?lang=en', { waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
// Wait for zoom control to be visible
console.log('⏳ Waiting for zoom control to load...');
const zoomControl = page.locator('#zoom-control');
await zoomControl.waitFor({ state: 'visible', timeout: 5000 });
console.log('✅ Zoom control is visible\n');
// Test 1: X Button Click
console.log('🧪 TEST 1: X Button Click');
console.log(' Locating X button...');
const closeButton = page.locator('#zoom-close');
await closeButton.waitFor({ state: 'visible' });
const closeButtonBox = await closeButton.boundingBox();
console.log(` X button position: x=${closeButtonBox.x}, y=${closeButtonBox.y}`);
console.log(' Clicking X button...');
await closeButton.click({ force: true });
await page.waitForTimeout(500);
// Check if zoom control is hidden
const isHidden = await zoomControl.evaluate(el => {
const style = window.getComputedStyle(el);
return style.display === 'none';
});
if (isHidden) {
console.log('✅ X button works! Zoom control is hidden');
} else {
console.log('❌ X button failed! Zoom control is still visible');
}
// Check if show button appeared
const showButton = page.locator('#show-zoom-menu-btn');
const showButtonVisible = await showButton.evaluate(el => {
const style = window.getComputedStyle(el);
return style.display !== 'none';
});
if (showButtonVisible) {
console.log('✅ Show zoom button is visible');
} else {
console.log('❌ Show zoom button did not appear');
}
// Test 2: Show Button Click
console.log('\n🧪 TEST 2: Show Button Click');
console.log(' Clicking show zoom button...');
await showButton.click();
await page.waitForTimeout(500);
const isVisibleAgain = await zoomControl.evaluate(el => {
const style = window.getComputedStyle(el);
return style.display !== 'none';
});
if (isVisibleAgain) {
console.log('✅ Show button works! Zoom control is visible again');
} else {
console.log('❌ Show button failed! Zoom control is still hidden');
}
// Test 3: Drag Functionality
console.log('\n🧪 TEST 3: Drag Functionality');
// Get initial position
const initialPosition = await zoomControl.boundingBox();
console.log(` Initial position: x=${Math.round(initialPosition.x)}, y=${Math.round(initialPosition.y)}`);
// Calculate drag target (click on the grey background, not on buttons)
const dragStartX = initialPosition.x + 100; // Middle of control
const dragStartY = initialPosition.y + initialPosition.height / 2;
console.log(' Starting drag operation...');
await page.mouse.move(dragStartX, dragStartY);
await page.mouse.down();
// Drag to new position
const newX = dragStartX + 200;
const newY = dragStartY - 100;
console.log(` Dragging to: x=${Math.round(newX)}, y=${Math.round(newY)}`);
await page.mouse.move(newX, newY, { steps: 10 });
await page.mouse.up();
await page.waitForTimeout(500);
// Get final position
const finalPosition = await zoomControl.boundingBox();
console.log(` Final position: x=${Math.round(finalPosition.x)}, y=${Math.round(finalPosition.y)}`);
const movedX = Math.abs(finalPosition.x - initialPosition.x);
const movedY = Math.abs(finalPosition.y - initialPosition.y);
if (movedX > 50 || movedY > 50) {
console.log(`✅ Drag works! Moved ${Math.round(movedX)}px horizontally, ${Math.round(movedY)}px vertically`);
} else {
console.log(`❌ Drag failed! Only moved ${Math.round(movedX)}px horizontally, ${Math.round(movedY)}px vertically`);
}
// Test 4: Slider Doesn't Trigger Drag
console.log('\n🧪 TEST 4: Slider Click Doesn\'t Trigger Drag');
const slider = page.locator('#zoom-slider');
const sliderBox = await slider.boundingBox();
console.log(' Clicking on slider...');
await slider.click({ force: true });
await page.waitForTimeout(500);
const positionAfterSlider = await zoomControl.boundingBox();
const sliderMovedX = Math.abs(positionAfterSlider.x - finalPosition.x);
const sliderMovedY = Math.abs(positionAfterSlider.y - finalPosition.y);
if (sliderMovedX < 5 && sliderMovedY < 5) {
console.log('✅ Slider click doesn\'t trigger drag mode');
} else {
console.log(`❌ Slider click triggered drag! Moved ${Math.round(sliderMovedX)}px, ${Math.round(sliderMovedY)}px`);
}
// Summary
console.log('\n' + '='.repeat(60));
console.log('📊 TEST SUMMARY');
console.log('='.repeat(60));
console.log(`${isHidden ? '✅' : '❌'} X button hides zoom control`);
console.log(`${showButtonVisible ? '✅' : '❌'} Show button appears when hidden`);
console.log(`${isVisibleAgain ? '✅' : '❌'} Show button reveals zoom control`);
console.log(`${movedX > 50 || movedY > 50 ? '✅' : '❌'} Drag functionality works`);
console.log(`${sliderMovedX < 5 && sliderMovedY < 5 ? '✅' : '❌'} Slider doesn't trigger drag`);
console.log('='.repeat(60));
console.log('\n⏸️ Browser will stay open for 10 seconds for manual inspection...');
await page.waitForTimeout(10000);
} catch (error) {
console.error('❌ Test failed with error:', error.message);
console.error(error.stack);
} finally {
await browser.close();
console.log('\n✅ Test completed');
}
}
testZoomControl();
+49
View File
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<title>Test Zoom Persistence</title>
</head>
<body>
<h1>Testing localStorage for zoom visibility</h1>
<button onclick="closeZoom()">Close Zoom (set to 'false')</button>
<button onclick="showZoom()">Show Zoom (set to 'true')</button>
<button onclick="clearZoom()">Clear localStorage</button>
<button onclick="checkValue()">Check Current Value</button>
<div id="output" style="margin-top: 20px; padding: 10px; background: #f0f0f0;"></div>
<script>
function closeZoom() {
localStorage.setItem('cv-zoom-visible', 'false');
checkValue();
}
function showZoom() {
localStorage.setItem('cv-zoom-visible', 'true');
checkValue();
}
function clearZoom() {
localStorage.removeItem('cv-zoom-visible');
checkValue();
}
function checkValue() {
const value = localStorage.getItem('cv-zoom-visible');
const output = document.getElementById('output');
output.innerHTML = `
<strong>Current value:</strong> ${value === null ? 'null (not set)' : `"${value}"`}<br>
<strong>Type:</strong> ${typeof value}<br>
<strong>Is 'false'?</strong> ${value === 'false'}<br>
<strong>Is 'true'?</strong> ${value === 'true'}<br>
<strong>Is null?</strong> ${value === null}
`;
}
// Check on load
checkValue();
</script>
</body>
</html>
+171
View File
@@ -0,0 +1,171 @@
import { chromium } from '@playwright/test';
(async () => {
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
console.log('🧪 Testing Zoom Toggle Button Implementation...\n');
// Navigate to the page
await page.goto('http://localhost:1999');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
console.log('✅ Page loaded');
// Test 1: Check for blinking - zoom control should be hidden from start
console.log('\n📋 Test 1: No blinking on load (zoom should start hidden)');
const zoomControl = page.locator('#zoom-control');
const isHidden = await zoomControl.evaluate(el => el.classList.contains('zoom-hidden'));
if (isHidden) {
console.log('✅ PASS: Zoom control starts hidden (no blinking)');
} else {
console.log('❌ FAIL: Zoom control is visible on load (will blink)');
}
// Test 2: Verify toggle button exists and is positioned correctly
console.log('\n📋 Test 2: Toggle button exists and positioned');
const toggleBtn = page.locator('#zoom-toggle-button');
await toggleBtn.waitFor({ state: 'visible', timeout: 5000 });
const toggleBox = await toggleBtn.boundingBox();
console.log(`✅ Toggle button found at position: left=${toggleBox.x}px, top=${toggleBox.y}px`);
// Test 3: Check initial state (should be dimmed)
console.log('\n📋 Test 3: Toggle button initial state (should be dimmed)');
const hasActiveClass = await toggleBtn.evaluate(el => el.classList.contains('zoom-active'));
const opacity = await toggleBtn.evaluate(el => window.getComputedStyle(el).opacity);
if (!hasActiveClass && parseFloat(opacity) === 0.5) {
console.log('✅ PASS: Toggle button is dimmed (opacity 0.5, no zoom-active class)');
} else {
console.log(`❌ FAIL: Toggle button state incorrect (zoom-active: ${hasActiveClass}, opacity: ${opacity})`);
}
// Test 4: Click toggle button to show zoom
console.log('\n📋 Test 4: Click toggle button to show zoom');
await toggleBtn.click();
await page.waitForTimeout(500);
const zoomVisible = await zoomControl.evaluate(el => !el.classList.contains('zoom-hidden'));
const toggleActive = await toggleBtn.evaluate(el => el.classList.contains('zoom-active'));
const newOpacity = await toggleBtn.evaluate(el => window.getComputedStyle(el).opacity);
if (zoomVisible && toggleActive && parseFloat(newOpacity) === 1) {
console.log('✅ PASS: Zoom control shown, toggle button active (opacity 1, blue background)');
} else {
console.log(`❌ FAIL: Zoom toggle failed (visible: ${zoomVisible}, active: ${toggleActive}, opacity: ${newOpacity})`);
}
// Test 5: Click toggle button again to hide zoom
console.log('\n📋 Test 5: Click toggle button to hide zoom');
await toggleBtn.click();
await page.waitForTimeout(500);
const zoomHidden = await zoomControl.evaluate(el => el.classList.contains('zoom-hidden'));
const toggleInactive = await toggleBtn.evaluate(el => !el.classList.contains('zoom-active'));
const dimOpacity = await toggleBtn.evaluate(el => window.getComputedStyle(el).opacity);
if (zoomHidden && toggleInactive && parseFloat(dimOpacity) === 0.5) {
console.log('✅ PASS: Zoom control hidden, toggle button dimmed again');
} else {
console.log(`❌ FAIL: Zoom toggle hide failed (hidden: ${zoomHidden}, inactive: ${toggleInactive}, opacity: ${dimOpacity})`);
}
// Test 6: Verify localStorage persistence
console.log('\n📋 Test 6: Verify localStorage persistence');
const storageValue = await page.evaluate(() => localStorage.getItem('cv-zoom-visible'));
console.log(` localStorage cv-zoom-visible: "${storageValue}"`);
if (storageValue === 'false') {
console.log('✅ PASS: localStorage correctly set to "false"');
} else {
console.log(`❌ FAIL: localStorage incorrect (expected "false", got "${storageValue}")`);
}
// Test 7: Refresh page and verify zoom stays hidden
console.log('\n📋 Test 7: Refresh page and verify no blinking + stays hidden');
await page.reload();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500); // Wait a bit to catch any blinking
const stillHidden = await zoomControl.evaluate(el => el.classList.contains('zoom-hidden'));
const toggleStillDimmed = await toggleBtn.evaluate(el => !el.classList.contains('zoom-active'));
if (stillHidden && toggleStillDimmed) {
console.log('✅ PASS: After refresh, zoom stays hidden and toggle stays dimmed');
} else {
console.log(`❌ FAIL: After refresh, state incorrect (hidden: ${stillHidden}, dimmed: ${toggleStillDimmed})`);
}
// Test 8: Show zoom again and refresh to test persistence
console.log('\n📋 Test 8: Show zoom, refresh, verify it stays visible');
await toggleBtn.click();
await page.waitForTimeout(500);
await page.reload();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
const staysVisible = await zoomControl.evaluate(el => !el.classList.contains('zoom-hidden'));
const toggleStaysActive = await toggleBtn.evaluate(el => el.classList.contains('zoom-active'));
if (staysVisible && toggleStaysActive) {
console.log('✅ PASS: After refresh, zoom stays visible and toggle stays active');
} else {
console.log(`❌ FAIL: Persistence failed (visible: ${staysVisible}, active: ${toggleStaysActive})`);
}
// Test 9: Test X button still works
console.log('\n📋 Test 9: Test X button close functionality');
const closeBtn = page.locator('#zoom-close');
await closeBtn.click();
await page.waitForTimeout(500);
const closedByX = await zoomControl.evaluate(el => el.classList.contains('zoom-hidden'));
const toggleDimmedAgain = await toggleBtn.evaluate(el => !el.classList.contains('zoom-active'));
if (closedByX && toggleDimmedAgain) {
console.log('✅ PASS: X button closes zoom and syncs toggle button state');
} else {
console.log(`❌ FAIL: X button failed (hidden: ${closedByX}, toggle dimmed: ${toggleDimmedAgain})`);
}
// Test 10: Verify button positioning relative to shortcuts button
console.log('\n📋 Test 10: Verify button positioning');
const shortcutsBtn = page.locator('#shortcuts-button');
const shortcutsBox = await shortcutsBtn.boundingBox();
console.log(` Shortcuts button: bottom=${1080 - shortcutsBox.y}px (should be ~6rem = ~96px)`);
console.log(` Toggle button: bottom=${1080 - toggleBox.y}px (should be ~10rem = ~160px)`);
const verticalGap = (1080 - toggleBox.y - toggleBox.height) - (1080 - shortcutsBox.y);
console.log(` Vertical gap between buttons: ${Math.abs(verticalGap)}px`);
if (Math.abs(verticalGap) > 50) {
console.log('✅ PASS: Toggle button is clearly above shortcuts button');
} else {
console.log('⚠️ WARNING: Buttons might be too close together');
}
console.log('\n🎯 All tests completed! Browser will stay open for 10 seconds for visual inspection...\n');
await page.waitForTimeout(10000);
await browser.close();
console.log('✅ Test suite finished');
})();