feat: Add CMD+K command palette with ninja-keys integration

Implement a command palette accessible via CMD+K/Ctrl+K using the ninja-keys
web component. Features include:

- New /api/cmd-k endpoint serving dynamic CV entries (experiences, projects, courses)
- Language-aware responses with 1-hour cache headers
- Scroll-to-section functionality for quick navigation
- Enhanced keyboard shortcuts modal with CMD+K documentation
- Comprehensive test coverage for API and UI interactions

Also includes cleanup of deprecated debug test files and various UI polish
improvements to contact form, themes, and action bar components.
This commit is contained in:
juanatsap
2025-12-01 13:03:06 +00:00
parent 976b8ae2e2
commit 9a848e8c53
45 changed files with 3070 additions and 1587 deletions
+208
View File
@@ -0,0 +1,208 @@
#!/usr/bin/env bun
/**
* CMD+K API SCROLL POSITION TEST
* ==============================
* Tests the /api/cmd-k endpoint integration with ninja-keys command palette.
*
* Tests:
* 1. API returns valid JSON with experiences, projects, courses
* 2. ninja-keys receives data from API (not hardcoded)
* 3. Scroll navigation works for dynamic entries:
* - Experience items (e.g., Olympic Broadcasting)
* - Project items (e.g., Somos Una Ola)
* - Course items (e.g., Codecademy Certifications)
* 4. Static navigation actions still work (e.g., Skills section)
*
* Related: doc/16-CMD-K-API.md
*/
import { chromium } from 'playwright';
const URL = "http://localhost:1999";
async function testCmdKScroll() {
console.log('🎯 CMD+K SCROLL POSITION TEST\n');
console.log('='.repeat(70));
const browser = await chromium.launch({ headless: process.env.HEADLESS === 'true' });
const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } });
const errors = [];
const testResults = [];
page.on('console', msg => {
if (msg.type() === 'error') {
errors.push(msg.text());
console.log(`❌ ERROR: ${msg.text()}`);
}
});
console.log("\n1️⃣ Loading page...");
await page.goto(URL);
await page.waitForTimeout(2000);
// Helper function to check if element is in viewport
async function isElementInViewport(selector) {
return await page.evaluate((sel) => {
const el = document.querySelector(sel);
if (!el) return { found: false };
const rect = el.getBoundingClientRect();
return {
found: true,
top: rect.top,
inViewport: rect.top >= 0 && rect.top < window.innerHeight
};
}, selector);
}
// Helper function to scroll to top
async function scrollToTop() {
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(300);
}
// Helper function to invoke ninja-keys action directly by ID
async function invokeNinjaKeyAction(actionId) {
const result = await page.evaluate((id) => {
const nk = document.getElementById('cmd-k-bar');
if (!nk || !nk.data) return { success: false, reason: 'ninja-keys not found' };
const action = nk.data.find(a => a.id === id);
if (!action) return { success: false, reason: `action ${id} not found` };
if (action.handler) {
action.handler();
return { success: true, actionTitle: action.title };
}
return { success: false, reason: 'no handler' };
}, actionId);
console.log(` [DEBUG] Action "${actionId}": ${result.success ? 'invoked ' + result.actionTitle : result.reason}`);
await page.waitForTimeout(1000);
return result.success;
}
// ========================================================================
// TEST 1: Scroll to Experience item (Olympic Broadcasting)
// ========================================================================
console.log("\n2️⃣ Testing scroll to Experience (Olympic Broadcasting)...");
await scrollToTop();
const expBefore = await isElementInViewport('#exp-olympic-broadcasting');
console.log(` Element found: ${expBefore.found}`);
console.log(` Before: Element top position = ${expBefore.top?.toFixed(0) || 'N/A'}`);
// API now returns exp-olympic-broadcasting as the action ID
await invokeNinjaKeyAction('exp-olympic-broadcasting');
const expAfter = await isElementInViewport('#exp-olympic-broadcasting');
console.log(` After: Element top position = ${expAfter.top?.toFixed(0) || 'N/A'}`);
const expPassed = expBefore.found && expAfter.found && expAfter.inViewport;
console.log(` ${expPassed ? '✅ PASS' : '❌ FAIL'} - Scrolled to Olympic Broadcasting`);
testResults.push({ test: 'Experience - Olympic Broadcasting', passed: expPassed });
// ========================================================================
// TEST 2: Scroll to Project item (Somos Una Ola)
// ========================================================================
console.log("\n3️⃣ Testing scroll to Project (Somos Una Ola)...");
await scrollToTop();
const projBefore = await isElementInViewport('#proj-somos-una-ola');
console.log(` Element found: ${projBefore.found}`);
console.log(` Before: Element top position = ${projBefore.top?.toFixed(0) || 'N/A'}`);
// API now returns proj-somos-una-ola as the action ID
await invokeNinjaKeyAction('proj-somos-una-ola');
const projAfter = await isElementInViewport('#proj-somos-una-ola');
console.log(` After: Element top position = ${projAfter.top?.toFixed(0) || 'N/A'}`);
const projPassed = projBefore.found && projAfter.found && projAfter.inViewport;
console.log(` ${projPassed ? '✅ PASS' : '❌ FAIL'} - Scrolled to Somos Una Ola`);
testResults.push({ test: 'Project - Somos Una Ola', passed: projPassed });
// ========================================================================
// TEST 3: Scroll to Course item (Codecademy)
// ========================================================================
console.log("\n4️⃣ Testing scroll to Course (Codecademy)...");
await scrollToTop();
const courseBefore = await isElementInViewport('#course-codecademy-certifications');
console.log(` Element found: ${courseBefore.found}`);
console.log(` Before: Element top position = ${courseBefore.top?.toFixed(0) || 'N/A'}`);
// API now returns course-codecademy-certifications as the action ID
await invokeNinjaKeyAction('course-codecademy-certifications');
const courseAfter = await isElementInViewport('#course-codecademy-certifications');
console.log(` After: Element top position = ${courseAfter.top?.toFixed(0) || 'N/A'}`);
const coursePassed = courseBefore.found && courseAfter.found && courseAfter.inViewport;
console.log(` ${coursePassed ? '✅ PASS' : '❌ FAIL'} - Scrolled to Codecademy Certifications`);
testResults.push({ test: 'Course - Codecademy', passed: coursePassed });
// ========================================================================
// TEST 4: Scroll to section (Skills)
// ========================================================================
console.log("\n5️⃣ Testing scroll to Section (Skills)...");
await scrollToTop();
const skillsBefore = await isElementInViewport('#skills');
console.log(` Element found: ${skillsBefore.found}`);
console.log(` Before: Element top position = ${skillsBefore.top?.toFixed(0) || 'N/A'}`);
await invokeNinjaKeyAction('nav-skills');
const skillsAfter = await isElementInViewport('#skills');
console.log(` After: Element top position = ${skillsAfter.top?.toFixed(0) || 'N/A'}`);
const skillsPassed = skillsBefore.found && skillsAfter.found && skillsAfter.inViewport;
console.log(` ${skillsPassed ? '✅ PASS' : '❌ FAIL'} - Scrolled to Skills section`);
testResults.push({ test: 'Section - Skills', passed: skillsPassed });
// ========================================================================
// 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 CMD+K SCROLL TESTS PASSED!");
} else {
console.log("⚠️ SOME TESTS FAILED - See details above");
}
// Auto-close after tests if HEADLESS env is set, otherwise keep open
if (process.env.HEADLESS === 'true') {
await browser.close();
process.exit(failedTests === 0 ? 0 : 1);
} else {
console.log("\nBrowser will stay open for manual inspection.");
console.log("Press Ctrl+C when done.\n");
await new Promise(() => {}); // Keep browser open
}
}
await testCmdKScroll();
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env bun
/**
* CMD+K BUTTON FUNCTIONALITY TEST
* ================================
* Tests that the CMD+K button:
* - Has a distinct icon from zoom button
* - Click handler opens ninja-keys
* - At-bottom illumination works
* - Has distinct color from zoom button
*/
import { chromium } from 'playwright';
const URL = "http://localhost:1999";
async function testCmdKButton() {
console.log('🎯 CMD+K BUTTON FUNCTIONALITY TEST\n');
console.log('='.repeat(70));
const browser = await chromium.launch({ headless: process.env.HEADLESS === 'true' });
const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } });
const testResults = [];
await page.goto(URL);
await page.waitForTimeout(2000);
// ========================================================================
// TEST 1: Button exists with distinct icon
// ========================================================================
console.log('\n1️⃣ Testing button exists with distinct icon...');
const cmdKButton = await page.$('#cmd-k-button');
const buttonExists = cmdKButton !== null;
console.log(` Button exists: ${buttonExists}`);
let iconsDistinct = false;
if (buttonExists) {
const cmdKIcon = await page.$eval('#cmd-k-button iconify-icon', el => el.getAttribute('icon'));
const zoomIcon = await page.$eval('#zoom-toggle-button iconify-icon', el => el.getAttribute('icon'));
console.log(` CMD+K icon: ${cmdKIcon}`);
console.log(` Zoom icon: ${zoomIcon}`);
iconsDistinct = cmdKIcon !== zoomIcon;
console.log(` ${iconsDistinct ? '✅ PASS' : '❌ FAIL'} - Icons are distinct`);
}
testResults.push({ test: 'Icons are distinct', passed: buttonExists && iconsDistinct });
// ========================================================================
// TEST 2: Button click opens ninja-keys
// ========================================================================
console.log('\n2️⃣ Testing button click opens ninja-keys...');
await page.click('#cmd-k-button');
await page.waitForTimeout(500);
const ninjaKeysOpen = await page.$eval('#cmd-k-bar', el => el.hasAttribute('open'));
console.log(` Ninja-keys open: ${ninjaKeysOpen}`);
console.log(` ${ninjaKeysOpen ? '✅ PASS' : '❌ FAIL'} - Click opens ninja-keys`);
testResults.push({ test: 'Click opens ninja-keys', passed: ninjaKeysOpen });
// Close ninja-keys
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
// ========================================================================
// TEST 3: At-bottom illumination
// ========================================================================
console.log('\n3️⃣ Testing at-bottom illumination...');
// Scroll to very top - use scroll() which fires event naturally
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForTimeout(100);
// Trigger handleScroll manually via hyperscript's behavior
await page.evaluate(() => window.dispatchEvent(new Event('scroll')));
await page.waitForTimeout(300);
const atTopClass = await page.$eval('#cmd-k-button', el => el.classList.contains('at-bottom'));
console.log(` At top - has at-bottom class: ${atTopClass}`);
// Scroll to bottom using mouse wheel to trigger actual scroll
await page.mouse.wheel(0, 100000);
await page.waitForTimeout(1000);
const atBottomClass = await page.$eval('#cmd-k-button', el => el.classList.contains('at-bottom'));
const zoomAtBottomClass = await page.$eval('#zoom-toggle-button', el => el.classList.contains('at-bottom'));
const infoAtBottomClass = await page.$eval('#info-button', el => el.classList.contains('at-bottom'));
console.log(` At bottom - CMD+K has at-bottom: ${atBottomClass}`);
console.log(` At bottom - Zoom has at-bottom: ${zoomAtBottomClass}`);
console.log(` At bottom - Info has at-bottom: ${infoAtBottomClass}`);
const illuminationWorks = !atTopClass && atBottomClass;
console.log(` ${illuminationWorks ? '✅ PASS' : '❌ FAIL'} - At-bottom illumination works`);
testResults.push({ test: 'At-bottom illumination', passed: illuminationWorks });
// ========================================================================
// TEST 4: Distinct color from zoom button
// ========================================================================
console.log('\n4️⃣ Testing distinct color at bottom...');
const cmdKBgColor = await page.$eval('#cmd-k-button', el => window.getComputedStyle(el).backgroundColor);
const zoomBgColor = await page.$eval('#zoom-toggle-button', el => window.getComputedStyle(el).backgroundColor);
console.log(` CMD+K bg color: ${cmdKBgColor}`);
console.log(` Zoom bg color: ${zoomBgColor}`);
const colorsDistinct = cmdKBgColor !== zoomBgColor;
console.log(` ${colorsDistinct ? '✅ PASS' : '❌ FAIL'} - Colors are distinct`);
testResults.push({ test: 'Colors are distinct', passed: colorsDistinct });
// ========================================================================
// 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`);
console.log("=".repeat(70) + "\n");
if (failedTests === 0) {
console.log("🎉 ALL CMD+K BUTTON TESTS PASSED!");
} else {
console.log("⚠️ SOME TESTS FAILED - See details above");
}
// Auto-close after tests if HEADLESS env is set
if (process.env.HEADLESS === 'true') {
await browser.close();
process.exit(failedTests === 0 ? 0 : 1);
} else {
console.log("\nBrowser will stay open for manual inspection.");
console.log("Press Ctrl+C when done.\n");
await new Promise(() => {}); // Keep browser open
}
}
await testCmdKButton();
+329
View File
@@ -0,0 +1,329 @@
#!/usr/bin/env bun
/**
* CONTACT FORM TEST
* =================
* Tests contact form functionality and error handling
* - Modal opens correctly
* - Form elements and validation
* - HTMX attributes for error handling
* - Timestamp reset on modal open
* - Form submission flow
*/
import { chromium } from 'playwright';
const URL = "http://localhost:1999";
async function testContactForm() {
console.log('📧 CONTACT FORM TEST\n');
console.log('='.repeat(70));
const browser = await chromium.launch({ headless: true });
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}`);
}
});
console.log("\n1️⃣ Loading page...");
await page.goto(URL);
await page.waitForTimeout(2000);
// ========================================================================
// TEST 1: Contact modal exists
// ========================================================================
console.log("\n2️⃣ Testing Contact Modal Elements...");
const modalTest = await page.evaluate(() => {
const modal = document.querySelector('#contact-modal');
const form = document.querySelector('#contact-form');
const emailField = document.querySelector('#contact-email');
const messageField = document.querySelector('#contact-message');
const submitBtn = document.querySelector('.contact-submit-btn');
return {
modalExists: !!modal,
formExists: !!form,
emailExists: !!emailField,
messageExists: !!messageField,
submitExists: !!submitBtn
};
});
console.log(` Contact modal: ${modalTest.modalExists ? '✅' : '❌'}`);
console.log(` Contact form: ${modalTest.formExists ? '✅' : '❌'}`);
console.log(` Email field: ${modalTest.emailExists ? '✅' : '❌'}`);
console.log(` Message field: ${modalTest.messageExists ? '✅' : '❌'}`);
console.log(` Submit button: ${modalTest.submitExists ? '✅' : '❌'}`);
const allElementsExist = modalTest.modalExists && modalTest.formExists &&
modalTest.emailExists && modalTest.messageExists && modalTest.submitExists;
console.log(` ${allElementsExist ? '✅ PASS' : '❌ FAIL'} - Contact form elements exist`);
testResults.push({ test: 'Contact Form Elements Exist', passed: allElementsExist });
// ========================================================================
// TEST 2: Contact button opens modal
// ========================================================================
console.log("\n3️⃣ Testing Contact Button Opens Modal...");
const contactBtn = await page.$('.fixed-btn.contact-btn, .contact-btn, [data-modal-trigger="contact"]');
if (contactBtn) {
await contactBtn.click();
await page.waitForTimeout(500);
const modalOpened = await page.evaluate(() => {
const modal = document.querySelector('#contact-modal');
if (!modal) return false;
return modal.hasAttribute('open') || modal.classList.contains('open') ||
window.getComputedStyle(modal).display !== 'none';
});
console.log(` Modal opened: ${modalOpened ? '✅' : '❌'}`);
console.log(` ${modalOpened ? '✅ PASS' : '❌ FAIL'} - Contact modal opens`);
testResults.push({ test: 'Contact Modal Opens', passed: modalOpened });
// Close it for next test
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
} else {
console.log(` ⚠️ SKIP - Contact button not found`);
testResults.push({ test: 'Contact Modal Opens', passed: false });
}
// ========================================================================
// TEST 3: Form has hyperscript for success detection (content-based, not HTTP status)
// ========================================================================
console.log("\n4️⃣ Testing Hyperscript Success Detection...");
const hsTest = await page.evaluate(() => {
const form = document.querySelector('#contact-form');
if (!form) return { found: false };
const hsAttribute = form.getAttribute('_') || '';
const hasAfterRequest = hsAttribute.includes('htmx:afterRequest');
const checksSuccessElement = hsAttribute.includes('.contact-success');
return {
found: true,
hasAfterRequest,
checksSuccessElement
};
});
if (hsTest.found) {
console.log(` Handles htmx:afterRequest: ${hsTest.hasAfterRequest ? '✅' : '❌'}`);
console.log(` Checks for .contact-success: ${hsTest.checksSuccessElement ? '✅' : '❌'}`);
const handlerCorrect = hsTest.hasAfterRequest && hsTest.checksSuccessElement;
console.log(` ${handlerCorrect ? '✅ PASS' : '❌ FAIL'} - Success detection via content (not HTTP status)`);
testResults.push({ test: 'Hyperscript Success Detection', passed: handlerCorrect });
} else {
console.log(` ❌ FAIL - Form not found`);
testResults.push({ test: 'Hyperscript Success Detection', passed: false });
}
// ========================================================================
// TEST 4: Timestamp field exists and resets on modal open
// ========================================================================
console.log("\n5️⃣ Testing Timestamp Reset...");
// Get initial timestamp
const initialTimestamp = await page.$eval('#contact-form-loaded-at', el => el.value);
console.log(` Initial timestamp: ${initialTimestamp}`);
// Open modal again
if (contactBtn) {
await page.waitForTimeout(1000);
await contactBtn.click();
await page.waitForTimeout(500);
const newTimestamp = await page.$eval('#contact-form-loaded-at', el => el.value);
console.log(` After modal open: ${newTimestamp}`);
const timestampReset = parseInt(newTimestamp) > parseInt(initialTimestamp);
console.log(` Timestamp updated: ${timestampReset ? '✅' : '❌'}`);
console.log(` ${timestampReset ? '✅ PASS' : '❌ FAIL'} - Timestamp resets on modal open`);
testResults.push({ test: 'Timestamp Reset on Modal Open', passed: timestampReset });
// Keep modal open for next test
} else {
console.log(` ⚠️ SKIP - Contact button not found`);
testResults.push({ test: 'Timestamp Reset on Modal Open', passed: false });
}
// ========================================================================
// TEST 5: Form HTMX attributes
// ========================================================================
console.log("\n6️⃣ Testing Form HTMX Configuration...");
const formConfig = await page.evaluate(() => {
const form = document.querySelector('#contact-form');
if (!form) return { found: false };
return {
found: true,
hxPost: form.getAttribute('hx-post'),
hxTarget: form.getAttribute('hx-target'),
hxSwap: form.getAttribute('hx-swap'),
hxIndicator: form.getAttribute('hx-indicator'),
hasHeaders: form.hasAttribute('hx-headers')
};
});
if (formConfig.found) {
console.log(` hx-post: ${formConfig.hxPost || 'N/A'}`);
console.log(` hx-target: ${formConfig.hxTarget || 'N/A'}`);
console.log(` hx-swap: ${formConfig.hxSwap || 'N/A'}`);
console.log(` hx-indicator: ${formConfig.hxIndicator || 'N/A'}`);
console.log(` hx-headers: ${formConfig.hasHeaders ? '✅' : '❌'}`);
const configCorrect = formConfig.hxPost && formConfig.hxTarget && formConfig.hxSwap;
console.log(` ${configCorrect ? '✅ PASS' : '❌ FAIL'} - Form HTMX configuration`);
testResults.push({ test: 'Form HTMX Configuration', passed: configCorrect });
} else {
console.log(` ❌ FAIL - Form not found`);
testResults.push({ test: 'Form HTMX Configuration', passed: false });
}
// ========================================================================
// TEST 6: Bot protection fields
// ========================================================================
console.log("\n7️⃣ Testing Bot Protection...");
const botProtection = await page.evaluate(() => {
const honeypot = document.querySelector('#contact-website, [name="website"]');
const timestampField = document.querySelector('#contact-form-loaded-at, [name="form_loaded_at"]');
return {
honeypotExists: !!honeypot,
honeypotHidden: honeypot ? (honeypot.closest('[style*="left: -9999px"]') !== null ||
window.getComputedStyle(honeypot.parentElement).position === 'absolute') : false,
timestampExists: !!timestampField,
timestampHidden: timestampField ? timestampField.type === 'hidden' : false
};
});
console.log(` Honeypot field: ${botProtection.honeypotExists ? '✅' : '❌'}`);
console.log(` Honeypot hidden: ${botProtection.honeypotHidden ? '✅' : '❌'}`);
console.log(` Timestamp field: ${botProtection.timestampExists ? '✅' : '❌'}`);
console.log(` Timestamp hidden: ${botProtection.timestampHidden ? '✅' : '❌'}`);
const botProtectionOk = botProtection.honeypotExists && botProtection.timestampExists;
console.log(` ${botProtectionOk ? '✅ PASS' : '❌ FAIL'} - Bot protection configured`);
testResults.push({ test: 'Bot Protection Fields', passed: botProtectionOk });
// ========================================================================
// TEST 7: REAL submission test - verify no console errors on 400 response
// ========================================================================
console.log("\n8️⃣ Testing Form Submission Error Handling (REAL TEST)...");
// Clear any previous errors for this specific test
const errorsBefore = errors.length;
// Make sure modal is open
if (contactBtn) {
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
await contactBtn.click();
await page.waitForTimeout(500);
// Fill form with invalid data (message too short to trigger 400)
await page.fill('#contact-email', 'test@example.com');
await page.fill('#contact-message', 'hi'); // Too short - will trigger 400
// Wait for timing validation (>2s)
await page.waitForTimeout(2500);
// Submit form via JavaScript to bypass HTML5 validation
await page.evaluate(() => {
const form = document.querySelector('#contact-form');
if (form) {
// Remove HTML5 required attributes temporarily
const emailField = form.querySelector('#contact-email');
const msgField = form.querySelector('#contact-message');
emailField.removeAttribute('required');
msgField.removeAttribute('required');
// Trigger HTMX request
if (window.htmx) {
window.htmx.trigger(form, 'submit');
}
}
});
await page.waitForTimeout(2000);
const errorsAfter = errors.length;
const newErrors = errorsAfter - errorsBefore;
// With HTTP 200 for validation errors, there should be NO console errors at all
// HTMX only logs errors for non-2xx responses
console.log(` Errors before submit: ${errorsBefore}`);
console.log(` Errors after submit: ${errorsAfter}`);
console.log(` New errors during submit: ${newErrors}`);
if (newErrors > 0) {
const newErrorList = errors.slice(errorsBefore);
newErrorList.forEach(e => console.log(`${e}`));
}
const noErrors = newErrors === 0;
console.log(` ${noErrors ? '✅ PASS' : '❌ FAIL'} - Zero console errors on validation error`);
testResults.push({ test: 'Zero Console Errors on Validation', passed: noErrors });
// Check that error message is displayed in the form
const errorDisplayed = await page.evaluate(() => {
const response = document.querySelector('#contact-response');
return response && response.innerHTML.length > 0;
});
console.log(` Error message displayed in form: ${errorDisplayed ? '✅' : '❌'}`);
} else {
console.log(` ⚠️ SKIP - Contact button not found`);
testResults.push({ test: 'No Console Errors on 400 Response', passed: false });
}
// ========================================================================
// 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`);
errors.forEach(e => console.log(` - ${e}`));
}
console.log("=".repeat(70) + "\n");
if (failedTests === 0) {
console.log("🎉 CONTACT FORM FUNCTIONALITY VALIDATED!");
} else {
console.log("⚠️ SOME TESTS FAILED - See details above");
}
// Close browser and exit
await browser.close();
// Exit with appropriate code
process.exit(failedTests === 0 ? 0 : 1);
}
await testContactForm();
-48
View File
@@ -1,48 +0,0 @@
#!/usr/bin/env bun
import { chromium } from 'playwright';
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto('http://localhost:1999/');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Get all CSS rules affecting the theme button
const cssRules = await page.evaluate(() => {
const btn = document.getElementById('color-theme-switcher');
if (!btn) return null;
const matchedRules = [];
// Get all stylesheets
for (const sheet of document.styleSheets) {
try {
const rules = sheet.cssRules || sheet.rules;
for (const rule of rules) {
if (rule.selectorText && btn.matches(rule.selectorText)) {
matchedRules.push({
selector: rule.selectorText,
position: rule.style.position || 'not set',
bottom: rule.style.bottom || 'not set',
left: rule.style.left || 'not set',
source: sheet.href || 'inline',
cssText: rule.cssText
});
}
}
} catch (e) {
// Skip CORS blocked stylesheets
}
}
return matchedRules;
});
console.log('📋 CSS Rules matching #color-theme-switcher:');
console.log(JSON.stringify(cssRules, null, 2));
await page.waitForTimeout(5000);
await browser.close();
})();
-69
View File
@@ -1,69 +0,0 @@
#!/usr/bin/env bun
import { chromium } from 'playwright';
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
// Navigate to page
await page.goto('http://localhost:1999/');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Check if theme button exists
const themeButton = page.locator('#color-theme-switcher');
const exists = await themeButton.count() > 0;
console.log('✅ Theme button exists in DOM:', exists);
if (exists) {
// Get computed styles
const isVisible = await themeButton.isVisible();
console.log('🔍 Button isVisible():', isVisible);
const box = await themeButton.boundingBox();
console.log('📦 Bounding box:', box);
// Get computed styles manually
const styles = await page.evaluate(() => {
const btn = document.getElementById('color-theme-switcher');
if (!btn) return null;
const computed = window.getComputedStyle(btn);
return {
display: computed.display,
visibility: computed.visibility,
opacity: computed.opacity,
position: computed.position,
bottom: computed.bottom,
left: computed.left,
width: computed.width,
height: computed.height,
background: computed.background,
backgroundColor: computed.backgroundColor,
zIndex: computed.zIndex,
transform: computed.transform,
};
});
console.log('🎨 Computed styles:', JSON.stringify(styles, null, 2));
// Check --black-bar variable
const blackBar = await page.evaluate(() => {
return getComputedStyle(document.documentElement).getPropertyValue('--black-bar');
});
console.log('🔧 --black-bar variable:', blackBar || 'NOT DEFINED');
// Take screenshot
await page.screenshot({
path: '/Users/txeo/Git/yo/cv/tests/mjs/screenshots/theme-button-debug.png',
fullPage: false
});
console.log('📸 Screenshot saved');
}
// Keep browser open for manual inspection
console.log('\n⏸️ Browser kept open for inspection. Press Ctrl+C to close.');
await page.waitForTimeout(60000);
await browser.close();
})();
-134
View File
@@ -1,134 +0,0 @@
#!/usr/bin/env bun
/**
* Quick debug to see what's happening with tooltips
*/
import { chromium } from 'playwright';
const URL = "http://localhost:1999";
async function debugTooltip() {
console.log('🔍 TOOLTIP DEBUG\n');
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } });
await page.goto(URL);
await page.waitForTimeout(2000);
console.log("\n1️⃣ Checking button and tooltip setup...");
const buttonInfo = await page.evaluate(() => {
const btn = document.querySelector('#action-bar-pdf-btn');
if (!btn) return { found: false };
const computedBefore = window.getComputedStyle(btn, '::before');
return {
found: true,
classes: Array.from(btn.classList),
dataTooltip: btn.getAttribute('data-tooltip'),
beforeStyles: {
content: computedBefore.content,
position: computedBefore.position,
opacity: computedBefore.opacity,
visibility: computedBefore.visibility,
display: computedBefore.display,
fontSize: computedBefore.fontSize,
fontWeight: computedBefore.fontWeight,
background: computedBefore.background,
color: computedBefore.color,
left: computedBefore.left,
top: computedBefore.top,
transform: computedBefore.transform,
zIndex: computedBefore.zIndex
}
};
});
console.log('\n📊 Button Information:');
console.log(' Classes:', buttonInfo.classes.join(', '));
console.log(' data-tooltip:', buttonInfo.dataTooltip);
console.log('\n🎨 ::before Pseudo-element Styles:');
console.log(' content:', buttonInfo.beforeStyles.content);
console.log(' position:', buttonInfo.beforeStyles.position);
console.log(' opacity:', buttonInfo.beforeStyles.opacity);
console.log(' visibility:', buttonInfo.beforeStyles.visibility);
console.log(' display:', buttonInfo.beforeStyles.display);
console.log(' fontSize:', buttonInfo.beforeStyles.fontSize);
console.log(' fontWeight:', buttonInfo.beforeStyles.fontWeight);
console.log(' background:', buttonInfo.beforeStyles.background);
console.log(' color:', buttonInfo.beforeStyles.color);
console.log(' left:', buttonInfo.beforeStyles.left);
console.log(' top:', buttonInfo.beforeStyles.top);
console.log(' transform:', buttonInfo.beforeStyles.transform);
console.log(' z-index:', buttonInfo.beforeStyles.zIndex);
console.log('\n2️⃣ Checking if tooltip CSS file is loaded...');
const cssCheck = await page.evaluate(() => {
// Check all link tags
const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));
const cssLinks = links.map(l => l.href);
// Try to find tooltip rules
let tooltipRulesFound = [];
for (const sheet of document.styleSheets) {
try {
const rules = Array.from(sheet.cssRules || []);
for (const rule of rules) {
if (rule.cssText && (rule.cssText.includes('.has-tooltip') || rule.cssText.includes('has-tooltip'))) {
tooltipRulesFound.push(rule.cssText.substring(0, 100));
}
}
} catch (e) {
// Skip cross-origin sheets
}
}
return {
cssLinks,
tooltipRulesFound
};
});
console.log('\n📄 CSS Files Loaded:');
cssCheck.cssLinks.forEach(link => console.log(' -', link));
console.log('\n📋 Tooltip CSS Rules Found:', cssCheck.tooltipRulesFound.length);
if (cssCheck.tooltipRulesFound.length > 0) {
console.log(' Sample rules:');
cssCheck.tooltipRulesFound.slice(0, 3).forEach(rule => console.log(' -', rule));
}
console.log('\n3️⃣ Testing hover...');
console.log('Hovering over PDF button in 2 seconds...');
await page.waitForTimeout(2000);
await page.hover('#action-bar-pdf-btn');
await page.waitForTimeout(1000);
const afterHover = await page.evaluate(() => {
const btn = document.querySelector('#action-bar-pdf-btn');
const computedBefore = window.getComputedStyle(btn, '::before');
return {
opacity: computedBefore.opacity,
visibility: computedBefore.visibility,
transform: computedBefore.transform
};
});
console.log('\n🖱️ After Hover:');
console.log(' opacity:', afterHover.opacity, '(should be 1)');
console.log(' visibility:', afterHover.visibility, '(should be visible)');
console.log(' transform:', afterHover.transform);
console.log('\n💡 Browser is open. Try hovering over the buttons yourself!');
console.log('Press Ctrl+C when done.\n');
await new Promise(() => {}); // Keep browser open
}
await debugTooltip();
@@ -1,133 +0,0 @@
import { chromium } from 'playwright';
/**
* Test preference migration from old to new values
*
* Tests:
* 1. Old 'long' → migrates to 'extended'
* 2. Old 'true'/'false' → migrates to 'show'/'hide'
* 3. Toggles work correctly with new values
*/
async function testPreferenceMigration() {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();
try {
console.log('\n=== Testing Preference Migration ===\n');
// Test 1: Set old values and verify migration
console.log('Test 1: Setting old localStorage values...');
await page.goto('http://localhost:1999/?lang=en');
// Set OLD values
await page.evaluate(() => {
localStorage.setItem('cv-length', 'extended');
localStorage.setItem('cv-icons', 'true');
});
console.log(' ✓ Old values set: length=extended, icons=true');
// Reload page to trigger migration
console.log('\nTest 2: Reloading page to trigger migration...');
await page.reload();
await page.waitForTimeout(500);
// Check that values were migrated
const migratedLength = await page.evaluate(() => localStorage.getItem('cv-length'));
const migratedIcons = await page.evaluate(() => localStorage.getItem('cv-icons'));
console.log(` Migration result: length="${migratedLength}", icons="${migratedIcons}"`);
if (migratedLength === 'long' && migratedIcons === 'show') {
console.log(' ✅ Migration successful!');
} else {
console.error(` ❌ Migration failed! Expected: length="long", icons="show"`);
process.exit(1);
}
// Test 3: Verify UI state matches migrated values
console.log('\nTest 3: Verifying UI state...');
const hasLongClass = await page.evaluate(() => {
return document.querySelector('.cv-paper')?.classList.contains('cv-long');
});
const hasIconsClass = await page.evaluate(() => {
return document.querySelector('.cv-paper')?.classList.contains('show-icons');
});
console.log(` UI state: cv-long=${hasLongClass}, show-icons=${hasIconsClass}`);
if (hasLongClass && hasIconsClass) {
console.log(' ✅ UI state correct!');
} else {
console.error(' ❌ UI state incorrect!');
process.exit(1);
}
// Test 4: Toggle and verify new values are used
console.log('\nTest 4: Testing toggles with new values...');
// Toggle length
const lengthToggle = await page.$('#lengthToggle');
if (lengthToggle) {
await lengthToggle.click();
await page.waitForTimeout(500);
const newLength = await page.evaluate(() => localStorage.getItem('cv-length'));
console.log(` Length toggle clicked, new value: "${newLength}"`);
if (newLength === 'short') {
console.log(' ✅ Length toggle works correctly!');
} else {
console.error(` ❌ Length toggle failed! Expected "short", got "${newLength}"`);
process.exit(1);
}
}
// Toggle icons
const iconToggle = await page.$('#iconToggle');
if (iconToggle) {
await iconToggle.click();
await page.waitForTimeout(500);
const newIcons = await page.evaluate(() => localStorage.getItem('cv-icons'));
console.log(` Icon toggle clicked, new value: "${newIcons}"`);
if (newIcons === 'hide') {
console.log(' ✅ Icon toggle works correctly!');
} else {
console.error(` ❌ Icon toggle failed! Expected "hide", got "${newIcons}"`);
process.exit(1);
}
}
// Test 5: Test with 'false' old value
console.log('\nTest 5: Testing migration of "false" value...');
await page.evaluate(() => {
localStorage.setItem('cv-icons', 'false');
});
await page.reload();
await page.waitForTimeout(500);
const migratedFalse = await page.evaluate(() => localStorage.getItem('cv-icons'));
console.log(` Migration of "false": "${migratedFalse}"`);
if (migratedFalse === 'hide') {
console.log(' ✅ "false" migrated to "hide" correctly!');
} else {
console.error(` ❌ "false" migration failed! Expected "hide", got "${migratedFalse}"`);
process.exit(1);
}
console.log('\n=== ✅ All tests passed! ===\n');
} catch (error) {
console.error('\n❌ Test failed with error:', error);
process.exit(1);
} finally {
await browser.close();
}
}
testPreferenceMigration();
-44
View File
@@ -1,44 +0,0 @@
import { chromium } from 'playwright';
async function verifyMigration() {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext();
const page = await context.newPage();
try {
console.log('\n=== Quick Migration Verification ===\n');
// Set old values
await page.goto('http://localhost:1999/?lang=en');
await page.evaluate(() => {
localStorage.setItem('cv-length', 'extended');
localStorage.setItem('cv-icons', 'true');
});
console.log('✓ Set old values: length="extended", icons="true"');
// Reload to trigger migration
await page.reload();
await page.waitForTimeout(500);
// Check migration
const length = await page.evaluate(() => localStorage.getItem('cv-length'));
const icons = await page.evaluate(() => localStorage.getItem('cv-icons'));
console.log(`✓ After migration: length="${length}", icons="${icons}"`);
if (length === 'long' && icons === 'show') {
console.log('\n✅ Migration SUCCESS!\n');
process.exit(0);
} else {
console.error('\n❌ Migration FAILED!\n');
process.exit(1);
}
} catch (error) {
console.error('\n❌ Error:', error.message, '\n');
process.exit(1);
} finally {
await browser.close();
}
}
verifyMigration();
-86
View File
@@ -1,86 +0,0 @@
#!/usr/bin/env bun
import { chromium } from 'playwright';
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } });
console.log('🔍 Testing theme button visibility on DESKTOP...\n');
await page.goto('http://localhost:1999/');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Check theme button
const themeButton = page.locator('#color-theme-switcher');
const exists = await themeButton.count() > 0;
console.log(exists ? '✅ Theme button exists in DOM' : '❌ Theme button NOT in DOM');
if (exists) {
const isVisible = await themeButton.isVisible();
console.log(isVisible ? '✅ Theme button is VISIBLE' : '❌ Theme button is NOT visible');
const box = await themeButton.boundingBox();
console.log('\n📦 Bounding box:', box);
// Check if Y position is positive (on screen)
if (box && box.y > 0) {
console.log('✅ Button is ON SCREEN (y > 0)');
} else if (box && box.y < 0) {
console.log('❌ Button is ABOVE SCREEN (y < 0) - STILL BROKEN');
}
// Get computed position
const position = await page.evaluate(() => {
const btn = document.getElementById('color-theme-switcher');
const computed = window.getComputedStyle(btn);
return {
position: computed.position,
bottom: computed.bottom,
left: computed.left,
top: computed.top
};
});
console.log('\n🎨 Computed position:', position);
if (position.position === 'fixed') {
console.log('✅ Position is FIXED (correct!)');
} else {
console.log(`❌ Position is ${position.position} (should be fixed)`);
}
// Take screenshot
await page.screenshot({
path: '/Users/txeo/Git/yo/cv/tests/mjs/screenshots/theme-button-fixed.png',
fullPage: false
});
console.log('\n📸 Screenshot saved to screenshots/theme-button-fixed.png');
// Test tooltip
console.log('\n🎯 Testing tooltip...');
await themeButton.hover();
await page.waitForTimeout(500);
const tooltipVisible = await page.evaluate(() => {
const btn = document.getElementById('color-theme-switcher');
const computed = window.getComputedStyle(btn, '::before');
return {
opacity: computed.opacity,
visibility: computed.visibility,
content: computed.content
};
});
console.log('💬 Tooltip state:', tooltipVisible);
if (parseFloat(tooltipVisible.opacity) > 0.5) {
console.log('✅ Tooltip is VISIBLE on hover');
} else {
console.log('⚠️ Tooltip opacity is low or hidden');
}
}
console.log('\n✅ Test complete - browser will close in 5 seconds');
await page.waitForTimeout(5000);
await browser.close();
})();