fix: icon toggle real-time rendering + hyperscript architecture cleanup
CRITICAL FIX: Icon toggle now works without page refresh - Changed class name from 'show-logos' to 'show-icons' (CSS mismatch bug) - Updated localStorage key from 'cv-logos' to 'cv-icons' - Fixed toggleIcons() function in cv-functions.js HYPERSCRIPT ARCHITECTURE: - Moved 6 toggle functions from hyperscript to JavaScript (cv-functions.js) - Solves hyperscript 0.9.14 parser limitation (max 3 def statements total) - Upgraded hyperscript from 0.9.12 to 0.9.14 - Fixed operator precedence in keyboard shortcuts - Cleaned view-controls.html templates (inline → function calls) NEW FILES: - static/js/cv-functions.js - Global toggle functions (6 functions) - HYPERSCRIPT-RULES.md - Permanent architecture documentation - tests/mjs/0-zoom.test.mjs - Zoom functionality test - tests/mjs/1-toggles.test.mjs - Comprehensive toggle test with real-time verification - tests/TEST-SUMMARY.md - Test suite documentation TESTS: - Real-time DOM update verification (no refresh required) - Screenshot capture for visual regression - localStorage persistence validation - Toggle synchronization between action bar and menu BREAKING CHANGE: localStorage key changed from 'cv-logos' to 'cv-icons' Users may need to re-toggle icons preference on first load after update.
This commit is contained in:
@@ -0,0 +1,134 @@
|
|||||||
|
# Hyperscript Development Rules
|
||||||
|
|
||||||
|
## MANDATORY RULES - ALWAYS FOLLOW
|
||||||
|
|
||||||
|
### Rule 1: Code Cleanliness
|
||||||
|
**More than 3 lines of hyperscript → Move to function in file**
|
||||||
|
|
||||||
|
- Inline hyperscript in HTML should be kept minimal (≤3 lines)
|
||||||
|
- Longer logic MUST be extracted to named functions in .\_hs files
|
||||||
|
- HTML templates should be clean and readable
|
||||||
|
|
||||||
|
### Rule 2: File Structure - Hyperscript 0.9.12 Limitation
|
||||||
|
**Maximum 3 `def` statements TOTAL across ALL files**
|
||||||
|
|
||||||
|
⚠️ **CRITICAL**: Hyperscript 0.9.12 has a parser limitation - more than 3 `def` statements **across ALL loaded .\_hs files** causes:
|
||||||
|
```
|
||||||
|
Error: Expected 'end' but found 'def'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution for Global Reusable Functions**: Use regular JavaScript instead
|
||||||
|
- `static/js/cv-functions.js` - Global toggle and utility functions
|
||||||
|
- toggleCVLength(), toggleIcons(), toggleTheme()
|
||||||
|
- syncPdfHover(), syncPrintHover(), highlightZoomControl()
|
||||||
|
|
||||||
|
**Solution for Hyperscript-Specific Logic**: Keep max 3 defs
|
||||||
|
- `static/hyperscript/functions._hs` - ONLY hyperscript-specific utilities (printFriendly, initScrollBehavior, handleScroll)
|
||||||
|
|
||||||
|
**Why JavaScript for Global Functions:**
|
||||||
|
- ✅ No artificial limits
|
||||||
|
- ✅ Better performance (native JS)
|
||||||
|
- ✅ Better debugging
|
||||||
|
- ✅ Can still be called from hyperscript using `call toggleIcons(my.checked)`
|
||||||
|
|
||||||
|
### Rule 3: HTML Structure Cleanliness
|
||||||
|
**HTML must be as clean as possible regarding hyperscript**
|
||||||
|
|
||||||
|
✅ **GOOD** - Clean, readable:
|
||||||
|
```html
|
||||||
|
<input type="checkbox"
|
||||||
|
id="lengthToggle"
|
||||||
|
_="on change call toggleCVLength(my.checked)">
|
||||||
|
```
|
||||||
|
|
||||||
|
❌ **BAD** - Inline logic nightmare:
|
||||||
|
```html
|
||||||
|
<input type="checkbox"
|
||||||
|
id="lengthToggle"
|
||||||
|
_="on change
|
||||||
|
if my.checked
|
||||||
|
remove .cv-short from .cv-paper
|
||||||
|
add .cv-long to .cv-paper
|
||||||
|
set localStorage['cv-length'] to 'long'
|
||||||
|
set #lengthToggleMenu's checked to true
|
||||||
|
else
|
||||||
|
remove .cv-long from .cv-paper
|
||||||
|
add .cv-short to .cv-paper
|
||||||
|
set localStorage['cv-length'] to 'short'
|
||||||
|
set #lengthToggleMenu's checked to false
|
||||||
|
end">
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Organization
|
||||||
|
|
||||||
|
```
|
||||||
|
static/hyperscript/
|
||||||
|
├── functions._hs → Core utilities (3 defs max)
|
||||||
|
├── toggles._hs → Toggle functions (3 defs max)
|
||||||
|
└── hover._hs → Hover sync functions (3 defs max)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Load Order in templates/index.html:
|
||||||
|
```html
|
||||||
|
<script type="text/hyperscript" src="/static/hyperscript/functions._hs"></script>
|
||||||
|
<script type="text/hyperscript" src="/static/hyperscript/toggles._hs"></script>
|
||||||
|
<script type="text/hyperscript" src="/static/hyperscript/hover._hs"></script>
|
||||||
|
<script src="https://unpkg.com/hyperscript.org@0.9.12"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Required Functions
|
||||||
|
|
||||||
|
### Core Functions (functions._hs)
|
||||||
|
1. `printFriendly()` - Handle print-friendly view
|
||||||
|
2. `initScrollBehavior()` - Initialize scroll variables
|
||||||
|
3. `handleScroll()` - Manage scroll behavior and fixed button positioning
|
||||||
|
|
||||||
|
### Toggle Functions (toggles._hs)
|
||||||
|
1. `toggleCVLength(isLong)` - Switch between short/long CV
|
||||||
|
2. `toggleIcons(showIcons)` - Show/hide icons
|
||||||
|
3. `toggleTheme(isClean)` - Switch between default/clean theme
|
||||||
|
|
||||||
|
### Hover Sync Functions (hover._hs)
|
||||||
|
1. `syncPdfHover(show)` - Sync hover state across PDF buttons
|
||||||
|
2. `syncPrintHover(show)` - Sync hover state across print buttons
|
||||||
|
3. `highlightZoomControl(show)` - Highlight zoom control on hover
|
||||||
|
|
||||||
|
## Why These Rules Exist
|
||||||
|
|
||||||
|
### Maintainability
|
||||||
|
- Functions with descriptive names are self-documenting
|
||||||
|
- Easier to test and debug
|
||||||
|
- Changes in one place instead of scattered across templates
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Browser caches .\_hs files
|
||||||
|
- Reduces HTML payload size
|
||||||
|
- Cleaner separation of concerns
|
||||||
|
|
||||||
|
### Hyperscript 0.9.12 Limitation
|
||||||
|
- Parser breaks with >3 `def` in single file
|
||||||
|
- MUST split into multiple files
|
||||||
|
- Each file: ≤3 `def` statements
|
||||||
|
|
||||||
|
## Common Mistakes to Avoid
|
||||||
|
|
||||||
|
❌ **DON'T**: Put all functions in one file if you have >3 defs
|
||||||
|
❌ **DON'T**: Write long inline hyperscript in HTML
|
||||||
|
❌ **DON'T**: Delete functions to work around the 3-def limit
|
||||||
|
|
||||||
|
✅ **DO**: Split functions across multiple .\_hs files
|
||||||
|
✅ **DO**: Keep HTML clean with function calls
|
||||||
|
✅ **DO**: Maintain all required functions for clean architecture
|
||||||
|
|
||||||
|
## Testing After Changes
|
||||||
|
|
||||||
|
1. Check browser console for parse errors
|
||||||
|
2. Verify all functions are defined (no "X is null" errors)
|
||||||
|
3. Test all toggles work correctly
|
||||||
|
4. Hard refresh browser (Ctrl+Shift+R) to clear cache
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: 2025-01-17
|
||||||
|
**Hyperscript Version**: 0.9.12
|
||||||
|
**Status**: MANDATORY - ALWAYS FOLLOW
|
||||||
@@ -126,111 +126,6 @@ def handleScroll()
|
|||||||
set :lastScroll to currentScroll
|
set :lastScroll to currentScroll
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ==============================================================================
|
|
||||||
-- TOGGLE FUNCTIONS
|
|
||||||
-- ==============================================================================
|
|
||||||
|
|
||||||
def toggleCVLength(isLong)
|
|
||||||
set paper to the first .cv-paper
|
|
||||||
set otherToggle to (#lengthToggle or #lengthToggleMenu)
|
|
||||||
|
|
||||||
if isLong is true
|
|
||||||
remove .cv-short from paper
|
|
||||||
add .cv-long to paper
|
|
||||||
call localStorage.setItem('cv-length', 'long')
|
|
||||||
if otherToggle exists set otherToggle's checked to true
|
|
||||||
end
|
|
||||||
|
|
||||||
if isLong is false
|
|
||||||
remove .cv-long from paper
|
|
||||||
add .cv-short to paper
|
|
||||||
call localStorage.setItem('cv-length', 'short')
|
|
||||||
if otherToggle exists set otherToggle's checked to false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def toggleIcons(showIcons)
|
|
||||||
set paper to the first .cv-paper
|
|
||||||
set otherToggle to (#iconToggle or #iconToggleMenu)
|
|
||||||
|
|
||||||
if showIcons is true
|
|
||||||
add .show-icons to paper
|
|
||||||
call localStorage.setItem('cv-icons', 'true')
|
|
||||||
if otherToggle exists set otherToggle's checked to true
|
|
||||||
end
|
|
||||||
|
|
||||||
if showIcons is false
|
|
||||||
remove .show-icons from paper
|
|
||||||
call localStorage.setItem('cv-icons', 'false')
|
|
||||||
if otherToggle exists set otherToggle's checked to false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def toggleTheme(isClean)
|
|
||||||
set container to the first .cv-container
|
|
||||||
set otherToggle to (#themeToggle or #themeToggleMenu)
|
|
||||||
|
|
||||||
if isClean is true
|
|
||||||
add .theme-clean to container
|
|
||||||
call localStorage.setItem('cv-theme', 'clean')
|
|
||||||
if otherToggle exists set otherToggle's checked to true
|
|
||||||
end
|
|
||||||
|
|
||||||
if isClean is false
|
|
||||||
remove .theme-clean from container
|
|
||||||
call localStorage.setItem('cv-theme', 'default')
|
|
||||||
if otherToggle exists set otherToggle's checked to false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ==============================================================================
|
|
||||||
-- HOVER SYNC FUNCTIONS
|
|
||||||
-- ==============================================================================
|
|
||||||
|
|
||||||
def syncPdfHover(show)
|
|
||||||
set pdfButtons to .pdf-download-button
|
|
||||||
|
|
||||||
if show is true
|
|
||||||
for button in pdfButtons
|
|
||||||
add .pdf-hover-sync to button
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if show is false
|
|
||||||
for button in pdfButtons
|
|
||||||
remove .pdf-hover-sync from button
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def syncPrintHover(show)
|
|
||||||
set printButtons to .print-button
|
|
||||||
|
|
||||||
if show is true
|
|
||||||
for button in printButtons
|
|
||||||
add .print-hover-sync to button
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if show is false
|
|
||||||
for button in printButtons
|
|
||||||
remove .print-hover-sync from button
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def highlightZoomControl(show)
|
|
||||||
set zoomWrapper to the first #zoom-wrapper
|
|
||||||
|
|
||||||
if show is true
|
|
||||||
add .highlight to zoomWrapper
|
|
||||||
end
|
|
||||||
|
|
||||||
if show is false
|
|
||||||
remove .highlight from zoomWrapper
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ==============================================================================
|
-- ==============================================================================
|
||||||
-- KEYBOARD SHORTCUTS
|
-- KEYBOARD SHORTCUTS
|
||||||
-- ==============================================================================
|
-- ==============================================================================
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
/**
|
||||||
|
* CV Site - Core Toggle Functions
|
||||||
|
* =================================
|
||||||
|
* Global JavaScript functions for CV toggles and interactions
|
||||||
|
* These replace hyperscript def functions to avoid the 3-function parser limit
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle CV Length (Short/Long)
|
||||||
|
* @param {boolean} isLong - true for long CV, false for short
|
||||||
|
*/
|
||||||
|
function toggleCVLength(isLong) {
|
||||||
|
const paper = document.querySelector('.cv-paper');
|
||||||
|
const otherToggle = document.querySelector('#lengthToggle') || document.querySelector('#lengthToggleMenu');
|
||||||
|
|
||||||
|
if (isLong) {
|
||||||
|
paper?.classList.remove('cv-short');
|
||||||
|
paper?.classList.add('cv-long');
|
||||||
|
localStorage.setItem('cv-length', 'long');
|
||||||
|
if (otherToggle) otherToggle.checked = true;
|
||||||
|
} else {
|
||||||
|
paper?.classList.remove('cv-long');
|
||||||
|
paper?.classList.add('cv-short');
|
||||||
|
localStorage.setItem('cv-length', 'short');
|
||||||
|
if (otherToggle) otherToggle.checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle Icons Display
|
||||||
|
* @param {boolean} showIcons - true to show icons, false to hide
|
||||||
|
*/
|
||||||
|
function toggleIcons(showIcons) {
|
||||||
|
const paper = document.querySelector('.cv-paper');
|
||||||
|
const otherToggle = document.querySelector('#iconToggle') || document.querySelector('#iconToggleMenu');
|
||||||
|
|
||||||
|
if (showIcons) {
|
||||||
|
paper?.classList.add('show-icons');
|
||||||
|
localStorage.setItem('cv-icons', 'true');
|
||||||
|
if (otherToggle) otherToggle.checked = true;
|
||||||
|
} else {
|
||||||
|
paper?.classList.remove('show-icons');
|
||||||
|
localStorage.setItem('cv-icons', 'false');
|
||||||
|
if (otherToggle) otherToggle.checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle Theme (Default/Clean)
|
||||||
|
* @param {boolean} isClean - true for clean theme, false for default
|
||||||
|
*/
|
||||||
|
function toggleTheme(isClean) {
|
||||||
|
const body = document.body;
|
||||||
|
const otherToggle = document.querySelector('#themeToggle') || document.querySelector('#themeToggleMenu');
|
||||||
|
|
||||||
|
if (isClean) {
|
||||||
|
body?.classList.add('theme-clean');
|
||||||
|
localStorage.setItem('cv-theme', 'clean');
|
||||||
|
if (otherToggle) otherToggle.checked = true;
|
||||||
|
} else {
|
||||||
|
body?.classList.remove('theme-clean');
|
||||||
|
localStorage.setItem('cv-theme', 'default');
|
||||||
|
if (otherToggle) otherToggle.checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync PDF Button Hover State
|
||||||
|
* @param {boolean} show - true to add hover class, false to remove
|
||||||
|
*/
|
||||||
|
function syncPdfHover(show) {
|
||||||
|
const pdfButtons = document.querySelectorAll('.pdf-btn');
|
||||||
|
|
||||||
|
pdfButtons.forEach(button => {
|
||||||
|
if (show) {
|
||||||
|
button.classList.add('pdf-hover-sync');
|
||||||
|
} else {
|
||||||
|
button.classList.remove('pdf-hover-sync');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync Print Button Hover State
|
||||||
|
* @param {boolean} show - true to add hover class, false to remove
|
||||||
|
*/
|
||||||
|
function syncPrintHover(show) {
|
||||||
|
const printButtons = document.querySelectorAll('.print-btn');
|
||||||
|
|
||||||
|
printButtons.forEach(button => {
|
||||||
|
if (show) {
|
||||||
|
button.classList.add('print-hover-sync');
|
||||||
|
} else {
|
||||||
|
button.classList.remove('print-hover-sync');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlight Zoom Control
|
||||||
|
* @param {boolean} show - true to highlight, false to remove highlight
|
||||||
|
*/
|
||||||
|
function highlightZoomControl(show) {
|
||||||
|
const zoomWrapper = document.querySelector('#zoom-wrapper');
|
||||||
|
|
||||||
|
if (show) {
|
||||||
|
zoomWrapper?.classList.add('highlight');
|
||||||
|
} else {
|
||||||
|
zoomWrapper?.classList.remove('highlight');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make functions globally available
|
||||||
|
window.toggleCVLength = toggleCVLength;
|
||||||
|
window.toggleIcons = toggleIcons;
|
||||||
|
window.toggleTheme = toggleTheme;
|
||||||
|
window.syncPdfHover = syncPdfHover;
|
||||||
|
window.syncPrintHover = syncPrintHover;
|
||||||
|
window.highlightZoomControl = highlightZoomControl;
|
||||||
+14
-13
@@ -44,11 +44,15 @@
|
|||||||
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
|
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<!-- CV Core Functions - Regular JavaScript (global reusable functions) -->
|
||||||
|
<script src="/static/js/cv-functions.js"></script>
|
||||||
|
|
||||||
<!-- Hyperscript Functions - Must load BEFORE hyperscript library -->
|
<!-- Hyperscript Functions - Must load BEFORE hyperscript library -->
|
||||||
|
<!-- CRITICAL: Max 3 def total (hyperscript 0.9.14 limitation) -->
|
||||||
<script type="text/hyperscript" src="/static/hyperscript/functions._hs"></script>
|
<script type="text/hyperscript" src="/static/hyperscript/functions._hs"></script>
|
||||||
|
|
||||||
<!-- Hyperscript - Declarative event handling for enhanced interactivity -->
|
<!-- Hyperscript - Declarative event handling for enhanced interactivity -->
|
||||||
<script src="https://unpkg.com/hyperscript.org@0.9.12"></script>
|
<script src="https://unpkg.com/hyperscript.org@0.9.14"></script>
|
||||||
|
|
||||||
<!-- Iconify - Load synchronously for immediate rendering -->
|
<!-- Iconify - Load synchronously for immediate rendering -->
|
||||||
<script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
|
<script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
|
||||||
@@ -119,27 +123,24 @@
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Toggle CV length with 'L'
|
-- Toggle CV length with 'L'
|
||||||
if event.key is 'l' or event.key is 'L' and not event.ctrlKey and not event.metaKey and not event.altKey and not isInputField
|
if (event.key is 'l' or event.key is 'L') and not event.ctrlKey and not event.metaKey and not event.altKey and not isInputField
|
||||||
halt the event
|
halt the event
|
||||||
set paper to the first .cv-paper
|
set lengthToggle to (#lengthToggle or #lengthToggleMenu)
|
||||||
set isCurrentlyLong to paper.classList.contains('cv-long')
|
if lengthToggle then set lengthToggle's checked to (not lengthToggle's checked) then send change to lengthToggle end
|
||||||
call toggleCVLength(not isCurrentlyLong)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Toggle icons with 'I'
|
-- Toggle icons with 'I'
|
||||||
if event.key is 'i' or event.key is 'I' and not event.ctrlKey and not event.metaKey and not event.altKey and not isInputField
|
if (event.key is 'i' or event.key is 'I') and not event.ctrlKey and not event.metaKey and not event.altKey and not isInputField
|
||||||
halt the event
|
halt the event
|
||||||
set paper to the first .cv-paper
|
set iconToggle to (#iconToggle or #iconToggleMenu)
|
||||||
set hasIcons to paper.classList.contains('show-icons')
|
if iconToggle then set iconToggle's checked to (not iconToggle's checked) then send change to iconToggle end
|
||||||
call toggleIcons(not hasIcons)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Toggle theme with 'V'
|
-- Toggle theme with 'V'
|
||||||
if event.key is 'v' or event.key is 'V' and not event.ctrlKey and not event.metaKey and not event.altKey and not isInputField
|
if (event.key is 'v' or event.key is 'V') and not event.ctrlKey and not event.metaKey and not event.altKey and not isInputField
|
||||||
halt the event
|
halt the event
|
||||||
set container to the first .cv-container
|
set themeToggle to (#themeToggle or #themeToggleMenu)
|
||||||
set isClean to container.classList.contains('theme-clean')
|
if themeToggle then set themeToggle's checked to (not themeToggle's checked) then send change to themeToggle end
|
||||||
call toggleTheme(not isClean)
|
|
||||||
end
|
end
|
||||||
end">
|
end">
|
||||||
<!-- Top anchor for back-to-top link -->
|
<!-- Top anchor for back-to-top link -->
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Test Suite Summary
|
||||||
|
|
||||||
|
## Test Organization
|
||||||
|
|
||||||
|
All tests are now organized in `/tests/mjs/` with numbered prefixes for execution order.
|
||||||
|
|
||||||
|
### Available Tests
|
||||||
|
|
||||||
|
| Test File | Purpose | Status |
|
||||||
|
|-----------|---------|--------|
|
||||||
|
| `0-zoom.test.mjs` | Zoom control functionality | ✅ Ready |
|
||||||
|
| `1-toggles.test.mjs` | Comprehensive toggle testing with real-time verification | ✅ Ready |
|
||||||
|
|
||||||
|
## Test Improvements
|
||||||
|
|
||||||
|
### 1-toggles.test.mjs Enhancements
|
||||||
|
|
||||||
|
**Key Features Added**:
|
||||||
|
1. ✅ **Real-time visual verification** - Tests verify DOM updates happen immediately without refresh
|
||||||
|
2. ✅ **Screenshot capture** - Takes before/after screenshots for icon toggle
|
||||||
|
3. ✅ **localStorage validation** - Verifies state persistence
|
||||||
|
4. ✅ **Synchronization testing** - Ensures action bar and menu toggles stay in sync
|
||||||
|
5. ✅ **Detailed reporting** - Clear pass/fail for each test with explanations
|
||||||
|
|
||||||
|
**Tests Performed**:
|
||||||
|
- Length Toggle (Action Bar)
|
||||||
|
- Icon Toggle (Action Bar) - **with screenshot verification**
|
||||||
|
- Theme Toggle (Action Bar)
|
||||||
|
- Length Toggle (Menu + Sync)
|
||||||
|
- Icon Toggle (Menu + Sync) - **with real-time rendering check**
|
||||||
|
- Theme Toggle (Menu + Sync)
|
||||||
|
|
||||||
|
**Critical Addition**: Tests explicitly check if visual changes happen without page refresh (the bug reported by user)
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Individual test
|
||||||
|
bun tests/mjs/0-zoom.test.mjs
|
||||||
|
bun tests/mjs/1-toggles.test.mjs
|
||||||
|
|
||||||
|
# All tests in order
|
||||||
|
for test in tests/mjs/*.test.mjs; do bun "$test"; done
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Output
|
||||||
|
|
||||||
|
Each test provides:
|
||||||
|
- Clear ✅/❌ indicators
|
||||||
|
- Before/after state comparison
|
||||||
|
- localStorage verification
|
||||||
|
- Console error detection
|
||||||
|
- Summary with total pass/fail count
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
Toggle test saves screenshots to `tests/screenshots/`:
|
||||||
|
- `before-icon-toggle.png`
|
||||||
|
- `after-icon-toggle.png`
|
||||||
|
|
||||||
|
Use these to visually verify rendering happens without refresh.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Server must be running on http://localhost:1999
|
||||||
|
- Tests run in headed mode (browser visible) for manual verification
|
||||||
|
- Press Ctrl+C to exit after reviewing results
|
||||||
|
- All tests are executable (`chmod +x` already applied)
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Additional tests to add:
|
||||||
|
- Keyboard shortcuts test (L, I, V keys)
|
||||||
|
- Hamburger menu animation test
|
||||||
|
- Print/PDF button tests
|
||||||
|
- Responsive design tests
|
||||||
Executable
+175
@@ -0,0 +1,175 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
/**
|
||||||
|
* ZOOM FUNCTIONALITY TEST
|
||||||
|
* ========================
|
||||||
|
* Tests zoom control visibility, interaction, and real-time updates
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
|
||||||
|
const URL = "http://localhost:1999";
|
||||||
|
|
||||||
|
async function testZoom() {
|
||||||
|
console.log('🔍 ZOOM FUNCTIONALITY TEST\n');
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
headless: false,
|
||||||
|
args: ['--disable-http-cache', '--disable-cache']
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = await browser.newContext({
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
bypassCSP: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
// Track errors
|
||||||
|
const errors = [];
|
||||||
|
page.on('console', msg => {
|
||||||
|
if (msg.type() === 'error') {
|
||||||
|
errors.push(msg.text());
|
||||||
|
console.log(' ❌ ERROR:', msg.text());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
page.on('pageerror', err => {
|
||||||
|
errors.push(err.message);
|
||||||
|
console.log(' ❌ PAGE ERROR:', err.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`\n1️⃣ Loading: ${URL}\n`);
|
||||||
|
await page.goto(URL, { waitUntil: 'networkidle' });
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// TEST 1: Check zoom-control exists
|
||||||
|
console.log('2️⃣ ZOOM CONTROL ELEMENTS:');
|
||||||
|
const elements = await page.evaluate(() => {
|
||||||
|
return {
|
||||||
|
zoomControl: !!document.querySelector('#zoom-control'),
|
||||||
|
zoomSlider: !!document.querySelector('#zoom-slider'),
|
||||||
|
zoomWrapper: !!document.querySelector('#zoom-wrapper'),
|
||||||
|
zoomToggleButton: !!document.querySelector('#zoom-toggle-button')
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const allExist = Object.values(elements).every(v => v);
|
||||||
|
|
||||||
|
console.log(` - Zoom control: ${elements.zoomControl ? '✅' : '❌'}`);
|
||||||
|
console.log(` - Zoom slider: ${elements.zoomSlider ? '✅' : '❌'}`);
|
||||||
|
console.log(` - Zoom wrapper: ${elements.zoomWrapper ? '✅' : '❌'}`);
|
||||||
|
console.log(` - Zoom toggle button: ${elements.zoomToggleButton ? '✅' : '❌'}`);
|
||||||
|
console.log(` ${allExist ? '✅ All elements present' : '❌ Some elements missing'}\n`);
|
||||||
|
|
||||||
|
// TEST 2: Check if zoom control is visible
|
||||||
|
console.log('3️⃣ ZOOM CONTROL VISIBILITY:');
|
||||||
|
const visibility = await page.evaluate(() => {
|
||||||
|
const ctrl = document.querySelector('#zoom-control');
|
||||||
|
return {
|
||||||
|
hasHiddenClass: ctrl?.classList.contains('zoom-hidden'),
|
||||||
|
displayStyle: ctrl ? window.getComputedStyle(ctrl).display : 'N/A',
|
||||||
|
visibilityStyle: ctrl ? window.getComputedStyle(ctrl).visibility : 'N/A'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const isVisible = !visibility.hasHiddenClass && visibility.displayStyle !== 'none';
|
||||||
|
console.log(` - Has .zoom-hidden class: ${visibility.hasHiddenClass ? 'YES' : 'NO'}`);
|
||||||
|
console.log(` - Display: ${visibility.displayStyle}`);
|
||||||
|
console.log(` - Visibility: ${visibility.visibilityStyle}`);
|
||||||
|
console.log(` ${isVisible ? '✅ Visible' : '⚠️ Hidden (expected on load)'}\n`);
|
||||||
|
|
||||||
|
// TEST 3: Show zoom control (click toggle button)
|
||||||
|
console.log('4️⃣ SHOWING ZOOM CONTROL:');
|
||||||
|
await page.click('#zoom-toggle-button');
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
const afterShow = await page.evaluate(() => {
|
||||||
|
const ctrl = document.querySelector('#zoom-control');
|
||||||
|
return {
|
||||||
|
hasHiddenClass: ctrl?.classList.contains('zoom-hidden'),
|
||||||
|
displayStyle: ctrl ? window.getComputedStyle(ctrl).display : 'N/A'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const isVisibleAfter = !afterShow.hasHiddenClass && afterShow.displayStyle !== 'none';
|
||||||
|
console.log(` - After clicking toggle:`);
|
||||||
|
console.log(` - Has .zoom-hidden class: ${afterShow.hasHiddenClass ? 'YES' : 'NO'}`);
|
||||||
|
console.log(` - Display: ${afterShow.displayStyle}`);
|
||||||
|
console.log(` ${isVisibleAfter ? '✅ Now visible' : '❌ Still hidden - BUG!'}\n`);
|
||||||
|
|
||||||
|
// TEST 4: Test zoom functionality
|
||||||
|
console.log('5️⃣ ZOOM FUNCTIONALITY TEST:');
|
||||||
|
|
||||||
|
const zoomTest = await page.evaluate(() => {
|
||||||
|
const slider = document.querySelector('#zoom-slider');
|
||||||
|
const wrapper = document.querySelector('#zoom-wrapper');
|
||||||
|
|
||||||
|
if (!slider || !wrapper) {
|
||||||
|
return { error: 'Zoom elements not found' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get initial state
|
||||||
|
const initialZoom = wrapper.style.zoom || window.getComputedStyle(wrapper).zoom || '1';
|
||||||
|
|
||||||
|
// Set zoom to 150%
|
||||||
|
slider.value = '150';
|
||||||
|
slider.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
|
||||||
|
// Wait for event to process
|
||||||
|
const newZoom = wrapper.style.zoom || window.getComputedStyle(wrapper).zoom || '1';
|
||||||
|
|
||||||
|
// Reset to 100%
|
||||||
|
slider.value = '100';
|
||||||
|
slider.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
|
||||||
|
const resetZoom = wrapper.style.zoom || window.getComputedStyle(wrapper).zoom || '1';
|
||||||
|
|
||||||
|
return {
|
||||||
|
initialZoom,
|
||||||
|
zoomAt150: newZoom,
|
||||||
|
zoomAfterReset: resetZoom,
|
||||||
|
sliderValue: slider.value
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(` - Initial zoom: ${zoomTest.initialZoom}`);
|
||||||
|
console.log(` - Zoom after setting to 150%: ${zoomTest.zoomAt150}`);
|
||||||
|
console.log(` - Zoom after reset to 100%: ${zoomTest.zoomAfterReset}`);
|
||||||
|
console.log(` - Slider value: ${zoomTest.sliderValue}`);
|
||||||
|
|
||||||
|
const zoomWorks = zoomTest.zoomAt150 !== '1' && zoomTest.zoomAt150 !== zoomTest.initialZoom;
|
||||||
|
console.log(` ${zoomWorks ? '✅ Zoom changes value (WORKING)' : '❌ Zoom does not change (BROKEN)'}\n`);
|
||||||
|
|
||||||
|
// SUMMARY
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
console.log('📊 TEST SUMMARY\n');
|
||||||
|
|
||||||
|
const allTestsPassed = allExist && isVisibleAfter && zoomWorks;
|
||||||
|
|
||||||
|
console.log(` ${allExist ? '✅' : '❌'} All zoom elements present`);
|
||||||
|
console.log(` ${isVisibleAfter ? '✅' : '❌'} Zoom control shows on toggle`);
|
||||||
|
console.log(` ${zoomWorks ? '✅' : '❌'} Zoom functionality works`);
|
||||||
|
console.log(` ${errors.length === 0 ? '✅' : '❌'} No console errors`);
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
console.log(`\n Errors found: ${errors.length}`);
|
||||||
|
errors.forEach((err, i) => console.log(` ${i + 1}. ${err}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n' + '='.repeat(70));
|
||||||
|
|
||||||
|
if (allTestsPassed && errors.length === 0) {
|
||||||
|
console.log('\n✅ ALL ZOOM TESTS PASSED!');
|
||||||
|
} else {
|
||||||
|
console.log('\n❌ SOME ZOOM TESTS FAILED - See details above');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n💡 Browser window left open for manual testing');
|
||||||
|
console.log(' Try moving the slider manually to verify');
|
||||||
|
console.log('\nPress Ctrl+C to exit\n');
|
||||||
|
|
||||||
|
await new Promise(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
await testZoom();
|
||||||
Executable
+302
@@ -0,0 +1,302 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
/**
|
||||||
|
* COMPREHENSIVE TOGGLE TEST
|
||||||
|
* ==========================
|
||||||
|
* Tests ALL toggles work with REAL-TIME visual verification
|
||||||
|
* - Checks that toggles update DOM immediately (no refresh needed)
|
||||||
|
* - Verifies localStorage persistence
|
||||||
|
* - Tests synchronization between action bar and menu toggles
|
||||||
|
* - Validates visual rendering changes
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { chromium } from "playwright";
|
||||||
|
|
||||||
|
const URL = "http://localhost:1999";
|
||||||
|
|
||||||
|
async function testAllToggles() {
|
||||||
|
console.log("🧪 COMPREHENSIVE TOGGLE TEST\n");
|
||||||
|
console.log("=".repeat(70));
|
||||||
|
|
||||||
|
const browser = await chromium.launch({ headless: false });
|
||||||
|
const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } });
|
||||||
|
|
||||||
|
const errors = [];
|
||||||
|
const testResults = [];
|
||||||
|
|
||||||
|
page.on('console', msg => {
|
||||||
|
const text = msg.text();
|
||||||
|
if (msg.type() === 'error') {
|
||||||
|
errors.push(text);
|
||||||
|
console.log(`❌ ERROR: ${text}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
page.on('pageerror', err => {
|
||||||
|
errors.push(err.message);
|
||||||
|
console.log(`❌ PAGE ERROR: ${err.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("\n1️⃣ Loading page...");
|
||||||
|
await page.goto(URL);
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// TEST 1: Length Toggle (Action Bar)
|
||||||
|
// ========================================================================
|
||||||
|
console.log("\n2️⃣ Testing Length Toggle (Action Bar)...");
|
||||||
|
const lengthToggle = await page.$('#lengthToggle');
|
||||||
|
if (lengthToggle) {
|
||||||
|
const paper = await page.$('.cv-paper');
|
||||||
|
|
||||||
|
// Get initial state
|
||||||
|
const before = await paper.evaluate(el => ({
|
||||||
|
className: el.className,
|
||||||
|
isLong: el.classList.contains('cv-long'),
|
||||||
|
isShort: el.classList.contains('cv-short')
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Click toggle
|
||||||
|
await lengthToggle.click();
|
||||||
|
await page.waitForTimeout(300); // Wait for DOM update
|
||||||
|
|
||||||
|
// Get state after click
|
||||||
|
const after = await paper.evaluate(el => ({
|
||||||
|
className: el.className,
|
||||||
|
isLong: el.classList.contains('cv-long'),
|
||||||
|
isShort: el.classList.contains('cv-short')
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Verify localStorage
|
||||||
|
const localStorage = await page.evaluate(() => localStorage.getItem('cv-length'));
|
||||||
|
|
||||||
|
const changed = before.isLong !== after.isLong;
|
||||||
|
const testPassed = changed && (after.isLong ? localStorage === 'long' : localStorage === 'short');
|
||||||
|
|
||||||
|
console.log(` Before: ${before.isLong ? 'long' : 'short'}`);
|
||||||
|
console.log(` After: ${after.isLong ? 'long' : 'short'}`);
|
||||||
|
console.log(` localStorage: ${localStorage}`);
|
||||||
|
console.log(` Visual change: ${changed ? '✅ YES' : '❌ NO'}`);
|
||||||
|
console.log(` ${testPassed ? '✅ PASS' : '❌ FAIL'}`);
|
||||||
|
|
||||||
|
testResults.push({ test: 'Length Toggle (Action Bar)', passed: testPassed });
|
||||||
|
} else {
|
||||||
|
console.log(` ❌ Toggle not found`);
|
||||||
|
testResults.push({ test: 'Length Toggle (Action Bar)', passed: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// TEST 2: Icon Toggle (Action Bar)
|
||||||
|
// ========================================================================
|
||||||
|
console.log("\n3️⃣ Testing Icon/Logo Toggle (Action Bar)...");
|
||||||
|
const iconToggle = await page.$('#iconToggle');
|
||||||
|
if (iconToggle) {
|
||||||
|
const paper = await page.$('.cv-paper');
|
||||||
|
|
||||||
|
// Take screenshot BEFORE toggle
|
||||||
|
await page.screenshot({ path: 'tests/screenshots/before-icon-toggle.png', fullPage: false });
|
||||||
|
|
||||||
|
const before = await paper.evaluate(el => ({
|
||||||
|
className: el.className,
|
||||||
|
showIcons: el.classList.contains('show-icons')
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Click toggle
|
||||||
|
await iconToggle.click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
// Take screenshot AFTER toggle
|
||||||
|
await page.screenshot({ path: 'tests/screenshots/after-icon-toggle.png', fullPage: false });
|
||||||
|
|
||||||
|
const after = await paper.evaluate(el => ({
|
||||||
|
className: el.className,
|
||||||
|
showIcons: el.classList.contains('show-icons')
|
||||||
|
}));
|
||||||
|
|
||||||
|
const localStorage = await page.evaluate(() => localStorage.getItem('cv-icons'));
|
||||||
|
|
||||||
|
const changed = before.showIcons !== after.showIcons;
|
||||||
|
const testPassed = changed && (after.showIcons ? localStorage === 'true' : localStorage === 'false');
|
||||||
|
|
||||||
|
console.log(` Before: ${before.showIcons ? 'icons shown' : 'icons hidden'}`);
|
||||||
|
console.log(` After: ${after.showIcons ? 'icons shown' : 'icons hidden'}`);
|
||||||
|
console.log(` localStorage: ${localStorage}`);
|
||||||
|
console.log(` Visual change: ${changed ? '✅ YES (no refresh needed)' : '❌ NO (requires refresh - BUG!)'}`);
|
||||||
|
console.log(` Screenshots saved: before-icon-toggle.png, after-icon-toggle.png`);
|
||||||
|
console.log(` ${testPassed ? '✅ PASS' : '❌ FAIL'}`);
|
||||||
|
|
||||||
|
testResults.push({ test: 'Icon Toggle (Action Bar)', passed: testPassed });
|
||||||
|
} else {
|
||||||
|
console.log(` ❌ Toggle not found`);
|
||||||
|
testResults.push({ test: 'Icon Toggle (Action Bar)', passed: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// TEST 3: Theme Toggle (Action Bar)
|
||||||
|
// ========================================================================
|
||||||
|
console.log("\n4️⃣ Testing Theme Toggle (Action Bar)...");
|
||||||
|
const themeToggle = await page.$('#themeToggle');
|
||||||
|
if (themeToggle) {
|
||||||
|
const body = await page.$('body');
|
||||||
|
|
||||||
|
const before = await body.evaluate(el => ({
|
||||||
|
className: el.className,
|
||||||
|
isClean: el.classList.contains('theme-clean')
|
||||||
|
}));
|
||||||
|
|
||||||
|
await themeToggle.click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
const after = await body.evaluate(el => ({
|
||||||
|
className: el.className,
|
||||||
|
isClean: el.classList.contains('theme-clean')
|
||||||
|
}));
|
||||||
|
|
||||||
|
const localStorage = await page.evaluate(() => localStorage.getItem('cv-theme'));
|
||||||
|
|
||||||
|
const changed = before.isClean !== after.isClean;
|
||||||
|
const testPassed = changed && (after.isClean ? localStorage === 'clean' : localStorage === 'default');
|
||||||
|
|
||||||
|
console.log(` Before: ${before.isClean ? 'clean' : 'default'}`);
|
||||||
|
console.log(` After: ${after.isClean ? 'clean' : 'default'}`);
|
||||||
|
console.log(` localStorage: ${localStorage}`);
|
||||||
|
console.log(` Visual change: ${changed ? '✅ YES' : '❌ NO'}`);
|
||||||
|
console.log(` ${testPassed ? '✅ PASS' : '❌ FAIL'}`);
|
||||||
|
|
||||||
|
testResults.push({ test: 'Theme Toggle (Action Bar)', passed: testPassed });
|
||||||
|
} else {
|
||||||
|
console.log(` ❌ Toggle not found`);
|
||||||
|
testResults.push({ test: 'Theme Toggle (Action Bar)', passed: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// TEST 4: Hamburger Menu Toggles + Synchronization
|
||||||
|
// ========================================================================
|
||||||
|
console.log("\n5️⃣ Testing Hamburger Menu + Toggle Synchronization...");
|
||||||
|
const hamburger = await page.$('.hamburger-btn');
|
||||||
|
if (hamburger) {
|
||||||
|
await hamburger.click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
const menu = await page.$('.navigation-menu');
|
||||||
|
const isOpen = await menu.evaluate(el => el.classList.contains('menu-open'));
|
||||||
|
console.log(` ${isOpen ? '✅ Menu opened' : '❌ Menu failed to open'}`);
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
// Test Menu Length Toggle
|
||||||
|
console.log("\n6️⃣ Testing Length Toggle (Menu)...");
|
||||||
|
const menuLengthToggle = await page.$('#lengthToggleMenu');
|
||||||
|
if (menuLengthToggle) {
|
||||||
|
const paper = await page.$('.cv-paper');
|
||||||
|
const before = await paper.evaluate(el => el.classList.contains('cv-long'));
|
||||||
|
|
||||||
|
await menuLengthToggle.click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
const after = await paper.evaluate(el => el.classList.contains('cv-long'));
|
||||||
|
|
||||||
|
// Check if action bar toggle synchronized
|
||||||
|
const actionBarSynced = await page.$eval('#lengthToggle', el => el.checked);
|
||||||
|
const menuChecked = await page.$eval('#lengthToggleMenu', el => el.checked);
|
||||||
|
|
||||||
|
const changed = before !== after;
|
||||||
|
const synced = actionBarSynced === menuChecked;
|
||||||
|
|
||||||
|
console.log(` Visual change: ${changed ? '✅ YES' : '❌ NO'}`);
|
||||||
|
console.log(` Synchronization: ${synced ? '✅ YES' : '❌ NO'}`);
|
||||||
|
console.log(` ${changed && synced ? '✅ PASS' : '❌ FAIL'}`);
|
||||||
|
|
||||||
|
testResults.push({ test: 'Length Toggle (Menu + Sync)', passed: changed && synced });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Menu Icon Toggle
|
||||||
|
console.log("\n7️⃣ Testing Icon Toggle (Menu)...");
|
||||||
|
const menuIconToggle = await page.$('#iconToggleMenu');
|
||||||
|
if (menuIconToggle) {
|
||||||
|
const paper = await page.$('.cv-paper');
|
||||||
|
const before = await paper.evaluate(el => el.classList.contains('show-icons'));
|
||||||
|
|
||||||
|
await menuIconToggle.click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
const after = await paper.evaluate(el => el.classList.contains('show-icons'));
|
||||||
|
|
||||||
|
const actionBarSynced = await page.$eval('#iconToggle', el => el.checked);
|
||||||
|
const menuChecked = await page.$eval('#iconToggleMenu', el => el.checked);
|
||||||
|
|
||||||
|
const changed = before !== after;
|
||||||
|
const synced = actionBarSynced === menuChecked;
|
||||||
|
|
||||||
|
console.log(` Visual change: ${changed ? '✅ YES (no refresh!)' : '❌ NO (BUG!)'}`);
|
||||||
|
console.log(` Synchronization: ${synced ? '✅ YES' : '❌ NO'}`);
|
||||||
|
console.log(` ${changed && synced ? '✅ PASS' : '❌ FAIL'}`);
|
||||||
|
|
||||||
|
testResults.push({ test: 'Icon Toggle (Menu + Sync)', passed: changed && synced });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Menu Theme Toggle
|
||||||
|
console.log("\n8️⃣ Testing Theme Toggle (Menu)...");
|
||||||
|
const menuThemeToggle = await page.$('#themeToggleMenu');
|
||||||
|
if (menuThemeToggle) {
|
||||||
|
const body = await page.$('body');
|
||||||
|
const before = await body.evaluate(el => el.classList.contains('theme-clean'));
|
||||||
|
|
||||||
|
await menuThemeToggle.click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
const after = await body.evaluate(el => el.classList.contains('theme-clean'));
|
||||||
|
|
||||||
|
const actionBarSynced = await page.$eval('#themeToggle', el => el.checked);
|
||||||
|
const menuChecked = await page.$eval('#themeToggleMenu', el => el.checked);
|
||||||
|
|
||||||
|
const changed = before !== after;
|
||||||
|
const synced = actionBarSynced === menuChecked;
|
||||||
|
|
||||||
|
console.log(` Visual change: ${changed ? '✅ YES' : '❌ NO'}`);
|
||||||
|
console.log(` Synchronization: ${synced ? '✅ YES' : '❌ NO'}`);
|
||||||
|
console.log(` ${changed && synced ? '✅ PASS' : '❌ FAIL'}`);
|
||||||
|
|
||||||
|
testResults.push({ test: 'Theme Toggle (Menu + Sync)', passed: changed && synced });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// 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 FOUND:\n`);
|
||||||
|
errors.forEach((err, i) => {
|
||||||
|
console.log(`${i + 1}. ${err}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("=".repeat(70) + "\n");
|
||||||
|
|
||||||
|
if (failedTests === 0 && errors.length === 0) {
|
||||||
|
console.log("🎉 ALL TESTS PASSED! All toggles work with real-time rendering.");
|
||||||
|
} 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 testAllToggles();
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# CV Project Test Suite
|
||||||
|
|
||||||
|
Organized test files for the CV application. All tests use Playwright for browser automation.
|
||||||
|
|
||||||
|
## Test Files
|
||||||
|
|
||||||
|
### 0-zoom.test.mjs
|
||||||
|
**Purpose**: Test zoom control functionality
|
||||||
|
- Verifies zoom control elements exist
|
||||||
|
- Tests visibility toggle
|
||||||
|
- Validates zoom slider interaction
|
||||||
|
- Checks real-time zoom updates
|
||||||
|
|
||||||
|
**Run**: `bun tests/mjs/0-zoom.test.mjs`
|
||||||
|
|
||||||
|
### 1-toggles.test.mjs
|
||||||
|
**Purpose**: Comprehensive toggle testing with real-time visual verification
|
||||||
|
- Tests all 3 toggles (Length, Icons, Theme)
|
||||||
|
- Validates action bar toggles
|
||||||
|
- Tests hamburger menu toggles
|
||||||
|
- Verifies synchronization between action bar and menu
|
||||||
|
- Checks localStorage persistence
|
||||||
|
- **Critical**: Validates that toggles update DOM immediately (no refresh needed)
|
||||||
|
- Takes screenshots for visual comparison
|
||||||
|
|
||||||
|
**Run**: `bun tests/mjs/1-toggles.test.mjs`
|
||||||
|
|
||||||
|
## Running All Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run individual tests
|
||||||
|
bun tests/mjs/0-zoom.test.mjs
|
||||||
|
bun tests/mjs/1-toggles.test.mjs
|
||||||
|
|
||||||
|
# Run all tests sequentially
|
||||||
|
for test in tests/mjs/*.test.mjs; do
|
||||||
|
echo "Running $test..."
|
||||||
|
bun "$test"
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Requirements
|
||||||
|
|
||||||
|
- Server must be running on http://localhost:1999
|
||||||
|
- Browser window will stay open after tests for manual verification
|
||||||
|
- Press Ctrl+C to exit test
|
||||||
|
|
||||||
|
## Test Output
|
||||||
|
|
||||||
|
All tests provide:
|
||||||
|
- ✅ Clear pass/fail indicators
|
||||||
|
- 📊 Summary of results
|
||||||
|
- ❌ Detailed error messages
|
||||||
|
- 🎉 Success confirmation
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
Toggle tests save screenshots to `tests/screenshots/`:
|
||||||
|
- `before-icon-toggle.png` - Before clicking icon toggle
|
||||||
|
- `after-icon-toggle.png` - After clicking icon toggle
|
||||||
|
|
||||||
|
Use these to visually verify rendering changes.
|
||||||
Reference in New Issue
Block a user