Files
cv-site/tests/comprehensive-features.spec.js
T
juanatsap 25e9ebafe7 bf fixes
2025-11-16 10:11:58 +00:00

619 lines
22 KiB
JavaScript

/**
* 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');
});
});