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:
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
Executable
+283
@@ -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();
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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!"
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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** ✅
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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 |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
@@ -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>
|
||||
@@ -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');
|
||||
})();
|
||||
Vendored
-69
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
-53
@@ -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": []
|
||||
}
|
||||
Vendored
-35
@@ -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"
|
||||
}
|
||||
}
|
||||
Vendored
-35
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user