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();
|