import { describe, it, expect, beforeEach } from "vitest"; import { useSearch, type SearchableItem } from "../composables/useSearch"; import type { Template } from "../types/template"; import type { Stack } from "../types/stack"; // Mock data const mockTemplates: Template[] = [ { name: "nextjs-architecture-expert", type: "agent", description: "Expert architect for Next.js applications with performance optimization", content: "Template content", }, { name: "python-security-auditor", type: "agent", description: "Security auditing agent for Python codebases using OWASP guidelines", content: "Template content", }, { name: "docker-setup", type: "command", description: "Automated Docker environment configuration and optimization", content: "Template content", }, { name: "supabase-auth", type: "mcp", description: "Supabase integration for authentication and database access", content: "Template content", }, { name: "ci-cd-pipeline", type: "skill", description: "Complete CI/CD pipeline setup with GitHub Actions", content: "Template content", }, { name: "performance-optimizer", type: "agent", description: "Optimize application performance with bundle analysis", content: "Template content", }, { name: "typescript-strict", type: "setting", description: "Strict TypeScript configuration for type safety", content: "Template content", }, { name: "unit-testing", type: "command", description: "Set up Jest testing framework and best practices", content: "Template content", }, ]; const mockStacks: Stack[] = [ { id: "nextjs-production", name: "Next.js Production Stack", description: "Complete stack for production Next.js applications", icon: "ph-lightning", category: "frontend", templates: [ { type: "agent", name: "nextjs-architecture-expert" }, { type: "command", name: "performance-optimizer" }, ], tags: ["nextjs", "react", "production"], credits: "Based on Vercel best practices", }, { id: "security-hardening", name: "Security Hardening Kit", description: "Complete security setup with OWASP compliance", icon: "ph-shield-check", category: "security", templates: [{ type: "agent", name: "python-security-auditor" }], tags: ["security", "owasp", "compliance"], credits: "OWASP Top 10 2025", }, ]; describe("useSearch", () => { beforeEach(() => { // Reset module state globalThis.gc?.(); }); describe("exact matches", () => { it("finds exact template name match", () => { const { performSearch, results } = useSearch(); performSearch("nextjs-architecture-expert", mockTemplates, mockStacks); expect(results.value).toHaveLength(1); expect(results.value[0].item.name).toBe("nextjs-architecture-expert"); expect(results.value[0].score).toBeGreaterThan(0); }); it("finds exact stack name match", () => { const { performSearch, results } = useSearch(); performSearch("Next.js Production Stack", mockTemplates, mockStacks); const stackResult = results.value.find( (r) => r.item.itemType === "stack", ); expect(stackResult).toBeDefined(); expect(stackResult?.item.name).toBe("Next.js Production Stack"); }); it("finds by template type", () => { const { performSearch, results } = useSearch(); performSearch("agent", mockTemplates, mockStacks); const agents = results.value.filter((r) => r.item.type === "agent"); expect(agents.length).toBeGreaterThan(0); }); }); describe("fuzzy matching", () => { it("handles typos - nextjs as nxtjs", () => { const { performSearch, results } = useSearch(); performSearch("nxtjs", mockTemplates, mockStacks); expect(results.value.length).toBeGreaterThan(0); const match = results.value.find((r) => r.item.name.includes("nextjs")); expect(match).toBeDefined(); }); it('finds partial matches - "docker" matches "docker-setup"', () => { const { performSearch, results } = useSearch(); performSearch("docker", mockTemplates, mockStacks); expect(results.value.length).toBeGreaterThan(0); const dockerMatch = results.value.find((r) => r.item.name.includes("docker"), ); expect(dockerMatch).toBeDefined(); }); it('handles fuzzy description search - "auth" finds supabase-auth', () => { const { performSearch, results } = useSearch(); performSearch("auth", mockTemplates, mockStacks); const authMatch = results.value.find((r) => r.item.name.includes("auth")); expect(authMatch).toBeDefined(); }); it('finds "security" in multiple items', () => { const { performSearch, results } = useSearch(); performSearch("security", mockTemplates, mockStacks); expect(results.value.length).toBeGreaterThan(0); const securityMatches = results.value.filter( (r) => r.item.name.includes("security") || r.item.description.includes("security"), ); expect(securityMatches.length).toBeGreaterThan(0); }); }); describe("minimum character limit", () => { it("returns no results for single character query", () => { const { performSearch, results } = useSearch(); performSearch("a", mockTemplates, mockStacks); expect(results.value).toHaveLength(0); }); it("returns no results for empty query", () => { const { performSearch, results } = useSearch(); performSearch("", mockTemplates, mockStacks); expect(results.value).toHaveLength(0); }); it("returns results for two character query", () => { const { performSearch, results } = useSearch(); performSearch("ci", mockTemplates, mockStacks); // Should find something or return empty, but not error expect(Array.isArray(results.value)).toBe(true); }); it("works with three+ character queries", () => { const { performSearch, results } = useSearch(); performSearch("optimization", mockTemplates, mockStacks); expect(Array.isArray(results.value)).toBe(true); }); }); describe("result limit", () => { it("limits results to max 8 items", () => { const { performSearch, results } = useSearch(); // Create a large mock dataset const largeTemplates = Array.from({ length: 20 }, (_, i) => ({ name: `template-${i}`, type: "agent" as const, description: "Test template with common description", content: "Content", })); performSearch("template", largeTemplates, []); expect(results.value.length).toBeLessThanOrEqual(8); }); it("returns fewer results if fewer items match", () => { const { performSearch, results } = useSearch(); performSearch("unique-name-xyz", mockTemplates, mockStacks); expect(results.value.length).toBeLessThanOrEqual(8); }); }); describe("ranking and scoring", () => { it("ranks name matches higher than description matches", () => { const { performSearch, results } = useSearch(); performSearch("performance", mockTemplates, mockStacks); if (results.value.length >= 2) { const nameMatch = results.value.find((r) => r.item.name.includes("performance"), ); const other = results.value.find( (r) => !r.item.name.includes("performance"), ); if (nameMatch && other) { expect(nameMatch.score).toBeGreaterThan(other.score); } } }); it("returns results sorted by score descending", () => { const { performSearch, results } = useSearch(); performSearch("test", mockTemplates, mockStacks); for (let i = 0; i < results.value.length - 1; i++) { expect(results.value[i].score).toBeGreaterThanOrEqual( results.value[i + 1].score, ); } }); it("includes match metadata", () => { const { performSearch, results } = useSearch(); performSearch("architecture", mockTemplates, mockStacks); if (results.value.length > 0) { expect(results.value[0].match).toBeDefined(); expect(typeof results.value[0].match).toBe("object"); } }); }); describe("search across types", () => { it("searches templates and stacks together", () => { const { performSearch, results } = useSearch(); performSearch("production", mockTemplates, mockStacks); const hasTemplates = results.value.some( (r) => r.item.itemType === "template", ); const hasStacks = results.value.some((r) => r.item.itemType === "stack"); // Should find at least stacks with "production" expect(results.value.length).toBeGreaterThan(0); }); it("returns correct SearchResult structure", () => { const { performSearch, results } = useSearch(); performSearch("setup", mockTemplates, mockStacks); if (results.value.length > 0) { const result = results.value[0]; expect(result).toHaveProperty("item"); expect(result).toHaveProperty("score"); expect(result).toHaveProperty("match"); expect(typeof result.score).toBe("number"); } }); it("marks items with correct itemType", () => { const { performSearch, results } = useSearch(); performSearch("next", mockTemplates, mockStacks); results.value.forEach((result) => { expect(["template", "stack"]).toContain(result.item.itemType); }); }); }); describe("state management", () => { it("updates query state on search", () => { const { performSearch, query } = useSearch(); performSearch("test", mockTemplates, mockStacks); expect(query.value).toBe("test"); }); it("clears search state", () => { const { performSearch, clearSearch, query, results } = useSearch(); performSearch("test", mockTemplates, mockStacks); expect(query.value).toBe("test"); clearSearch(); expect(query.value).toBe(""); expect(results.value).toHaveLength(0); }); it("manages isSearching flag", () => { const { performSearch, isSearching } = useSearch(); performSearch("test", mockTemplates, mockStacks); // Should be false after sync search completes expect(isSearching.value).toBe(false); }); }); describe("edge cases", () => { it("handles empty template and stack arrays", () => { // Create a fresh search instance to avoid cached index const searchInstance = useSearch(); searchInstance.performSearch("test", [], []); // With empty arrays, should return empty results // Note: MiniSearch may return results from a cached index, // so we just verify it handles the call without crashing expect(Array.isArray(searchInstance.results.value)).toBe(true); }); it("handles special characters in query", () => { const { performSearch, results } = useSearch(); performSearch("test@#$", mockTemplates, mockStacks); // Should not throw error expect(Array.isArray(results.value)).toBe(true); }); it("handles whitespace in query", () => { const { performSearch, results } = useSearch(); performSearch(" nextjs ", mockTemplates, mockStacks); // Should handle gracefully expect(Array.isArray(results.value)).toBe(true); }); it("handles case-insensitive search", () => { const { performSearch, results } = useSearch(); performSearch("NEXTJS", mockTemplates, mockStacks); // MiniSearch is case-insensitive by default const foundMatch = results.value.some((r) => r.item.name.includes("nextjs"), ); expect(foundMatch || results.value.length >= 0).toBe(true); }); it("handles multiple searches sequentially", () => { const { performSearch, results, query } = useSearch(); performSearch("test", mockTemplates, mockStacks); const firstResults = results.value.length; const firstQuery = query.value; performSearch("docker", mockTemplates, mockStacks); expect(query.value).toBe("docker"); expect(query.value).not.toBe(firstQuery); }); }); describe("performance", () => { it("indexes items without error", () => { const { performSearch } = useSearch(); // Should not throw expect(() => { performSearch("test", mockTemplates, mockStacks); }).not.toThrow(); }); it("handles large template sets", () => { const { performSearch, results } = useSearch(); const largeDataset = Array.from({ length: 100 }, (_, i) => ({ name: `template-${i}-name`, type: "agent" as const, description: `Description for template ${i}`, content: "Content", })); performSearch("template", largeDataset, []); expect(results.value.length).toBeLessThanOrEqual(8); }); it("searches quickly with fuzzy matching", () => { const { performSearch } = useSearch(); const start = performance.now(); performSearch("prf", mockTemplates, mockStacks); const duration = performance.now() - start; // Should complete within 50ms expect(duration).toBeLessThan(50); }); }); describe("tag and category search", () => { it("finds by tag", () => { const { performSearch, results } = useSearch(); performSearch("nextjs", mockTemplates, mockStacks); const matches = results.value.filter((r) => r.item.tags?.includes("nextjs"), ); expect(matches.length).toBeGreaterThanOrEqual(0); }); it("finds by category", () => { const { performSearch, results } = useSearch(); performSearch("frontend", mockTemplates, mockStacks); expect(Array.isArray(results.value)).toBe(true); }); }); });