fix: update domain from morenoyrubio.com to morenorub.io

Updated personal website domain across all configuration files:
- Changed website URL in CV data (English and Spanish)
- Updated robots.txt domain reference and sitemap location
- Updated all sitemap.xml URLs (English, Spanish, default, health check)
- Removed obsolete test files (styling comparison, shortcuts button report)

Domain change: morenoyrubio.com → morenorub.io
This commit is contained in:
juanatsap
2025-11-17 13:40:05 +00:00
parent cd450837a2
commit e9d650d152
58 changed files with 295 additions and 7410 deletions
+1 -1
View File
@@ -11,7 +11,7 @@
"linkedin": "https://www.linkedin.com/in/juan-andres-moreno-rubio",
"github": "https://github.com/juanatsap",
"domestika": "https://www.domestika.org/es/txeo/portfolio",
"website": "https://juan.andres.morenoyrubio.com",
"website": "https://juan.andres.morenorub.io",
"photo": "/static/images/profile.jpg"
},
"summary": "Full-stack developer specialized in high-availability systems. I've worked on Olympic Games platforms, airport authentication systems with millions of users, and built around 20 websites for diverse sectors (e-commerce, enterprise, institutional). Certified SAP Customer Data Cloud consultant, advising 35-40 international clients on digital identity solutions.",
+1 -1
View File
@@ -11,7 +11,7 @@
"linkedin": "https://www.linkedin.com/in/juan-andres-moreno-rubio",
"github": "https://github.com/juanatsap",
"domestika": "https://www.domestika.org/es/txeo/portfolio",
"website": "https://juan.andres.morenoyrubio.com",
"website": "https://juan.andres.morenorub.io",
"photo": "/static/images/profile.jpg"
},
"summary": "Desarrollador full-stack especializado en sistemas de alta disponibilidad. He participado en plataformas de Juegos Olímpicos, sistemas de autenticación aeroportuaria con millones de usuarios, y desarrollado unos 20 sitios web para diversos sectores (e-commerce, empresariales, institucionales). Consultor certificado de SAP Customer Data Cloud, asesorando a 35-40 clientes internacionales en soluciones de identidad digital.",
+2 -2
View File
@@ -1,4 +1,4 @@
# robots.txt for juan.andres.morenoyrubio.com
# robots.txt for juan.andres.morenorub.io
# Allow all search engines
User-agent: *
@@ -11,7 +11,7 @@ Disallow: /.git/
Disallow: /.env
# Sitemap location
Sitemap: https://juan.andres.morenoyrubio.com/static/sitemap.xml
Sitemap: https://juan.andres.morenorub.io/static/sitemap.xml
# Crawl-delay (optional, helps prevent server overload)
# Crawl-delay: 1
+8 -8
View File
@@ -4,27 +4,27 @@
<!-- English Version -->
<url>
<loc>https://juan.andres.morenoyrubio.com/?lang=en</loc>
<loc>https://juan.andres.morenorub.io/?lang=en</loc>
<lastmod>2024-10-18</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
<xhtml:link rel="alternate" hreflang="es" href="https://juan.andres.morenoyrubio.com/?lang=es"/>
<xhtml:link rel="alternate" hreflang="en" href="https://juan.andres.morenoyrubio.com/?lang=en"/>
<xhtml:link rel="alternate" hreflang="es" href="https://juan.andres.morenorub.io/?lang=es"/>
<xhtml:link rel="alternate" hreflang="en" href="https://juan.andres.morenorub.io/?lang=en"/>
</url>
<!-- Spanish Version -->
<url>
<loc>https://juan.andres.morenoyrubio.com/?lang=es</loc>
<loc>https://juan.andres.morenorub.io/?lang=es</loc>
<lastmod>2024-10-18</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
<xhtml:link rel="alternate" hreflang="es" href="https://juan.andres.morenoyrubio.com/?lang=es"/>
<xhtml:link rel="alternate" hreflang="en" href="https://juan.andres.morenoyrubio.com/?lang=en"/>
<xhtml:link rel="alternate" hreflang="es" href="https://juan.andres.morenorub.io/?lang=es"/>
<xhtml:link rel="alternate" hreflang="en" href="https://juan.andres.morenorub.io/?lang=en"/>
</url>
<!-- Default (redirects to English) -->
<url>
<loc>https://juan.andres.morenoyrubio.com/</loc>
<loc>https://juan.andres.morenorub.io/</loc>
<lastmod>2024-10-18</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
@@ -32,7 +32,7 @@
<!-- Health Check Endpoint -->
<url>
<loc>https://juan.andres.morenoyrubio.com/health</loc>
<loc>https://juan.andres.morenorub.io/health</loc>
<lastmod>2024-10-18</lastmod>
<changefreq>daily</changefreq>
<priority>0.3</priority>
-314
View File
@@ -1,314 +0,0 @@
# Shortcuts Button Visibility Fix - Test Report
**Date:** 2025-11-15
**Issue:** Shortcuts button exists with icon but appears nearly invisible
**Status:****RESOLVED**
---
## Problem Summary
The keyboard shortcuts button (`#shortcuts-button`) was correctly implemented with:
- ✅ Proper HTML structure
- ✅ Iconify keyboard icon (`mdi:keyboard-outline`, 28x28px)
- ✅ Click functionality working
- ✅ ARIA labels and accessibility attributes
However, the button appeared **nearly invisible** to users due to:
- ❌ Default opacity of `0.2` (80% transparent)
- ❌ Only became visible on hover or when scrolling to bottom
- ❌ Poor discoverability for new users
---
## Root Cause Analysis
### Original CSS Implementation
```css
.shortcuts-btn {
/* ... other styles ... */
opacity: 0.2; /* ❌ Too low - nearly invisible */
}
.shortcuts-btn:hover {
opacity: 1; /* Only visible on hover */
}
.shortcuts-btn.at-bottom {
opacity: 1; /* Only visible when at page bottom */
}
```
### Why This Was Problematic
1. **User Discovery**: Users couldn't find the button without hovering in the exact spot
2. **Test Automation**: Automated tests detected button as having no visible content
3. **UX Inconsistency**: Other fixed buttons (back-to-top) had better visibility
4. **Accessibility**: Low contrast made button hard to see for users with visual impairments
---
## Solution Implemented
### CSS Changes
**File:** `/Users/txeo/Git/yo/cv/static/css/main.css`
#### 1. Shortcuts Button (lines 3988-4006)
```diff
.shortcuts-btn {
position: fixed;
bottom: 6rem;
left: 2rem;
width: 50px;
height: 50px;
background: var(--black-bar);
color: white;
/* ... */
- opacity: 0.2;
+ opacity: 0.6; /* Increased from 0.2 for better discoverability */
}
```
#### 2. Info Button (lines 2867-2885) - Consistency Update
```diff
.info-button {
position: fixed;
bottom: 2rem;
left: 2rem;
/* ... */
- opacity: 0.2;
+ opacity: 0.6; /* Increased from 0.2 for better discoverability */
}
```
### Rationale for 0.6 Opacity
- **Visible but Subtle**: Button is discoverable without being obtrusive
- **Still Enhances on Hover**: Hover state (opacity: 1) remains effective
- **Accessibility**: Meets minimum contrast requirements
- **UX Pattern**: Matches common fixed button opacity patterns (0.5-0.7)
---
## Verification Tests
### 1. Visual Test
Created: `/Users/txeo/Git/yo/cv/tests/test-shortcuts-button-visibility.html`
**Test Cases:**
- ✅ Compare old (0.2) vs new (0.6) opacity side-by-side
- ✅ Verify iconify-icon renders correctly
- ✅ Confirm hover state transitions smoothly
- ✅ Check button positioning and styling
**Results:**
- ✅ Old opacity (0.2): Hard to see, poor discoverability
- ✅ New opacity (0.6): Clearly visible, good UX
- ✅ Hover state (1.0): Full visibility with blue background
### 2. Live Site Test
**URL:** `http://localhost:1999/?lang=en`
**Verified:**
- ✅ Button renders with keyboard icon visible at opacity 0.6
- ✅ Icon: `mdi:keyboard-outline` at 28x28px
- ✅ Button positioned: bottom-left, above info-button
- ✅ Click functionality: Opens shortcuts modal
- ✅ Hover effect: Opacity increases to 1.0, background turns blue
- ✅ Accessibility: `aria-label="Keyboard shortcuts"` present
### 3. HTML Structure Verification
```html
<button
id="shortcuts-button"
class="fixed-btn shortcuts-btn no-print"
onclick="document.getElementById('shortcuts-modal').showModal()"
aria-label="Keyboard shortcuts"
title="Keyboard shortcuts (?)">
<iconify-icon icon="mdi:keyboard-outline" width="28" height="28"></iconify-icon>
</button>
```
**Status:** ✅ Perfect implementation
### 4. CSS Verification
```bash
$ grep -A10 "\.shortcuts-btn {" static/css/main.css
```
**Results:**
- ✅ Opacity: 0.6 (updated from 0.2)
- ✅ Position: Fixed bottom-left (6rem from bottom, 2rem from left)
- ✅ Size: 50x50px (45x45px on mobile)
- ✅ Hover: opacity: 1, transform: translateY(-3px), background: #3498db
- ✅ At-bottom state: opacity: 1, background: #3498db
---
## Comparison: Before vs After
| Aspect | Before (opacity: 0.2) | After (opacity: 0.6) |
|--------|----------------------|---------------------|
| **Visibility** | Nearly invisible | Clearly visible |
| **Discoverability** | Poor - hover required | Good - immediately visible |
| **User Experience** | Frustrating | Intuitive |
| **Accessibility** | Low contrast | Improved contrast |
| **Test Detection** | Appears as "no text" | Detectable as button |
| **Hover Effect** | Still valuable (5x increase) | Still valuable (1.67x increase) |
---
## Related Files Modified
1. **CSS:** `/Users/txeo/Git/yo/cv/static/css/main.css`
- Line 2884: `.info-button` opacity 0.2 → 0.6
- Line 4005: `.shortcuts-btn` opacity 0.2 → 0.6
2. **Test Files Created:**
- `/Users/txeo/Git/yo/cv/tests/test-shortcuts-button-visibility.html`
- `/Users/txeo/Git/yo/cv/tests/SHORTCUTS-BUTTON-FIX-REPORT.md`
3. **No Template Changes:** HTML already correct in:
- `/Users/txeo/Git/yo/cv/templates/partials/widgets/shortcuts-button.html`
---
## Regression Testing
### Tested Scenarios
- ✅ Desktop viewport (>768px): Button visible at 50x50px
- ✅ Mobile viewport (<768px): Button visible at 45x45px
- ✅ Hover interaction: Smooth opacity transition to 1.0
- ✅ Click interaction: Opens modal correctly
- ✅ Scroll to bottom: `.at-bottom` class applies correctly
- ✅ Print mode: `.no-print` class hides button
- ✅ Zoom control: Hyperscript zoom adjusts button correctly
### Browser Testing
- ✅ Chrome/Edge: Icon renders, opacity correct
- ✅ Firefox: Icon renders, opacity correct
- ✅ Safari: Icon renders, opacity correct
---
## Performance Impact
- **CSS File Size:** No change (single character diff: 0.2 → 0.6)
- **Render Performance:** No impact (same CSS properties)
- **Iconify Load:** No change (already loaded for other icons)
- **Bundle Size:** No change (CSS already included)
---
## Accessibility Improvements
### WCAG Compliance
-**Contrast Ratio:** Improved from ~1.2:1 to ~2.8:1 (still enhances to ~4.5:1 on hover)
-**Discoverability:** Users can now see the button without trial-and-error
-**Focus Indicators:** Button remains focusable via keyboard
-**Screen Readers:** aria-label provides context
### Keyboard Navigation
- ✅ Tab order: Button is in logical sequence
- ✅ Enter/Space: Opens modal (native button behavior)
- ✅ Focus visible: Browser default focus ring applies
---
## User Experience Improvements
### Before Fix
1. User lands on page
2. User doesn't see shortcuts button (opacity: 0.2)
3. User accidentally hovers over left side
4. Button appears! (opacity: 1)
5. User moves mouse away
6. Button disappears again (opacity: 0.2)
7. User confused about how to access it
### After Fix
1. User lands on page
2. User sees faint keyboard icon button (opacity: 0.6)
3. User recognizes it as interactive element
4. User hovers or clicks
5. Button highlights (opacity: 1, blue background)
6. User understands the pattern
7. Clear mental model established
---
## Deployment Checklist
- ✅ CSS changes applied to main.css
- ✅ Server rebuilt with `make build`
- ✅ Server restarted with updated CSS
- ✅ Visual testing completed
- ✅ Live site verification completed
- ✅ Test report documented
- ✅ No regressions detected
---
## Recommendations for Future
### Consider These Enhancements
1. **First-Time User Hint:** Add a subtle pulse animation on first page load
2. **Tooltip on Load:** Show tooltip for 3 seconds on first visit
3. **Help Indicator:** Add "?" badge or "Press ?" hint
4. **Progressive Enhancement:** Store "has-seen-shortcuts" in localStorage
### CSS Enhancement Example
```css
/* Optional: Pulse animation for first-time discovery */
@keyframes pulse-hint {
0%, 100% { opacity: 0.6; }
50% { opacity: 0.9; }
}
.shortcuts-btn.first-visit {
animation: pulse-hint 2s ease-in-out 3;
}
```
---
## Conclusion
### Problem
Shortcuts button icon was invisible due to 80% transparency (opacity: 0.2)
### Solution
Increased default opacity to 0.6 (60% opacity / 40% transparency)
### Result
**Button is now clearly visible and discoverable**
**Maintains subtle, non-obtrusive design**
**Hover effect remains effective**
**Accessibility improved**
**User experience enhanced**
### Status
**RESOLVED** - Ready for production deployment
---
**Fix Verified By:** HTMX Frontend Specialist Agent
**Test Environment:** Local development server (localhost:1999)
**Build Status:** ✅ All tests passing
**Deployment Status:** ✅ Ready for commit
-76
View File
@@ -1,76 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Styling Comparison</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
background: #f0f0f0;
}
.comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
max-width: 1400px;
margin: 0 auto;
}
.panel {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h2 {
margin-top: 0;
color: #333;
}
img {
width: 100%;
border: 1px solid #ddd;
}
.specs {
margin-top: 10px;
font-family: monospace;
font-size: 12px;
background: #f9f9f9;
padding: 10px;
border-left: 3px solid #4CAF50;
}
.specs div {
margin: 5px 0;
}
</style>
</head>
<body>
<h1 style="text-align: center;">CV Header Styling Comparison</h1>
<div class="comparison">
<div class="panel">
<h2>Original Design</h2>
<img src="../screenshots/old-full-rendered.png" alt="Original">
<div class="specs">
<div><strong>Expected Styling:</strong></div>
<div>• Font: Quicksand</div>
<div>• Size: ~0.85em (smaller than name)</div>
<div>• Weight: 400 (normal)</div>
<div>• Color: #666 (medium gray)</div>
<div>• Margin: minimal (4px top)</div>
<div>• Alignment: left with name</div>
</div>
</div>
<div class="panel">
<h2>Current Implementation</h2>
<img src="../../tmp/cv-years-test.png" alt="Current">
<div class="specs">
<div><strong>Applied CSS:</strong></div>
<div>font-family: 'Quicksand', sans-serif;</div>
<div>font-size: 0.85em;</div>
<div>font-weight: 400;</div>
<div>color: #666;</div>
<div>margin: 4px 0 0 0;</div>
<div>line-height: 1.4;</div>
</div>
</div>
</div>
</body>
</html>
-192
View File
@@ -1,192 +0,0 @@
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
async function compareRendered() {
const browser = await chromium.launch({ headless: true });
console.log('\n=== COMPARING RENDERED SITES ===\n');
// OLD SITE
const pageOld = await browser.newPage({ viewport: { width: 1920, height: 1080 } });
console.log('Loading OLD site (React)...');
await pageOld.goto('http://localhost:3000', { waitUntil: 'networkidle', timeout: 30000 });
// Wait for React to render
await pageOld.waitForTimeout(2000);
// Take screenshot
await pageOld.screenshot({
path: './tests/screenshots/old-full-rendered.png',
fullPage: true
});
// Get actual rendered content
const oldContent = await pageOld.evaluate(() => {
const app = document.getElementById('app') || document.body;
return {
innerHTML: app.innerHTML.substring(0, 2000),
hasContent: app.innerHTML.length > 100,
classes: Array.from(document.querySelectorAll('[class]')).map(el => el.className).filter(c => c).slice(0, 50)
};
});
console.log('OLD site content loaded:', oldContent.hasContent);
console.log('OLD site classes found:', oldContent.classes.length);
if (oldContent.classes.length > 0) {
console.log('Sample classes:', oldContent.classes.slice(0, 10));
}
// NEW SITE
const pageNew = await browser.newPage({ viewport: { width: 1920, height: 1080 } });
console.log('\nLoading NEW site (Go+HTMX)...');
await pageNew.goto('http://localhost:1999', { waitUntil: 'networkidle' });
await pageNew.screenshot({
path: './tests/screenshots/new-full-rendered.png',
fullPage: true
});
// SIDE-BY-SIDE COMPARISON
console.log('\n=== HEADER BADGES COMPARISON ===\n');
// Try multiple selectors for old site
const oldBadgeSelectors = [
'[class*="badge"]',
'[class*="title"]',
'div[class*="cv"]',
'.badge',
'.title-badge'
];
let oldBadges = null;
for (const selector of oldBadgeSelectors) {
try {
const count = await pageOld.locator(selector).count();
if (count > 0) {
console.log(`Found ${count} elements with selector: ${selector}`);
oldBadges = await pageOld.$$eval(selector, elements =>
elements.slice(0, 5).map(el => {
const computed = window.getComputedStyle(el);
return {
tag: el.tagName,
class: el.className,
text: el.textContent?.substring(0, 50),
styles: {
fontSize: computed.fontSize,
fontWeight: computed.fontWeight,
color: computed.color,
backgroundColor: computed.backgroundColor,
padding: computed.padding,
height: computed.height
}
};
})
);
break;
}
} catch (e) {
// Try next selector
}
}
const newBadges = await pageNew.$$eval('.title-badge', elements =>
elements.slice(0, 5).map(el => {
const computed = window.getComputedStyle(el);
return {
tag: el.tagName,
class: el.className,
text: el.textContent?.trim(),
styles: {
fontSize: computed.fontSize,
fontWeight: computed.fontWeight,
color: computed.color,
backgroundColor: computed.backgroundColor,
padding: computed.padding,
height: computed.height
}
};
})
);
console.log('\nOLD site badges:');
console.log(JSON.stringify(oldBadges, null, 2));
console.log('\nNEW site badges:');
console.log(JSON.stringify(newBadges, null, 2));
// VISUAL PIXEL COMPARISON
console.log('\n=== VISUAL COMPARISON ===\n');
// Get dimensions
const oldDimensions = await pageOld.evaluate(() => ({
width: document.documentElement.scrollWidth,
height: document.documentElement.scrollHeight
}));
const newDimensions = await pageNew.evaluate(() => ({
width: document.documentElement.scrollWidth,
height: document.documentElement.scrollHeight
}));
console.log('OLD site dimensions:', oldDimensions);
console.log('NEW site dimensions:', newDimensions);
// Screenshot specific sections
try {
// Header comparison
const oldHeader = pageOld.locator('header, [class*="header"], div').first();
const newHeader = pageNew.locator('.cv-title-badges-header').first();
if (await oldHeader.count() > 0) {
await oldHeader.screenshot({ path: './tests/screenshots/old-header-section.png' });
}
await newHeader.screenshot({ path: './tests/screenshots/new-header-section.png' });
// Sidebar comparison
const oldSidebar = pageOld.locator('[class*="sidebar"], aside').first();
const newSidebar = pageNew.locator('.cv-sidebar').first();
if (await oldSidebar.count() > 0) {
await oldSidebar.screenshot({ path: './tests/screenshots/old-sidebar-section.png' });
}
await newSidebar.screenshot({ path: './tests/screenshots/new-sidebar-section.png' });
} catch (e) {
console.log('Error capturing sections:', e.message);
}
// CREATE COMPARISON REPORT
const report = {
timestamp: new Date().toISOString(),
oldSite: {
url: 'http://localhost:3000',
hasContent: oldContent.hasContent,
classesFound: oldContent.classes.length,
dimensions: oldDimensions,
badges: oldBadges
},
newSite: {
url: 'http://localhost:1999',
dimensions: newDimensions,
badges: newBadges
},
comparison: {
dimensionsMatch: Math.abs(oldDimensions.width - newDimensions.width) < 50 &&
Math.abs(oldDimensions.height - newDimensions.height) < 50,
pixelPerfect: null // To be determined by visual inspection
}
};
fs.writeFileSync(
'./tests/screenshots/comparison-report.json',
JSON.stringify(report, null, 2)
);
console.log('\n✓ Comparison complete!');
console.log('✓ Screenshots saved to tests/screenshots/');
console.log('✓ Report saved to comparison-report.json');
await browser.close();
}
compareRendered().catch(console.error);
-618
View File
@@ -1,618 +0,0 @@
/**
* COMPREHENSIVE FEATURE TEST SUITE
* Tests all 5 features in the CV application
*
* Features:
* 001: Keyboard Shortcuts Help Modal
* 002: Skeleton Loader for Language Transitions
* 003: HTMX Loading Indicators
* 004: Theme Switcher
* 005: PDF Download Modal
*/
import { test, expect } from '@playwright/test';
const BASE_URL = 'http://localhost:1999';
// Helper to wait for animations
const waitForAnimation = (ms = 700) => new Promise(resolve => setTimeout(resolve, ms));
test.describe('PHASE 1: DISCOVERY - Feature Detection', () => {
test('should load page and capture initial state', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
// Take screenshot of initial state
await page.screenshot({ path: 'test-results/01-initial-state.png', fullPage: true });
// Check for interactive elements
const shortcuts = await page.locator('button[data-action="show-shortcuts"], button:has-text("shortcuts"), button:has-text("atajos")').count();
const langButtons = await page.locator('button[data-lang], [hx-get*="lang"]').count();
const themeButton = await page.locator('button[data-theme], [data-action="toggle-theme"]').count();
const pdfButton = await page.locator('button:has-text("PDF"), button:has-text("download")').count();
const toggles = await page.locator('input[type="checkbox"][hx-get], input[type="checkbox"][hx-post]').count();
console.log('=== FEATURE DETECTION ===');
console.log(`Shortcuts button found: ${shortcuts > 0}`);
console.log(`Language buttons found: ${langButtons}`);
console.log(`Theme button found: ${themeButton > 0}`);
console.log(`PDF button found: ${pdfButton > 0}`);
console.log(`Toggle controls found: ${toggles}`);
expect(langButtons).toBeGreaterThan(0);
});
});
test.describe('FEATURE 001: Keyboard Shortcuts Help Modal', () => {
test('should open shortcuts modal on button click', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
// Find shortcuts button (try multiple selectors)
const shortcutsBtn = page.locator('button[data-action="show-shortcuts"]').or(
page.locator('button:has-text("shortcuts")').first()
).or(
page.locator('button:has-text("?")').first()
);
const btnExists = await shortcutsBtn.count() > 0;
console.log(`Shortcuts button exists: ${btnExists}`);
if (!btnExists) {
console.log('⚠️ Shortcuts button NOT FOUND - Feature may not be implemented');
return;
}
// Click button
await shortcutsBtn.click();
await waitForAnimation(300);
// Verify modal opened (check for dialog or modal element)
const dialog = page.locator('dialog[open], [role="dialog"]:visible, .modal:visible');
const dialogVisible = await dialog.count() > 0;
await page.screenshot({ path: 'test-results/01-shortcuts-modal-open.png', fullPage: true });
expect(dialogVisible).toBe(true);
console.log('✅ Shortcuts modal opens on button click');
});
test('should close modal with ESC key', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
const shortcutsBtn = page.locator('button[data-action="show-shortcuts"]').or(
page.locator('button:has-text("shortcuts")').first()
);
if (await shortcutsBtn.count() === 0) return;
await shortcutsBtn.click();
await waitForAnimation(300);
// Press ESC
await page.keyboard.press('Escape');
await waitForAnimation(300);
// Verify modal closed
const dialog = page.locator('dialog[open], [role="dialog"]:visible');
const dialogClosed = await dialog.count() === 0;
await page.screenshot({ path: 'test-results/01-shortcuts-modal-closed-esc.png', fullPage: true });
expect(dialogClosed).toBe(true);
console.log('✅ Modal closes with ESC key');
});
test('should close modal on backdrop click', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
const shortcutsBtn = page.locator('button[data-action="show-shortcuts"]').or(
page.locator('button:has-text("shortcuts")').first()
);
if (await shortcutsBtn.count() === 0) return;
await shortcutsBtn.click();
await waitForAnimation(300);
// Click backdrop (click dialog element itself, not content)
const dialog = page.locator('dialog[open]');
if (await dialog.count() > 0) {
await dialog.click({ position: { x: 5, y: 5 } });
await waitForAnimation(300);
const dialogClosed = await page.locator('dialog[open]').count() === 0;
expect(dialogClosed).toBe(true);
console.log('✅ Modal closes on backdrop click');
}
});
test('should show keyboard shortcuts content', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
const shortcutsBtn = page.locator('button[data-action="show-shortcuts"]').or(
page.locator('button:has-text("shortcuts")').first()
);
if (await shortcutsBtn.count() === 0) return;
await shortcutsBtn.click();
await waitForAnimation(300);
// Check for keyboard shortcut content (look for kbd tags or shortcut listings)
const kbdElements = await page.locator('kbd').count();
const hasShortcutContent = kbdElements > 0;
console.log(`Keyboard shortcut elements found: ${kbdElements}`);
expect(hasShortcutContent).toBe(true);
console.log('✅ Modal displays keyboard shortcuts');
});
test('should support bilingual content (EN/ES)', async ({ page }) => {
// Test English
await page.goto(`${BASE_URL}/?lang=en`);
const shortcutsBtn = page.locator('button[data-action="show-shortcuts"]').or(
page.locator('button:has-text("shortcuts")').first()
);
if (await shortcutsBtn.count() === 0) return;
await shortcutsBtn.click();
await waitForAnimation(300);
const enContent = await page.locator('dialog, [role="dialog"]').textContent();
await page.keyboard.press('Escape');
// Test Spanish
await page.goto(`${BASE_URL}/?lang=es`);
const shortcutsBtnEs = page.locator('button[data-action="show-shortcuts"]').or(
page.locator('button:has-text("atajos")').first()
);
if (await shortcutsBtnEs.count() > 0) {
await shortcutsBtnEs.click();
await waitForAnimation(300);
const esContent = await page.locator('dialog, [role="dialog"]').textContent();
const isDifferent = enContent !== esContent;
console.log(`Content differs between EN/ES: ${isDifferent}`);
expect(isDifferent).toBe(true);
console.log('✅ Modal supports bilingual content');
}
});
});
test.describe('FEATURE 002: Skeleton Loader for Language Transitions', () => {
test('should show skeleton loader during language switch', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
// Find language toggle button
const langButton = page.locator('button[data-lang="es"]').or(
page.locator('button:has-text("ES")').first()
).or(
page.locator('[hx-get*="lang=es"]').first()
);
const btnExists = await langButton.count() > 0;
console.log(`Language button exists: ${btnExists}`);
if (!btnExists) {
console.log('⚠️ Language button NOT FOUND');
return;
}
// Monitor for skeleton loader
let skeletonAppeared = false;
// Set up observer before clicking
await page.evaluate(() => {
window.skeletonDetected = false;
const observer = new MutationObserver(() => {
const skeleton = document.querySelector('.skeleton, [data-skeleton], .skeleton-loader, .shimmer');
if (skeleton && window.getComputedStyle(skeleton).opacity !== '0') {
window.skeletonDetected = true;
}
});
observer.observe(document.body, { childList: true, subtree: true, attributes: true });
});
// Click language button
await langButton.click();
await waitForAnimation(100);
// Check if skeleton appeared
skeletonAppeared = await page.evaluate(() => window.skeletonDetected);
await waitForAnimation(600);
await page.screenshot({ path: 'test-results/02-skeleton-loader.png', fullPage: true });
console.log(`Skeleton loader appeared: ${skeletonAppeared}`);
expect(skeletonAppeared).toBe(true);
console.log('✅ Skeleton loader appears during language transition');
});
test('should complete transition within 500-700ms', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
const langButton = page.locator('button[data-lang="es"]').or(
page.locator('[hx-get*="lang=es"]').first()
);
if (await langButton.count() === 0) return;
const startTime = Date.now();
await langButton.click();
// Wait for HTMX to complete (htmx:afterSwap event)
await page.waitForFunction(() => {
return !document.body.classList.contains('htmx-swapping') &&
!document.querySelector('.htmx-swapping');
}, { timeout: 2000 });
const endTime = Date.now();
const duration = endTime - startTime;
console.log(`Transition duration: ${duration}ms`);
expect(duration).toBeGreaterThanOrEqual(400);
expect(duration).toBeLessThanOrEqual(1000);
console.log('✅ Transition completes within acceptable time range');
});
test('should handle rapid language switching without breaking', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
const enButton = page.locator('button[data-lang="en"]').or(
page.locator('[hx-get*="lang=en"]').first()
);
const esButton = page.locator('button[data-lang="es"]').or(
page.locator('[hx-get*="lang=es"]').first()
);
if (await enButton.count() === 0 || await esButton.count() === 0) return;
// Rapid clicking
await esButton.click();
await waitForAnimation(100);
await enButton.click();
await waitForAnimation(100);
await esButton.click();
await waitForAnimation(800);
// Check no errors in console
const errors = [];
page.on('console', msg => {
if (msg.type() === 'error') errors.push(msg.text());
});
await page.screenshot({ path: 'test-results/02-rapid-switch.png', fullPage: true });
console.log(`Console errors during rapid switching: ${errors.length}`);
expect(errors.length).toBe(0);
console.log('✅ Handles rapid language switching without errors');
});
});
test.describe('FEATURE 003: HTMX Loading Indicators', () => {
test('should show loading indicator on language button click', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
const langButton = page.locator('button[data-lang="es"]').or(
page.locator('[hx-get*="lang=es"]').first()
);
if (await langButton.count() === 0) return;
// Look for loading indicator
let indicatorAppeared = false;
await page.evaluate(() => {
window.indicatorDetected = false;
const observer = new MutationObserver(() => {
const indicator = document.querySelector('.htmx-indicator, .loading-indicator, .spinner, [data-loading]');
if (indicator && window.getComputedStyle(indicator).opacity !== '0') {
window.indicatorDetected = true;
}
});
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
});
await langButton.click();
await waitForAnimation(50);
indicatorAppeared = await page.evaluate(() => window.indicatorDetected);
await waitForAnimation(600);
console.log(`Loading indicator appeared: ${indicatorAppeared}`);
expect(indicatorAppeared).toBe(true);
console.log('✅ Loading indicator appears on language button click');
});
test('should show loading indicators on toggle controls', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
const toggles = page.locator('input[type="checkbox"][hx-get], input[type="checkbox"][hx-post]');
const toggleCount = await toggles.count();
console.log(`Toggle controls found: ${toggleCount}`);
if (toggleCount === 0) {
console.log('⚠️ No toggle controls found');
return;
}
// Test first toggle
const firstToggle = toggles.first();
await page.evaluate(() => {
window.toggleIndicatorDetected = false;
const observer = new MutationObserver(() => {
const indicator = document.querySelector('.htmx-indicator, .loading-indicator, .spinner');
if (indicator && window.getComputedStyle(indicator).opacity !== '0') {
window.toggleIndicatorDetected = true;
}
});
observer.observe(document.body, { childList: true, subtree: true, attributes: true });
});
await firstToggle.click();
await waitForAnimation(50);
const indicatorAppeared = await page.evaluate(() => window.toggleIndicatorDetected);
await waitForAnimation(500);
await page.screenshot({ path: 'test-results/03-toggle-indicator.png', fullPage: true });
console.log(`Toggle loading indicator appeared: ${indicatorAppeared}`);
console.log('✅ Loading indicators work on toggle controls');
});
test('should hide indicators after request completes', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
const langButton = page.locator('button[data-lang="es"]').or(
page.locator('[hx-get*="lang=es"]').first()
);
if (await langButton.count() === 0) return;
await langButton.click();
await waitForAnimation(800);
// Check that all indicators are hidden
const visibleIndicators = await page.locator('.htmx-indicator:visible, .loading-indicator:visible, .spinner:visible').count();
console.log(`Visible indicators after completion: ${visibleIndicators}`);
expect(visibleIndicators).toBe(0);
console.log('✅ Indicators hide after request completion');
});
});
test.describe('FEATURE 004: Theme Switcher', () => {
test('should detect theme switcher button', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
const themeButton = page.locator('button[data-theme], button[data-action="toggle-theme"], button:has-text("theme")').first();
const exists = await themeButton.count() > 0;
console.log(`Theme switcher button exists: ${exists}`);
if (!exists) {
console.log('⚠️ Theme switcher NOT IMPLEMENTED');
return;
}
await page.screenshot({ path: 'test-results/04-theme-button.png', fullPage: true });
expect(exists).toBe(true);
});
test('should expand to show theme options', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
const themeButton = page.locator('button[data-theme], button[data-action="toggle-theme"]').first();
if (await themeButton.count() === 0) {
console.log('⚠️ Theme switcher NOT FOUND');
return;
}
await themeButton.click();
await waitForAnimation(300);
// Look for theme options (Light, Dark, Auto)
const lightOption = await page.locator('button:has-text("Light"), [data-theme="light"]').count();
const darkOption = await page.locator('button:has-text("Dark"), [data-theme="dark"]').count();
const autoOption = await page.locator('button:has-text("Auto"), [data-theme="auto"]').count();
console.log(`Light option: ${lightOption}, Dark option: ${darkOption}, Auto option: ${autoOption}`);
await page.screenshot({ path: 'test-results/04-theme-options.png', fullPage: true });
const hasOptions = lightOption > 0 || darkOption > 0 || autoOption > 0;
expect(hasOptions).toBe(true);
console.log('✅ Theme switcher shows options');
});
test('should persist theme selection in localStorage', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
const themeButton = page.locator('button[data-theme], button[data-action="toggle-theme"]').first();
if (await themeButton.count() === 0) return;
await themeButton.click();
await waitForAnimation(300);
const darkOption = page.locator('button:has-text("Dark"), [data-theme="dark"]').first();
if (await darkOption.count() > 0) {
await darkOption.click();
await waitForAnimation(300);
// Check localStorage
const storedTheme = await page.evaluate(() => localStorage.getItem('theme'));
console.log(`Stored theme: ${storedTheme}`);
// Reload and verify persistence
await page.reload();
await waitForAnimation(300);
const themeAfterReload = await page.evaluate(() => localStorage.getItem('theme'));
expect(themeAfterReload).toBe(storedTheme);
console.log('✅ Theme selection persists in localStorage');
}
});
});
test.describe('FEATURE 005: PDF Download Modal', () => {
test('should detect PDF modal trigger button', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
const pdfButton = page.locator('button:has-text("PDF"), button:has-text("download"), [data-action="show-pdf"]').first();
const exists = await pdfButton.count() > 0;
console.log(`PDF modal button exists: ${exists}`);
if (!exists) {
console.log('⚠️ PDF MODAL NOT IMPLEMENTED');
return;
}
await page.screenshot({ path: 'test-results/05-pdf-button.png', fullPage: true });
expect(exists).toBe(true);
});
test('should show three thumbnail cards', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
const pdfButton = page.locator('button:has-text("PDF"), button:has-text("download")').first();
if (await pdfButton.count() === 0) return;
await pdfButton.click();
await waitForAnimation(300);
// Look for thumbnail cards
const thumbnails = await page.locator('.thumbnail, .pdf-card, [data-pdf-type]').count();
console.log(`Thumbnail cards found: ${thumbnails}`);
await page.screenshot({ path: 'test-results/05-pdf-modal-open.png', fullPage: true });
expect(thumbnails).toBeGreaterThanOrEqual(2);
console.log('✅ PDF modal shows thumbnail cards');
});
test('should enable download button after selection', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
const pdfButton = page.locator('button:has-text("PDF"), button:has-text("download")').first();
if (await pdfButton.count() === 0) return;
await pdfButton.click();
await waitForAnimation(300);
// Find download button (should be disabled initially)
const downloadBtn = page.locator('button:has-text("Download"), button[data-action="download"]').first();
if (await downloadBtn.count() > 0) {
const initiallyDisabled = await downloadBtn.isDisabled();
console.log(`Download button initially disabled: ${initiallyDisabled}`);
// Click first thumbnail
const thumbnail = page.locator('.thumbnail, .pdf-card, [data-pdf-type]').first();
if (await thumbnail.count() > 0) {
await thumbnail.click();
await waitForAnimation(200);
const enabledAfterSelection = !(await downloadBtn.isDisabled());
console.log(`Download button enabled after selection: ${enabledAfterSelection}`);
expect(enabledAfterSelection).toBe(true);
console.log('✅ Download button enables after selection');
}
}
});
});
test.describe('INTEGRATION TESTS: Cross-Feature Interactions', () => {
test('should handle language switch while modal is open', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
// Open shortcuts modal if exists
const shortcutsBtn = page.locator('button[data-action="show-shortcuts"]').first();
if (await shortcutsBtn.count() > 0) {
await shortcutsBtn.click();
await waitForAnimation(300);
// Switch language
const langButton = page.locator('button[data-lang="es"]').first();
if (await langButton.count() > 0) {
await langButton.click();
await waitForAnimation(800);
await page.screenshot({ path: 'test-results/int-modal-lang-switch.png', fullPage: true });
console.log('✅ Language switch works with modal open');
}
}
});
test('should handle multiple rapid feature interactions', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
const errors = [];
page.on('console', msg => {
if (msg.type() === 'error') errors.push(msg.text());
});
// Rapid interactions
const langButton = page.locator('button[data-lang="es"]').first();
const toggle = page.locator('input[type="checkbox"]').first();
if (await langButton.count() > 0) await langButton.click();
await waitForAnimation(100);
if (await toggle.count() > 0) await toggle.click();
await waitForAnimation(100);
if (await langButton.count() > 0) await langButton.click();
await waitForAnimation(800);
console.log(`Errors during rapid interactions: ${errors.length}`);
expect(errors.length).toBe(0);
console.log('✅ Handles rapid feature interactions without errors');
});
});
test.describe('PERFORMANCE & ACCESSIBILITY', () => {
test('should have no console errors on page load', async ({ page }) => {
const errors = [];
page.on('console', msg => {
if (msg.type() === 'error') errors.push(msg.text());
});
await page.goto(`${BASE_URL}/?lang=en`);
await waitForAnimation(1000);
console.log('Console errors on load:', errors);
expect(errors.length).toBe(0);
console.log('✅ No console errors on page load');
});
test('should measure Core Web Vitals', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
await waitForAnimation(1000);
const metrics = await page.evaluate(() => {
const paint = performance.getEntriesByType('paint');
const navigation = performance.getEntriesByType('navigation')[0];
return {
fcp: paint.find(p => p.name === 'first-contentful-paint')?.startTime,
domContentLoaded: navigation?.domContentLoadedEventEnd - navigation?.domContentLoadedEventStart,
loadComplete: navigation?.loadEventEnd - navigation?.loadEventStart
};
});
console.log('Performance metrics:', metrics);
expect(metrics.fcp).toBeLessThan(3000);
console.log('✅ Performance metrics within acceptable range');
});
});
-98
View File
@@ -1,98 +0,0 @@
const { chromium } = require('playwright');
async function inspectStructure() {
const browser = await chromium.launch();
const page = await browser.newPage();
console.log('\n=== INSPECTING OLD SITE (localhost:3000) ===\n');
await page.goto('http://localhost:3000', { waitUntil: 'networkidle' });
// Get all class names
const classes = await page.evaluate(() => {
const allElements = document.querySelectorAll('*');
const classSet = new Set();
allElements.forEach(el => {
if (el.className && typeof el.className === 'string') {
el.className.split(' ').forEach(cls => {
if (cls.trim()) classSet.add(cls.trim());
});
}
});
return Array.from(classSet).sort();
});
console.log('All classes found:');
console.log(classes.filter(c => c.includes('badge') || c.includes('title') || c.includes('cv') || c.includes('sidebar')).join('\n'));
// Get main structure
const structure = await page.evaluate(() => {
const getStructure = (el, depth = 0) => {
if (depth > 3) return null;
const tag = el.tagName.toLowerCase();
const classes = el.className || '';
const id = el.id || '';
return {
tag,
classes,
id,
children: Array.from(el.children).map(child => getStructure(child, depth + 1)).filter(Boolean)
};
};
return getStructure(document.body);
});
console.log('\n\nMain structure:');
console.log(JSON.stringify(structure, null, 2).substring(0, 5000));
// Find elements with "badge" or "title" in their classes
const badgeElements = await page.$$eval('[class*="badge"], [class*="title"]', elements =>
elements.slice(0, 20).map(el => ({
tag: el.tagName,
class: el.className,
text: el.textContent?.substring(0, 100),
computedStyles: (() => {
const computed = window.getComputedStyle(el);
return {
fontSize: computed.fontSize,
fontWeight: computed.fontWeight,
color: computed.color,
backgroundColor: computed.backgroundColor,
padding: computed.padding,
height: computed.height
};
})()
}))
);
console.log('\n\nBadge/Title elements:');
console.log(JSON.stringify(badgeElements, null, 2));
console.log('\n\n=== INSPECTING NEW SITE (localhost:1999) ===\n');
await page.goto('http://localhost:1999', { waitUntil: 'networkidle' });
const newBadgeElements = await page.$$eval('[class*="badge"], [class*="title"]', elements =>
elements.slice(0, 20).map(el => ({
tag: el.tagName,
class: el.className,
text: el.textContent?.substring(0, 100),
computedStyles: (() => {
const computed = window.getComputedStyle(el);
return {
fontSize: computed.fontSize,
fontWeight: computed.fontWeight,
color: computed.color,
backgroundColor: computed.backgroundColor,
padding: computed.padding,
height: computed.height
};
})()
}))
);
console.log('Badge/Title elements:');
console.log(JSON.stringify(newBadgeElements, null, 2));
await browser.close();
}
inspectStructure().catch(console.error);
-408
View File
@@ -1,408 +0,0 @@
/**
* MANUAL INSPECTION - Deep Dive into Features
* Investigates specific issues found in comprehensive tests
*/
import { test, expect } from '@playwright/test';
const BASE_URL = 'http://localhost:1999';
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
test.describe('MANUAL INSPECTION: Feature Deep Dive', () => {
test('Inspect page structure and all interactive elements', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
await wait(1000);
console.log('\n=== PAGE STRUCTURE INSPECTION ===\n');
// Find all buttons
const allButtons = await page.$$('button');
console.log(`Total buttons: ${allButtons.length}`);
for (let i = 0; i < allButtons.length; i++) {
const btn = allButtons[i];
const text = await btn.textContent();
const id = await btn.getAttribute('id');
const dataAction = await btn.getAttribute('data-action');
const classes = await btn.getAttribute('class');
console.log(`Button ${i + 1}: text="${text?.trim()}" id="${id}" data-action="${dataAction}" class="${classes}"`);
}
// Find all toggles
console.log('\n=== TOGGLE CONTROLS ===\n');
const toggles = await page.$$('input[type="checkbox"]');
console.log(`Total checkboxes: ${toggles.length}`);
for (let i = 0; i < toggles.length; i++) {
const toggle = toggles[i];
const id = await toggle.getAttribute('id');
const hxGet = await toggle.getAttribute('hx-get');
const hxPost = await toggle.getAttribute('hx-post');
const hxIndicator = await toggle.getAttribute('hx-indicator');
console.log(`Toggle ${i + 1}: id="${id}" hx-get="${hxGet}" hx-post="${hxPost}" hx-indicator="${hxIndicator}"`);
}
// Find modals/dialogs
console.log('\n=== MODALS/DIALOGS ===\n');
const dialogs = await page.$$('dialog');
console.log(`Native dialogs: ${dialogs.length}`);
for (let i = 0; i < dialogs.length; i++) {
const dialog = dialogs[i];
const id = await dialog.getAttribute('id');
const classes = await dialog.getAttribute('class');
const textPreview = (await dialog.textContent())?.substring(0, 50);
console.log(`Dialog ${i + 1}: id="${id}" class="${classes}" preview="${textPreview}..."`);
}
// Find HTMX indicators
console.log('\n=== HTMX INDICATORS ===\n');
const indicators = await page.$$('.htmx-indicator, [class*="indicator"], [class*="loading"], [class*="spinner"]');
console.log(`Indicator elements: ${indicators.length}`);
for (let i = 0; i < indicators.length; i++) {
const indicator = indicators[i];
const classes = await indicator.getAttribute('class');
const id = await indicator.getAttribute('id');
console.log(`Indicator ${i + 1}: id="${id}" class="${classes}"`);
}
// Find skeleton loaders
console.log('\n=== SKELETON LOADERS ===\n');
const skeletons = await page.$$('[class*="skeleton"], [class*="shimmer"]');
console.log(`Skeleton elements: ${skeletons.length}`);
for (let i = 0; i < skeletons.length; i++) {
const skeleton = skeletons[i];
const classes = await skeleton.getAttribute('class');
const id = await skeleton.getAttribute('id');
console.log(`Skeleton ${i + 1}: id="${id}" class="${classes}"`);
}
await page.screenshot({ path: 'test-results/inspect-full-page.png', fullPage: true });
});
test('Test language switch with detailed timing', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
await wait(1000);
console.log('\n=== LANGUAGE SWITCH DETAILED TIMING ===\n');
// Find ES button
const esButton = await page.locator('button').filter({ hasText: 'ES' }).first();
// Monitor all DOM changes during switch
await page.evaluate(() => {
window.transitionLog = [];
window.startTime = Date.now();
// Monitor skeleton
const observer = new MutationObserver(() => {
const skeleton = document.querySelector('[class*="skeleton"]');
if (skeleton) {
const opacity = window.getComputedStyle(skeleton).opacity;
const display = window.getComputedStyle(skeleton).display;
window.transitionLog.push({
time: Date.now() - window.startTime,
event: 'skeleton',
opacity,
display
});
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class', 'style']
});
// Monitor HTMX events
document.body.addEventListener('htmx:beforeSwap', () => {
window.transitionLog.push({ time: Date.now() - window.startTime, event: 'beforeSwap' });
});
document.body.addEventListener('htmx:afterSwap', () => {
window.transitionLog.push({ time: Date.now() - window.startTime, event: 'afterSwap' });
});
document.body.addEventListener('htmx:afterSettle', () => {
window.transitionLog.push({ time: Date.now() - window.startTime, event: 'afterSettle' });
});
});
// Click ES button
const clickTime = Date.now();
await esButton.click();
// Wait and capture screenshots at different stages
await wait(100);
await page.screenshot({ path: 'test-results/lang-switch-100ms.png', fullPage: true });
await wait(200);
await page.screenshot({ path: 'test-results/lang-switch-300ms.png', fullPage: true });
await wait(300);
await page.screenshot({ path: 'test-results/lang-switch-600ms.png', fullPage: true });
await wait(200);
const endTime = Date.now();
// Get transition log
const log = await page.evaluate(() => window.transitionLog);
console.log('Transition timeline:');
log.forEach(entry => {
console.log(` ${entry.time}ms: ${entry.event}${entry.opacity ? ` (opacity: ${entry.opacity})` : ''}`);
});
console.log(`\nTotal measured time: ${endTime - clickTime}ms`);
});
test('Inspect HTMX loading indicators in detail', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
await wait(1000);
console.log('\n=== HTMX INDICATOR INSPECTION ===\n');
// Find language button with hx attributes
const langButtons = await page.$$('button[hx-get], button[data-lang]');
console.log(`Buttons with HTMX attributes: ${langButtons.length}`);
for (let i = 0; i < langButtons.length; i++) {
const btn = langButtons[i];
const hxIndicator = await btn.getAttribute('hx-indicator');
const text = await btn.textContent();
console.log(`Button "${text?.trim()}": hx-indicator="${hxIndicator}"`);
if (hxIndicator) {
const indicatorExists = await page.locator(hxIndicator).count();
console.log(` → Indicator "${hxIndicator}" exists: ${indicatorExists > 0}`);
if (indicatorExists > 0) {
const classes = await page.locator(hxIndicator).getAttribute('class');
const styles = await page.locator(hxIndicator).evaluate(el => ({
display: window.getComputedStyle(el).display,
opacity: window.getComputedStyle(el).opacity,
visibility: window.getComputedStyle(el).visibility
}));
console.log(` → Classes: "${classes}"`);
console.log(` → Computed styles: ${JSON.stringify(styles)}`);
}
}
}
// Test clicking and monitoring
const esButton = page.locator('button').filter({ hasText: 'ES' }).first();
await page.evaluate(() => {
window.indicatorStates = [];
const observer = new MutationObserver(() => {
const indicators = document.querySelectorAll('.htmx-indicator, [class*="loading"]');
indicators.forEach((ind, idx) => {
const styles = window.getComputedStyle(ind);
window.indicatorStates.push({
time: Date.now(),
indicator: idx,
opacity: styles.opacity,
display: styles.display,
classes: ind.className
});
});
});
observer.observe(document.body, {
attributes: true,
childList: true,
subtree: true,
attributeFilter: ['class', 'style']
});
});
await esButton.click();
await wait(50);
await page.screenshot({ path: 'test-results/indicator-active-50ms.png', fullPage: true });
await wait(700);
const states = await page.evaluate(() => window.indicatorStates);
console.log('\nIndicator state changes:');
states.forEach(state => {
console.log(` ${state.time}: Indicator ${state.indicator} - opacity=${state.opacity}, display=${state.display}`);
});
});
test('Test PDF modal structure', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
await wait(1000);
console.log('\n=== PDF MODAL INSPECTION ===\n');
// Find PDF button
const pdfButtons = await page.$$('button');
let pdfButton = null;
for (const btn of pdfButtons) {
const text = (await btn.textContent())?.toLowerCase() || '';
if (text.includes('pdf') || text.includes('download')) {
pdfButton = btn;
console.log(`Found PDF button: "${await btn.textContent()}"`);
break;
}
}
if (!pdfButton) {
console.log('❌ No PDF button found');
return;
}
// Click to open modal
await pdfButton.click();
await wait(500);
await page.screenshot({ path: 'test-results/pdf-modal-detailed.png', fullPage: true });
// Inspect modal structure
const modalContent = await page.evaluate(() => {
const dialog = document.querySelector('dialog[open]');
if (!dialog) return { found: false };
const allElements = dialog.querySelectorAll('*');
const structure = {
found: true,
totalElements: allElements.length,
images: dialog.querySelectorAll('img').length,
cards: dialog.querySelectorAll('[class*="card"], [class*="thumbnail"], [data-pdf]').length,
buttons: dialog.querySelectorAll('button').length,
textContent: dialog.textContent?.substring(0, 200)
};
return structure;
});
console.log('Modal structure:', JSON.stringify(modalContent, null, 2));
// Look for specific PDF-related elements
const pdfElements = await page.$$('[data-pdf-type], [class*="pdf"], .thumbnail, .card');
console.log(`\nPDF-related elements found: ${pdfElements.length}`);
for (let i = 0; i < pdfElements.length; i++) {
const el = pdfElements[i];
const classes = await el.getAttribute('class');
const dataPdf = await el.getAttribute('data-pdf-type');
const tagName = await el.evaluate(node => node.tagName);
console.log(` Element ${i + 1}: <${tagName}> class="${classes}" data-pdf-type="${dataPdf}"`);
}
});
test('Search for shortcuts button systematically', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
await wait(1000);
console.log('\n=== SHORTCUTS BUTTON SEARCH ===\n');
// Try all possible button texts
const searchTerms = ['shortcuts', 'shortcut', 'keyboard', 'help', '?', 'atajos', 'ayuda'];
for (const term of searchTerms) {
const count = await page.locator(`button:has-text("${term}")`).count();
console.log(`Buttons containing "${term}": ${count}`);
}
// Try data attributes
const dataActions = await page.$$('[data-action]');
console.log(`\nElements with data-action: ${dataActions.length}`);
for (const el of dataActions) {
const action = await el.getAttribute('data-action');
const tagName = await el.evaluate(node => node.tagName);
const text = (await el.textContent())?.trim();
console.log(` <${tagName}> data-action="${action}" text="${text}"`);
}
// Look for info icon or help icon
const icons = await page.$$('[class*="icon"], i, svg');
console.log(`\nIcon elements: ${icons.length}`);
for (let i = 0; i < Math.min(icons.length, 20); i++) {
const icon = icons[i];
const classes = await icon.getAttribute('class');
const parent = await icon.evaluateHandle(node => node.parentElement);
const parentTag = await parent.evaluate(node => node?.tagName);
if (classes?.includes('info') || classes?.includes('help') || classes?.includes('question')) {
console.log(` Icon ${i + 1}: class="${classes}" parent=<${parentTag}>`);
}
}
});
test('Test theme switcher detection', async ({ page }) => {
await page.goto(`${BASE_URL}/?lang=en`);
await wait(1000);
console.log('\n=== THEME SWITCHER SEARCH ===\n');
// Search for theme-related elements
const themeElements = await page.$$('[data-theme], [class*="theme"], button:has-text("theme")');
console.log(`Theme-related elements: ${themeElements.length}`);
for (const el of themeElements) {
const tagName = await el.evaluate(node => node.tagName);
const classes = await el.getAttribute('class');
const dataTheme = await el.getAttribute('data-theme');
const text = (await el.textContent())?.substring(0, 30);
console.log(` <${tagName}> class="${classes}" data-theme="${dataTheme}" text="${text}"`);
}
// Check localStorage
const themeInStorage = await page.evaluate(() => localStorage.getItem('theme'));
console.log(`\nTheme in localStorage: "${themeInStorage}"`);
// Check for moon/sun icons (common theme toggle icons)
const moonSun = await page.$$('[class*="moon"], [class*="sun"], [class*="dark"], [class*="light"]');
console.log(`Moon/sun/dark/light elements: ${moonSun.length}`);
});
test('Console error monitoring', async ({ page }) => {
const errors = [];
const warnings = [];
page.on('console', msg => {
if (msg.type() === 'error') errors.push({ text: msg.text(), location: msg.location() });
if (msg.type() === 'warning') warnings.push(msg.text());
});
page.on('pageerror', error => {
errors.push({ text: error.message, stack: error.stack });
});
await page.goto(`${BASE_URL}/?lang=en`);
await wait(2000);
// Interact with features
const esButton = page.locator('button').filter({ hasText: 'ES' }).first();
if (await esButton.count() > 0) {
await esButton.click();
await wait(1000);
}
console.log('\n=== CONSOLE MONITORING ===\n');
console.log(`Errors: ${errors.length}`);
errors.forEach((err, i) => {
console.log(` Error ${i + 1}: ${err.text}`);
if (err.stack) console.log(` Stack: ${err.stack.substring(0, 100)}...`);
});
console.log(`\nWarnings: ${warnings.length}`);
warnings.forEach((warn, i) => {
console.log(` Warning ${i + 1}: ${warn}`);
});
});
});
+283
View File
@@ -0,0 +1,283 @@
#!/usr/bin/env bun
/**
* MOBILE RESPONSIVE TEST
* =======================
* Tests mobile viewport rendering and interactions
* - Mobile viewport sizing (375px, 768px, 1024px)
* - Touch interactions
* - Mobile menu functionality
* - Responsive layout breakpoints
* - Text readability at small sizes
*/
import { chromium } from 'playwright';
const URL = "http://localhost:1999";
// Common mobile viewports
const VIEWPORTS = {
mobile: { width: 375, height: 667 }, // iPhone SE
tablet: { width: 768, height: 1024 }, // iPad
desktop: { width: 1920, height: 1080 } // Desktop baseline
};
async function testMobileResponsive() {
console.log('📱 MOBILE RESPONSIVE TEST\n');
console.log('='.repeat(70));
const browser = await chromium.launch({ headless: false });
const errors = [];
const testResults = [];
// ========================================================================
// TEST 1: Mobile viewport (375px)
// ========================================================================
console.log("\n1️⃣ Testing Mobile Viewport (375px)...");
const mobilePage = await browser.newPage({ viewport: VIEWPORTS.mobile });
mobilePage.on('console', msg => {
if (msg.type() === 'error') {
errors.push(msg.text());
console.log(`❌ ERROR: ${msg.text()}`);
}
});
await mobilePage.goto(URL);
await mobilePage.waitForTimeout(2000);
const mobileTest = await mobilePage.evaluate(() => {
const paper = document.querySelector('.cv-paper');
const body = document.body;
const hamburger = document.querySelector('.hamburger-btn');
// Check for horizontal overflow
const hasHorizontalScroll = document.documentElement.scrollWidth > window.innerWidth;
// Check if text is readable (not too small)
const paragraphs = Array.from(document.querySelectorAll('p, li'));
const fontSizes = paragraphs.map(p => {
const size = parseFloat(window.getComputedStyle(p).fontSize);
return size;
});
const minFontSize = Math.min(...fontSizes);
// Check if hamburger menu exists (mobile navigation)
const hasHamburger = !!hamburger;
const hamburgerVisible = hasHamburger ?
window.getComputedStyle(hamburger).display !== 'none' : false;
return {
width: window.innerWidth,
height: window.innerHeight,
hasHorizontalScroll,
minFontSize,
hasHamburger,
hamburgerVisible,
paperWidth: paper ? paper.offsetWidth : 0
};
});
console.log(` Viewport: ${mobileTest.width}x${mobileTest.height}`);
console.log(` Horizontal scroll: ${mobileTest.hasHorizontalScroll ? '❌ YES (BAD)' : '✅ NO (GOOD)'}`);
console.log(` Min font size: ${mobileTest.minFontSize.toFixed(1)}px ${mobileTest.minFontSize >= 14 ? '✅' : '⚠️'}`);
console.log(` Hamburger menu: ${mobileTest.hasHamburger ? '✅ Present' : '⚠️ Not found'}`);
console.log(` Hamburger visible: ${mobileTest.hamburgerVisible ? '✅ YES' : '⚠️ NO'}`);
const mobileViewportPassed = !mobileTest.hasHorizontalScroll && mobileTest.minFontSize >= 14;
console.log(` ${mobileViewportPassed ? '✅ PASS' : '❌ FAIL'} - Mobile viewport`);
testResults.push({ test: 'Mobile Viewport (375px)', passed: mobileViewportPassed });
// ========================================================================
// TEST 2: Touch interactions (hamburger menu)
// ========================================================================
console.log("\n2️⃣ Testing Touch Interactions...");
const hamburger = await mobilePage.$('.hamburger-btn');
if (hamburger) {
// Tap hamburger to open menu
await hamburger.tap();
await mobilePage.waitForTimeout(500);
const menuTest = await mobilePage.evaluate(() => {
const menu = document.querySelector('.navigation-menu');
if (!menu) return { found: false };
const isOpen = menu.classList.contains('menu-open') ||
window.getComputedStyle(menu).display !== 'none';
return {
found: true,
isOpen,
isVisible: menu.offsetHeight > 0
};
});
console.log(` Menu found: ${menuTest.found ? '✅' : '❌'}`);
console.log(` Menu opens on tap: ${menuTest.isOpen ? '✅' : '❌'}`);
console.log(` ${menuTest.found && menuTest.isOpen ? '✅ PASS' : '❌ FAIL'} - Touch interactions`);
testResults.push({ test: 'Touch Interactions', passed: menuTest.found && menuTest.isOpen });
// Close menu
if (menuTest.isOpen) {
await hamburger.tap();
await mobilePage.waitForTimeout(300);
}
} else {
console.log(` ⚠️ SKIP - Hamburger menu not found`);
testResults.push({ test: 'Touch Interactions', passed: true });
}
// ========================================================================
// TEST 3: Tablet viewport (768px)
// ========================================================================
console.log("\n3️⃣ Testing Tablet Viewport (768px)...");
const tabletPage = await browser.newPage({ viewport: VIEWPORTS.tablet });
await tabletPage.goto(URL);
await tabletPage.waitForTimeout(2000);
const tabletTest = await tabletPage.evaluate(() => {
const hasHorizontalScroll = document.documentElement.scrollWidth > window.innerWidth;
const paper = document.querySelector('.cv-paper');
const actionBar = document.querySelector('.action-bar, .cv-controls');
return {
width: window.innerWidth,
hasHorizontalScroll,
paperWidth: paper ? paper.offsetWidth : 0,
hasActionBar: !!actionBar,
actionBarVisible: actionBar ? window.getComputedStyle(actionBar).display !== 'none' : false
};
});
console.log(` Viewport: ${tabletTest.width}px`);
console.log(` Horizontal scroll: ${tabletTest.hasHorizontalScroll ? '❌ YES' : '✅ NO'}`);
console.log(` Action bar: ${tabletTest.hasActionBar ? '✅ Present' : '⚠️ Not found'}`);
const tabletViewportPassed = !tabletTest.hasHorizontalScroll;
console.log(` ${tabletViewportPassed ? '✅ PASS' : '❌ FAIL'} - Tablet viewport`);
testResults.push({ test: 'Tablet Viewport (768px)', passed: tabletViewportPassed });
await tabletPage.close();
// ========================================================================
// TEST 4: Responsive breakpoints
// ========================================================================
console.log("\n4️⃣ Testing Responsive Breakpoints...");
const breakpoints = [
{ name: 'Small Mobile', width: 320 },
{ name: 'Mobile', width: 375 },
{ name: 'Large Mobile', width: 414 },
{ name: 'Small Tablet', width: 600 },
{ name: 'Tablet', width: 768 },
{ name: 'Large Tablet', width: 1024 },
{ name: 'Desktop', width: 1920 }
];
const breakpointResults = [];
for (const bp of breakpoints) {
await mobilePage.setViewportSize({ width: bp.width, height: 800 });
await mobilePage.waitForTimeout(200);
const result = await mobilePage.evaluate(() => {
return {
hasHorizontalScroll: document.documentElement.scrollWidth > window.innerWidth,
bodyWidth: document.body.offsetWidth
};
});
const passed = !result.hasHorizontalScroll;
breakpointResults.push({ name: bp.name, width: bp.width, passed });
console.log(` ${bp.name} (${bp.width}px): ${passed ? '✅' : '❌'}`);
}
const allBreakpointsPassed = breakpointResults.every(r => r.passed);
console.log(` ${allBreakpointsPassed ? '✅ PASS' : '❌ FAIL'} - All breakpoints`);
testResults.push({ test: 'Responsive Breakpoints', passed: allBreakpointsPassed });
// ========================================================================
// TEST 5: Mobile-specific features
// ========================================================================
console.log("\n5️⃣ Testing Mobile-Specific Features...");
await mobilePage.setViewportSize(VIEWPORTS.mobile);
await mobilePage.waitForTimeout(500);
const mobileFeatures = await mobilePage.evaluate(() => {
// Check viewport meta tag
const viewportMeta = document.querySelector('meta[name="viewport"]');
const hasViewportMeta = !!viewportMeta;
const viewportContent = viewportMeta?.getAttribute('content') || '';
// Check for touch-friendly button sizes (minimum 44x44px)
const buttons = Array.from(document.querySelectorAll('button, a[role="button"], .btn, input[type="checkbox"]'));
const buttonSizes = buttons.map(btn => {
const rect = btn.getBoundingClientRect();
return { width: rect.width, height: rect.height };
});
const tooSmallButtons = buttonSizes.filter(s => s.width < 44 || s.height < 44).length;
// Check for text overflow
const hasTextOverflow = Array.from(document.querySelectorAll('*')).some(el => {
return el.scrollWidth > el.clientWidth && window.getComputedStyle(el).overflow === 'visible';
});
return {
hasViewportMeta,
viewportContent,
totalButtons: buttons.length,
tooSmallButtons,
hasTextOverflow
};
});
console.log(` Viewport meta tag: ${mobileFeatures.hasViewportMeta ? '✅' : '❌'}`);
console.log(` Content: "${mobileFeatures.viewportContent}"`);
console.log(` Touch-friendly buttons: ${mobileFeatures.totalButtons - mobileFeatures.tooSmallButtons}/${mobileFeatures.totalButtons}`);
console.log(` Too small (<44px): ${mobileFeatures.tooSmallButtons} ${mobileFeatures.tooSmallButtons === 0 ? '✅' : '⚠️'}`);
console.log(` Text overflow: ${mobileFeatures.hasTextOverflow ? '⚠️ YES' : '✅ NO'}`);
const mobileFeaturesPass = mobileFeatures.hasViewportMeta && !mobileFeatures.hasTextOverflow;
console.log(` ${mobileFeaturesPass ? '✅ PASS' : '❌ FAIL'} - Mobile features`);
testResults.push({ test: 'Mobile Features', passed: mobileFeaturesPass });
await mobilePage.close();
// ========================================================================
// 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("🎉 MOBILE RESPONSIVE 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 testMobileResponsive();
-105
View File
@@ -1,105 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 1200 });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
console.log('🎬 Starting comprehensive feature test...\n');
await page.goto('http://localhost:1999/?lang=en');
await page.waitForLoadState('networkidle');
console.log('✅ Step 1: Initial URL check');
let url = page.url();
console.log(` URL: ${url}`);
console.log(` Clean: ${!url.includes('#') ? '✓' : '✗'}\n`);
console.log('🔄 Step 2: Testing language switch (atomic OOB swaps)');
await page.click('button[aria-label="Español"]');
await page.waitForTimeout(1000);
url = page.url();
const contentES = await page.locator('.sidebar-accordion-header span').first().textContent();
console.log(` URL: ${url}`);
console.log(` Content: "${contentES}"`);
console.log(` Success: ${contentES === 'Competencias Técnicas' && url.includes('lang=es') ? '✓' : '✗'}\n`);
console.log('🎨 Step 3: Testing theme toggle (atomic OOB swaps)');
await page.click('#themeToggle');
await page.waitForTimeout(800);
const hasCleanTheme = await page.evaluate(() => document.body.classList.contains('theme-clean'));
url = page.url();
console.log(` Theme: ${hasCleanTheme ? 'clean' : 'default'}`);
console.log(` URL still clean: ${!url.includes('#') ? '✓' : '✗'}\n`);
console.log('📏 Step 4: Testing length toggle (atomic OOB swaps)');
await page.click('#lengthToggle');
await page.waitForTimeout(800);
const isLong = await page.locator('.cv-paper').evaluate(el => el.classList.contains('cv-long'));
url = page.url();
console.log(` Length: ${isLong ? 'long' : 'short'}`);
console.log(` URL still clean: ${!url.includes('#') ? '✓' : '✗'}\n`);
console.log('🖼️ Step 5: Testing logo toggle (atomic OOB swaps)');
await page.click('#logoToggle');
await page.waitForTimeout(800);
const showIcons = await page.locator('.cv-paper').evaluate(el => el.classList.contains('show-icons'));
url = page.url();
console.log(` Icons: ${showIcons ? 'visible' : 'hidden'}`);
console.log(` URL still clean: ${!url.includes('#') ? '✓' : '✗'}\n`);
console.log('⬆️ Step 6: Testing back-to-top (URL cleanliness)');
await page.evaluate(() => window.scrollTo(0, 500));
await page.waitForTimeout(500);
const backToTopBtn = await page.locator('.back-to-top').isVisible();
console.log(` Back-to-top visible after scroll: ${backToTopBtn ? '✓' : '✗'}`);
await page.click('.back-to-top');
await page.waitForTimeout(1000);
url = page.url();
const scrollPos = await page.evaluate(() => window.pageYOffset);
console.log(` URL after click: ${url}`);
console.log(` No #top anchor: ${!url.includes('#top') ? '✓' : '✗'}`);
console.log(` Scrolled to top: ${scrollPos < 50 ? '✓' : '✗'}\n`);
console.log('🔄 Step 7: Switch back to English (verify everything persists)');
await page.click('button[aria-label="English"]');
await page.waitForTimeout(1000);
url = page.url();
const contentEN = await page.locator('.sidebar-accordion-header span').first().textContent();
const stillClean = await page.evaluate(() => document.body.classList.contains('theme-clean'));
const stillLong = await page.locator('.cv-paper').evaluate(el => el.classList.contains('cv-long'));
const stillShowIcons = await page.locator('.cv-paper').evaluate(el => el.classList.contains('show-icons'));
console.log(` Language: ${contentEN === 'Technical Skills' ? 'English ✓' : 'Failed ✗'}`);
console.log(` Theme persisted: ${stillClean ? 'clean ✓' : 'default ✗'}`);
console.log(` Length persisted: ${stillLong ? 'long ✓' : 'short ✗'}`);
console.log(` Icons persisted: ${stillShowIcons ? 'visible ✓' : 'hidden ✗'}`);
console.log(` URL: ${url}`);
console.log(` Clean URL: ${!url.includes('#') ? '✓' : '✗'}\n`);
const allPassed =
contentES === 'Competencias Técnicas' &&
contentEN === 'Technical Skills' &&
hasCleanTheme && stillClean &&
isLong && stillLong &&
showIcons && stillShowIcons &&
!url.includes('#');
console.log(`\n${allPassed ? '✅ ALL FEATURES WORKING PERFECTLY!' : '❌ SOME TESTS FAILED'}`);
console.log('\n📊 IMPLEMENTATION SUMMARY:');
console.log(' ✅ Language switching - Atomic OOB swaps');
console.log(' ✅ Theme toggle - Atomic OOB swaps');
console.log(' ✅ Length toggle - Atomic OOB swaps');
console.log(' ✅ Logo toggle - Atomic OOB swaps');
console.log(' ✅ URL cleanliness - No anchor pollution');
console.log(' ✅ State persistence - All preferences maintained');
console.log(' ✅ Smooth scrolling - Hyperscript powered');
console.log(' ✅ Minimal payloads - <5KB per toggle');
console.log(' ✅ Zero JavaScript bloat - Pure HTMX + Hyperscript!');
await page.waitForTimeout(3000);
await browser.close();
})();
-116
View File
@@ -1,116 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 500 });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
// Listen for console errors
page.on('console', msg => {
const text = msg.text();
const type = msg.type();
if (type === 'error' || text.toLowerCase().includes('error')) {
console.log('❌ CONSOLE ERROR:', text);
}
});
page.on('pageerror', error => console.log('❌ PAGE EXCEPTION:', error.message));
console.log('📄 Loading page...');
await page.goto('http://localhost:1999/?lang=en');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Scroll down to test scroll preservation
console.log('\n📜 Scrolling down 500px...');
await page.evaluate(() => window.scrollTo(0, 500));
await page.waitForTimeout(500);
const scrollBefore = await page.evaluate(() => window.pageYOffset);
console.log(` Current scroll position: ${scrollBefore}px`);
// Test Theme Toggle (pure hyperscript - should not affect scroll)
console.log('\n🎨 TEST 1: Theme toggle (hyperscript)...');
await page.locator('.selector-group').filter({ hasText: 'View' }).locator('label.icon-toggle').click();
await page.waitForTimeout(1000);
const scrollAfterTheme = await page.evaluate(() => window.pageYOffset);
console.log(` Scroll after theme toggle: ${scrollAfterTheme}px`);
if (scrollAfterTheme === scrollBefore) {
console.log(' ✅ Scroll preserved!');
} else {
console.log(` ❌ Scroll changed! (${scrollBefore}px → ${scrollAfterTheme}px)`);
}
// Test Length Toggle (HTMX - should preserve scroll)
console.log('\n📄 TEST 2: Length toggle (HTMX)...');
const scrollBeforeLength = await page.evaluate(() => window.pageYOffset);
await page.locator('.selector-group').filter({ hasText: 'Length' }).locator('label.icon-toggle').click();
await page.waitForTimeout(2000); // Wait for HTMX swap
const scrollAfterLength = await page.evaluate(() => window.pageYOffset);
console.log(` Scroll before: ${scrollBeforeLength}px, after: ${scrollAfterLength}px`);
if (scrollAfterLength === scrollBeforeLength) {
console.log(' ✅ Scroll preserved!');
} else {
console.log(` ❌ Scroll changed! (${scrollBeforeLength}px → ${scrollAfterLength}px)`);
}
// Test Logo Toggle (HTMX - should preserve scroll)
console.log('\n🖼️ TEST 3: Logo toggle (HTMX)...');
const scrollBeforeLogo = await page.evaluate(() => window.pageYOffset);
await page.locator('.selector-group').filter({ hasText: 'Icons' }).locator('label.icon-toggle').click();
await page.waitForTimeout(2000); // Wait for HTMX swap
const scrollAfterLogo = await page.evaluate(() => window.pageYOffset);
console.log(` Scroll before: ${scrollBeforeLogo}px, after: ${scrollAfterLogo}px`);
if (scrollAfterLogo === scrollBeforeLogo) {
console.log(' ✅ Scroll preserved!');
} else {
console.log(` ❌ Scroll changed! (${scrollBeforeLogo}px → ${scrollAfterLogo}px)`);
}
// Test Toggle Sync on Mobile
console.log('\n📱 TEST 4: Toggle sync on mobile...');
await page.setViewportSize({ width: 600, height: 800 });
await page.waitForTimeout(500);
console.log(' 🍔 Opening hamburger menu...');
await page.click('.hamburger-btn');
await page.waitForTimeout(500);
// Check if toggles are synced
const desktopTheme = await page.locator('#themeToggle').isChecked();
const mobileTheme = await page.locator('#themeToggleMenu').isChecked();
console.log(` Desktop theme: ${desktopTheme}, Mobile theme: ${mobileTheme}`);
const desktopLength = await page.locator('#lengthToggle').isChecked();
const mobileLength = await page.locator('#lengthToggleMenu').isChecked();
console.log(` Desktop length: ${desktopLength}, Mobile length: ${mobileLength}`);
const desktopLogo = await page.locator('#logoToggle').isChecked();
const mobileLogo = await page.locator('#logoToggleMenu').isChecked();
console.log(` Desktop logo: ${desktopLogo}, Mobile logo: ${mobileLogo}`);
if (desktopTheme === mobileTheme && desktopLength === mobileLength && desktopLogo === mobileLogo) {
console.log(' ✅ All toggles are SYNCED!');
} else {
console.log(' ❌ Toggles are OUT OF SYNC!');
}
// Test mobile logo toggle to verify sync works
console.log('\n📱 TEST 5: Toggle logo from mobile menu...');
await page.locator('#logoToggleMenu').click();
await page.waitForTimeout(2000);
const desktopLogoAfter = await page.locator('#logoToggle').isChecked();
const mobileLogoAfter = await page.locator('#logoToggleMenu').isChecked();
console.log(` Desktop logo after: ${desktopLogoAfter}, Mobile logo after: ${mobileLogoAfter}`);
if (desktopLogoAfter === mobileLogoAfter) {
console.log(' ✅ Logo toggle synced correctly!');
} else {
console.log(' ❌ Logo toggle NOT synced!');
}
console.log('\n✅ All tests complete - Check results above');
await page.waitForTimeout(1000);
await browser.close();
})();
-61
View File
@@ -1,61 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 1000 });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
console.log('📄 Loading English page...\n');
await page.goto('http://localhost:1999/?lang=en');
await page.waitForLoadState('networkidle');
console.log('✅ Initial state check:');
const enActive1 = await page.locator('button[aria-label="English"]').evaluate(el => el.classList.contains('active'));
const esActive1 = await page.locator('button[aria-label="Español"]').evaluate(el => el.classList.contains('active'));
const content1 = await page.locator('.sidebar-accordion-header span').first().textContent();
console.log(` EN button active: ${enActive1} (expected: true)`);
console.log(` ES button active: ${esActive1} (expected: false)`);
console.log(` Content language: "${content1}" (expected: "Technical Skills")\n`);
console.log('🌍 Clicking Spanish button...');
await page.click('button[aria-label="Español"]');
await page.waitForTimeout(1500); // Wait for 200ms fade + extra time
console.log('✅ After Spanish click:');
const enActive2 = await page.locator('button[aria-label="English"]').evaluate(el => el.classList.contains('active'));
const esActive2 = await page.locator('button[aria-label="Español"]').evaluate(el => el.classList.contains('active'));
const content2 = await page.locator('.sidebar-accordion-header span').first().textContent();
console.log(` EN button active: ${enActive2} (expected: false)`);
console.log(` ES button active: ${esActive2} (expected: true)`);
console.log(` Content language: "${content2}" (expected: "Competencias Técnicas")\n`);
console.log('🌍 Clicking English button...');
await page.click('button[aria-label="English"]');
await page.waitForTimeout(1500);
console.log('✅ After English click:');
const enActive3 = await page.locator('button[aria-label="English"]').evaluate(el => el.classList.contains('active'));
const esActive3 = await page.locator('button[aria-label="Español"]').evaluate(el => el.classList.contains('active'));
const content3 = await page.locator('.sidebar-accordion-header span').first().textContent();
console.log(` EN button active: ${enActive3} (expected: true)`);
console.log(` ES button active: ${esActive3} (expected: false)`);
console.log(` Content language: "${content3}" (expected: "Technical Skills")\n`);
const buttonsCorrect = enActive1 && !esActive1 && !enActive2 && esActive2 && enActive3 && !esActive3;
const contentCorrect = content1 === 'Technical Skills' && content2 === 'Competencias Técnicas' && content3 === 'Technical Skills';
console.log(`\n${buttonsCorrect && contentCorrect ? '✅ ALL TESTS PASSED!' : '❌ SOME TESTS FAILED'}`);
console.log('\n📊 ATOMIC UPDATE IMPLEMENTATION:');
console.log(' ✅ Single endpoint: /switch-language?lang=XX');
console.log(' ✅ Out-of-band swaps: 3 components updated atomically');
console.log(' ✅ Language buttons swap with active state');
console.log(' ✅ Page 1 content swaps with 200ms fade');
console.log(' ✅ Page 2 content swaps with 200ms fade');
console.log(' ✅ URL updates to /?lang=XX');
console.log(' ✅ Zero custom JavaScript - pure HTMX!');
await page.waitForTimeout(2000);
await browser.close();
})();
-216
View File
@@ -1,216 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bottom Buttons Test</title>
<style>
body {
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
min-height: 300vh; /* Make page scrollable */
background: linear-gradient(to bottom, #f0f0f0 0%, #e0e0e0 100%);
}
.test-info {
position: fixed;
top: 20px;
left: 20px;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
z-index: 1000;
max-width: 400px;
}
.test-info h2 {
margin-top: 0;
color: #333;
}
.status {
margin: 10px 0;
padding: 10px;
border-radius: 4px;
font-family: monospace;
}
.status.at-bottom {
background: #27ae60;
color: white;
}
.status.not-bottom {
background: #e0e0e0;
color: #333;
}
/* Copy styles from main.css */
.back-to-top {
position: fixed;
bottom: 2rem;
right: 2rem;
width: 35px;
height: 35px;
background: #2a2a2a;
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 99;
transition: all 0.3s ease;
opacity: 0.2;
font-size: 20px;
}
.back-to-top:hover {
opacity: 1;
transform: translateY(-3px) scale(1.43);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
background: #3a3a3a;
}
.back-to-top.at-bottom {
opacity: 1;
background: #27ae60;
}
.info-button {
position: fixed;
bottom: 2rem;
left: 2rem;
width: 50px;
height: 50px;
background: #2a2a2a;
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 99;
transition: all 0.3s ease;
opacity: 0.2;
font-size: 24px;
}
.info-button:hover {
opacity: 1;
transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
background: #3a3a3a;
}
.info-button.at-bottom {
opacity: 1;
background: #27ae60;
}
.content-marker {
padding: 20px;
margin: 50px 0;
background: white;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<div class="test-info">
<h2>🧪 Bottom Buttons Test</h2>
<p>Scroll to the bottom to see the buttons turn green!</p>
<div id="status" class="status not-bottom">
Not at bottom
</div>
<div style="margin-top: 15px;">
<strong>Current Scroll:</strong> <span id="scroll-pos">0</span>px<br>
<strong>Distance from Bottom:</strong> <span id="distance">0</span>px
</div>
</div>
<div class="content-marker">
<h1>Top of Page</h1>
<p>Start scrolling down to test the bottom detection...</p>
</div>
<div class="content-marker" style="margin-top: 100vh;">
<h2>Middle Section</h2>
<p>Keep scrolling...</p>
</div>
<div class="content-marker" style="margin-top: 100vh;">
<h2>Almost There...</h2>
<p>Just a bit more...</p>
</div>
<div class="content-marker" style="margin-top: 50vh; margin-bottom: 100px;">
<h2>🎯 Bottom of Page</h2>
<p>You should see the buttons turn green now!</p>
<p><strong>The info button (️ bottom-left) and back-to-top button (↑ bottom-right) should both be GREEN when you're at the bottom.</strong></p>
</div>
<!-- Test buttons -->
<button class="info-button" onclick="alert('Info button clicked!')"></button>
<button class="back-to-top" onclick="window.scrollTo({top: 0, behavior: 'smooth'})"></button>
<script>
// Test implementation matching the actual code
function updateBottomStatus() {
const currentScroll = window.pageYOffset || document.documentElement.scrollTop;
const scrollHeight = document.documentElement.scrollHeight;
const clientHeight = document.documentElement.clientHeight;
const distanceFromBottom = scrollHeight - currentScroll - clientHeight;
const isAtBottom = distanceFromBottom < 50;
// Update test info
const statusDiv = document.getElementById('status');
const scrollPos = document.getElementById('scroll-pos');
const distance = document.getElementById('distance');
scrollPos.textContent = Math.round(currentScroll);
distance.textContent = Math.round(distanceFromBottom);
if (isAtBottom) {
statusDiv.textContent = '✅ AT BOTTOM - Buttons should be GREEN!';
statusDiv.className = 'status at-bottom';
} else {
statusDiv.textContent = '❌ Not at bottom yet';
statusDiv.className = 'status not-bottom';
}
// Update button states
const backToTopBtn = document.querySelector('.back-to-top');
const infoBtn = document.querySelector('.info-button');
if (isAtBottom) {
backToTopBtn.classList.add('at-bottom');
infoBtn.classList.add('at-bottom');
} else {
backToTopBtn.classList.remove('at-bottom');
infoBtn.classList.remove('at-bottom');
}
// Show back-to-top button when scrolled down
if (currentScroll > 300) {
backToTopBtn.style.display = 'flex';
} else {
backToTopBtn.style.display = 'none';
}
}
// Update on scroll
window.addEventListener('scroll', updateBottomStatus);
// Initial update
updateBottomStatus();
</script>
</body>
</html>
-94
View File
@@ -1,94 +0,0 @@
#!/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
@@ -1,18 +0,0 @@
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();
})();
-92
View File
@@ -1,92 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 500 });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
console.log('🧪 Testing Double Toggle Bug Fix (Using Hamburger Menu)\n');
// Listen for HTMX errors - this is the critical test
let hasError = false;
let errorMessages = [];
page.on('console', msg => {
const text = msg.text();
if (msg.type() === 'error' || text.includes('htmx:swapError') || text.includes('insertBefore')) {
console.log(`❌ Browser error: ${text}`);
hasError = true;
errorMessages.push(text);
}
});
await page.goto('http://localhost:1999/?lang=en');
await page.waitForLoadState('networkidle');
console.log('📜 Scrolling slightly to reveal hamburger menu...');
await page.evaluate(() => window.scrollTo(0, 300));
await page.waitForTimeout(1000);
// Hover over hamburger to open menu
console.log('🍔 Opening hamburger menu...');
await page.hover('.hamburger-btn');
await page.waitForTimeout(1000);
// Wait for menu toggle to be visible
await page.waitForSelector('#lengthToggleMenu', { state: 'visible', timeout: 5000 });
console.log('\n🔄 Testing Double Toggle (This should NOT error):\n');
// First toggle
console.log(' 1️⃣ First toggle...');
await page.click('#lengthToggleMenu');
await page.waitForTimeout(800);
// Check state after first toggle
const isCheckedAfter1 = await page.locator('#lengthToggleMenu').isChecked();
console.log(` ✅ After first toggle: ${isCheckedAfter1 ? 'Long' : 'Short'}`);
// Second toggle (THIS IS WHERE THE BUG OCCURRED)
console.log(' 2️⃣ Second toggle (CRITICAL - this caused the error before)...');
await page.click('#lengthToggleMenu');
await page.waitForTimeout(800);
// Check state after second toggle
const isCheckedAfter2 = await page.locator('#lengthToggleMenu').isChecked();
console.log(` ✅ After second toggle: ${isCheckedAfter2 ? 'Long' : 'Short'}`);
// Third toggle to be thorough
console.log(' 3️⃣ Third toggle (extra verification)...');
await page.click('#lengthToggleMenu');
await page.waitForTimeout(800);
const isCheckedAfter3 = await page.locator('#lengthToggleMenu').isChecked();
console.log(` ✅ After third toggle: ${isCheckedAfter3 ? 'Long' : 'Short'}`);
// Fourth toggle - be really sure
console.log(' 4️⃣ Fourth toggle (thorough test)...');
await page.click('#lengthToggleMenu');
await page.waitForTimeout(800);
const isCheckedAfter4 = await page.locator('#lengthToggleMenu').isChecked();
console.log(` ✅ After fourth toggle: ${isCheckedAfter4 ? 'Long' : 'Short'}`);
console.log('\n📊 TEST RESULTS:');
if (hasError) {
console.log('❌ FAILED: HTMX errors detected!');
console.log('❌ Error messages:');
errorMessages.forEach(msg => console.log(` - ${msg}`));
console.log('\n⚠️ The bug is NOT fixed!');
} else {
console.log('✅ SUCCESS: No htmx:swapError detected!');
console.log('✅ No insertBefore errors!');
console.log('✅ Toggle survived 4 consecutive clicks');
console.log('✅ DOM element preserved (not destroyed/recreated)');
console.log('✅ Smooth CSS transitions maintained');
console.log('\n🎉 THE BUG IS FIXED!');
}
await page.waitForTimeout(2000);
await browser.close();
})();
-81
View File
@@ -1,81 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 400 });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
console.log('🎬 Final Toggle Test - HTMX Out-of-Band Swaps\n');
// Monitor network requests
page.on('response', async (response) => {
if (response.url().includes('/toggle/')) {
console.log(` 📡 Server response from: ${response.url()}`);
const text = await response.text();
const hasOOB = text.includes('hx-swap-oob="true"');
console.log(` ${hasOOB ? '✅' : '❌'} Out-of-band swap: ${hasOOB}`);
}
});
await page.goto('http://localhost:1999/?lang=en');
await page.waitForLoadState('networkidle');
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(500);
// TEST 1: Desktop toggle
console.log('TEST 1: Click Desktop Toggle\n');
const desktopLabel = page.locator('#desktop-length-toggle .icon-toggle');
await desktopLabel.click();
await page.waitForTimeout(1500);
let desktopChecked = await page.locator('#lengthToggle').isChecked();
let mobileChecked = await page.locator('#lengthToggleMenu').isChecked();
console.log(`\n Desktop: ${desktopChecked}, Mobile: ${mobileChecked}`);
console.log(` Sync: ${desktopChecked === mobileChecked ? '✅ YES' : '❌ NO'}\n`);
// TEST 2: Mobile toggle - but DON'T close menu
console.log('TEST 2: Open Menu & Click Mobile Toggle (keep menu open)\n');
const hamburger = page.locator('.hamburger-btn');
await hamburger.click();
await page.waitForTimeout(800);
console.log(' Menu is now open, clicking mobile toggle...');
const mobileLabel = page.locator('#mobile-length-toggle .icon-toggle');
await mobileLabel.click();
// Wait for HTMX to complete
await page.waitForTimeout(2000);
// Check states AFTER the swap completes
desktopChecked = await page.locator('#lengthToggle').isChecked();
mobileChecked = await page.locator('#lengthToggleMenu').isChecked();
console.log(`\n Desktop: ${desktopChecked}, Mobile: ${mobileChecked}`);
console.log(` Sync: ${desktopChecked === mobileChecked ? '✅ YES' : '❌ NO'}\n`);
// TEST 3: Check if elements exist
const desktopExists = await page.locator('#desktop-length-toggle').count();
const mobileExists = await page.locator('#mobile-length-toggle').count();
console.log('📊 ELEMENT CHECK:');
console.log(` Desktop toggle exists: ${desktopExists > 0 ? '✅' : '❌'}`);
console.log(` Mobile toggle exists: ${mobileExists > 0 ? '✅' : '❌'}`);
console.log('\n🔍 DIAGNOSIS:');
if (desktopChecked === mobileChecked) {
console.log(' ✅ PERFECT! Both toggles are in sync!');
console.log(' ✅ HTMX out-of-band swaps working correctly!');
console.log(' ✅ Desktop/mobile sync via server response!');
} else {
console.log(' ❌ Out of sync - possible issues:');
console.log(' 1. OOB swap timing problem');
console.log(' 2. Element not found during swap');
console.log(' 3. Hyperscript executing before swap');
}
await page.waitForTimeout(3000);
await browser.close();
})();
-188
View File
@@ -1,188 +0,0 @@
#!/bin/bash
# Test HTMX Atomic Updates Implementation
# Tests URL cleanliness, toggles, and language switching
set -e
BASE_URL="http://localhost:1999"
COOKIES="/tmp/test-cookies.txt"
RESULTS="/tmp/test-results.txt"
echo "🧪 Testing HTMX Atomic Updates Implementation"
echo "=============================================="
echo ""
# Clean up previous test data
rm -f $COOKIES $RESULTS
# Test 1: Server Health
echo "✓ Test 1: Server Health Check"
HEALTH=$(curl -s "$BASE_URL/health" | jq -r .status)
if [ "$HEALTH" = "ok" ]; then
echo " ✓ Server is running"
else
echo " ✗ Server health check failed"
exit 1
fi
echo ""
# Test 2: Theme Toggle
echo "✓ Test 2: Theme Toggle (Atomic Out-of-Band Swaps)"
THEME_RESPONSE=$(curl -s -X POST -c $COOKIES "$BASE_URL/toggle/theme?lang=en")
OOB_COUNT=$(echo "$THEME_RESPONSE" | grep -c "hx-swap-oob" || true)
if [ "$OOB_COUNT" -eq 1 ]; then
echo " ✓ Theme toggle returns 1 out-of-band swap (mobile toggle)"
else
echo " ✗ Expected 1 OOB swap, got $OOB_COUNT"
fi
if echo "$THEME_RESPONSE" | grep -q "desktop-theme-toggle"; then
echo " ✓ Desktop toggle present"
else
echo " ✗ Desktop toggle missing"
fi
if echo "$THEME_RESPONSE" | grep -q "mobile-theme-toggle"; then
echo " ✓ Mobile toggle present"
else
echo " ✗ Mobile toggle missing"
fi
if grep -q "cv-theme" $COOKIES; then
THEME_VALUE=$(grep "cv-theme" $COOKIES | awk '{print $7}')
echo " ✓ Theme cookie set: $THEME_VALUE"
else
echo " ✗ Theme cookie not set"
fi
echo ""
# Test 3: Length Toggle
echo "✓ Test 3: Length Toggle (Atomic Out-of-Band Swaps)"
LENGTH_RESPONSE=$(curl -s -X POST -c $COOKIES "$BASE_URL/toggle/length?lang=en")
OOB_COUNT=$(echo "$LENGTH_RESPONSE" | grep -c "hx-swap-oob" || true)
if [ "$OOB_COUNT" -eq 1 ]; then
echo " ✓ Length toggle returns 1 out-of-band swap"
else
echo " ✗ Expected 1 OOB swap, got $OOB_COUNT"
fi
if echo "$LENGTH_RESPONSE" | grep -q "desktop-length-toggle"; then
echo " ✓ Desktop toggle present"
else
echo " ✗ Desktop toggle missing"
fi
echo ""
# Test 4: Logo Toggle
echo "✓ Test 4: Logo Toggle (Atomic Out-of-Band Swaps)"
LOGO_RESPONSE=$(curl -s -X POST -c $COOKIES "$BASE_URL/toggle/logos?lang=en")
OOB_COUNT=$(echo "$LOGO_RESPONSE" | grep -c "hx-swap-oob" || true)
if [ "$OOB_COUNT" -eq 1 ]; then
echo " ✓ Logo toggle returns 1 out-of-band swap"
else
echo " ✗ Expected 1 OOB swap, got $OOB_COUNT"
fi
echo ""
# Test 5: Language Switch
echo "✓ Test 5: Language Switching (Out-of-Band Swaps)"
LANG_RESPONSE=$(curl -s "$BASE_URL/switch-language?lang=es")
OOB_COUNT=$(echo "$LANG_RESPONSE" | grep -c "hx-swap-oob" || true)
if [ "$OOB_COUNT" -eq 2 ]; then
echo " ✓ Language switch returns 2 out-of-band swaps (page 1 + page 2)"
else
echo " ✗ Expected 2 OOB swaps, got $OOB_COUNT"
fi
if echo "$LANG_RESPONSE" | grep -q "Competencias Técnicas"; then
echo " ✓ Spanish content present"
else
echo " ✗ Spanish content missing"
fi
echo ""
# Test 6: URL Cleanliness (check homepage doesn't have anchors)
echo "✓ Test 6: URL Cleanliness (No Anchor Pollution)"
HOME_PAGE=$(curl -s "$BASE_URL/?lang=en")
if echo "$HOME_PAGE" | grep -q 'scrollIntoView'; then
echo " ✓ Hyperscript smooth scroll implemented"
else
echo " ✗ Hyperscript smooth scroll missing"
fi
if echo "$HOME_PAGE" | grep -q 'id="back-to-top"'; then
echo " ✓ Back-to-top button present"
else
echo " ✗ Back-to-top button missing"
fi
# Check that back-to-top uses hyperscript, not href="#top"
if echo "$HOME_PAGE" | grep -q 'window.scrollTo({top: 0, behavior'; then
echo " ✓ Back-to-top uses hyperscript (no URL pollution)"
else
echo " ✗ Back-to-top might use anchors"
fi
echo ""
# Test 7: Cookie Persistence
echo "✓ Test 7: Cookie Persistence Across Requests"
if grep -q "cv-theme" $COOKIES && grep -q "cv-length" $COOKIES && grep -q "cv-logos" $COOKIES; then
echo " ✓ All preference cookies persisted:"
grep "cv-" $COOKIES | awk '{print " - " $6 ": " $7}'
else
echo " ✗ Some cookies missing"
fi
echo ""
# Test 8: Hyperscript Integration
echo "✓ Test 8: Hyperscript + HTMX Integration"
if echo "$THEME_RESPONSE" | grep -q 'on htmx:afterRequest'; then
echo " ✓ Hyperscript event handlers present"
else
echo " ✗ Hyperscript event handlers missing"
fi
if echo "$THEME_RESPONSE" | grep -q 'localStorage'; then
echo " ✓ LocalStorage integration present"
else
echo " ✗ LocalStorage integration missing"
fi
echo ""
# Test 9: Response Size (toggles should be small)
echo "✓ Test 9: Response Payload Size (Should be minimal)"
THEME_SIZE=$(echo "$THEME_RESPONSE" | wc -c)
LENGTH_SIZE=$(echo "$LENGTH_RESPONSE" | wc -c)
LOGO_SIZE=$(echo "$LOGO_RESPONSE" | wc -c)
echo " - Theme toggle: $THEME_SIZE bytes"
echo " - Length toggle: $LENGTH_SIZE bytes"
echo " - Logo toggle: $LOGO_SIZE bytes"
if [ "$THEME_SIZE" -lt 5000 ]; then
echo " ✓ Toggle payloads are minimal (<5KB)"
else
echo " ✗ Toggle payloads are too large"
fi
echo ""
# Summary
echo "=============================================="
echo "✅ All HTMX Atomic Updates Tests Passed!"
echo ""
echo "Summary:"
echo "- ✅ Theme toggle: Atomic OOB swaps working"
echo "- ✅ Length toggle: Atomic OOB swaps working"
echo "- ✅ Logo toggle: Atomic OOB swaps working"
echo "- ✅ Language switch: Atomic OOB swaps working"
echo "- ✅ URL cleanliness: No anchor pollution"
echo "- ✅ Cookie persistence: All cookies saved"
echo "- ✅ Hyperscript integration: Working"
echo "- ✅ Minimal payloads: <5KB per toggle"
echo ""
# Clean up
rm -f $COOKIES $RESULTS
echo "🎉 Testing complete!"
-37
View File
@@ -1,37 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 800 });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
// Capture console logs
page.on('console', msg => console.log('BROWSER:', msg.text()));
// Capture network requests
page.on('response', response => {
if (response.url().includes('switch-language')) {
console.log(`\n📡 NETWORK: ${response.url()}`);
console.log(` Status: ${response.status()}`);
}
});
console.log('📄 Loading page...\n');
await page.goto('http://localhost:1999/?lang=en');
await page.waitForLoadState('networkidle');
console.log('\n🌍 Clicking Spanish button...\n');
await page.click('button[aria-label="Español"]');
await page.waitForTimeout(2000);
// Check what actually happened
const htmlAfter = await page.content();
console.log('\n📊 Checking results:');
console.log(` Spanish button has 'active': ${htmlAfter.includes('selector-btn active')}`);
console.log(` Content has Spanish text: ${htmlAfter.includes('Competencias Técnicas')}`);
await page.waitForTimeout(2000);
await browser.close();
})();
-176
View File
@@ -1,176 +0,0 @@
<!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>
-73
View File
@@ -1,73 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 800 });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
console.log('🔍 HTMX Timing & State Analysis\n');
// Intercept and log ALL console messages
page.on('console', msg => {
const text = msg.text();
if (text.includes('HTMX') || text.includes('swap') || text.includes('Toggle')) {
console.log(` 🔧 ${text}`);
}
});
// Add detailed event logging to the page
await page.goto('http://localhost:1999/?lang=en');
await page.waitForLoadState('networkidle');
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(500);
// Inject logging into the page
await page.evaluate(() => {
const desktop = document.querySelector('#lengthToggle');
const mobile = document.querySelector('#lengthToggleMenu');
// Log all HTMX events
document.body.addEventListener('htmx:beforeRequest', (e) => {
const target = e.detail.elt;
const id = target.id;
const checked = target.checked;
console.log(`HTMX beforeRequest: #${id} checked=${checked}`);
});
document.body.addEventListener('htmx:afterSwap', (e) => {
console.log(`HTMX afterSwap completed`);
console.log(` Desktop: #lengthToggle checked=${document.querySelector('#lengthToggle')?.checked}`);
console.log(` Mobile: #lengthToggleMenu checked=${document.querySelector('#lengthToggleMenu')?.checked}`);
});
document.body.addEventListener('htmx:oobAfterSwap', (e) => {
console.log(`HTMX oobAfterSwap: ${e.detail.target?.id}`);
});
});
// TEST SEQUENCE
console.log('▶ Step 1: Click Desktop Toggle\n');
await page.locator('#desktop-length-toggle .icon-toggle').click();
await page.waitForTimeout(2000);
let desktopState = await page.evaluate(() => document.querySelector('#lengthToggle').checked);
let mobileState = await page.evaluate(() => document.querySelector('#lengthToggleMenu').checked);
console.log(`\nResult: Desktop=${desktopState}, Mobile=${mobileState}, Sync=${desktopState === mobileState ? '✅' : '❌'}\n`);
console.log('▶ Step 2: Open Menu\n');
await page.locator('.hamburger-btn').click();
await page.waitForTimeout(1000);
console.log('▶ Step 3: Click Mobile Toggle\n');
await page.locator('#mobile-length-toggle .icon-toggle').click();
await page.waitForTimeout(2500);
desktopState = await page.evaluate(() => document.querySelector('#lengthToggle').checked);
mobileState = await page.evaluate(() => document.querySelector('#lengthToggleMenu').checked);
console.log(`\nResult: Desktop=${desktopState}, Mobile=${mobileState}, Sync=${desktopState === mobileState ? '✅' : '❌'}\n`);
await page.waitForTimeout(2000);
await browser.close();
})();
-229
View File
@@ -1,229 +0,0 @@
<!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>
-225
View File
@@ -1,225 +0,0 @@
# Keyboard Shortcuts Feature - Test Results
## Test Date: 2025-11-15
## ✅ Implementation Verification
### Files Created
1.`/templates/partials/modals/shortcuts-modal.html` - Modal dialog with all shortcuts
2.`/templates/partials/widgets/shortcuts-button.html` - Fixed button widget
3.`/static/hyperscript/functions._hs` - Updated with `initKeyboardShortcuts()` function
4.`/static/css/main.css` - Added CSS for shortcuts button and modal
5.`/data/ui-en.json` - English translations
6.`/data/ui-es.json` - Spanish translations
7.`/internal/models/cv.go` - Go struct definitions for ShortcutsModal
### Integration Points
1.`templates/index.html` - Modal and button templates included
2.`templates/index.html` - Hyperscript initialization updated to call `initKeyboardShortcuts()`
3. ✅ No backend changes required (frontend-only feature)
## ✅ Functionality Tests
### Test 1: Button Visibility
- ✅ Button renders on page (id: `shortcuts-button`)
- ✅ Button positioned bottom-right (CSS: `position: fixed; bottom: 2rem; right: 2rem`)
- ✅ Keyboard icon visible (`mdi:keyboard-outline`)
- ✅ Proper ARIA labels present
- ✅ Opacity 0.2 by default, 1.0 on hover (matches info-button pattern)
### Test 2: Modal Structure
- ✅ Native `<dialog>` element used (id: `shortcuts-modal`)
- ✅ Opens on button click via `onclick="document.getElementById('shortcuts-modal').showModal()"`
- ✅ Closes on ESC key (native dialog behavior)
- ✅ Closes on backdrop click (hyperscript handler)
- ✅ Closes on X button click
### Test 3: Keyboard Shortcut `?`
-`initKeyboardShortcuts()` function defined in `functions._hs`
- ✅ Listens for `?` key press (Shift + /)
- ✅ Excludes modifier keys (Ctrl, Cmd, Alt)
- ✅ Prevents triggering in input/textarea fields
- ✅ Opens shortcuts modal when pressed
### Test 4: Shortcuts Content
**Total Shortcuts: 14**
#### Zoom Control (3 shortcuts)
- ✅ Ctrl/Cmd + Plus: Zoom in (+10%)
- ✅ Ctrl/Cmd + Minus: Zoom out (-10%)
- ✅ Ctrl/Cmd + 0: Reset zoom to 100%
#### View Controls (3 shortcuts)
- ✅ Tab to Length: Toggle CV length (Short/Long)
- ✅ Tab to Logos: Show/hide company logos
- ✅ Tab to View: Switch theme (Default/Clean)
#### Navigation (3 shortcuts)
- ✅ Menu → Expand All: Expand all CV sections
- ✅ Menu → Collapse All: Collapse all CV sections
- ✅ Click ↑ Button: Scroll back to top
#### Actions (3 shortcuts)
- ✅ Ctrl/Cmd + P: Print or save as PDF
- ✅ ESC: Close any open modal
- ✅ ?: Show this shortcuts help
#### Browser Defaults (2 shortcuts)
- ✅ Tab: Navigate between controls
- ✅ Enter/Space: Activate focused control
### Test 5: Bilingual Support
**English (lang=en)**
- ✅ Title: "Keyboard Shortcuts"
- ✅ Button aria-label: "Keyboard shortcuts"
- ✅ All section titles in English
- ✅ All descriptions in English
**Spanish (lang=es)**
- ✅ Title: "Atajos de Teclado"
- ✅ Button aria-label: "Atajos de teclado"
- ✅ All section titles in Spanish (e.g., "Control de Zoom")
- ✅ All descriptions in Spanish (e.g., "Aumentar zoom (+10%)")
### Test 6: Styling
- ✅ Modal uses existing `info-modal` class (consistency)
- ✅ Button uses `shortcuts-btn` class with matching style to `info-button`
- ✅ Keyboard keys styled as `<kbd>` elements with professional appearance
- ✅ Sections organized with icons (iconify-icon)
- ✅ Responsive design (mobile adjustments present)
- ✅ Hover effects working (blue highlight on hover)
### Test 7: Accessibility
- ✅ ARIA labels on button ("Keyboard shortcuts")
- ✅ Native `<dialog>` element (built-in focus trap)
- ✅ ESC key support (native)
- ✅ Semantic HTML (`<kbd>` for shortcuts, `<h3>` for sections)
- ✅ Keyboard navigation support (Tab through controls)
### Test 8: Performance
- ✅ No JavaScript bloat (uses native dialog, hyperscript)
- ✅ CSS loaded inline in main.css (no extra HTTP request)
- ✅ Templates automatically loaded by Go template engine
- ✅ No server-side processing needed (static content)
## ✅ Server Response Tests
### HTTP Responses
```bash
# Health check
curl http://localhost:1999/health
# Response: {"status":"ok","timestamp":"...","version":"1.1.0"} ✅
# English page
curl http://localhost:1999/?lang=en | grep "shortcuts-button"
# Found: <button id="shortcuts-button"...> ✅
# Spanish page
curl http://localhost:1999/?lang=es | grep "Atajos de Teclado"
# Found: <h2>Atajos de Teclado</h2> ✅
# CSS verification
curl http://localhost:1999/static/css/main.css | grep "shortcuts-btn"
# Found: .shortcuts-btn { position: fixed; ... } ✅
# Hyperscript verification
curl http://localhost:1999/static/hyperscript/functions._hs | grep "initKeyboardShortcuts"
# Found: def initKeyboardShortcuts() ... ✅
```
### Template Count
- ✅ Server reports: "✓ Loaded 27 partial templates"
- Previous: 25 templates
- New: +2 templates (shortcuts-modal.html, shortcuts-button.html)
## ✅ Code Quality
### Go Code
- ✅ Proper struct definitions with JSON tags
- ✅ Nested types for shortcuts sections
- ✅ Pointer fields with `omitempty` for flexibility
- ✅ Follows existing patterns (InfoModal structure)
### Templates
- ✅ Consistent with existing modal pattern (info-modal)
- ✅ Proper Go template syntax
- ✅ Bilingual support via `.UI.ShortcutsModal`
- ✅ Clean, readable HTML structure
### CSS
- ✅ Matches existing button patterns (info-button)
- ✅ Proper responsive breakpoints (768px)
- ✅ Hardware-accelerated properties (transform, opacity)
- ✅ Professional kbd styling with 3D effect
### Hyperscript
- ✅ Clean function definition
- ✅ Proper event filtering (excluding modifiers)
- ✅ Input/textarea exclusion (UX consideration)
- ✅ Follows existing patterns (initScrollBehavior)
## ✅ Project Philosophy Compliance
### Modern Web Techniques ✅
- ✅ Native `<dialog>` element (zero JavaScript for modal logic)
- ✅ Hyperscript for declarative behavior
- ✅ No JavaScript frameworks
- ✅ Progressive enhancement (works without JS for button click)
- ✅ CSS-first approach for animations
### HTMX Philosophy ✅
- ✅ Server-side rendering (no client-side JSON manipulation)
- ✅ Hypermedia-driven (templates from server)
- ✅ Minimal JavaScript (only keyboard listener)
### Accessibility ✅
- ✅ Semantic HTML
- ✅ ARIA labels
- ✅ Keyboard navigation
- ✅ Native focus management
## 🎯 Success Criteria Met
1. ✅ Keyboard shortcuts button visible near info icon
2. ✅ Modal displays comprehensive shortcuts list (14 items)
3. ✅ Native `<dialog>` pattern used (like info modal)
4. ✅ Shortcuts logically organized into 5 groups
5. ✅ Bilingual support working (ES/EN)
6. ✅ Zero JavaScript files required
7. ✅ Follows project philosophy and patterns
8.`?` keyboard shortcut opens modal
9. ✅ All shortcuts documented and accurate
## 📊 Summary
**Status**: ✅ **ALL TESTS PASSED**
- **Files Created**: 7
- **Files Modified**: 3
- **Total Shortcuts**: 14
- **Languages Supported**: 2 (English, Spanish)
- **Zero JavaScript Files**: ✅
- **Zero Backend Changes**: ✅
- **Zero Bundle Size Increase**: ✅ (inline CSS, native APIs)
**Implementation Time**: ~1 hour (orchestrated across 6 expert agents)
**Quality**: Production-ready
**Performance**: Excellent (no performance impact)
**Accessibility**: WCAG AA compliant
**Maintainability**: High (follows existing patterns)
## 🚀 Deployment Checklist
- ✅ Build successful (`go build`)
- ✅ Server starts without errors
- ✅ Templates load successfully (27 partials)
- ✅ English page renders correctly
- ✅ Spanish page renders correctly
- ✅ CSS loads correctly
- ✅ Hyperscript functions load correctly
- ✅ No console errors
- ✅ All 14 shortcuts present
- ✅ Bilingual translations complete
**Ready for production deployment**
-50
View File
@@ -1,50 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 800 });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
console.log('📄 Loading English page...\n');
await page.goto('http://localhost:1999/?lang=en');
await page.waitForLoadState('networkidle');
console.log('✅ Checking initial state:');
const enActive1 = await page.locator('button[aria-label="English"]').evaluate(el => el.classList.contains('active'));
const esActive1 = await page.locator('button[aria-label="Español"]').evaluate(el => el.classList.contains('active'));
console.log(` EN button active: ${enActive1} (expected: true)`);
console.log(` ES button active: ${esActive1} (expected: false)\n`);
console.log('🌍 Clicking Spanish button...');
await page.click('button[aria-label="Español"]');
await page.waitForTimeout(1000);
console.log('✅ Checking after Spanish click:');
const enActive2 = await page.locator('button[aria-label="English"]').evaluate(el => el.classList.contains('active'));
const esActive2 = await page.locator('button[aria-label="Español"]').evaluate(el => el.classList.contains('active'));
console.log(` EN button active: ${enActive2} (expected: false)`);
console.log(` ES button active: ${esActive2} (expected: true)\n`);
console.log('🌍 Clicking English button...');
await page.click('button[aria-label="English"]');
await page.waitForTimeout(1000);
console.log('✅ Checking after English click:');
const enActive3 = await page.locator('button[aria-label="English"]').evaluate(el => el.classList.contains('active'));
const esActive3 = await page.locator('button[aria-label="Español"]').evaluate(el => el.classList.contains('active'));
console.log(` EN button active: ${enActive3} (expected: true)`);
console.log(` ES button active: ${esActive3} (expected: false)\n`);
const allCorrect = enActive1 && !esActive1 && !enActive2 && esActive2 && enActive3 && !esActive3;
console.log(`\n${allCorrect ? '✅ ALL TESTS PASSED!' : '❌ SOME TESTS FAILED'}`);
console.log('\n📊 FIXES:');
console.log(' ✅ Language buttons now update automatically');
console.log(' ✅ Only CV content fades (not the white paper)');
console.log(' ✅ Navigation bar stays solid');
console.log(' ✅ Smooth, professional transition!');
await page.waitForTimeout(2000);
await browser.close();
})();
-43
View File
@@ -1,43 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 500 });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
console.log('📄 Loading English page...\n');
await page.goto('http://localhost:1999/?lang=en');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
console.log('🌍 Switching to Spanish (watch for smooth fade transition)...');
await page.click('button[hx-get*="lang=es"]');
await page.waitForTimeout(3000); // Wait to see the transition
console.log('✅ Spanish loaded');
await page.waitForTimeout(1000);
console.log('\n🌍 Switching back to English (watch for smooth fade transition)...');
await page.click('button[hx-get*="lang=en"]');
await page.waitForTimeout(3000); // Wait to see the transition
console.log('✅ English loaded\n');
await page.waitForTimeout(1000);
console.log('📊 SUMMARY:');
console.log(' - Language transitions now use HTMX CSS transitions');
console.log(' - 200ms fade out (htmx-swapping class)');
console.log(' - 200ms fade in (htmx-settling class)');
console.log(' - Smooth, professional experience! 🎉\n');
console.log('💡 BENEFITS:');
console.log(' ✅ Eliminates jarring page flash');
console.log(' ✅ Smooth visual continuity');
console.log(' ✅ Professional feel');
console.log(' ✅ No JavaScript needed - pure CSS!');
await page.waitForTimeout(2000);
await browser.close();
})();
-64
View File
@@ -1,64 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 600 });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
console.log('🔍 Mobile Toggle Click Investigation\n');
// Capture ALL console messages
page.on('console', msg => {
console.log(` [${msg.type().toUpperCase()}]`, msg.text());
});
// Capture page errors
page.on('pageerror', error => {
console.log(` ❌ PAGE ERROR: ${error.message}`);
});
await page.goto('http://localhost:1999/?lang=en');
await page.waitForLoadState('networkidle');
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(500);
// First click desktop to set state
console.log('Step 1: Click Desktop (to set up state)\n');
await page.locator('#desktop-length-toggle .icon-toggle').click();
await page.waitForTimeout(2000);
// Open menu
console.log('\nStep 2: Open Hamburger Menu\n');
await page.locator('.hamburger-btn').click();
await page.waitForTimeout(1000);
// Check if mobile toggle is visible and clickable
const mobileToggle = page.locator('#mobile-length-toggle');
const isVisible = await mobileToggle.isVisible();
console.log(` Mobile toggle visible: ${isVisible}`);
const mobileInput = page.locator('#lengthToggleMenu');
const inputVisible = await mobileInput.isVisible();
console.log(` Mobile input visible: ${inputVisible}`);
// Try clicking directly on the input
console.log('\nStep 3: Attempting to click mobile toggle label\n');
try {
await page.locator('#mobile-length-toggle .icon-toggle').click({timeout: 5000});
console.log(' ✅ Click executed');
} catch (e) {
console.log(` ❌ Click failed: ${e.message}`);
}
await page.waitForTimeout(3000);
// Final state check
const finalDesktop = await page.evaluate(() => document.querySelector('#lengthToggle')?.checked);
const finalMobile = await page.evaluate(() => document.querySelector('#lengthToggleMenu')?.checked);
console.log(`\nFinal State: Desktop=${finalDesktop}, Mobile=${finalMobile}`);
await page.waitForTimeout(2000);
await browser.close();
})();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

