diff --git a/HYPERSCRIPT-RULES.md b/HYPERSCRIPT-RULES.md
new file mode 100644
index 0000000..6d34280
--- /dev/null
+++ b/HYPERSCRIPT-RULES.md
@@ -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
+
+```
+
+❌ **BAD** - Inline logic nightmare:
+```html
+
+```
+
+## 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
+
+
+
+
+```
+
+## 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
diff --git a/static/hyperscript/functions._hs b/static/hyperscript/functions._hs
index c77204b..946e5f8 100644
--- a/static/hyperscript/functions._hs
+++ b/static/hyperscript/functions._hs
@@ -126,111 +126,6 @@ def handleScroll()
set :lastScroll to currentScroll
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
-- ==============================================================================
diff --git a/static/js/cv-functions.js b/static/js/cv-functions.js
new file mode 100644
index 0000000..d3275a2
--- /dev/null
+++ b/static/js/cv-functions.js
@@ -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;
diff --git a/templates/index.html b/templates/index.html
index 2809e3b..1dd57ae 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -44,11 +44,15 @@
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
crossorigin="anonymous">
+
+
+
+
-
+
@@ -119,27 +123,24 @@
end
-- 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
- set paper to the first .cv-paper
- set isCurrentlyLong to paper.classList.contains('cv-long')
- call toggleCVLength(not isCurrentlyLong)
+ set lengthToggle to (#lengthToggle or #lengthToggleMenu)
+ if lengthToggle then set lengthToggle's checked to (not lengthToggle's checked) then send change to lengthToggle end
end
-- 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
- set paper to the first .cv-paper
- set hasIcons to paper.classList.contains('show-icons')
- call toggleIcons(not hasIcons)
+ set iconToggle to (#iconToggle or #iconToggleMenu)
+ if iconToggle then set iconToggle's checked to (not iconToggle's checked) then send change to iconToggle end
end
-- 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
- set container to the first .cv-container
- set isClean to container.classList.contains('theme-clean')
- call toggleTheme(not isClean)
+ set themeToggle to (#themeToggle or #themeToggleMenu)
+ if themeToggle then set themeToggle's checked to (not themeToggle's checked) then send change to themeToggle end
end
end">
diff --git a/tests/TEST-SUMMARY.md b/tests/TEST-SUMMARY.md
new file mode 100644
index 0000000..429c1cb
--- /dev/null
+++ b/tests/TEST-SUMMARY.md
@@ -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
diff --git a/tests/mjs/0-zoom.test.mjs b/tests/mjs/0-zoom.test.mjs
new file mode 100755
index 0000000..9d21a11
--- /dev/null
+++ b/tests/mjs/0-zoom.test.mjs
@@ -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();
diff --git a/tests/mjs/1-toggles.test.mjs b/tests/mjs/1-toggles.test.mjs
new file mode 100755
index 0000000..6086ff3
--- /dev/null
+++ b/tests/mjs/1-toggles.test.mjs
@@ -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();
diff --git a/tests/mjs/README.md b/tests/mjs/README.md
new file mode 100644
index 0000000..9711779
--- /dev/null
+++ b/tests/mjs/README.md
@@ -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.