docs: Update skeleton loader implementation from hyperscript to JavaScript
MIGRATION SUMMARY: - Moved skeleton loader logic from hyperscript to JavaScript (main.js) - Changed from htmx:oobAfterSwap to htmx:afterSettle event - Changed OOB swap from innerHTML to outerHTML for proper element replacement - Added languageSwitching flag for state tracking - Added 100ms delay after afterSettle for final render completion DOCUMENTATION UPDATES: - 2-MODERN-WEB-TECHNIQUES.md: Updated skeleton loader section with
This commit is contained in:
@@ -0,0 +1,318 @@
|
|||||||
|
# Skeleton Loader Implementation - Debug Status
|
||||||
|
|
||||||
|
**Date**: 2025-11-18
|
||||||
|
**Status**: Feature works manually, automated test fails
|
||||||
|
**Issue**: Discrepancy between manual testing and Playwright automation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 GOAL
|
||||||
|
Implement skeleton loaders that appear during language transitions to provide visual feedback while content is being swapped.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ WHAT'S WORKING
|
||||||
|
|
||||||
|
### 1. Visual Layout (PERFECT)
|
||||||
|
- Skeleton matches actual CV layout pixel-perfectly
|
||||||
|
- Photo: 150x200px, positioned absolutely top-right
|
||||||
|
- Text blocks: Correctly sized to match real content
|
||||||
|
- **File**: `static/css/skeleton.css` - VERIFIED CORRECT
|
||||||
|
|
||||||
|
### 2. Skeleton Structure (WORKING)
|
||||||
|
- Dual-state component wrapper structure exists
|
||||||
|
- `.actual-content` and `.skeleton-content` properly nested
|
||||||
|
- CSS animations (shimmer effect) working
|
||||||
|
- **Files**: `templates/partials/sections/header.html`, `static/css/skeleton.css`
|
||||||
|
|
||||||
|
### 3. HTMX Events (CONFIRMED FIRING)
|
||||||
|
All HTMX events are firing correctly:
|
||||||
|
- `htmx:beforeRequest` ✅ (1 event)
|
||||||
|
- `htmx:afterSwap` ✅ (3 events)
|
||||||
|
- `htmx:oobAfterSwap` ✅ (5 events)
|
||||||
|
- `htmx:afterSettle` ✅ (3 events)
|
||||||
|
|
||||||
|
### 4. Loading Class Addition (WORKING)
|
||||||
|
- `.loading` class IS being added to page containers
|
||||||
|
- Confirmed via MutationObserver: 2 events (page-1 and page-2)
|
||||||
|
- **Mechanism**: Hyperscript on `<body>` in `templates/index.html:133-138`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❌ WHAT'S FAILING (IN AUTOMATED TESTS)
|
||||||
|
|
||||||
|
### The Problem
|
||||||
|
**The `.loading` class is NOT being removed after language switch completes**
|
||||||
|
|
||||||
|
**Test Output**:
|
||||||
|
```
|
||||||
|
.loading ADDED: 2 ✅
|
||||||
|
.loading REMOVED: 0 ❌
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Behavior**:
|
||||||
|
1. User clicks language button
|
||||||
|
2. `.loading` class added to `#cv-inner-content-page-1` and `#cv-inner-content-page-2`
|
||||||
|
3. Skeleton becomes visible (CSS: `.loading .component-wrapper`)
|
||||||
|
4. HTMX swaps content via OOB (out-of-band)
|
||||||
|
5. `.loading` class removed after swap settles
|
||||||
|
6. Skeleton fades away, real content appears
|
||||||
|
|
||||||
|
**Actual Behavior in Tests**:
|
||||||
|
- Steps 1-4 work perfectly
|
||||||
|
- Step 5 FAILS - `.loading` never removed
|
||||||
|
- Step 6 never happens - skeleton stays visible
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 ROOT CAUSE INVESTIGATION
|
||||||
|
|
||||||
|
### Hypothesis 1: Hyperscript Not Executing (LIKELY)
|
||||||
|
The hyperscript on `<body>` that should remove `.loading` is not executing in Playwright:
|
||||||
|
|
||||||
|
**Current Code** (`templates/index.html:141-149`):
|
||||||
|
```hyperscript
|
||||||
|
on htmx:afterSettle
|
||||||
|
if $languageSwitching
|
||||||
|
wait 100ms
|
||||||
|
remove .loading from #cv-inner-content-page-1
|
||||||
|
remove .loading from #cv-inner-content-page-2
|
||||||
|
set $languageSwitching to false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Evidence**:
|
||||||
|
- JavaScript event listeners CAN detect `htmx:afterSettle`
|
||||||
|
- Hyperscript event handlers on same `<body>` element DO NOT execute
|
||||||
|
- Variable `$languageSwitching` gets set to `true` on beforeRequest
|
||||||
|
- But the afterSettle handler never runs (or the condition fails)
|
||||||
|
|
||||||
|
### Attempts Made to Fix
|
||||||
|
|
||||||
|
#### Attempt 1: OOB Swap innerHTML → outerHTML
|
||||||
|
**Theory**: Using `innerHTML` loses hyperscript attributes on swapped elements
|
||||||
|
**Change**: Modified `templates/language-switch.html` line 27, 71
|
||||||
|
**Result**: ❌ No change - still not working
|
||||||
|
|
||||||
|
#### Attempt 2: Event on Page Containers
|
||||||
|
**Theory**: Put cleanup handler directly on the elements being swapped
|
||||||
|
**Change**: Added `_="on htmx:oobAfterSwap..."` to page containers
|
||||||
|
**Result**: ❌ Hyperscript on swapped elements doesn't re-initialize
|
||||||
|
|
||||||
|
#### Attempt 3: Changed Event from oobAfterSwap → afterSettle
|
||||||
|
**Theory**: `afterSettle` is more reliable and fires after all swaps complete
|
||||||
|
**Change**: Switched cleanup trigger to `htmx:afterSettle`
|
||||||
|
**Result**: ❌ Still not executing
|
||||||
|
|
||||||
|
#### Attempt 4: Check Element on afterSettle
|
||||||
|
**Theory**: Maybe we need to check which element triggered
|
||||||
|
**Change**: Check `event.detail.elt.classList.contains('selector-btn')`
|
||||||
|
**Result**: ❌ Fails because `event.detail.elt` is the SWAPPED element, not the button
|
||||||
|
|
||||||
|
#### Attempt 5: Use Flag Variable (CURRENT)
|
||||||
|
**Theory**: Track state with a hyperscript variable instead of checking element
|
||||||
|
**Change**: Set `$languageSwitching` flag on beforeRequest, check it on afterSettle
|
||||||
|
**Result**: ❌ STILL NOT WORKING - flag approach fails too
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 TEST EVIDENCE
|
||||||
|
|
||||||
|
### Test File
|
||||||
|
`tests/mjs/12-skeleton-language-transitions.test.mjs`
|
||||||
|
|
||||||
|
### Debug Tests Created
|
||||||
|
1. `test-oob-events.mjs` - Monitors all HTMX events and class changes
|
||||||
|
2. `test-aftersettle-check.mjs` - Inspects afterSettle event details
|
||||||
|
3. `test-skeleton-verify.mjs` - Visual layout validation
|
||||||
|
|
||||||
|
### Key Test Output
|
||||||
|
```javascript
|
||||||
|
📊 EVENTS CAPTURED:
|
||||||
|
beforeRequest ✅
|
||||||
|
classChange container 1 ✅ .loading
|
||||||
|
classChange container 2 ✅ .loading
|
||||||
|
oobAfterSwap language-selector ✅
|
||||||
|
oobAfterSwap cv-inner-content-page-1 ✅
|
||||||
|
oobAfterSwap cv-inner-content-page-2 ✅
|
||||||
|
afterSwap cv-inner-content-page-1 ✅
|
||||||
|
afterSwap cv-inner-content-page-2 ✅
|
||||||
|
afterSettle cv-inner-content-page-1 ✅
|
||||||
|
afterSettle cv-inner-content-page-2 ✅
|
||||||
|
|
||||||
|
📈 SUMMARY:
|
||||||
|
htmx:beforeRequest: 1 ✅
|
||||||
|
htmx:afterSwap: 3 ✅
|
||||||
|
htmx:oobAfterSwap: 5 ✅
|
||||||
|
htmx:afterSettle: 3 ✅
|
||||||
|
.loading ADDED: 2 ✅
|
||||||
|
.loading REMOVED: 0 ❌ ← THE PROBLEM
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤔 THE MYSTERY
|
||||||
|
|
||||||
|
### Why Hyperscript Isn't Working
|
||||||
|
|
||||||
|
**Facts**:
|
||||||
|
1. Hyperscript library IS loaded (`templates/index.html:68`)
|
||||||
|
2. Hyperscript on `<body>` DOES work for other events (keyboard shortcuts)
|
||||||
|
3. The SAME `<body>` element's beforeRequest handler DOES execute
|
||||||
|
4. But the afterSettle handler DOES NOT execute
|
||||||
|
|
||||||
|
**Possible Explanations**:
|
||||||
|
1. **Playwright incompatibility** - Hyperscript doesn't work in automated browser
|
||||||
|
2. **Event timing issue** - afterSettle fires before hyperscript initializes swapped content
|
||||||
|
3. **Variable scope issue** - `$languageSwitching` variable doesn't persist between events
|
||||||
|
4. **Hyperscript bug** - Issue with how hyperscript handles HTMX events
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎭 MANUAL VS AUTOMATED TESTING
|
||||||
|
|
||||||
|
### User Reports
|
||||||
|
**User confirms**: "it is not working manually for me as well. IT WAS at some point."
|
||||||
|
|
||||||
|
### Current Status
|
||||||
|
- ❌ **Manual testing in real browser**: NOT WORKING - skeleton stays stuck
|
||||||
|
- ❌ **Automated Playwright test**: NOT WORKING - skeleton stays stuck
|
||||||
|
- ✅ **It worked previously**: Feature was working at some earlier point
|
||||||
|
|
||||||
|
### Critical Issue
|
||||||
|
**REGRESSION**: The feature stopped working at some point during development. Need to identify what changed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 FILES INVOLVED
|
||||||
|
|
||||||
|
### Modified Files
|
||||||
|
1. `templates/index.html` (lines 127-149)
|
||||||
|
- Hyperscript on `<body>` for skeleton loader control
|
||||||
|
|
||||||
|
2. `templates/language-switch.html` (lines 25-28, 69-72)
|
||||||
|
- OOB swap targets with `outerHTML` and hyperscript
|
||||||
|
|
||||||
|
3. `templates/cv-content.html` (lines 7-8, 51-52)
|
||||||
|
- Page containers with hyperscript cleanup handlers
|
||||||
|
|
||||||
|
4. `static/css/skeleton.css` (entire file)
|
||||||
|
- Skeleton layout matching actual CV
|
||||||
|
|
||||||
|
5. `templates/partials/sections/header.html`
|
||||||
|
- Dual-state component wrapper structure
|
||||||
|
|
||||||
|
6. `static/js/main.js` (lines 13-14) - INCOMPLETE
|
||||||
|
- Started adding JavaScript flag variable (not finished)
|
||||||
|
|
||||||
|
### Test Files
|
||||||
|
1. `tests/mjs/12-skeleton-language-transitions.test.mjs` - Main test
|
||||||
|
2. `test-oob-events.mjs` - Debug test for events
|
||||||
|
3. Other debug tests in root directory
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ RESOLUTION - FEATURE WORKING PERFECTLY
|
||||||
|
|
||||||
|
**Date**: 2025-11-18
|
||||||
|
**Status**: ✅ **FULLY RESOLVED**
|
||||||
|
**Solution**: Migrated from hyperscript to JavaScript
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 THE FIX
|
||||||
|
|
||||||
|
### Root Cause Identified
|
||||||
|
The hyperscript variable `$languageSwitching` and JavaScript variable `languageSwitching` were **completely separate** - they don't share state. The JavaScript variable existed (line 14 in main.js) but was **never connected to event handlers**.
|
||||||
|
|
||||||
|
### Solution Implemented
|
||||||
|
**Completed the JavaScript migration** that was already partially started:
|
||||||
|
|
||||||
|
1. **Added JavaScript event handlers** (`static/js/main.js` lines 231-273):
|
||||||
|
- `htmx:beforeRequest` - Adds `.loading` class when language button clicked
|
||||||
|
- `htmx:afterSettle` - Removes `.loading` class after swap completes (with 100ms delay)
|
||||||
|
|
||||||
|
2. **Removed hyperscript handlers** from `templates/index.html`:
|
||||||
|
- Deleted lines 133-149 (hyperscript skeleton loader logic)
|
||||||
|
- Kept only scroll and keyboard shortcut handlers
|
||||||
|
|
||||||
|
3. **Exposed flag for testing** (`static/js/main.js` lines 16-19):
|
||||||
|
- Read-only `window.languageSwitching` property for test verification
|
||||||
|
- Maintains encapsulation while enabling testing
|
||||||
|
|
||||||
|
4. **Updated automated tests** (`tests/mjs/12-skeleton-language-transitions.test.mjs`):
|
||||||
|
- Changed from MutationObserver to console.log monitoring
|
||||||
|
- Updated Test 7 to verify JavaScript instead of hyperscript
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 VERIFICATION RESULTS
|
||||||
|
|
||||||
|
### Manual Testing
|
||||||
|
✅ **WORKING PERFECTLY**
|
||||||
|
- Skeleton appears during language transitions
|
||||||
|
- Skeleton disappears after content loads
|
||||||
|
- No stuck loading states
|
||||||
|
- Consistent behavior across multiple switches
|
||||||
|
|
||||||
|
### Automated Testing
|
||||||
|
✅ **7/7 TESTS PASS**
|
||||||
|
```
|
||||||
|
✅ Component Wrapper Structure
|
||||||
|
✅ Skeleton CSS
|
||||||
|
✅ First Language Switch
|
||||||
|
✅ Second Language Switch
|
||||||
|
✅ Third Language Switch
|
||||||
|
✅ No Stuck Loading States
|
||||||
|
✅ JavaScript Event Handlers
|
||||||
|
```
|
||||||
|
|
||||||
|
### Browser Console Verification
|
||||||
|
```
|
||||||
|
Skeleton loader: Added .loading class to page containers
|
||||||
|
Skeleton loader: Removed .loading class from page containers
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 FINAL STATE
|
||||||
|
|
||||||
|
**All files working correctly:**
|
||||||
|
- ✅ `static/js/main.js` - JavaScript event handlers implemented
|
||||||
|
- ✅ `templates/index.html` - Hyperscript skeleton handlers removed
|
||||||
|
- ✅ `static/css/skeleton.css` - Skeleton layout perfect
|
||||||
|
- ✅ `templates/partials/sections/header.html` - Component wrapper structure
|
||||||
|
- ✅ `tests/mjs/12-skeleton-language-transitions.test.mjs` - All tests passing
|
||||||
|
|
||||||
|
**No stuck states:**
|
||||||
|
- ✅ Page 1 clean (no .loading)
|
||||||
|
- ✅ Page 2 clean (no .loading)
|
||||||
|
- ✅ JavaScript flag properly resets
|
||||||
|
|
||||||
|
**Console logs:**
|
||||||
|
- ✅ NO ERRORS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎓 LESSONS LEARNED
|
||||||
|
|
||||||
|
1. **Hyperscript variables ≠ JavaScript variables** - They live in different scopes
|
||||||
|
2. **JavaScript is more reliable** for HTMX event handling in Playwright tests
|
||||||
|
3. **Always complete migrations** - The JavaScript variable existed but wasn't used
|
||||||
|
4. **Console.log monitoring** more reliable than MutationObserver for rapid changes
|
||||||
|
5. **Test what matters** - Expose implementation details only when necessary for testing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧹 CLEANUP NEEDED
|
||||||
|
|
||||||
|
Optional cleanup tasks:
|
||||||
|
- [ ] Remove temporary test files (`test-oob-events.mjs`, `test-aftersettle-check.mjs`, `test-manual-skeleton.mjs`)
|
||||||
|
- [ ] Archive this debug status document or move to `/doc`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Resolution Date**: 2025-11-18 19:12 UTC
|
||||||
|
**Resolved By**: JavaScript migration (Option 2 from original options)
|
||||||
|
**Final Status**: 🎉 **SKELETON LOADERS VALIDATED!**
|
||||||
@@ -1930,31 +1930,57 @@ Time 600ms+: opacity=NaN ← Element destroyed!
|
|||||||
```
|
```
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Language switch with skeleton loading
|
// static/js/main.js - Skeleton loader for language transitions
|
||||||
htmx.on('htmx:beforeSwap', function(evt) {
|
let languageSwitching = false;
|
||||||
// Show skeletons BEFORE swap
|
|
||||||
document.querySelectorAll('.component-wrapper').forEach(wrapper => {
|
// Add .loading class when language button is clicked
|
||||||
wrapper.classList.add('loading');
|
document.addEventListener('htmx:beforeRequest', function(evt) {
|
||||||
});
|
const element = evt.detail.elt;
|
||||||
|
if (element && element.classList && element.classList.contains('selector-btn')) {
|
||||||
|
// Set flag to track language switching
|
||||||
|
languageSwitching = true;
|
||||||
|
|
||||||
|
// Add loading class to page containers
|
||||||
|
const page1 = document.getElementById('cv-inner-content-page-1');
|
||||||
|
const page2 = document.getElementById('cv-inner-content-page-2');
|
||||||
|
if (page1) page1.classList.add('loading');
|
||||||
|
if (page2) page2.classList.add('loading');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
htmx.on('htmx:afterSettle', function(evt) {
|
// Remove .loading class after language transition completes
|
||||||
// Hide skeletons AFTER content settles
|
document.addEventListener('htmx:afterSettle', function(evt) {
|
||||||
document.querySelectorAll('.component-wrapper').forEach(wrapper => {
|
if (languageSwitching) {
|
||||||
wrapper.classList.remove('loading');
|
// Wait for final render to complete
|
||||||
});
|
setTimeout(function() {
|
||||||
|
const page1 = document.getElementById('cv-inner-content-page-1');
|
||||||
|
const page2 = document.getElementById('cv-inner-content-page-2');
|
||||||
|
if (page1) page1.classList.remove('loading');
|
||||||
|
if (page2) page2.classList.remove('loading');
|
||||||
|
|
||||||
|
// Reset flag
|
||||||
|
languageSwitching = false;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
**Architecture Pattern:**
|
**Architecture Pattern:**
|
||||||
1. **User clicks language toggle** → HTMX `beforeSwap` event fires
|
1. **User clicks language button** → HTMX `htmx:beforeRequest` event fires
|
||||||
2. **JavaScript adds `.loading` class** → Triggers CSS transition (actual-content opacity: 1 → 0, skeleton-content opacity: 0 → 1)
|
2. **JavaScript detects `.selector-btn` click** → Sets `languageSwitching` flag
|
||||||
3. **Skeleton appears** → Smooth 250ms fade-in with shimmer animation
|
3. **JavaScript adds `.loading` to parent containers** → Triggers CSS cascade to child `.component-wrapper` elements
|
||||||
4. **HTMX fetches new language content** → Server renders and returns HTML
|
4. **Skeleton appears** → CSS transition (actual-content opacity: 1 → 0, skeleton-content opacity: 0 → 1) + shimmer animation
|
||||||
5. **HTMX swaps content** → New actual-content replaces old
|
5. **HTMX fetches new language content** → Server renders and returns HTML via OOB swaps
|
||||||
6. **afterSettle event fires** → JavaScript removes `.loading` class
|
6. **HTMX swaps content** → Out-of-band (OOB) swap replaces page containers
|
||||||
7. **Skeleton fades out** → Smooth 250ms fade (skeleton opacity: 1 → 0, actual-content opacity: 0 → 1)
|
7. **`htmx:afterSettle` event fires** → JavaScript waits 100ms for final render
|
||||||
8. **Result** → Smooth, professional loading experience with zero layout shift
|
8. **JavaScript removes `.loading` class** → CSS transition reverses (skeleton opacity: 1 → 0, actual-content opacity: 0 → 1)
|
||||||
|
9. **Result** → Smooth, professional loading experience with zero layout shift
|
||||||
|
|
||||||
|
**Why JavaScript Instead of Hyperscript:**
|
||||||
|
- ✅ **Reliable Playwright testing** - JavaScript event handlers work consistently in automated tests
|
||||||
|
- ✅ **Debugging** - Console.log statements provide clear execution tracking
|
||||||
|
- ✅ **Maintainability** - Standard JavaScript patterns familiar to all developers
|
||||||
|
- ✅ **Performance** - Direct DOM manipulation, no hyperscript parser overhead
|
||||||
|
|
||||||
**Benefits:**
|
**Benefits:**
|
||||||
- ✅ **Zero layout shift** - Skeletons match exact dimensions of actual content
|
- ✅ **Zero layout shift** - Skeletons match exact dimensions of actual content
|
||||||
@@ -1966,16 +1992,24 @@ htmx.on('htmx:afterSettle', function(evt) {
|
|||||||
- ✅ **Reusable** - Works for any HTMX swap operation, not just language switch
|
- ✅ **Reusable** - Works for any HTMX swap operation, not just language switch
|
||||||
|
|
||||||
**Implementation Locations:**
|
**Implementation Locations:**
|
||||||
- **CSS:** `static/css/skeleton.css` (341 lines) - Complete skeleton system
|
- **CSS:** `static/css/skeleton.css` - Complete skeleton system with shimmer animations
|
||||||
- **Template:** `templates/partials/skeleton-loader.html` - Skeleton placeholders
|
- **JavaScript:** `static/js/main.js` (lines 231-273) - HTMX event handlers for skeleton control
|
||||||
- **Component wrappers:** Each CV section wrapped with `.component-wrapper`
|
- **Templates:** `templates/partials/sections/header.html` - Component wrapper structure
|
||||||
- **HTMX events:** `static/js/main.js` - beforeSwap/afterSettle listeners
|
- **Page Containers:** `templates/cv-content.html` - Parent containers receiving `.loading` class
|
||||||
|
- **Language Switch:** `templates/language-switch.html` - `.selector-btn` triggers skeleton display
|
||||||
|
|
||||||
**Testing:** Automated tests in `tests/mjs/12-skeleton-language-transitions.test.mjs` verify:
|
**Testing:** Automated tests in `tests/mjs/12-skeleton-language-transitions.test.mjs` verify:
|
||||||
- Skeletons appear during language switch
|
- ✅ Component wrapper structure (dual-state: actual + skeleton content)
|
||||||
- Content replaced without full page reload
|
- ✅ Skeleton CSS loaded (shimmer animation verified)
|
||||||
- Skeletons removed after content loads
|
- ✅ First language switch (EN → ES) - Loading class added/removed
|
||||||
- Zero layout shift during transition
|
- ✅ Second language switch (ES → EN) - Consistent behavior
|
||||||
|
- ✅ Third language switch (EN → ES) - Regression check
|
||||||
|
- ✅ No stuck loading states (all containers clean after transition)
|
||||||
|
- ✅ JavaScript event handlers configured (languageSwitching flag)
|
||||||
|
|
||||||
|
**Test Results:** 7/7 tests pass - Complete validation of skeleton loader functionality
|
||||||
|
|
||||||
|
**Run Test:** `bun tests/mjs/12-skeleton-language-transitions.test.mjs`
|
||||||
|
|
||||||
**Pixel-Perfect Matching:**
|
**Pixel-Perfect Matching:**
|
||||||
|
|
||||||
@@ -2354,7 +2388,7 @@ set #shortcuts-button's *zoom to inverseZoom
|
|||||||
3. `templates/partials/navigation/hamburger-menu.html`
|
3. `templates/partials/navigation/hamburger-menu.html`
|
||||||
- Removed conflicting hyperscript from show zoom button
|
- Removed conflicting hyperscript from show zoom button
|
||||||
|
|
||||||
4. `MODERN-WEB-TECHNIQUES.md`
|
4. `2-MODERN-WEB-TECHNIQUES.md`
|
||||||
- Updated documentation to reflect fixes
|
- Updated documentation to reflect fixes
|
||||||
- Added technical lessons learned
|
- Added technical lessons learned
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1190,7 +1190,7 @@ curl -H "Referer: https://evil.com/" \
|
|||||||
**Current Configuration:**
|
**Current Configuration:**
|
||||||
- **Endpoint:** `/export/pdf`
|
- **Endpoint:** `/export/pdf`
|
||||||
- **Limit:** 3 requests per minute per IP
|
- **Limit:** 3 requests per minute per IP
|
||||||
- **Window:** 1 minute (rolling)
|
- **Window:** 1 minute (rolling)
|
||||||
- **Response:** 429 Too Many Requests when exceeded
|
- **Response:** 429 Too Many Requests when exceeded
|
||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
@@ -1266,7 +1266,7 @@ pdfRateLimiter := middleware.NewRateLimiter(5, 1*time.Hour)
|
|||||||
- Input validation
|
- Input validation
|
||||||
- Error message sanitization (internal errors hidden)
|
- Error message sanitization (internal errors hidden)
|
||||||
- Timeouts on all operations
|
- Timeouts on all operations
|
||||||
- Graceful shutdown
|
- Graceful shutdown
|
||||||
- Origin checking (prevents external hotlinking)
|
- Origin checking (prevents external hotlinking)
|
||||||
- Rate limiting (PDF endpoint: 3 requests/min per IP)
|
- Rate limiting (PDF endpoint: 3 requests/min per IP)
|
||||||
- IP-based tracking (supports reverse proxies)
|
- IP-based tracking (supports reverse proxies)
|
||||||
@@ -1974,7 +1974,7 @@ go tool trace trace.out
|
|||||||
---
|
---
|
||||||
|
|
||||||
**Last Updated:** November 12, 2025
|
**Last Updated:** November 12, 2025
|
||||||
**API Version:** 1.1.0
|
**API Version:** 1.1.0
|
||||||
**Documentation Version:** 1.1.0
|
**Documentation Version:** 1.1.0
|
||||||
### Support
|
### Support
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ This CV/Resume application is designed to be easily customizable. You can adapt
|
|||||||
|
|
||||||
### Tools Needed
|
### Tools Needed
|
||||||
- **Text editor**: VS Code, Sublime Text, or any editor
|
- **Text editor**: VS Code, Sublime Text, or any editor
|
||||||
- **Go 1.25.1+**: For building and testing (see [DEPLOYMENT.md](DEPLOYMENT.md))
|
- **Go 1.25.1+**: For building and testing (see [8-DEPLOYMENT.md](8-DEPLOYMENT.md))
|
||||||
- **Git**: For version control (optional but recommended)
|
- **Git**: For version control (optional but recommended)
|
||||||
- **Browser**: For testing (Chrome/Firefox recommended)
|
- **Browser**: For testing (Chrome/Firefox recommended)
|
||||||
|
|
||||||
@@ -1103,7 +1103,7 @@ open http://localhost:1999
|
|||||||
- ✅ Comply with GDPR, CCPA, or local privacy laws
|
- ✅ Comply with GDPR, CCPA, or local privacy laws
|
||||||
- ✅ Update privacy policy when changing analytics providers
|
- ✅ Update privacy policy when changing analytics providers
|
||||||
|
|
||||||
**See [PRIVACY.md](PRIVACY.md) for privacy policy template.**
|
**See [10-PRIVACY.md](10-PRIVACY.md) for privacy policy template.**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1751,7 +1751,7 @@ python3 -m json.tool data/cv-en.json
|
|||||||
After customization:
|
After customization:
|
||||||
1. **Test thoroughly** with checklist above
|
1. **Test thoroughly** with checklist above
|
||||||
2. **Generate PDF** and verify quality
|
2. **Generate PDF** and verify quality
|
||||||
3. **Deploy** using [DEPLOYMENT.md](DEPLOYMENT.md) guide
|
3. **Deploy** using [8-DEPLOYMENT.md](8-DEPLOYMENT.md) guide
|
||||||
4. **Set up CI/CD** for automatic deployments
|
4. **Set up CI/CD** for automatic deployments
|
||||||
5. **Share** your customized CV!
|
5. **Share** your customized CV!
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1050,4 +1050,4 @@ curl http://localhost:1999/health
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**For customization, see CUSTOMIZATION.md**
|
**For customization, see [7-CUSTOMIZATION.md](7-CUSTOMIZATION.md)**
|
||||||
|
|||||||
+1
-1
@@ -435,7 +435,7 @@ proxy_set_header X-Real-IP $remote_addr;
|
|||||||
**Solution:** This is normal browser behavior. The middleware checks `Referer` header as fallback, which browsers do send for navigation.
|
**Solution:** This is normal browser behavior. The middleware checks `Referer` header as fallback, which browsers do send for navigation.
|
||||||
|
|
||||||
For technical API details, see [3-API.md](3-API.md#security-protection).
|
For technical API details, see [3-API.md](3-API.md#security-protection).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Known Security Considerations
|
## Known Security Considerations
|
||||||
|
|||||||
+31
-31
@@ -9,25 +9,25 @@
|
|||||||
### For Developers
|
### For Developers
|
||||||
|
|
||||||
**Getting Started**
|
**Getting Started**
|
||||||
- [Architecture Overview](ARCHITECTURE.md) - System design and Go backend architecture
|
- [1. Architecture Overview](1-ARCHITECTURE.md) - System design and Go backend architecture
|
||||||
- [Modern Web Techniques](MODERN-WEB-TECHNIQUES.md) - Frontend architecture (HTMX, Hyperscript, CSS) ⭐
|
- [2. Modern Web Techniques](2-MODERN-WEB-TECHNIQUES.md) - Frontend architecture (HTMX, Hyperscript, CSS) ⭐
|
||||||
- [API Reference](API.md) - Complete API documentation with endpoints and responses
|
- [3. API Reference](3-API.md) - Complete API documentation with endpoints and responses
|
||||||
|
|
||||||
**Technical Implementation**
|
**Technical Implementation**
|
||||||
- [Hyperscript Rules](HYPERSCRIPT-RULES.md) - Hyperscript conventions and best practices
|
- [4. Hyperscript Rules](4-HYPERSCRIPT-RULES.md) - Hyperscript conventions and best practices
|
||||||
- [Zoom Implementation](ZOOM_IMPLEMENTATION.md) - Custom zoom feature technical details
|
- [5. Zoom Implementation](5-ZOOM-IMPLEMENTATION.md) - Custom zoom feature technical details
|
||||||
|
|
||||||
**Deployment & Operations**
|
**Deployment & Operations**
|
||||||
- [Deployment Guide](DEPLOYMENT.md) - Production deployment instructions
|
- [8. Deployment Guide](8-DEPLOYMENT.md) - Production deployment instructions
|
||||||
- [Security Policies](SECURITY.md) - Security guidelines and vulnerability reporting
|
- [9. Security Policies](9-SECURITY.md) - Security guidelines and vulnerability reporting
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### For Users & Customizers
|
### For Users & Customizers
|
||||||
|
|
||||||
- [User Guide](USER_GUIDE.md) - End-user documentation for CV features
|
- [6. User Guide](6-USER-GUIDE.md) - End-user documentation for CV features
|
||||||
- [Customization Guide](CUSTOMIZATION.md) - How to customize your CV content and styling
|
- [7. Customization Guide](7-CUSTOMIZATION.md) - How to customize your CV content and styling
|
||||||
- [Privacy Policy](PRIVACY.md) - Data handling and privacy information
|
- [10. Privacy Policy](10-PRIVACY.md) - Data handling and privacy information
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -35,23 +35,23 @@
|
|||||||
|
|
||||||
### Core Technical Documentation
|
### Core Technical Documentation
|
||||||
|
|
||||||
| Document | Purpose | Audience |
|
| # | Document | Purpose | Audience |
|
||||||
|----------|---------|----------|
|
|---|----------|---------|----------|
|
||||||
| [ARCHITECTURE.md](ARCHITECTURE.md) | Go backend architecture, package structure, design patterns | Backend developers |
|
| 1 | [ARCHITECTURE.md](1-ARCHITECTURE.md) | Go backend architecture, package structure, design patterns | Backend developers |
|
||||||
| [MODERN-WEB-TECHNIQUES.md](MODERN-WEB-TECHNIQUES.md) | HTMX/Hyperscript frontend architecture, component patterns, ADRs | Frontend developers |
|
| 2 | [MODERN-WEB-TECHNIQUES.md](2-MODERN-WEB-TECHNIQUES.md) | HTMX/Hyperscript frontend architecture, component patterns, ADRs | Frontend developers |
|
||||||
| [API.md](API.md) | Complete API reference with all endpoints | API consumers, integrators |
|
| 3 | [API.md](3-API.md) | Complete API reference with all endpoints | API consumers, integrators |
|
||||||
| [ZOOM_IMPLEMENTATION.md](ZOOM_IMPLEMENTATION.md) | Zoom feature implementation details | Feature developers |
|
| 4 | [HYPERSCRIPT-RULES.md](4-HYPERSCRIPT-RULES.md) | Hyperscript coding conventions | Frontend developers |
|
||||||
| [HYPERSCRIPT-RULES.md](HYPERSCRIPT-RULES.md) | Hyperscript coding conventions | Frontend developers |
|
| 5 | [ZOOM_IMPLEMENTATION.md](5-ZOOM-IMPLEMENTATION.md) | Zoom feature implementation details | Feature developers |
|
||||||
|
|
||||||
### User & Operations Documentation
|
### User & Operations Documentation
|
||||||
|
|
||||||
| Document | Purpose | Audience |
|
| # | Document | Purpose | Audience |
|
||||||
|----------|---------|----------|
|
|---|----------|---------|----------|
|
||||||
| [USER_GUIDE.md](USER_GUIDE.md) | End-user feature documentation | CV users |
|
| 6 | [USER_GUIDE.md](6-USER-GUIDE.md) | End-user feature documentation | CV users |
|
||||||
| [CUSTOMIZATION.md](CUSTOMIZATION.md) | Content and style customization | CV customizers |
|
| 7 | [CUSTOMIZATION.md](7-CUSTOMIZATION.md) | Content and style customization | CV customizers |
|
||||||
| [DEPLOYMENT.md](DEPLOYMENT.md) | Deployment instructions and operations | DevOps, site operators |
|
| 8 | [DEPLOYMENT.md](8-DEPLOYMENT.md) | Deployment instructions and operations | DevOps, site operators |
|
||||||
| [SECURITY.md](SECURITY.md) | Security policies and reporting | Security teams |
|
| 9 | [SECURITY.md](9-SECURITY.md) | Security policies and reporting | Security teams |
|
||||||
| [PRIVACY.md](PRIVACY.md) | Privacy policy and data handling | Legal, compliance |
|
| 10 | [PRIVACY.md](10-PRIVACY.md) | Privacy policy and data handling | Legal, compliance |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -85,25 +85,25 @@
|
|||||||
### "I want to..."
|
### "I want to..."
|
||||||
|
|
||||||
**...understand the system architecture**
|
**...understand the system architecture**
|
||||||
→ Start with [ARCHITECTURE.md](ARCHITECTURE.md) (backend) and [MODERN-WEB-TECHNIQUES.md](MODERN-WEB-TECHNIQUES.md) (frontend)
|
→ Start with [1-ARCHITECTURE.md](1-ARCHITECTURE.md) (backend) and [2-MODERN-WEB-TECHNIQUES.md](2-MODERN-WEB-TECHNIQUES.md) (frontend)
|
||||||
|
|
||||||
**...add a new feature**
|
**...add a new feature**
|
||||||
→ Read [MODERN-WEB-TECHNIQUES.md](MODERN-WEB-TECHNIQUES.md) for frontend patterns, [API.md](API.md) for backend APIs
|
→ Read [2-MODERN-WEB-TECHNIQUES.md](2-MODERN-WEB-TECHNIQUES.md) for frontend patterns, [3-API.md](3-API.md) for backend APIs
|
||||||
|
|
||||||
**...customize my CV content**
|
**...customize my CV content**
|
||||||
→ Follow [CUSTOMIZATION.md](CUSTOMIZATION.md) for content and styling changes
|
→ Follow [7-CUSTOMIZATION.md](7-CUSTOMIZATION.md) for content and styling changes
|
||||||
|
|
||||||
**...deploy to production**
|
**...deploy to production**
|
||||||
→ Use [DEPLOYMENT.md](DEPLOYMENT.md) for step-by-step deployment instructions
|
→ Use [8-DEPLOYMENT.md](8-DEPLOYMENT.md) for step-by-step deployment instructions
|
||||||
|
|
||||||
**...understand HTMX patterns**
|
**...understand HTMX patterns**
|
||||||
→ Check [MODERN-WEB-TECHNIQUES.md](MODERN-WEB-TECHNIQUES.md) Section 6 (HTMX Patterns)
|
→ Check [2-MODERN-WEB-TECHNIQUES.md](2-MODERN-WEB-TECHNIQUES.md) Section 6 (HTMX Patterns)
|
||||||
|
|
||||||
**...write Hyperscript code**
|
**...write Hyperscript code**
|
||||||
→ Follow conventions in [HYPERSCRIPT-RULES.md](HYPERSCRIPT-RULES.md)
|
→ Follow conventions in [4-HYPERSCRIPT-RULES.md](4-HYPERSCRIPT-RULES.md)
|
||||||
|
|
||||||
**...report a security issue**
|
**...report a security issue**
|
||||||
→ See [SECURITY.md](SECURITY.md) for responsible disclosure process
|
→ See [9-SECURITY.md](9-SECURITY.md) for responsible disclosure process
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,14 @@
|
|||||||
// Flag to keep header visible after navigation
|
// Flag to keep header visible after navigation
|
||||||
let keepHeaderVisible = false;
|
let keepHeaderVisible = false;
|
||||||
|
|
||||||
|
// Flag to track language switch in progress
|
||||||
|
let languageSwitching = false;
|
||||||
|
|
||||||
|
// Expose for testing (read-only access)
|
||||||
|
Object.defineProperty(window, 'languageSwitching', {
|
||||||
|
get: () => languageSwitching
|
||||||
|
});
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// NAVIGATION & MENU SYSTEM
|
// NAVIGATION & MENU SYSTEM
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -225,6 +233,50 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Skeleton loader for language transitions
|
||||||
|
// Add .loading class when language button is clicked
|
||||||
|
document.addEventListener('htmx:beforeRequest', function(evt) {
|
||||||
|
try {
|
||||||
|
const element = evt.detail.elt;
|
||||||
|
if (element && element.classList && element.classList.contains('selector-btn')) {
|
||||||
|
// Set flag to track language switching
|
||||||
|
languageSwitching = true;
|
||||||
|
|
||||||
|
// Add loading class to page containers
|
||||||
|
const page1 = document.getElementById('cv-inner-content-page-1');
|
||||||
|
const page2 = document.getElementById('cv-inner-content-page-2');
|
||||||
|
if (page1) page1.classList.add('loading');
|
||||||
|
if (page2) page2.classList.add('loading');
|
||||||
|
|
||||||
|
console.log('Skeleton loader: Added .loading class to page containers');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error in skeleton loader beforeRequest handler:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove .loading class after language transition completes
|
||||||
|
document.addEventListener('htmx:afterSettle', function(evt) {
|
||||||
|
try {
|
||||||
|
if (languageSwitching) {
|
||||||
|
// Wait for final render to complete
|
||||||
|
setTimeout(function() {
|
||||||
|
const page1 = document.getElementById('cv-inner-content-page-1');
|
||||||
|
const page2 = document.getElementById('cv-inner-content-page-2');
|
||||||
|
if (page1) page1.classList.remove('loading');
|
||||||
|
if (page2) page2.classList.remove('loading');
|
||||||
|
|
||||||
|
// Reset flag
|
||||||
|
languageSwitching = false;
|
||||||
|
|
||||||
|
console.log('Skeleton loader: Removed .loading class from page containers');
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error in skeleton loader afterSettle handler:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Sync toggle states between desktop and mobile menu
|
// Sync toggle states between desktop and mobile menu
|
||||||
document.addEventListener('htmx:afterSwap', function(evt) {
|
document.addEventListener('htmx:afterSwap', function(evt) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<!-- PAGE 1 -->
|
<!-- PAGE 1 -->
|
||||||
<div class="cv-page page-1">
|
<div class="cv-page page-1">
|
||||||
<div id="cv-inner-content-page-1" class="cv-page-content-wrapper"
|
<div id="cv-inner-content-page-1" class="cv-page-content-wrapper"
|
||||||
_="on htmx:oobAfterSwap wait 100ms then remove .loading from me">
|
_="on htmx:afterSettle wait 100ms then remove .loading from me">
|
||||||
{{template "title-badges" .}}
|
{{template "title-badges" .}}
|
||||||
|
|
||||||
<!-- Page 1 Content Grid: Left Sidebar + Main Content -->
|
<!-- Page 1 Content Grid: Left Sidebar + Main Content -->
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
<!-- PAGE 2 -->
|
<!-- PAGE 2 -->
|
||||||
<div class="cv-page page-2">
|
<div class="cv-page page-2">
|
||||||
<div id="cv-inner-content-page-2" class="cv-page-content-wrapper"
|
<div id="cv-inner-content-page-2" class="cv-page-content-wrapper"
|
||||||
_="on htmx:oobAfterSwap wait 100ms then remove .loading from me">
|
_="on htmx:afterSettle wait 100ms then remove .loading from me">
|
||||||
{{template "title-badges" .}}
|
{{template "title-badges" .}}
|
||||||
|
|
||||||
<!-- Page 2 Content Grid: Main Content + Right Sidebar -->
|
<!-- Page 2 Content Grid: Main Content + Right Sidebar -->
|
||||||
|
|||||||
@@ -128,15 +128,6 @@
|
|||||||
_="on load call initScrollBehavior()
|
_="on load call initScrollBehavior()
|
||||||
on scroll from window call handleScroll()
|
on scroll from window call handleScroll()
|
||||||
|
|
||||||
-- Skeleton loader for language transitions (global listener)
|
|
||||||
-- Add .loading to PARENT containers that persist across OOB swaps
|
|
||||||
on htmx:beforeRequest
|
|
||||||
if event.detail.elt.classList.contains('selector-btn')
|
|
||||||
add .loading to #cv-inner-content-page-1
|
|
||||||
add .loading to #cv-inner-content-page-2
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
on keydown
|
on keydown
|
||||||
set tagName to event.target.tagName
|
set tagName to event.target.tagName
|
||||||
set isInputField to (tagName is 'INPUT' or tagName is 'TEXTAREA')
|
set isInputField to (tagName is 'INPUT' or tagName is 'TEXTAREA')
|
||||||
|
|||||||
@@ -24,8 +24,8 @@
|
|||||||
<!-- Out-of-band swap: Page 1 content wrapper with fade transition -->
|
<!-- Out-of-band swap: Page 1 content wrapper with fade transition -->
|
||||||
<div id="cv-inner-content-page-1"
|
<div id="cv-inner-content-page-1"
|
||||||
class="cv-page-content-wrapper"
|
class="cv-page-content-wrapper"
|
||||||
hx-swap-oob="innerHTML"
|
hx-swap-oob="outerHTML"
|
||||||
_="on htmx:oobAfterSwap wait 100ms then remove .loading from me">
|
_="on htmx:afterSettle wait 100ms then remove .loading from me">
|
||||||
{{template "title-badges" .}}
|
{{template "title-badges" .}}
|
||||||
|
|
||||||
<!-- Page 1 Content Grid: Left Sidebar + Main Content -->
|
<!-- Page 1 Content Grid: Left Sidebar + Main Content -->
|
||||||
@@ -68,8 +68,8 @@
|
|||||||
<!-- Out-of-band swap: Page 2 content wrapper with fade transition -->
|
<!-- Out-of-band swap: Page 2 content wrapper with fade transition -->
|
||||||
<div id="cv-inner-content-page-2"
|
<div id="cv-inner-content-page-2"
|
||||||
class="cv-page-content-wrapper"
|
class="cv-page-content-wrapper"
|
||||||
hx-swap-oob="innerHTML"
|
hx-swap-oob="outerHTML"
|
||||||
_="on htmx:oobAfterSwap wait 100ms then remove .loading from me">
|
_="on htmx:afterSettle wait 100ms then remove .loading from me">
|
||||||
{{template "title-badges" .}}
|
{{template "title-badges" .}}
|
||||||
|
|
||||||
<!-- Page 2 Content Grid: Main Content + Right Sidebar -->
|
<!-- Page 2 Content Grid: Main Content + Right Sidebar -->
|
||||||
|
|||||||
+14
-5
@@ -292,11 +292,20 @@ When adding tests:
|
|||||||
**Philosophy**: Zero redundancy - Every test is essential and unique
|
**Philosophy**: Zero redundancy - Every test is essential and unique
|
||||||
|
|
||||||
### 12-skeleton-language-transitions.test.mjs
|
### 12-skeleton-language-transitions.test.mjs
|
||||||
**Purpose**: Skeleton loader display during language transitions
|
**Purpose**: Skeleton loader animations during language transitions
|
||||||
- ✅ Skeleton loaders appear during language switch
|
- ✅ Component wrapper structure (dual-state: actual + skeleton content)
|
||||||
- ✅ Content replaced without full page reload
|
- ✅ Skeleton CSS loaded (shimmer animation verified)
|
||||||
- ✅ Skeleton removed after content loads
|
- ✅ First language switch (EN → ES) - Loading class added/removed
|
||||||
- ✅ No layout shift during transition
|
- ✅ Second language switch (ES → EN) - Consistent behavior
|
||||||
|
- ✅ Third language switch (EN → ES) - Regression check
|
||||||
|
- ✅ No stuck loading states (all containers clean after transition)
|
||||||
|
- ✅ JavaScript event handlers configured (languageSwitching flag)
|
||||||
|
|
||||||
|
**Implementation**: JavaScript event handlers in `static/js/main.js`
|
||||||
|
- `htmx:beforeRequest` - Adds `.loading` class to page containers
|
||||||
|
- `htmx:afterSettle` - Removes `.loading` class after swap completes (100ms delay)
|
||||||
|
|
||||||
|
**Critical**: Migrated from hyperscript to JavaScript for reliable Playwright testing
|
||||||
|
|
||||||
**Run**: `bun tests/mjs/12-skeleton-language-transitions.test.mjs`
|
**Run**: `bun tests/mjs/12-skeleton-language-transitions.test.mjs`
|
||||||
|
|
||||||
|
|||||||
@@ -86,45 +86,27 @@ async function testSkeletonLoaders() {
|
|||||||
// ========================================================================
|
// ========================================================================
|
||||||
console.log("\n4️⃣ Testing First Language Switch (EN → ES)...");
|
console.log("\n4️⃣ Testing First Language Switch (EN → ES)...");
|
||||||
|
|
||||||
// Set up monitoring
|
// Set up console log monitoring to track our JavaScript skeleton loader messages
|
||||||
await page.evaluate(() => {
|
const consoleMessages = [];
|
||||||
window.loadingEvents = [];
|
page.on('console', msg => {
|
||||||
|
const text = msg.text();
|
||||||
const containers = [
|
if (text.includes('Skeleton loader:')) {
|
||||||
document.querySelector('#cv-inner-content-page-1'),
|
consoleMessages.push(text);
|
||||||
document.querySelector('#cv-inner-content-page-2')
|
|
||||||
];
|
|
||||||
|
|
||||||
containers.forEach((container, index) => {
|
|
||||||
if (container) {
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
mutations.forEach((mutation) => {
|
|
||||||
if (mutation.attributeName === 'class') {
|
|
||||||
window.loadingEvents.push({
|
|
||||||
time: Date.now(),
|
|
||||||
container: index + 1,
|
|
||||||
hasLoading: mutation.target.classList.contains('loading')
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
observer.observe(container, { attributes: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click Spanish button
|
// Click Spanish button
|
||||||
await page.click('.selector-btn[aria-label="Español"]');
|
await page.click('.selector-btn[aria-label="Español"]');
|
||||||
await page.waitForTimeout(800);
|
await page.waitForTimeout(800);
|
||||||
|
|
||||||
const switch1 = await page.evaluate(() => window.loadingEvents || []);
|
// Check the console messages
|
||||||
const loadingAdded1 = switch1.filter(e => e.hasLoading).length;
|
const addedMessages1 = consoleMessages.filter(m => m.includes('Added .loading')).length;
|
||||||
const loadingRemoved1 = switch1.filter(e => !e.hasLoading).length;
|
const removedMessages1 = consoleMessages.filter(m => m.includes('Removed .loading')).length;
|
||||||
|
|
||||||
console.log(` Parent containers got .loading: ${loadingAdded1 > 0 ? '✅' : '❌'} (${loadingAdded1} events)`);
|
console.log(` Skeleton loader added .loading: ${addedMessages1 > 0 ? '✅' : '❌'} (${addedMessages1} events)`);
|
||||||
console.log(` Parent containers lost .loading: ${loadingRemoved1 > 0 ? '✅' : '❌'} (${loadingRemoved1} events)`);
|
console.log(` Skeleton loader removed .loading: ${removedMessages1 > 0 ? '✅' : '❌'} (${removedMessages1} events)`);
|
||||||
|
|
||||||
const switch1Passed = loadingAdded1 > 0 && loadingRemoved1 > 0;
|
const switch1Passed = addedMessages1 > 0 && removedMessages1 > 0;
|
||||||
console.log(` ${switch1Passed ? '✅ PASS' : '❌ FAIL'} - Skeleton displayed during transition`);
|
console.log(` ${switch1Passed ? '✅ PASS' : '❌ FAIL'} - Skeleton displayed during transition`);
|
||||||
testResults.push({ test: 'First Language Switch', passed: switch1Passed });
|
testResults.push({ test: 'First Language Switch', passed: switch1Passed });
|
||||||
|
|
||||||
@@ -133,21 +115,22 @@ async function testSkeletonLoaders() {
|
|||||||
// ========================================================================
|
// ========================================================================
|
||||||
console.log("\n5️⃣ Testing Second Language Switch (ES → EN)...");
|
console.log("\n5️⃣ Testing Second Language Switch (ES → EN)...");
|
||||||
|
|
||||||
// Clear events
|
// Clear console messages
|
||||||
await page.evaluate(() => { window.loadingEvents = []; });
|
const beforeSwitch2 = consoleMessages.length;
|
||||||
|
|
||||||
// Click English button
|
// Click English button
|
||||||
await page.click('.selector-btn[aria-label="English"]');
|
await page.click('.selector-btn[aria-label="English"]');
|
||||||
await page.waitForTimeout(800);
|
await page.waitForTimeout(800);
|
||||||
|
|
||||||
const switch2 = await page.evaluate(() => window.loadingEvents || []);
|
// Check new console messages since last switch
|
||||||
const loadingAdded2 = switch2.filter(e => e.hasLoading).length;
|
const newMessages2 = consoleMessages.slice(beforeSwitch2);
|
||||||
const loadingRemoved2 = switch2.filter(e => !e.hasLoading).length;
|
const addedMessages2 = newMessages2.filter(m => m.includes('Added .loading')).length;
|
||||||
|
const removedMessages2 = newMessages2.filter(m => m.includes('Removed .loading')).length;
|
||||||
|
|
||||||
console.log(` Parent containers got .loading: ${loadingAdded2 > 0 ? '✅' : '❌'} (${loadingAdded2} events)`);
|
console.log(` Skeleton loader added .loading: ${addedMessages2 > 0 ? '✅' : '❌'} (${addedMessages2} events)`);
|
||||||
console.log(` Parent containers lost .loading: ${loadingRemoved2 > 0 ? '✅' : '❌'} (${loadingRemoved2} events)`);
|
console.log(` Skeleton loader removed .loading: ${removedMessages2 > 0 ? '✅' : '❌'} (${removedMessages2} events)`);
|
||||||
|
|
||||||
const switch2Passed = loadingAdded2 > 0 && loadingRemoved2 > 0;
|
const switch2Passed = addedMessages2 > 0 && removedMessages2 > 0;
|
||||||
console.log(` ${switch2Passed ? '✅ PASS' : '❌ FAIL'} - Skeleton still works on second switch`);
|
console.log(` ${switch2Passed ? '✅ PASS' : '❌ FAIL'} - Skeleton still works on second switch`);
|
||||||
testResults.push({ test: 'Second Language Switch', passed: switch2Passed });
|
testResults.push({ test: 'Second Language Switch', passed: switch2Passed });
|
||||||
|
|
||||||
@@ -156,18 +139,22 @@ async function testSkeletonLoaders() {
|
|||||||
// ========================================================================
|
// ========================================================================
|
||||||
console.log("\n6️⃣ Testing Third Language Switch (EN → ES)...");
|
console.log("\n6️⃣ Testing Third Language Switch (EN → ES)...");
|
||||||
|
|
||||||
await page.evaluate(() => { window.loadingEvents = []; });
|
// Clear console messages
|
||||||
|
const beforeSwitch3 = consoleMessages.length;
|
||||||
|
|
||||||
|
// Click Spanish button
|
||||||
await page.click('.selector-btn[aria-label="Español"]');
|
await page.click('.selector-btn[aria-label="Español"]');
|
||||||
await page.waitForTimeout(800);
|
await page.waitForTimeout(800);
|
||||||
|
|
||||||
const switch3 = await page.evaluate(() => window.loadingEvents || []);
|
// Check new console messages since last switch
|
||||||
const loadingAdded3 = switch3.filter(e => e.hasLoading).length;
|
const newMessages3 = consoleMessages.slice(beforeSwitch3);
|
||||||
const loadingRemoved3 = switch3.filter(e => !e.hasLoading).length;
|
const addedMessages3 = newMessages3.filter(m => m.includes('Added .loading')).length;
|
||||||
|
const removedMessages3 = newMessages3.filter(m => m.includes('Removed .loading')).length;
|
||||||
|
|
||||||
console.log(` Parent containers got .loading: ${loadingAdded3 > 0 ? '✅' : '❌'} (${loadingAdded3} events)`);
|
console.log(` Skeleton loader added .loading: ${addedMessages3 > 0 ? '✅' : '❌'} (${addedMessages3} events)`);
|
||||||
console.log(` Parent containers lost .loading: ${loadingRemoved3 > 0 ? '✅' : '❌'} (${loadingRemoved3} events)`);
|
console.log(` Skeleton loader removed .loading: ${removedMessages3 > 0 ? '✅' : '❌'} (${removedMessages3} events)`);
|
||||||
|
|
||||||
const switch3Passed = loadingAdded3 > 0 && loadingRemoved3 > 0;
|
const switch3Passed = addedMessages3 > 0 && removedMessages3 > 0;
|
||||||
console.log(` ${switch3Passed ? '✅ PASS' : '❌ FAIL'} - Consistent behavior on third switch`);
|
console.log(` ${switch3Passed ? '✅ PASS' : '❌ FAIL'} - Consistent behavior on third switch`);
|
||||||
testResults.push({ test: 'Third Language Switch', passed: switch3Passed });
|
testResults.push({ test: 'Third Language Switch', passed: switch3Passed });
|
||||||
|
|
||||||
@@ -197,31 +184,29 @@ async function testSkeletonLoaders() {
|
|||||||
testResults.push({ test: 'No Stuck Loading States', passed: noStuckStates });
|
testResults.push({ test: 'No Stuck Loading States', passed: noStuckStates });
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// TEST 7: Hyperscript event delegation works
|
// TEST 7: JavaScript event handlers work
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
console.log("\n8️⃣ Testing Hyperscript Event Delegation...");
|
console.log("\n8️⃣ Testing JavaScript Event Handlers...");
|
||||||
|
|
||||||
const hyperscriptCheck = await page.evaluate(() => {
|
const jsCheck = await page.evaluate(() => {
|
||||||
const body = document.body;
|
// Check if main.js loaded and languageSwitching variable exists
|
||||||
const hasHyperscript = body.hasAttribute('_');
|
const hasLanguageSwitchingFlag = typeof window.languageSwitching !== 'undefined';
|
||||||
const hyperscriptContent = body.getAttribute('_') || '';
|
|
||||||
const hasBeforeRequest = hyperscriptContent.includes('htmx:beforeRequest');
|
// Verify the flag is in clean state (false)
|
||||||
const hasOobAfterSwap = hyperscriptContent.includes('htmx:oobAfterSwap');
|
const flagIsClean = window.languageSwitching === false;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasHyperscript,
|
hasLanguageSwitchingFlag,
|
||||||
hasBeforeRequest,
|
flagIsClean
|
||||||
hasOobAfterSwap
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(` Body has _hyperscript: ${hyperscriptCheck.hasHyperscript ? '✅' : '❌'}`);
|
console.log(` JavaScript languageSwitching flag exists: ${jsCheck.hasLanguageSwitchingFlag ? '✅' : '❌'}`);
|
||||||
console.log(` Listens for htmx:beforeRequest: ${hyperscriptCheck.hasBeforeRequest ? '✅' : '❌'}`);
|
console.log(` Flag is in clean state (false): ${jsCheck.flagIsClean ? '✅' : '❌'}`);
|
||||||
console.log(` Listens for htmx:oobAfterSwap: ${hyperscriptCheck.hasOobAfterSwap ? '✅' : '❌'}`);
|
|
||||||
|
|
||||||
const hyperscriptPassed = hyperscriptCheck.hasHyperscript && hyperscriptCheck.hasBeforeRequest && hyperscriptCheck.hasOobAfterSwap;
|
const jsPassed = jsCheck.hasLanguageSwitchingFlag && jsCheck.flagIsClean;
|
||||||
console.log(` ${hyperscriptPassed ? '✅ PASS' : '❌ FAIL'} - Global event delegation configured`);
|
console.log(` ${jsPassed ? '✅ PASS' : '❌ FAIL'} - JavaScript event handlers configured`);
|
||||||
testResults.push({ test: 'Hyperscript Event Delegation', passed: hyperscriptPassed });
|
testResults.push({ test: 'JavaScript Event Handlers', passed: jsPassed });
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// FINAL SUMMARY
|
// FINAL SUMMARY
|
||||||
|
|||||||
Reference in New Issue
Block a user