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:
juanatsap
2025-11-17 13:00:03 +00:00
parent 7fc4f76706
commit 3f77fedeaf
8 changed files with 883 additions and 118 deletions
+76
View File
@@ -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
+175
View File
@@ -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();
+302
View File
@@ -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();
+63
View File
@@ -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.