9a848e8c53
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.
209 lines
8.2 KiB
JavaScript
209 lines
8.2 KiB
JavaScript
#!/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();
|