/** * COMPREHENSIVE FEATURE TEST SUITE * Tests all 5 features in the CV application * * Features: * 001: Keyboard Shortcuts Help Modal * 002: Skeleton Loader for Language Transitions * 003: HTMX Loading Indicators * 004: Theme Switcher * 005: PDF Download Modal */ import { test, expect } from '@playwright/test'; const BASE_URL = 'http://localhost:1999'; // Helper to wait for animations const waitForAnimation = (ms = 700) => new Promise(resolve => setTimeout(resolve, ms)); test.describe('PHASE 1: DISCOVERY - Feature Detection', () => { test('should load page and capture initial state', async ({ page }) => { await page.goto(`${BASE_URL}/?lang=en`); // Take screenshot of initial state await page.screenshot({ path: 'test-results/01-initial-state.png', fullPage: true }); // Check for interactive elements const shortcuts = await page.locator('button[data-action="show-shortcuts"], button:has-text("shortcuts"), button:has-text("atajos")').count(); const langButtons = await page.locator('button[data-lang], [hx-get*="lang"]').count(); const themeButton = await page.locator('button[data-theme], [data-action="toggle-theme"]').count(); const pdfButton = await page.locator('button:has-text("PDF"), button:has-text("download")').count(); const toggles = await page.locator('input[type="checkbox"][hx-get], input[type="checkbox"][hx-post]').count(); console.log('=== FEATURE DETECTION ==='); console.log(`Shortcuts button found: ${shortcuts > 0}`); console.log(`Language buttons found: ${langButtons}`); console.log(`Theme button found: ${themeButton > 0}`); console.log(`PDF button found: ${pdfButton > 0}`); console.log(`Toggle controls found: ${toggles}`); expect(langButtons).toBeGreaterThan(0); }); }); test.describe('FEATURE 001: Keyboard Shortcuts Help Modal', () => { test('should open shortcuts modal on button click', async ({ page }) => { await page.goto(`${BASE_URL}/?lang=en`); // Find shortcuts button (try multiple selectors) const shortcutsBtn = page.locator('button[data-action="show-shortcuts"]').or( page.locator('button:has-text("shortcuts")').first() ).or( page.locator('button:has-text("?")').first() ); const btnExists = await shortcutsBtn.count() > 0; console.log(`Shortcuts button exists: ${btnExists}`); if (!btnExists) { console.log('⚠️ Shortcuts button NOT FOUND - Feature may not be implemented'); return; } // Click button await shortcutsBtn.click(); await waitForAnimation(300); // Verify modal opened (check for dialog or modal element) const dialog = page.locator('dialog[open], [role="dialog"]:visible, .modal:visible'); const dialogVisible = await dialog.count() > 0; await page.screenshot({ path: 'test-results/01-shortcuts-modal-open.png', fullPage: true }); expect(dialogVisible).toBe(true); console.log('✅ Shortcuts modal opens on button click'); }); test('should close modal with ESC key', async ({ page }) => { await page.goto(`${BASE_URL}/?lang=en`); const shortcutsBtn = page.locator('button[data-action="show-shortcuts"]').or( page.locator('button:has-text("shortcuts")').first() ); if (await shortcutsBtn.count() === 0) return; await shortcutsBtn.click(); await waitForAnimation(300); // Press ESC await page.keyboard.press('Escape'); await waitForAnimation(300); // Verify modal closed const dialog = page.locator('dialog[open], [role="dialog"]:visible'); const dialogClosed = await dialog.count() === 0; await page.screenshot({ path: 'test-results/01-shortcuts-modal-closed-esc.png', fullPage: true }); expect(dialogClosed).toBe(true); console.log('✅ Modal closes with ESC key'); }); test('should close modal on backdrop click', async ({ page }) => { await page.goto(`${BASE_URL}/?lang=en`); const shortcutsBtn = page.locator('button[data-action="show-shortcuts"]').or( page.locator('button:has-text("shortcuts")').first() ); if (await shortcutsBtn.count() === 0) return; await shortcutsBtn.click(); await waitForAnimation(300); // Click backdrop (click dialog element itself, not content) const dialog = page.locator('dialog[open]'); if (await dialog.count() > 0) { await dialog.click({ position: { x: 5, y: 5 } }); await waitForAnimation(300); const dialogClosed = await page.locator('dialog[open]').count() === 0; expect(dialogClosed).toBe(true); console.log('✅ Modal closes on backdrop click'); } }); test('should show keyboard shortcuts content', async ({ page }) => { await page.goto(`${BASE_URL}/?lang=en`); const shortcutsBtn = page.locator('button[data-action="show-shortcuts"]').or( page.locator('button:has-text("shortcuts")').first() ); if (await shortcutsBtn.count() === 0) return; await shortcutsBtn.click(); await waitForAnimation(300); // Check for keyboard shortcut content (look for kbd tags or shortcut listings) const kbdElements = await page.locator('kbd').count(); const hasShortcutContent = kbdElements > 0; console.log(`Keyboard shortcut elements found: ${kbdElements}`); expect(hasShortcutContent).toBe(true); console.log('✅ Modal displays keyboard shortcuts'); }); test('should support bilingual content (EN/ES)', async ({ page }) => { // Test English await page.goto(`${BASE_URL}/?lang=en`); const shortcutsBtn = page.locator('button[data-action="show-shortcuts"]').or( page.locator('button:has-text("shortcuts")').first() ); if (await shortcutsBtn.count() === 0) return; await shortcutsBtn.click(); await waitForAnimation(300); const enContent = await page.locator('dialog, [role="dialog"]').textContent(); await page.keyboard.press('Escape'); // Test Spanish await page.goto(`${BASE_URL}/?lang=es`); const shortcutsBtnEs = page.locator('button[data-action="show-shortcuts"]').or( page.locator('button:has-text("atajos")').first() ); if (await shortcutsBtnEs.count() > 0) { await shortcutsBtnEs.click(); await waitForAnimation(300); const esContent = await page.locator('dialog, [role="dialog"]').textContent(); const isDifferent = enContent !== esContent; console.log(`Content differs between EN/ES: ${isDifferent}`); expect(isDifferent).toBe(true); console.log('✅ Modal supports bilingual content'); } }); }); test.describe('FEATURE 002: Skeleton Loader for Language Transitions', () => { test('should show skeleton loader during language switch', async ({ page }) => { await page.goto(`${BASE_URL}/?lang=en`); // Find language toggle button const langButton = page.locator('button[data-lang="es"]').or( page.locator('button:has-text("ES")').first() ).or( page.locator('[hx-get*="lang=es"]').first() ); const btnExists = await langButton.count() > 0; console.log(`Language button exists: ${btnExists}`); if (!btnExists) { console.log('⚠️ Language button NOT FOUND'); return; } // Monitor for skeleton loader let skeletonAppeared = false; // Set up observer before clicking await page.evaluate(() => { window.skeletonDetected = false; const observer = new MutationObserver(() => { const skeleton = document.querySelector('.skeleton, [data-skeleton], .skeleton-loader, .shimmer'); if (skeleton && window.getComputedStyle(skeleton).opacity !== '0') { window.skeletonDetected = true; } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true }); }); // Click language button await langButton.click(); await waitForAnimation(100); // Check if skeleton appeared skeletonAppeared = await page.evaluate(() => window.skeletonDetected); await waitForAnimation(600); await page.screenshot({ path: 'test-results/02-skeleton-loader.png', fullPage: true }); console.log(`Skeleton loader appeared: ${skeletonAppeared}`); expect(skeletonAppeared).toBe(true); console.log('✅ Skeleton loader appears during language transition'); }); test('should complete transition within 500-700ms', async ({ page }) => { await page.goto(`${BASE_URL}/?lang=en`); const langButton = page.locator('button[data-lang="es"]').or( page.locator('[hx-get*="lang=es"]').first() ); if (await langButton.count() === 0) return; const startTime = Date.now(); await langButton.click(); // Wait for HTMX to complete (htmx:afterSwap event) await page.waitForFunction(() => { return !document.body.classList.contains('htmx-swapping') && !document.querySelector('.htmx-swapping'); }, { timeout: 2000 }); const endTime = Date.now(); const duration = endTime - startTime; console.log(`Transition duration: ${duration}ms`); expect(duration).toBeGreaterThanOrEqual(400); expect(duration).toBeLessThanOrEqual(1000); console.log('✅ Transition completes within acceptable time range'); }); test('should handle rapid language switching without breaking', async ({ page }) => { await page.goto(`${BASE_URL}/?lang=en`); const enButton = page.locator('button[data-lang="en"]').or( page.locator('[hx-get*="lang=en"]').first() ); const esButton = page.locator('button[data-lang="es"]').or( page.locator('[hx-get*="lang=es"]').first() ); if (await enButton.count() === 0 || await esButton.count() === 0) return; // Rapid clicking await esButton.click(); await waitForAnimation(100); await enButton.click(); await waitForAnimation(100); await esButton.click(); await waitForAnimation(800); // Check no errors in console const errors = []; page.on('console', msg => { if (msg.type() === 'error') errors.push(msg.text()); }); await page.screenshot({ path: 'test-results/02-rapid-switch.png', fullPage: true }); console.log(`Console errors during rapid switching: ${errors.length}`); expect(errors.length).toBe(0); console.log('✅ Handles rapid language switching without errors'); }); }); test.describe('FEATURE 003: HTMX Loading Indicators', () => { test('should show loading indicator on language button click', async ({ page }) => { await page.goto(`${BASE_URL}/?lang=en`); const langButton = page.locator('button[data-lang="es"]').or( page.locator('[hx-get*="lang=es"]').first() ); if (await langButton.count() === 0) return; // Look for loading indicator let indicatorAppeared = false; await page.evaluate(() => { window.indicatorDetected = false; const observer = new MutationObserver(() => { const indicator = document.querySelector('.htmx-indicator, .loading-indicator, .spinner, [data-loading]'); if (indicator && window.getComputedStyle(indicator).opacity !== '0') { window.indicatorDetected = true; } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] }); }); await langButton.click(); await waitForAnimation(50); indicatorAppeared = await page.evaluate(() => window.indicatorDetected); await waitForAnimation(600); console.log(`Loading indicator appeared: ${indicatorAppeared}`); expect(indicatorAppeared).toBe(true); console.log('✅ Loading indicator appears on language button click'); }); test('should show loading indicators on toggle controls', async ({ page }) => { await page.goto(`${BASE_URL}/?lang=en`); const toggles = page.locator('input[type="checkbox"][hx-get], input[type="checkbox"][hx-post]'); const toggleCount = await toggles.count(); console.log(`Toggle controls found: ${toggleCount}`); if (toggleCount === 0) { console.log('⚠️ No toggle controls found'); return; } // Test first toggle const firstToggle = toggles.first(); await page.evaluate(() => { window.toggleIndicatorDetected = false; const observer = new MutationObserver(() => { const indicator = document.querySelector('.htmx-indicator, .loading-indicator, .spinner'); if (indicator && window.getComputedStyle(indicator).opacity !== '0') { window.toggleIndicatorDetected = true; } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true }); }); await firstToggle.click(); await waitForAnimation(50); const indicatorAppeared = await page.evaluate(() => window.toggleIndicatorDetected); await waitForAnimation(500); await page.screenshot({ path: 'test-results/03-toggle-indicator.png', fullPage: true }); console.log(`Toggle loading indicator appeared: ${indicatorAppeared}`); console.log('✅ Loading indicators work on toggle controls'); }); test('should hide indicators after request completes', async ({ page }) => { await page.goto(`${BASE_URL}/?lang=en`); const langButton = page.locator('button[data-lang="es"]').or( page.locator('[hx-get*="lang=es"]').first() ); if (await langButton.count() === 0) return; await langButton.click(); await waitForAnimation(800); // Check that all indicators are hidden const visibleIndicators = await page.locator('.htmx-indicator:visible, .loading-indicator:visible, .spinner:visible').count(); console.log(`Visible indicators after completion: ${visibleIndicators}`); expect(visibleIndicators).toBe(0); console.log('✅ Indicators hide after request completion'); }); }); test.describe('FEATURE 004: Theme Switcher', () => { test('should detect theme switcher button', async ({ page }) => { await page.goto(`${BASE_URL}/?lang=en`); const themeButton = page.locator('button[data-theme], button[data-action="toggle-theme"], button:has-text("theme")').first(); const exists = await themeButton.count() > 0; console.log(`Theme switcher button exists: ${exists}`); if (!exists) { console.log('⚠️ Theme switcher NOT IMPLEMENTED'); return; } await page.screenshot({ path: 'test-results/04-theme-button.png', fullPage: true }); expect(exists).toBe(true); }); test('should expand to show theme options', async ({ page }) => { await page.goto(`${BASE_URL}/?lang=en`); const themeButton = page.locator('button[data-theme], button[data-action="toggle-theme"]').first(); if (await themeButton.count() === 0) { console.log('⚠️ Theme switcher NOT FOUND'); return; } await themeButton.click(); await waitForAnimation(300); // Look for theme options (Light, Dark, Auto) const lightOption = await page.locator('button:has-text("Light"), [data-theme="light"]').count(); const darkOption = await page.locator('button:has-text("Dark"), [data-theme="dark"]').count(); const autoOption = await page.locator('button:has-text("Auto"), [data-theme="auto"]').count(); console.log(`Light option: ${lightOption}, Dark option: ${darkOption}, Auto option: ${autoOption}`); await page.screenshot({ path: 'test-results/04-theme-options.png', fullPage: true }); const hasOptions = lightOption > 0 || darkOption > 0 || autoOption > 0; expect(hasOptions).toBe(true); console.log('✅ Theme switcher shows options'); }); test('should persist theme selection in localStorage', async ({ page }) => { await page.goto(`${BASE_URL}/?lang=en`); const themeButton = page.locator('button[data-theme], button[data-action="toggle-theme"]').first(); if (await themeButton.count() === 0) return; await themeButton.click(); await waitForAnimation(300); const darkOption = page.locator('button:has-text("Dark"), [data-theme="dark"]').first(); if (await darkOption.count() > 0) { await darkOption.click(); await waitForAnimation(300); // Check localStorage const storedTheme = await page.evaluate(() => localStorage.getItem('theme')); console.log(`Stored theme: ${storedTheme}`); // Reload and verify persistence await page.reload(); await waitForAnimation(300); const themeAfterReload = await page.evaluate(() => localStorage.getItem('theme')); expect(themeAfterReload).toBe(storedTheme); console.log('✅ Theme selection persists in localStorage'); } }); }); test.describe('FEATURE 005: PDF Download Modal', () => { test('should detect PDF modal trigger button', async ({ page }) => { await page.goto(`${BASE_URL}/?lang=en`); const pdfButton = page.locator('button:has-text("PDF"), button:has-text("download"), [data-action="show-pdf"]').first(); const exists = await pdfButton.count() > 0; console.log(`PDF modal button exists: ${exists}`); if (!exists) { console.log('⚠️ PDF MODAL NOT IMPLEMENTED'); return; } await page.screenshot({ path: 'test-results/05-pdf-button.png', fullPage: true }); expect(exists).toBe(true); }); test('should show three thumbnail cards', async ({ page }) => { await page.goto(`${BASE_URL}/?lang=en`); const pdfButton = page.locator('button:has-text("PDF"), button:has-text("download")').first(); if (await pdfButton.count() === 0) return; await pdfButton.click(); await waitForAnimation(300); // Look for thumbnail cards const thumbnails = await page.locator('.thumbnail, .pdf-card, [data-pdf-type]').count(); console.log(`Thumbnail cards found: ${thumbnails}`); await page.screenshot({ path: 'test-results/05-pdf-modal-open.png', fullPage: true }); expect(thumbnails).toBeGreaterThanOrEqual(2); console.log('✅ PDF modal shows thumbnail cards'); }); test('should enable download button after selection', async ({ page }) => { await page.goto(`${BASE_URL}/?lang=en`); const pdfButton = page.locator('button:has-text("PDF"), button:has-text("download")').first(); if (await pdfButton.count() === 0) return; await pdfButton.click(); await waitForAnimation(300); // Find download button (should be disabled initially) const downloadBtn = page.locator('button:has-text("Download"), button[data-action="download"]').first(); if (await downloadBtn.count() > 0) { const initiallyDisabled = await downloadBtn.isDisabled(); console.log(`Download button initially disabled: ${initiallyDisabled}`); // Click first thumbnail const thumbnail = page.locator('.thumbnail, .pdf-card, [data-pdf-type]').first(); if (await thumbnail.count() > 0) { await thumbnail.click(); await waitForAnimation(200); const enabledAfterSelection = !(await downloadBtn.isDisabled()); console.log(`Download button enabled after selection: ${enabledAfterSelection}`); expect(enabledAfterSelection).toBe(true); console.log('✅ Download button enables after selection'); } } }); }); test.describe('INTEGRATION TESTS: Cross-Feature Interactions', () => { test('should handle language switch while modal is open', async ({ page }) => { await page.goto(`${BASE_URL}/?lang=en`); // Open shortcuts modal if exists const shortcutsBtn = page.locator('button[data-action="show-shortcuts"]').first(); if (await shortcutsBtn.count() > 0) { await shortcutsBtn.click(); await waitForAnimation(300); // Switch language const langButton = page.locator('button[data-lang="es"]').first(); if (await langButton.count() > 0) { await langButton.click(); await waitForAnimation(800); await page.screenshot({ path: 'test-results/int-modal-lang-switch.png', fullPage: true }); console.log('✅ Language switch works with modal open'); } } }); test('should handle multiple rapid feature interactions', async ({ page }) => { await page.goto(`${BASE_URL}/?lang=en`); const errors = []; page.on('console', msg => { if (msg.type() === 'error') errors.push(msg.text()); }); // Rapid interactions const langButton = page.locator('button[data-lang="es"]').first(); const toggle = page.locator('input[type="checkbox"]').first(); if (await langButton.count() > 0) await langButton.click(); await waitForAnimation(100); if (await toggle.count() > 0) await toggle.click(); await waitForAnimation(100); if (await langButton.count() > 0) await langButton.click(); await waitForAnimation(800); console.log(`Errors during rapid interactions: ${errors.length}`); expect(errors.length).toBe(0); console.log('✅ Handles rapid feature interactions without errors'); }); }); test.describe('PERFORMANCE & ACCESSIBILITY', () => { test('should have no console errors on page load', async ({ page }) => { const errors = []; page.on('console', msg => { if (msg.type() === 'error') errors.push(msg.text()); }); await page.goto(`${BASE_URL}/?lang=en`); await waitForAnimation(1000); console.log('Console errors on load:', errors); expect(errors.length).toBe(0); console.log('✅ No console errors on page load'); }); test('should measure Core Web Vitals', async ({ page }) => { await page.goto(`${BASE_URL}/?lang=en`); await waitForAnimation(1000); const metrics = await page.evaluate(() => { const paint = performance.getEntriesByType('paint'); const navigation = performance.getEntriesByType('navigation')[0]; return { fcp: paint.find(p => p.name === 'first-contentful-paint')?.startTime, domContentLoaded: navigation?.domContentLoadedEventEnd - navigation?.domContentLoadedEventStart, loadComplete: navigation?.loadEventEnd - navigation?.loadEventStart }; }); console.log('Performance metrics:', metrics); expect(metrics.fcp).toBeLessThan(3000); console.log('✅ Performance metrics within acceptable range'); }); });