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.
226 lines
6.5 KiB
Go
226 lines
6.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/juanatsap/cv-site/internal/config"
|
|
"github.com/juanatsap/cv-site/internal/templates"
|
|
)
|
|
|
|
// TestCmdKData tests the CmdKData handler
|
|
// NOTE: This test requires running from project root due to data file path resolution
|
|
// Run with: go test ./internal/handlers/ -run TestCmdKData -v
|
|
func TestCmdKData(t *testing.T) {
|
|
// Skip if running in short mode (CI) - requires project root
|
|
if testing.Short() {
|
|
t.Skip("Skipping CmdKData test - requires running from project root")
|
|
}
|
|
|
|
cfg := &config.TemplateConfig{
|
|
Dir: "../../templates",
|
|
PartialsDir: "../../templates/partials",
|
|
HotReload: true,
|
|
}
|
|
tmplManager, err := templates.NewManager(cfg)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create template manager: %v", err)
|
|
}
|
|
|
|
handler := NewCVHandler(tmplManager, "localhost:8080")
|
|
|
|
tests := []struct {
|
|
name string
|
|
lang string
|
|
expectStatus int
|
|
expectExperiences bool // should have experiences
|
|
expectProjects bool // should have projects
|
|
expectCourses bool // should have courses
|
|
expectMinExp int // minimum expected experiences
|
|
expectMinProj int // minimum expected projects
|
|
expectMinCourses int // minimum expected courses
|
|
}{
|
|
{
|
|
name: "Default language (English)",
|
|
lang: "",
|
|
expectStatus: http.StatusOK,
|
|
expectExperiences: true,
|
|
expectProjects: true,
|
|
expectCourses: true,
|
|
expectMinExp: 5,
|
|
expectMinProj: 3,
|
|
expectMinCourses: 2,
|
|
},
|
|
{
|
|
name: "English language",
|
|
lang: "en",
|
|
expectStatus: http.StatusOK,
|
|
expectExperiences: true,
|
|
expectProjects: true,
|
|
expectCourses: true,
|
|
expectMinExp: 5,
|
|
expectMinProj: 3,
|
|
expectMinCourses: 2,
|
|
},
|
|
{
|
|
name: "Spanish language",
|
|
lang: "es",
|
|
expectStatus: http.StatusOK,
|
|
expectExperiences: true,
|
|
expectProjects: true,
|
|
expectCourses: true,
|
|
expectMinExp: 5,
|
|
expectMinProj: 3,
|
|
expectMinCourses: 2,
|
|
},
|
|
{
|
|
name: "Invalid language defaults to English",
|
|
lang: "fr",
|
|
expectStatus: http.StatusOK,
|
|
expectExperiences: true,
|
|
expectProjects: true,
|
|
expectCourses: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Build query string
|
|
query := "/api/cmd-k"
|
|
if tt.lang != "" {
|
|
query += "?lang=" + tt.lang
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, query, nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.CmdKData(rec, req)
|
|
|
|
// Check status code
|
|
if rec.Code != tt.expectStatus {
|
|
t.Errorf("Expected status %d, got %d", tt.expectStatus, rec.Code)
|
|
}
|
|
|
|
// If success, validate JSON response
|
|
if rec.Code == http.StatusOK {
|
|
// Check content type
|
|
contentType := rec.Header().Get("Content-Type")
|
|
if contentType != "application/json" {
|
|
t.Errorf("Expected Content-Type application/json, got %s", contentType)
|
|
}
|
|
|
|
// Parse JSON response
|
|
var response CmdKResponse
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("Failed to parse JSON response: %v", err)
|
|
}
|
|
|
|
// Validate experiences
|
|
if tt.expectExperiences && len(response.Experiences) == 0 {
|
|
t.Error("Expected experiences but got none")
|
|
}
|
|
if tt.expectMinExp > 0 && len(response.Experiences) < tt.expectMinExp {
|
|
t.Errorf("Expected at least %d experiences, got %d", tt.expectMinExp, len(response.Experiences))
|
|
}
|
|
|
|
// Validate projects
|
|
if tt.expectProjects && len(response.Projects) == 0 {
|
|
t.Error("Expected projects but got none")
|
|
}
|
|
if tt.expectMinProj > 0 && len(response.Projects) < tt.expectMinProj {
|
|
t.Errorf("Expected at least %d projects, got %d", tt.expectMinProj, len(response.Projects))
|
|
}
|
|
|
|
// Validate courses
|
|
if tt.expectCourses && len(response.Courses) == 0 {
|
|
t.Error("Expected courses but got none")
|
|
}
|
|
if tt.expectMinCourses > 0 && len(response.Courses) < tt.expectMinCourses {
|
|
t.Errorf("Expected at least %d courses, got %d", tt.expectMinCourses, len(response.Courses))
|
|
}
|
|
|
|
// Validate structure of first experience (if present)
|
|
if len(response.Experiences) > 0 {
|
|
exp := response.Experiences[0]
|
|
if exp.ID == "" {
|
|
t.Error("Experience ID should not be empty")
|
|
}
|
|
if exp.Title == "" {
|
|
t.Error("Experience Title should not be empty")
|
|
}
|
|
if exp.Section != "Experience" {
|
|
t.Errorf("Experience Section should be 'Experience', got '%s'", exp.Section)
|
|
}
|
|
}
|
|
|
|
// Validate structure of first project (if present)
|
|
if len(response.Projects) > 0 {
|
|
proj := response.Projects[0]
|
|
if proj.ID == "" {
|
|
t.Error("Project ID should not be empty")
|
|
}
|
|
if proj.Title == "" {
|
|
t.Error("Project Title should not be empty")
|
|
}
|
|
if proj.Section != "Projects" {
|
|
t.Errorf("Project Section should be 'Projects', got '%s'", proj.Section)
|
|
}
|
|
}
|
|
|
|
// Validate structure of first course (if present)
|
|
if len(response.Courses) > 0 {
|
|
course := response.Courses[0]
|
|
if course.ID == "" {
|
|
t.Error("Course ID should not be empty")
|
|
}
|
|
if course.Title == "" {
|
|
t.Error("Course Title should not be empty")
|
|
}
|
|
if course.Section != "Courses" {
|
|
t.Errorf("Course Section should be 'Courses', got '%s'", course.Section)
|
|
}
|
|
}
|
|
|
|
// Log counts for debugging
|
|
t.Logf("Response: %d experiences, %d projects, %d courses",
|
|
len(response.Experiences), len(response.Projects), len(response.Courses))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCmdKDataCaching tests that the response has proper cache headers
|
|
func TestCmdKDataCaching(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping CmdKDataCaching test - requires running from project root")
|
|
}
|
|
|
|
cfg := &config.TemplateConfig{
|
|
Dir: "../../templates",
|
|
PartialsDir: "../../templates/partials",
|
|
HotReload: true,
|
|
}
|
|
tmplManager, err := templates.NewManager(cfg)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create template manager: %v", err)
|
|
}
|
|
|
|
handler := NewCVHandler(tmplManager, "localhost:8080")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/cmd-k", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.CmdKData(rec, req)
|
|
|
|
// Check cache header
|
|
cacheControl := rec.Header().Get("Cache-Control")
|
|
if cacheControl == "" {
|
|
t.Error("Expected Cache-Control header to be set")
|
|
}
|
|
if cacheControl != "public, max-age=3600" {
|
|
t.Errorf("Expected Cache-Control 'public, max-age=3600', got '%s'", cacheControl)
|
|
}
|
|
}
|