#!/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();