-248
View File
@@ -1,248 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shortcuts Button Visibility Test</title>
<script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
<style>
body {
margin: 0;
padding: 40px;
font-family: system-ui, -apple-system, sans-serif;
background: #f5f5f5;
}
.test-container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #2c3e50;
margin-bottom: 10px;
}
.test-info {
background: #e3f2fd;
padding: 15px;
border-radius: 4px;
margin-bottom: 30px;
border-left: 4px solid #2196f3;
}
.button-comparison {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 30px;
margin-bottom: 40px;
}
.button-test {
text-align: center;
}
.button-test h3 {
margin-bottom: 15px;
color: #555;
font-size: 14px;
}
/* OLD: Opacity 0.2 */
.shortcuts-btn-old {
position: relative;
width: 50px;
height: 50px;
background: #2c3e50;
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
opacity: 0.2;
margin: 0 auto;
}
.shortcuts-btn-old:hover {
opacity: 1;
transform: translateY(-3px);
}
/* NEW: Opacity 0.6 */
.shortcuts-btn-new {
position: relative;
width: 50px;
height: 50px;
background: #2c3e50;
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
opacity: 0.6;
margin: 0 auto;
}
.shortcuts-btn-new:hover {
opacity: 1;
transform: translateY(-3px);
background: #3498db;
}
/* Hover state */
.shortcuts-btn-hover {
position: relative;
width: 50px;
height: 50px;
background: #3498db;
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
transition: all 0.3s ease;
opacity: 1;
transform: translateY(-3px);
margin: 0 auto;
}
.status {
margin-top: 15px;
font-size: 12px;
color: #666;
}
.pass {
color: #27ae60;
font-weight: bold;
}
.fail {
color: #e74c3c;
font-weight: bold;
}
.checklist {
background: #f8f9fa;
padding: 20px;
border-radius: 4px;
}
.checklist h2 {
margin-top: 0;
color: #2c3e50;
}
.checklist li {
margin-bottom: 10px;
line-height: 1.6;
}
.verdict {
background: #d4edda;
border: 2px solid #28a745;
padding: 20px;
border-radius: 8px;
margin-top: 30px;
text-align: center;
}
.verdict h2 {
margin: 0 0 10px 0;
color: #155724;
}
</style>
</head>
<body>
<div class="test-container">
<h1>🎹 Shortcuts Button Visibility Test</h1>
<div class="test-info">
<strong>Issue:</strong> Shortcuts button exists with iconify-icon but appears nearly invisible due to low opacity (0.2)<br>
<strong>Fix:</strong> Increased default opacity from 0.2 to 0.6 for better discoverability
</div>
<div class="button-comparison">
<div class="button-test">
<h3>❌ OLD: Opacity 0.2</h3>
<button class="shortcuts-btn-old">
<iconify-icon icon="mdi:keyboard-outline" width="28" height="28"></iconify-icon>
</button>
<div class="status">
<span class="fail">HARD TO SEE</span><br>
Requires hover to discover
</div>
</div>
<div class="button-test">
<h3>✅ NEW: Opacity 0.6</h3>
<button class="shortcuts-btn-new">
<iconify-icon icon="mdi:keyboard-outline" width="28" height="28"></iconify-icon>
</button>
<div class="status">
<span class="pass">VISIBLE</span><br>
Easy to discover
</div>
</div>
<div class="button-test">
<h3>✨ Hover State</h3>
<button class="shortcuts-btn-hover">
<iconify-icon icon="mdi:keyboard-outline" width="28" height="28"></iconify-icon>
</button>
<div class="status">
<span class="pass">FULL OPACITY</span><br>
Background changes to blue
</div>
</div>
</div>
<div class="checklist">
<h2>✅ Verification Checklist</h2>
<ul>
<li><strong>Icon renders correctly</strong> - mdi:keyboard-outline displays at 28x28px</li>
<li><strong>Iconify library loaded</strong> - Script from code.iconify.design works</li>
<li><strong>Button structure correct</strong> - Circular button with flex centering</li>
<li><strong>Improved visibility</strong> - Opacity increased from 0.2 to 0.6</li>
<li><strong>Hover effect works</strong> - Full opacity (1.0) and blue background on hover</li>
<li><strong>Consistent with info-button</strong> - Both buttons use same opacity pattern</li>
<li><strong>Accessibility maintained</strong> - aria-label and title attributes present</li>
</ul>
</div>
<div class="verdict">
<h2>✅ ISSUE RESOLVED</h2>
<p style="margin: 0; color: #155724;">
The shortcuts button now has <strong>visible keyboard icon</strong> with improved discoverability.
Default opacity increased from 0.2 to 0.6 while maintaining smooth hover transitions.
</p>
</div>
</div>
<script>
// Verify iconify loaded
setTimeout(() => {
const icons = document.querySelectorAll('iconify-icon');
console.log(`✅ Found ${icons.length} iconify-icon elements`);
icons.forEach((icon, i) => {
console.log(` Icon ${i+1}: ${icon.getAttribute('icon')} - Width: ${icon.getAttribute('width')}`);
});
}, 1000);
</script>
</body>
</html>
-287
View File
@@ -1,287 +0,0 @@
<!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>
-62
View File
@@ -1,62 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 800 });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
console.log('🎬 Testing Smooth Toggle Animations\n');
await page.goto('http://localhost:1999/?lang=en');
await page.waitForLoadState('networkidle');
// Scroll down to make header visible
console.log('📜 Scrolling to reveal header...');
await page.evaluate(() => window.scrollTo(0, 500));
await page.waitForTimeout(1500);
console.log('\n🔄 Testing Length Toggle (Desktop)');
console.log(' Watch for SMOOTH slide animation...\n');
await page.click('#lengthToggle');
await page.waitForTimeout(2000);
console.log('✅ Animation completed!');
console.log(' Did you see a smooth 300ms slide? (analogical)');
console.log(' Or did it snap instantly? (digital)\n');
console.log('🔄 Clicking again (toggling back)...\n');
await page.click('#lengthToggle');
await page.waitForTimeout(2000);
console.log('🍔 Opening hamburger menu to test mobile toggle...');
await page.hover('.hamburger-btn');
await page.waitForTimeout(1000);
console.log('\n🔄 Testing Length Toggle (Mobile)');
console.log(' This should also be smooth...\n');
const menuToggle = await page.locator('#lengthToggleMenu').isVisible();
if (menuToggle) {
await page.click('#lengthToggleMenu');
await page.waitForTimeout(2000);
console.log('✅ Mobile toggle animation completed!');
console.log(' Both toggles should be in sync now.\n');
} else {
console.log('⚠️ Mobile toggle not visible\n');
}
console.log('📊 SMOOTH ANIMATION TEST SUMMARY:');
console.log(' ✅ Toggles use CSS transitions (0.3s ease)');
console.log(' ✅ No DOM replacement (hx-swap="none")');
console.log(' ✅ Element stays in DOM during animation');
console.log(' ✅ Desktop/mobile sync via hyperscript');
console.log('\n🎯 Expected: Smooth "analogical" slide');
console.log('🎯 You should see the slider move smoothly over 300ms');
await page.waitForTimeout(3000);
await browser.close();
})();
-57
View File
@@ -1,57 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 400 });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 } // Desktop - controls visible > 900px
});
const page = await context.newPage();
// Listen for console errors
page.on('console', msg => {
const text = msg.text();
const type = msg.type();
if (type === 'error' || text.toLowerCase().includes('error')) {
console.log('❌ CONSOLE ERROR:', text);
}
});
page.on('pageerror', error => console.log('❌ PAGE EXCEPTION:', error.message));
console.log('📄 Loading page (desktop 1920px - controls visible)...');
await page.goto('http://localhost:1999/?lang=en');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
console.log('\n🖱️ TEST 1: Click desktop theme toggle...');
await page.locator('.selector-group').filter({ hasText: 'View' }).locator('label.icon-toggle').click();
await page.waitForTimeout(2000);
console.log('✅ Desktop toggle complete - No errors!');
console.log('\n🖱️ TEST 2: Click desktop theme toggle again (toggle back)...');
await page.locator('.selector-group').filter({ hasText: 'View' }).locator('label.icon-toggle').click();
await page.waitForTimeout(2000);
console.log('✅ Desktop toggle back complete - No errors!');
console.log('\n📱 TEST 3: Resize to mobile and test menu sync...');
await page.setViewportSize({ width: 600, height: 800 });
await page.waitForTimeout(500);
console.log('🍔 Opening hamburger menu...');
await page.click('.hamburger-btn');
await page.waitForTimeout(500);
console.log('🔍 Checking mobile toggle state...');
const mobileChecked = await page.locator('#themeToggleMenu').isChecked();
const expectedState = false; // Should be default (not clean)
console.log(` Mobile toggle checked: ${mobileChecked} (expected: ${expectedState})`);
if (mobileChecked === expectedState) {
console.log(' ✅ Toggles are SYNCED!');
} else {
console.log(' ❌ Toggles are OUT OF SYNC!');
}
console.log('\n✅ All tests complete - Check for errors above');
await page.waitForTimeout(1000);
await browser.close();
})();
-119
View File
@@ -1,119 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 400 });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
console.log('✨ COMPLETE TOGGLE SYSTEM VALIDATION ✨\n');
console.log('═══════════════════════════════════════════════════\n');
let errors = [];
page.on('console', msg => {
if (msg.type() === 'error' && msg.text().includes('hyperscript')) {
errors.push(msg.text());
}
});
await page.goto('http://localhost:1999/?lang=en');
await page.waitForLoadState('networkidle');
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(500);
// TEST 1: Desktop toggles
console.log('TEST 1: Desktop Toggles\n');
console.log(' Length toggle...');
await page.locator('#desktop-length-toggle .icon-toggle').click();
await page.waitForTimeout(1000);
let dLen = await page.locator('#lengthToggle').isChecked();
let mLen = await page.locator('#lengthToggleMenu').isChecked();
console.log(` Sync: ${dLen === mLen ? '✅ PASS' : '❌ FAIL'} (Desktop=${dLen}, Mobile=${mLen})`);
console.log(' Logo toggle...');
await page.locator('#desktop-logo-toggle .icon-toggle').click();
await page.waitForTimeout(1000);
let dLogo = await page.locator('#logoToggle').isChecked();
let mLogo = await page.locator('#logoToggleMenu').isChecked();
console.log(` Sync: ${dLogo === mLogo ? '✅ PASS' : '❌ FAIL'} (Desktop=${dLogo}, Mobile=${mLogo})`);
console.log(' Theme toggle...');
await page.locator('#desktop-theme-toggle .icon-toggle').click();
await page.waitForTimeout(1000);
let dTheme = await page.locator('#themeToggle').isChecked();
let mTheme = await page.locator('#themeToggleMenu').isChecked();
console.log(` Sync: ${dTheme === mTheme ? '✅ PASS' : '❌ FAIL'} (Desktop=${dTheme}, Mobile=${mTheme})\n`);
// TEST 2: Mobile toggles
console.log('TEST 2: Mobile Menu Toggles\n');
console.log(' Opening menu...');
await page.locator('.hamburger-btn').click();
await page.waitForTimeout(800);
console.log(' Length toggle (mobile)...');
await page.locator('#mobile-length-toggle .icon-toggle').click();
await page.waitForTimeout(1000);
dLen = await page.locator('#lengthToggle').isChecked();
mLen = await page.locator('#lengthToggleMenu').isChecked();
console.log(` Sync: ${dLen === mLen ? '✅ PASS' : '❌ FAIL'} (Desktop=${dLen}, Mobile=${mLen})`);
console.log(' Logo toggle (mobile)...');
await page.locator('#mobile-logo-toggle .icon-toggle').click();
await page.waitForTimeout(1000);
dLogo = await page.locator('#logoToggle').isChecked();
mLogo = await page.locator('#logoToggleMenu').isChecked();
console.log(` Sync: ${dLogo === mLogo ? '✅ PASS' : '❌ FAIL'} (Desktop=${dLogo}, Mobile=${mLogo})`);
console.log(' Theme toggle (mobile)...');
await page.locator('#mobile-theme-toggle .icon-toggle').click();
await page.waitForTimeout(1000);
dTheme = await page.locator('#themeToggle').isChecked();
mTheme = await page.locator('#themeToggleMenu').isChecked();
console.log(` Sync: ${dTheme === mTheme ? '✅ PASS' : '❌ FAIL'} (Desktop=${dTheme}, Mobile=${mTheme})\n`);
// TEST 3: Animations
console.log('TEST 3: Smooth Animations\n');
console.log(' Clicking toggle and observing animation...');
await page.locator('#desktop-length-toggle .icon-toggle').click();
await page.waitForTimeout(1500);
console.log(' ✅ Visual check: Did the toggle slide smoothly? (300ms CSS transition)\n');
// TEST 4: Persistence
console.log('TEST 4: LocalStorage Persistence\n');
const storage = await page.evaluate(() => {
return {
length: localStorage.getItem('cv-length'),
icons: localStorage.getItem('cv-icons'),
theme: localStorage.getItem('cv-theme')
};
});
console.log(` Length: ${storage.length || 'not set'}`);
console.log(` Logos: ${storage.icons || 'not set'}`);
console.log(` Theme: ${storage.theme || 'not set'}\n`);
// Final summary
console.log('═══════════════════════════════════════════════════');
console.log('📊 FINAL VALIDATION SUMMARY:\n');
if (errors.length === 0) {
console.log('✅ No hyperscript syntax errors');
} else {
console.log(`❌ Found ${errors.length} hyperscript errors`);
}
console.log('✅ Desktop/mobile sync working perfectly');
console.log('✅ All 3 toggle types functional (length, icons, theme)');
console.log('✅ Smooth CSS animations (300ms transitions)');
console.log('✅ State persistence via localStorage');
console.log('✅ HTMX integration with hx-swap="none"');
console.log('✅ Server-side cookie storage\n');
console.log('🎉 TOGGLE SYSTEM FULLY OPERATIONAL! 🎉');
console.log('═══════════════════════════════════════════════════\n');
await page.waitForTimeout(2000);
await browser.close();
})();
-77
View File
@@ -1,77 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 300 });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
console.log('🔍 Debugging Toggle Sync Issue\n');
// Capture ALL console messages
page.on('console', msg => {
const type = msg.type();
const text = msg.text();
if (type === 'log') {
console.log(' 📝', text);
} else if (type === 'error') {
console.log(' ❌', text);
}
});
await page.goto('http://localhost:1999/?lang=en');
await page.waitForLoadState('networkidle');
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(500);
// Add debug logging to the page
await page.evaluate(() => {
console.log('🔧 Setting up debug listeners...');
const desktopToggle = document.querySelector('#lengthToggle');
const mobileToggle = document.querySelector('#lengthToggleMenu');
desktopToggle.addEventListener('change', (e) => {
console.log(`Desktop changed: ${e.target.checked}`);
}, true);
mobileToggle.addEventListener('change', (e) => {
console.log(`Mobile changed: ${e.target.checked}`);
}, true);
});
console.log('\n📱 Step 1: Click Desktop Toggle');
const desktopLabel = page.locator('#desktop-length-toggle .icon-toggle');
await desktopLabel.click();
await page.waitForTimeout(1500);
let desktopState = await page.locator('#lengthToggle').isChecked();
let mobileState = await page.locator('#lengthToggleMenu').isChecked();
console.log(`Result: Desktop=${desktopState}, Mobile=${mobileState}`);
console.log('\n🍔 Step 2: Open Menu');
const hamburger = page.locator('.hamburger-btn');
await hamburger.click();
await page.waitForTimeout(500);
console.log('\n📱 Step 3: Click Mobile Toggle');
const mobileLabel = page.locator('#mobile-length-toggle .icon-toggle');
await mobileLabel.click();
await page.waitForTimeout(1500);
desktopState = await page.locator('#lengthToggle').isChecked();
mobileState = await page.locator('#lengthToggleMenu').isChecked();
console.log(`Result: Desktop=${desktopState}, Mobile=${mobileState}`);
// Final check
console.log('\n📊 Final State Check:');
const finalDesktop = await page.evaluate(() => document.querySelector('#lengthToggle').checked);
const finalMobile = await page.evaluate(() => document.querySelector('#lengthToggleMenu').checked);
console.log(` Desktop (via evaluate): ${finalDesktop}`);
console.log(` Mobile (via evaluate): ${finalMobile}`);
console.log(` Match: ${finalDesktop === finalMobile ? '✅' : '❌'}`);
await page.waitForTimeout(3000);
await browser.close();
})();
-96
View File
@@ -1,96 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 500 });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
console.log('🎬 Testing Toggle Fix - Syntax Errors & Animations\n');
await page.goto('http://localhost:1999/?lang=en');
await page.waitForLoadState('networkidle');
// Ensure header is visible (scroll to top)
console.log('📜 Scrolling to top to ensure header is visible...');
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(1000);
// Check for hyperscript errors in console
const errors = [];
page.on('console', msg => {
if (msg.type() === 'error') {
errors.push(msg.text());
console.log('❌ Console Error:', msg.text());
}
});
console.log('\n✅ Testing Desktop Length Toggle');
console.log(' Expected: Smooth 300ms slide animation\n');
// Click the toggle
const lengthToggle = page.locator('#lengthToggle');
await lengthToggle.waitFor({ state: 'visible' });
console.log(' Clicking toggle...');
await lengthToggle.click();
await page.waitForTimeout(1500);
// Check if both toggles are synced
const desktopChecked = await page.locator('#lengthToggle').isChecked();
const mobileChecked = await page.locator('#lengthToggleMenu').isChecked();
console.log(` Desktop toggle checked: ${desktopChecked}`);
console.log(` Mobile toggle checked: ${mobileChecked}`);
console.log(` Sync status: ${desktopChecked === mobileChecked ? '✅ SYNCED' : '❌ OUT OF SYNC'}`);
console.log('\n✅ Testing Logo Toggle');
const logoToggle = page.locator('#logoToggle');
await logoToggle.click();
await page.waitForTimeout(1500);
console.log('\n✅ Testing Theme Toggle');
const themeToggle = page.locator('#themeToggle');
await themeToggle.click();
await page.waitForTimeout(1500);
console.log('\n🍔 Testing Mobile Menu Toggles');
console.log(' Opening hamburger menu...');
const hamburger = page.locator('.hamburger-btn');
await hamburger.click();
await page.waitForTimeout(1000);
console.log(' Testing mobile length toggle...');
const mobileToggle = page.locator('#lengthToggleMenu');
await mobileToggle.click();
await page.waitForTimeout(1500);
// Final sync check
const finalDesktopChecked = await page.locator('#lengthToggle').isChecked();
const finalMobileChecked = await page.locator('#lengthToggleMenu').isChecked();
console.log(`\n📊 Final Sync Status:`);
console.log(` Desktop: ${finalDesktopChecked}`);
console.log(` Mobile: ${finalMobileChecked}`);
console.log(` Synced: ${finalDesktopChecked === finalMobileChecked ? '✅' : '❌'}`);
// Check for hyperscript errors
console.log(`\n🔍 Hyperscript Errors:`);
if (errors.length === 0) {
console.log(' ✅ No hyperscript syntax errors!');
} else {
console.log(` ❌ Found ${errors.length} errors`);
errors.forEach(err => console.log(` - ${err}`));
}
console.log('\n✅ TEST COMPLETE');
console.log(' Key fixes:');
console.log(' 1. Fixed hyperscript syntax (removed <input/> selector)');
console.log(' 2. Using direct ID references (#lengthToggle, #lengthToggleMenu)');
console.log(' 3. Maintained hx-swap="none" for smooth animations');
console.log(' 4. Desktop/mobile sync working via hyperscript');
await page.waitForTimeout(3000);
await browser.close();
})();
-64
View File
@@ -1,64 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 1500 });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
console.log('🧪 Testing Toggle Visual State\n');
await page.goto('http://localhost:1999/?lang=en');
await page.waitForLoadState('networkidle');
// Take screenshot of toggles
console.log('📸 Taking screenshot of toggle controls...');
// Scroll to top to see header controls
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(500);
// Check if toggles are visible
const lengthToggleVisible = await page.locator('#lengthToggle').isVisible().catch(() => false);
const themeToggleVisible = await page.locator('#themeToggle').isVisible().catch(() => false);
const logoToggleVisible = await page.locator('#logoToggle').isVisible().catch(() => false);
console.log(`\n📊 Toggle Visibility:`);
console.log(` Length toggle: ${lengthToggleVisible ? '✅ Visible' : '❌ Hidden'}`);
console.log(` Theme toggle: ${themeToggleVisible ? '✅ Visible' : '❌ Hidden'}`);
console.log(` Logo toggle: ${logoToggleVisible ? '✅ Visible' : '❌ Hidden'}`);
if (!lengthToggleVisible) {
console.log(`\n⚠️ Desktop toggles are hidden (might be in hamburger menu)`);
// Try to find hamburger menu
const hamburgerExists = await page.locator('.hamburger-btn').count();
console.log(` Hamburger button exists: ${hamburgerExists > 0 ? '✅ Yes' : '❌ No'}`);
if (hamburgerExists > 0) {
console.log(`\n🍔 Opening hamburger menu...`);
await page.hover('.hamburger-btn');
await page.waitForTimeout(1000);
const menuLengthToggle = await page.locator('#lengthToggleMenu').isVisible().catch(() => false);
console.log(` Mobile length toggle visible: ${menuLengthToggle ? '✅ Yes' : '❌ No'}`);
if (menuLengthToggle) {
console.log(`\n🔄 Testing mobile toggle...`);
const isChecked = await page.locator('#lengthToggleMenu').isChecked();
console.log(` Current state: ${isChecked ? 'Long' : 'Short'}`);
await page.click('#lengthToggleMenu');
await page.waitForTimeout(1500);
const isCheckedAfter = await page.locator('#lengthToggleMenu').isChecked();
console.log(` After click: ${isCheckedAfter ? 'Long' : 'Short'}`);
console.log(` Toggle changed: ${isChecked !== isCheckedAfter ? '✅ Yes' : '❌ No'}`);
}
}
}
await page.waitForTimeout(3000);
await browser.close();
})();
-110
View File
@@ -1,110 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 500 });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
console.log('🎬 Testing Toggle Fix - Complete Validation\n');
await page.goto('http://localhost:1999/?lang=en');
await page.waitForLoadState('networkidle');
// Ensure header is visible
console.log('📜 Scrolling to top...');
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(500);
// Track console errors
const errors = [];
page.on('console', msg => {
if (msg.type() === 'error') {
const text = msg.text();
if (text.includes('hyperscript') || text.includes('Expected')) {
errors.push(text);
console.log('❌ Hyperscript Error:', text);
}
}
});
console.log('\n✅ TEST 1: Desktop Length Toggle (click on toggle label)');
// Click on the label element (visible toggle UI)
const lengthLabel = page.locator('#desktop-length-toggle .icon-toggle');
await lengthLabel.waitFor({ state: 'visible' });
console.log(' Clicking toggle...');
await lengthLabel.click();
await page.waitForTimeout(1000);
// Check both toggles' state
const desktopChecked = await page.locator('#lengthToggle').isChecked();
const mobileChecked = await page.locator('#lengthToggleMenu').isChecked();
console.log(` Desktop toggle: ${desktopChecked}`);
console.log(` Mobile toggle: ${mobileChecked}`);
console.log(` Sync: ${desktopChecked === mobileChecked ? '✅ SYNCED' : '❌ OUT OF SYNC'}`);
console.log('\n✅ TEST 2: Logo Toggle');
const logoLabel = page.locator('#desktop-logo-toggle .icon-toggle');
await logoLabel.click();
await page.waitForTimeout(1000);
const logoDesktop = await page.locator('#logoToggle').isChecked();
const logoMobile = await page.locator('#logoToggleMenu').isChecked();
console.log(` Sync: ${logoDesktop === logoMobile ? '✅ SYNCED' : '❌ OUT OF SYNC'}`);
console.log('\n✅ TEST 3: Theme Toggle');
const themeLabel = page.locator('#desktop-theme-toggle .icon-toggle');
await themeLabel.click();
await page.waitForTimeout(1000);
const themeDesktop = await page.locator('#themeToggle').isChecked();
const themeMobile = await page.locator('#themeToggleMenu').isChecked();
console.log(` Sync: ${themeDesktop === themeMobile ? '✅ SYNCED' : '❌ OUT OF SYNC'}`);
console.log('\n✅ TEST 4: Mobile Menu Toggles');
console.log(' Opening hamburger menu...');
const hamburger = page.locator('.hamburger-btn');
await hamburger.click();
await page.waitForTimeout(500);
console.log(' Clicking mobile length toggle...');
const mobileLengthLabel = page.locator('#mobile-length-toggle .icon-toggle');
await mobileLengthLabel.click();
await page.waitForTimeout(1000);
const finalDesktop = await page.locator('#lengthToggle').isChecked();
const finalMobile = await page.locator('#lengthToggleMenu').isChecked();
console.log(` Desktop: ${finalDesktop}`);
console.log(` Mobile: ${finalMobile}`);
console.log(` Sync: ${finalDesktop === finalMobile ? '✅ SYNCED' : '❌ OUT OF SYNC'}`);
// Final results
console.log('\n📊 FINAL RESULTS:');
console.log('═══════════════════════════════════════');
if (errors.length === 0) {
console.log('✅ No hyperscript syntax errors detected!');
} else {
console.log(`❌ Found ${errors.length} hyperscript errors:`);
errors.forEach(err => console.log(` ${err}`));
}
console.log('\n🎯 KEY FIXES IMPLEMENTED:');
console.log(' 1. ✅ Fixed hyperscript syntax errors');
console.log(' - Removed broken <input/> selector syntax');
console.log(' - Using direct ID references: #lengthToggle, #lengthToggleMenu');
console.log(' 2. ✅ Maintained smooth animations');
console.log(' - hx-swap="none" keeps elements in DOM');
console.log(' - CSS transitions work perfectly (300ms)');
console.log(' 3. ✅ Desktop/mobile sync working');
console.log(' - Both toggles update simultaneously');
console.log(' - State stored in localStorage');
await page.waitForTimeout(2000);
await browser.close();
})();
-75
View File
@@ -1,75 +0,0 @@
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 1000 });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
console.log('🧪 Testing URL Cleanliness and Language Switching\n');
console.log('📄 Step 1: Load English page');
await page.goto('http://localhost:1999/?lang=en');
await page.waitForLoadState('networkidle');
let url = page.url();
console.log(` URL: ${url}`);
console.log(` Clean (no anchors): ${!url.includes('#') ? '✅' : '❌'}\n`);
console.log('🌍 Step 2: Switch to Spanish');
await page.click('button[aria-label="Español"]');
await page.waitForTimeout(1000);
url = page.url();
const contentES = await page.locator('.sidebar-accordion-header span').first().textContent();
console.log(` URL: ${url}`);
console.log(` Content: "${contentES}"`);
console.log(` Success: ${contentES.includes('Competencias') && url.includes('lang=es') ? '✅' : '❌'}`);
console.log(` Clean (no anchors): ${!url.includes('#') ? '✅' : '❌'}\n`);
console.log('📜 Step 3: Scroll down page');
await page.evaluate(() => window.scrollTo(0, 800));
await page.waitForTimeout(1000);
url = page.url();
console.log(` URL after scroll: ${url}`);
console.log(` Still clean: ${!url.includes('#') ? '✅' : '❌'}\n`);
console.log('⬆️ Step 4: Click back-to-top button');
await page.waitForSelector('.back-to-top', { state: 'visible' });
await page.click('.back-to-top');
await page.waitForTimeout(1500);
url = page.url();
const scrollPos = await page.evaluate(() => window.pageYOffset);
console.log(` URL after back-to-top: ${url}`);
console.log(` No #top anchor: ${!url.includes('#top') ? '✅' : '❌'}`);
console.log(` No # at all: ${!url.includes('#') ? '✅' : '❌'}`);
console.log(` Scrolled to top (< 50px): ${scrollPos < 50 ? '✅' : '❌'}`);
console.log(` Current scroll position: ${scrollPos}px\n`);
console.log('🌍 Step 5: Switch back to English');
await page.click('button[aria-label="English"]');
await page.waitForTimeout(1000);
url = page.url();
const contentEN = await page.locator('.sidebar-accordion-header span').first().textContent();
console.log(` URL: ${url}`);
console.log(` Content: "${contentEN}"`);
console.log(` Success: ${contentEN.includes('Technical') && url.includes('lang=en') ? '✅' : '❌'}`);
console.log(` Still clean: ${!url.includes('#') ? '✅' : '❌'}\n`);
const allClean = !page.url().includes('#');
const bothLangsWork = contentES.includes('Competencias') && contentEN.includes('Technical');
const scrollWorked = scrollPos < 50;
console.log(`\n${allClean && bothLangsWork && scrollWorked ? '✅ ALL URL CLEANLINESS TESTS PASSED!' : '❌ SOME TESTS FAILED'}`);
console.log('\n📊 KEY ACHIEVEMENTS:');
console.log(` ${allClean ? '✅' : '❌'} URLs stay clean - no anchor pollution`);
console.log(` ${bothLangsWork ? '✅' : '❌'} Language switching works atomically`);
console.log(` ${scrollWorked ? '✅' : '❌'} Back-to-top scrolls without URL changes`);
console.log(' ✅ Smooth scrolling via hyperscript');
console.log(' ✅ Out-of-band swaps for atomic updates');
console.log(' ✅ Clean URL: Only ?lang=XX parameter');
await page.waitForTimeout(2000);
await browser.close();
})();
-578
View File
@@ -1,578 +0,0 @@
#!/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
@@ -1,122 +0,0 @@
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
@@ -1,401 +0,0 @@
<!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
@@ -1,104 +0,0 @@
#!/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
@@ -1,389 +0,0 @@
<!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
@@ -1,163 +0,0 @@
#!/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
@@ -1,49 +0,0 @@
<!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
@@ -1,171 +0,0 @@
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');
})();
-69
View File
@@ -1,69 +0,0 @@
{
"personal": {
"name": "Test User",
"email": "test@example.com",
"phone": "+1234567890",
"location": "Test City, USA",
"website": "https://test.example.com",
"github": "https://github.com/testuser",
"linkedin": "https://linkedin.com/in/testuser",
"title": "Test Engineer",
"summary": "Test summary for unit testing purposes. This is a minimal valid CV structure.",
"image": "https://example.com/test-profile.jpg"
},
"experience": [
{
"company": "Test Company Inc.",
"position": "Senior Test Engineer",
"start_date": "2020-01-01",
"end_date": "",
"description": "Test description of responsibilities and achievements.",
"location": "Test Location, USA",
"technologies": ["Go", "HTMX", "Testing"]
},
{
"company": "Previous Test Corp",
"position": "Junior Test Engineer",
"start_date": "2018-06-01",
"end_date": "2019-12-31",
"description": "Earlier role description for testing.",
"location": "Test City, USA",
"technologies": ["JavaScript", "React"]
}
],
"education": [
{
"institution": "Test University",
"degree": "Bachelor of Science",
"field": "Computer Science",
"start_date": "2015-09-01",
"end_date": "2019-06-01",
"location": "Test University, USA",
"gpa": "3.8"
}
],
"skills": {
"technical": ["Go", "HTMX", "Testing", "CI/CD", "Docker"],
"languages": ["English", "Spanish"],
"frameworks": ["Hono-style routing", "net/http"],
"tools": ["Git", "Make", "VSCode"]
},
"projects": [
{
"name": "Test Project",
"description": "Sample project for testing purposes",
"technologies": ["Go", "HTMX"],
"url": "https://github.com/testuser/test-project",
"start_date": "2023-01-01",
"end_date": ""
}
],
"certifications": [
{
"name": "Test Certification",
"issuer": "Test Organization",
"date": "2022-03-15",
"url": "https://example.com/cert"
}
]
}
-53
View File
@@ -1,53 +0,0 @@
{
"personal": {
"name": "Usuario de Prueba",
"email": "prueba@ejemplo.com",
"phone": "+34123456789",
"location": "Ciudad de Prueba, España",
"website": "https://prueba.ejemplo.com",
"github": "https://github.com/usuarioprueba",
"linkedin": "https://linkedin.com/in/usuarioprueba",
"title": "Ingeniero de Pruebas",
"summary": "Resumen de prueba para propósitos de testing. Esta es una estructura mínima válida de CV.",
"image": "https://ejemplo.com/perfil-prueba.jpg"
},
"experience": [
{
"company": "Empresa de Prueba S.L.",
"position": "Ingeniero Senior de Pruebas",
"start_date": "2020-01-01",
"end_date": "",
"description": "Descripción de prueba de responsabilidades y logros.",
"location": "Madrid, España",
"technologies": ["Go", "HTMX", "Testing"]
}
],
"education": [
{
"institution": "Universidad de Prueba",
"degree": "Grado en Informática",
"field": "Ciencias de la Computación",
"start_date": "2015-09-01",
"end_date": "2019-06-01",
"location": "Madrid, España",
"gpa": "8.5"
}
],
"skills": {
"technical": ["Go", "HTMX", "Testing", "CI/CD", "Docker"],
"languages": ["Español", "Inglés"],
"frameworks": ["Enrutamiento estilo Hono", "net/http"],
"tools": ["Git", "Make", "VSCode"]
},
"projects": [
{
"name": "Proyecto de Prueba",
"description": "Proyecto de ejemplo para propósitos de testing",
"technologies": ["Go", "HTMX"],
"url": "https://github.com/usuarioprueba/proyecto-prueba",
"start_date": "2023-01-01",
"end_date": ""
}
],
"certifications": []
}
-35
View File
@@ -1,35 +0,0 @@
{
"language_name": "English",
"language_code": "en",
"sections": {
"experience": "Experience",
"education": "Education",
"skills": "Skills",
"projects": "Projects",
"certifications": "Certifications",
"about": "About"
},
"labels": {
"present": "Present",
"location": "Location",
"download_pdf": "Download PDF",
"view_on_github": "View on GitHub",
"email": "Email",
"phone": "Phone",
"website": "Website",
"technologies": "Technologies",
"tools": "Tools",
"languages": "Languages"
},
"actions": {
"print": "Print",
"download": "Download",
"share": "Share",
"close": "Close"
},
"messages": {
"loading": "Loading...",
"error": "Error loading data",
"not_found": "Page not found"
}
}
-35
View File
@@ -1,35 +0,0 @@
{
"language_name": "Español",
"language_code": "es",
"sections": {
"experience": "Experiencia",
"education": "Educación",
"skills": "Habilidades",
"projects": "Proyectos",
"certifications": "Certificaciones",
"about": "Acerca de"
},
"labels": {
"present": "Presente",
"location": "Ubicación",
"download_pdf": "Descargar PDF",
"view_on_github": "Ver en GitHub",
"email": "Correo",
"phone": "Teléfono",
"website": "Sitio web",
"technologies": "Tecnologías",
"tools": "Herramientas",
"languages": "Idiomas"
},
"actions": {
"print": "Imprimir",
"download": "Descargar",
"share": "Compartir",
"close": "Cerrar"
},
"messages": {
"loading": "Cargando...",
"error": "Error al cargar datos",
"not_found": "Página no encontrada"
}
}
-107
View File
@@ -1,107 +0,0 @@
#!/bin/bash
# Test script to verify the "years of experience" styling matches the original design
echo "🧪 Testing Years of Experience Styling"
echo "========================================"
# Colors for output
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Test counter
PASSED=0
FAILED=0
# Test 1: Check CSS file contains correct styles
echo -n "1. CSS contains correct font-size (0.85em)... "
if grep -q "font-size: 0.85em;" /Users/txeo/Git/yo/cv/static/css/main.css; then
echo -e "${GREEN}✓ PASS${NC}"
((PASSED++))
else
echo -e "${RED}✗ FAIL${NC}"
((FAILED++))
fi
# Test 2: Check font-weight
echo -n "2. CSS contains correct font-weight (400)... "
if grep -A 6 ".years-experience" /Users/txeo/Git/yo/cv/static/css/main.css | grep -q "font-weight: 400;"; then
echo -e "${GREEN}✓ PASS${NC}"
((PASSED++))
else
echo -e "${RED}✗ FAIL${NC}"
((FAILED++))
fi
# Test 3: Check color
echo -n "3. CSS contains correct color (#666)... "
if grep -A 6 ".years-experience" /Users/txeo/Git/yo/cv/static/css/main.css | grep -q "color: #666;"; then
echo -e "${GREEN}✓ PASS${NC}"
((PASSED++))
else
echo -e "${RED}✗ FAIL${NC}"
((FAILED++))
fi
# Test 4: Check margin
echo -n "4. CSS contains minimal top margin (4px 0 0 0)... "
if grep -A 6 ".years-experience" /Users/txeo/Git/yo/cv/static/css/main.css | grep -q "margin: 4px 0 0 0;"; then
echo -e "${GREEN}✓ PASS${NC}"
((PASSED++))
else
echo -e "${RED}✗ FAIL${NC}"
((FAILED++))
fi
# Test 5: Check English version renders
echo -n "5. English version renders correctly... "
if curl -s "http://localhost:1999/?lang=en" | grep -q "20 years of experience"; then
echo -e "${GREEN}✓ PASS${NC}"
((PASSED++))
else
echo -e "${RED}✗ FAIL${NC}"
((FAILED++))
fi
# Test 6: Check Spanish version renders
echo -n "6. Spanish version renders correctly... "
if curl -s "http://localhost:1999/?lang=es" | grep -q "20 años de experiencia"; then
echo -e "${GREEN}✓ PASS${NC}"
((PASSED++))
else
echo -e "${RED}✗ FAIL${NC}"
((FAILED++))
fi
# Test 7: Check short version
echo -n "7. Short CV version includes years text... "
if curl -s "http://localhost:1999/?lang=en&version=short" | grep -q "years-experience"; then
echo -e "${GREEN}✓ PASS${NC}"
((PASSED++))
else
echo -e "${RED}✗ FAIL${NC}"
((FAILED++))
fi
# Test 8: Check long version
echo -n "8. Long CV version includes years text... "
if curl -s "http://localhost:1999/?lang=en&version=long" | grep -q "years-experience"; then
echo -e "${GREEN}✓ PASS${NC}"
((PASSED++))
else
echo -e "${RED}✗ FAIL${NC}"
((FAILED++))
fi
# Summary
echo ""
echo "========================================"
echo "Test Results: ${PASSED} passed, ${FAILED} failed"
if [ $FAILED -eq 0 ]; then
echo -e "${GREEN}✅ All tests passed!${NC}"
exit 0
else
echo -e "${RED}❌ Some tests failed${NC}"
exit 1
fi
-353
View File
@@ -1,353 +0,0 @@
/**
* Visual Comparison Test Suite
* Compares new Go + HTMX CV (localhost:1999) vs old React CV (localhost:3000)
*/
const { test, expect } = require('@playwright/test');
const fs = require('fs');
const path = require('path');
const OLD_SITE = 'http://localhost:3000';
const NEW_SITE = 'http://localhost:1999';
const SCREENSHOTS_DIR = path.join(__dirname, 'screenshots');
// Ensure screenshots directory exists
if (!fs.existsSync(SCREENSHOTS_DIR)) {
fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
}
test.describe('Visual Comparison: New vs Old CV', () => {
test('Full page screenshots', async ({ browser }) => {
const contextOld = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
const contextNew = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
const pageOld = await contextOld.newPage();
const pageNew = await contextNew.newPage();
// Load both sites
await pageOld.goto(OLD_SITE, { waitUntil: 'networkidle' });
await pageNew.goto(NEW_SITE, { waitUntil: 'networkidle' });
// Take full page screenshots
await pageOld.screenshot({
path: path.join(SCREENSHOTS_DIR, 'old-fullpage.png'),
fullPage: true
});
await pageNew.screenshot({
path: path.join(SCREENSHOTS_DIR, 'new-fullpage.png'),
fullPage: true
});
console.log('✓ Full page screenshots saved');
await contextOld.close();
await contextNew.close();
});
test('Header section comparison', async ({ browser }) => {
const contextOld = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
const contextNew = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
const pageOld = await contextOld.newPage();
const pageNew = await contextNew.newPage();
await pageOld.goto(OLD_SITE, { waitUntil: 'networkidle' });
await pageNew.goto(NEW_SITE, { waitUntil: 'networkidle' });
// Screenshot header sections
const headerOld = await pageOld.locator('.cv-title-badges-header, [class*="header"]').first();
const headerNew = await pageNew.locator('.cv-title-badges-header').first();
if (await headerOld.count() > 0) {
await headerOld.screenshot({ path: path.join(SCREENSHOTS_DIR, 'old-header.png') });
}
if (await headerNew.count() > 0) {
await headerNew.screenshot({ path: path.join(SCREENSHOTS_DIR, 'new-header.png') });
}
console.log('✓ Header screenshots saved');
await contextOld.close();
await contextNew.close();
});
test('Badge measurements comparison', async ({ browser }) => {
const contextOld = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
const contextNew = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
const pageOld = await contextOld.newPage();
const pageNew = await contextNew.newPage();
await pageOld.goto(OLD_SITE, { waitUntil: 'networkidle' });
await pageNew.goto(NEW_SITE, { waitUntil: 'networkidle' });
// Measure badge elements
const measurements = {
old: {},
new: {}
};
// New site badge measurements
const badgeNew = pageNew.locator('.title-badge').first();
if (await badgeNew.count() > 0) {
const box = await badgeNew.boundingBox();
const styles = await badgeNew.evaluate(el => {
const computed = window.getComputedStyle(el);
return {
height: computed.height,
padding: computed.padding,
fontSize: computed.fontSize,
fontWeight: computed.fontWeight,
color: computed.color,
backgroundColor: computed.backgroundColor,
borderRadius: computed.borderRadius,
display: computed.display,
alignItems: computed.alignItems
};
});
measurements.new.badge = { box, styles };
}
// Old site badge measurements
const badgeOld = pageOld.locator('.title-badge, [class*="badge"]').first();
if (await badgeOld.count() > 0) {
const box = await badgeOld.boundingBox();
const styles = await badgeOld.evaluate(el => {
const computed = window.getComputedStyle(el);
return {
height: computed.height,
padding: computed.padding,
fontSize: computed.fontSize,
fontWeight: computed.fontWeight,
color: computed.color,
backgroundColor: computed.backgroundColor,
borderRadius: computed.borderRadius,
display: computed.display,
alignItems: computed.alignItems
};
});
measurements.old.badge = { box, styles };
}
// Save measurements
fs.writeFileSync(
path.join(SCREENSHOTS_DIR, 'badge-measurements.json'),
JSON.stringify(measurements, null, 2)
);
console.log('✓ Badge measurements saved');
console.log('\nBadge Comparison:');
console.log('OLD:', JSON.stringify(measurements.old.badge?.styles, null, 2));
console.log('NEW:', JSON.stringify(measurements.new.badge?.styles, null, 2));
await contextOld.close();
await contextNew.close();
});
test('Typography comparison', async ({ browser }) => {
const contextOld = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
const contextNew = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
const pageOld = await contextOld.newPage();
const pageNew = await contextNew.newPage();
await pageOld.goto(OLD_SITE, { waitUntil: 'networkidle' });
await pageNew.goto(NEW_SITE, { waitUntil: 'networkidle' });
const typography = {
old: {},
new: {}
};
// Selectors to compare
const selectors = {
name: '.cv-name',
sidebarTitle: '.sidebar-title',
sectionTitle: '.section-title',
badge: '.title-badge'
};
// Measure new site typography
for (const [key, selector] of Object.entries(selectors)) {
const element = pageNew.locator(selector).first();
if (await element.count() > 0) {
typography.new[key] = await element.evaluate(el => {
const computed = window.getComputedStyle(el);
return {
fontFamily: computed.fontFamily,
fontSize: computed.fontSize,
fontWeight: computed.fontWeight,
lineHeight: computed.lineHeight,
color: computed.color,
letterSpacing: computed.letterSpacing
};
});
}
}
// Measure old site typography
for (const [key, selector] of Object.entries(selectors)) {
const element = pageOld.locator(selector).first();
if (await element.count() > 0) {
typography.old[key] = await element.evaluate(el => {
const computed = window.getComputedStyle(el);
return {
fontFamily: computed.fontFamily,
fontSize: computed.fontSize,
fontWeight: computed.fontWeight,
lineHeight: computed.lineHeight,
color: computed.color,
letterSpacing: computed.letterSpacing
};
});
}
}
// Save typography comparison
fs.writeFileSync(
path.join(SCREENSHOTS_DIR, 'typography-comparison.json'),
JSON.stringify(typography, null, 2)
);
console.log('✓ Typography comparison saved');
console.log('\nTypography Comparison:');
console.log('OLD:', JSON.stringify(typography.old, null, 2));
console.log('NEW:', JSON.stringify(typography.new, null, 2));
await contextOld.close();
await contextNew.close();
});
test('Sidebar comparison', async ({ browser }) => {
const contextOld = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
const contextNew = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
const pageOld = await contextOld.newPage();
const pageNew = await contextNew.newPage();
await pageOld.goto(OLD_SITE, { waitUntil: 'networkidle' });
await pageNew.goto(NEW_SITE, { waitUntil: 'networkidle' });
// Screenshot sidebars
const sidebarOld = pageOld.locator('.cv-sidebar, [class*="sidebar"]').first();
const sidebarNew = pageNew.locator('.cv-sidebar').first();
if (await sidebarOld.count() > 0) {
await sidebarOld.screenshot({ path: path.join(SCREENSHOTS_DIR, 'old-sidebar.png') });
}
if (await sidebarNew.count() > 0) {
await sidebarNew.screenshot({ path: path.join(SCREENSHOTS_DIR, 'new-sidebar.png') });
}
// Measure sidebar styles
const sidebarComparison = {
old: {},
new: {}
};
if (await sidebarNew.count() > 0) {
sidebarComparison.new = await sidebarNew.evaluate(el => {
const computed = window.getComputedStyle(el);
return {
backgroundColor: computed.backgroundColor,
padding: computed.padding,
width: computed.width,
minWidth: computed.minWidth
};
});
}
if (await sidebarOld.count() > 0) {
sidebarComparison.old = await sidebarOld.evaluate(el => {
const computed = window.getComputedStyle(el);
return {
backgroundColor: computed.backgroundColor,
padding: computed.padding,
width: computed.width,
minWidth: computed.minWidth
};
});
}
fs.writeFileSync(
path.join(SCREENSHOTS_DIR, 'sidebar-comparison.json'),
JSON.stringify(sidebarComparison, null, 2)
);
console.log('✓ Sidebar comparison saved');
console.log('\nSidebar Comparison:');
console.log('OLD:', JSON.stringify(sidebarComparison.old, null, 2));
console.log('NEW:', JSON.stringify(sidebarComparison.new, null, 2));
await contextOld.close();
await contextNew.close();
});
test('Critical elements style extraction', async ({ browser }) => {
const contextOld = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
const contextNew = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
const pageOld = await contextOld.newPage();
const pageNew = await contextNew.newPage();
await pageOld.goto(OLD_SITE, { waitUntil: 'networkidle' });
await pageNew.goto(NEW_SITE, { waitUntil: 'networkidle' });
const criticalElements = [
'.cv-title-badges-header',
'.title-badge',
'.badge-separator',
'.sidebar-title',
'.section-title',
'.cv-name'
];
const styleComparison = {
old: {},
new: {}
};
// Extract from new site
for (const selector of criticalElements) {
const element = pageNew.locator(selector).first();
if (await element.count() > 0) {
styleComparison.new[selector] = await element.evaluate(el => {
const computed = window.getComputedStyle(el);
const styles = {};
for (let i = 0; i < computed.length; i++) {
const prop = computed[i];
styles[prop] = computed.getPropertyValue(prop);
}
return styles;
});
}
}
// Extract from old site
for (const selector of criticalElements) {
const element = pageOld.locator(selector).first();
if (await element.count() > 0) {
styleComparison.old[selector] = await element.evaluate(el => {
const computed = window.getComputedStyle(el);
const styles = {};
for (let i = 0; i < computed.length; i++) {
const prop = computed[i];
styles[prop] = computed.getPropertyValue(prop);
}
return styles;
});
}
}
fs.writeFileSync(
path.join(SCREENSHOTS_DIR, 'critical-elements-full-styles.json'),
JSON.stringify(styleComparison, null, 2)
);
console.log('✓ Critical elements styles extracted');
await contextOld.close();
await contextNew.close();
});
});