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:
@@ -0,0 +1,266 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/juanatsap/cv-site/internal/config"
|
||||
"github.com/juanatsap/cv-site/internal/templates"
|
||||
)
|
||||
|
||||
// TestPlainText tests the PlainText handler
|
||||
// NOTE: This test requires running from project root due to template path resolution
|
||||
// Run with: go test ./internal/handlers/ -run TestPlainText -v
|
||||
// Or skip in CI: go test ./internal/handlers/ -run TestPlainText -short
|
||||
func TestPlainText(t *testing.T) {
|
||||
// Skip if running in short mode (CI) - requires project root
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping PlainText 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
|
||||
icons string
|
||||
download string
|
||||
expectStatus int
|
||||
expectHeader string
|
||||
expectContains string
|
||||
}{
|
||||
{
|
||||
name: "Default language (English)",
|
||||
lang: "",
|
||||
expectStatus: http.StatusOK,
|
||||
expectContains: "Juan",
|
||||
},
|
||||
{
|
||||
name: "English language",
|
||||
lang: "en",
|
||||
expectStatus: http.StatusOK,
|
||||
expectContains: "Juan",
|
||||
},
|
||||
{
|
||||
name: "Spanish language",
|
||||
lang: "es",
|
||||
expectStatus: http.StatusOK,
|
||||
expectContains: "Juan",
|
||||
},
|
||||
{
|
||||
name: "Invalid language",
|
||||
lang: "fr",
|
||||
expectStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "With icons disabled",
|
||||
lang: "en",
|
||||
icons: "false",
|
||||
expectStatus: http.StatusOK,
|
||||
expectContains: "Juan",
|
||||
},
|
||||
{
|
||||
name: "Download mode",
|
||||
lang: "en",
|
||||
download: "true",
|
||||
expectStatus: http.StatusOK,
|
||||
expectHeader: "attachment",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Build query string
|
||||
query := "/text"
|
||||
params := []string{}
|
||||
if tt.lang != "" {
|
||||
params = append(params, "lang="+tt.lang)
|
||||
}
|
||||
if tt.icons != "" {
|
||||
params = append(params, "icons="+tt.icons)
|
||||
}
|
||||
if tt.download != "" {
|
||||
params = append(params, "download="+tt.download)
|
||||
}
|
||||
if len(params) > 0 {
|
||||
query += "?" + strings.Join(params, "&")
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, query, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.PlainText(w, req)
|
||||
|
||||
if w.Code != tt.expectStatus {
|
||||
t.Errorf("Expected status %d, got %d", tt.expectStatus, w.Code)
|
||||
}
|
||||
|
||||
// Check Content-Type for successful requests
|
||||
if tt.expectStatus == http.StatusOK {
|
||||
contentType := w.Header().Get("Content-Type")
|
||||
if !strings.HasPrefix(contentType, "text/plain") {
|
||||
t.Errorf("Expected text/plain content type, got %s", contentType)
|
||||
}
|
||||
}
|
||||
|
||||
// Check Content-Disposition header for download mode
|
||||
if tt.expectHeader != "" {
|
||||
disposition := w.Header().Get("Content-Disposition")
|
||||
if !strings.Contains(disposition, tt.expectHeader) {
|
||||
t.Errorf("Expected Content-Disposition containing '%s', got '%s'", tt.expectHeader, disposition)
|
||||
}
|
||||
}
|
||||
|
||||
// Check response body contains expected content (if success)
|
||||
if tt.expectStatus == http.StatusOK && tt.expectContains != "" {
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, tt.expectContains) {
|
||||
t.Errorf("Expected body to contain '%s'", tt.expectContains)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPlainTextDownloadFilename tests that download filename is correctly formatted
|
||||
// NOTE: This test requires running from project root due to template path resolution
|
||||
func TestPlainTextDownloadFilename(t *testing.T) {
|
||||
// Skip if running in short mode (CI) - requires project root
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping PlainTextDownloadFilename 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
|
||||
expectPrefix string
|
||||
}{
|
||||
{
|
||||
name: "English download filename",
|
||||
lang: "en",
|
||||
expectPrefix: "cv-jamr-",
|
||||
},
|
||||
{
|
||||
name: "Spanish download filename",
|
||||
lang: "es",
|
||||
expectPrefix: "cv-jamr-",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/text?lang="+tt.lang+"&download=true", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.PlainText(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("Expected status OK, got %d", w.Code)
|
||||
}
|
||||
|
||||
disposition := w.Header().Get("Content-Disposition")
|
||||
if !strings.Contains(disposition, tt.expectPrefix) {
|
||||
t.Errorf("Expected filename to contain '%s', got '%s'", tt.expectPrefix, disposition)
|
||||
}
|
||||
|
||||
// Verify language suffix is in filename
|
||||
if !strings.Contains(disposition, "-"+tt.lang+".txt") {
|
||||
t.Errorf("Expected filename to end with '-%s.txt', got '%s'", tt.lang, disposition)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsTextBrowser tests the text browser detection
|
||||
func TestIsTextBrowser(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
userAgent string
|
||||
accept string
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
name: "Regular browser",
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
name: "curl",
|
||||
userAgent: "curl/7.79.1",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "wget",
|
||||
userAgent: "Wget/1.21",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "httpie",
|
||||
userAgent: "HTTPie/2.6.0",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "lynx",
|
||||
userAgent: "Lynx/2.9.0dev.10",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "w3m",
|
||||
userAgent: "w3m/0.5.3",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "Accept text/plain",
|
||||
userAgent: "Mozilla/5.0",
|
||||
accept: "text/plain",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "Accept text/html",
|
||||
userAgent: "Mozilla/5.0",
|
||||
accept: "text/html",
|
||||
expect: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
if tt.userAgent != "" {
|
||||
req.Header.Set("User-Agent", tt.userAgent)
|
||||
}
|
||||
if tt.accept != "" {
|
||||
req.Header.Set("Accept", tt.accept)
|
||||
}
|
||||
|
||||
result := isTextBrowser(req)
|
||||
if result != tt.expect {
|
||||
t.Errorf("isTextBrowser() = %v, expected %v for User-Agent: %s", result, tt.expect, tt.userAgent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user