import { highlightMatches, noteQuery, searchNotes, ScoredNote } from '@/lib/search' // Mock prisma and usage before importing search module jest.mock('@/lib/prisma', () => ({ prisma: { note: { findMany: jest.fn(), }, }, })) jest.mock('@/lib/usage', () => ({ getUsageStats: jest.fn(), })) import { prisma } from '@/lib/prisma' import { getUsageStats } from '@/lib/usage' describe('search.ts', () => { const mockNotes = [ { id: 'note-1', title: 'Docker Commands', content: 'docker build and docker run', type: 'command', isFavorite: true, isPinned: false, createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-15'), tags: [{ tag: { id: 'tag-1', name: 'docker' } }], }, { id: 'note-2', title: 'React Hooks', content: 'useState and useEffect hooks', type: 'snippet', isFavorite: false, isPinned: true, createdAt: new Date('2024-01-02'), updatedAt: new Date('2024-01-10'), tags: [{ tag: { id: 'tag-2', name: 'react' } }], }, { id: 'note-3', title: 'Git Commands', content: 'git commit and git push', type: 'command', isFavorite: false, isPinned: false, createdAt: new Date('2024-01-03'), updatedAt: new Date('2024-01-05'), tags: [{ tag: { id: 'tag-3', name: 'git' } }], }, ] beforeEach(() => { jest.clearAllMocks() // Default: no usage ;(getUsageStats as jest.Mock).mockResolvedValue({ views: 0, clicks: 0, relatedClicks: 0 }) }) describe('highlightMatches', () => { it('returns first 150 characters when query is empty', () => { const text = 'This is a long text that should be truncated to 150 characters. ' + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor.' const result = highlightMatches(text, '') expect(result.length).toBeLessThanOrEqual(150) }) it('finds and highlights matching words', () => { const text = 'This is a test document about JavaScript programming.' const result = highlightMatches(text, 'JavaScript') expect(result).toContain('JavaScript') }) it('handles multiple word queries', () => { const text = 'React is a JavaScript library for building user interfaces.' const result = highlightMatches(text, 'JavaScript React') expect(result).toContain('JavaScript') expect(result).toContain('React') }) it('escapes regex special characters in query', () => { const text = 'What is $100 + $200?' const result = highlightMatches(text, '$100') expect(result).toContain('$100') }) it('adds ellipsis when match is not at start', () => { const text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. JavaScript is great.' const result = highlightMatches(text, 'JavaScript') expect(result).toContain('JavaScript') }) it('returns plain text when no match found', () => { const text = 'This is a simple text without the word we are looking for.' const result = highlightMatches(text, 'xyz123') expect(result).not.toContain('') }) it('filters out single character words and returns excerpt', () => { const text = 'A B C D E F G H I J K L M N O P Q R S T U V W X Y Z' const result = highlightMatches(text, 'A B C') // Single char words (A, B, C) are filtered, returns 150 chars expect(result.length).toBeLessThanOrEqual(150) }) it('adds ellipsis when match is far from start', () => { // JavaScript is at position ~26, start = max(0, 26-75) = 0, so no ellipsis needed // We need a longer text where match is more than 75 chars from start const text = 'Lorem ipsum dolor sit amet. '.repeat(10) + 'JavaScript programming language.' const result = highlightMatches(text, 'JavaScript') expect(result).toContain('...') }) }) describe('noteQuery', () => { it('returns empty array when no notes exist', async () => { ;(prisma.note.findMany as jest.Mock).mockResolvedValue([]) const result = await noteQuery('docker') expect(result).toEqual([]) }) it('returns notes with exact title match scored highest', async () => { ;(prisma.note.findMany as jest.Mock).mockResolvedValue(mockNotes) const result = await noteQuery('docker') expect(result[0].id).toBe('note-1') // exact title match expect(result[0].matchType).toBe('exact') }) it('returns notes with content match', async () => { ;(prisma.note.findMany as jest.Mock).mockResolvedValue(mockNotes) const result = await noteQuery('hooks') expect(result.length).toBeGreaterThan(0) expect(result.some((n: ScoredNote) => n.id === 'note-2')).toBe(true) }) it('returns fuzzy matches when no exact match', async () => { ;(prisma.note.findMany as jest.Mock).mockResolvedValue(mockNotes) const result = await noteQuery('docer') // typo expect(result.length).toBeGreaterThan(0) expect(result[0].matchType).toBe('fuzzy') }) it('excludes notes with no match (low similarity)', async () => { ;(prisma.note.findMany as jest.Mock).mockResolvedValue(mockNotes) const result = await noteQuery('xyz123nonexistent') expect(result.length).toBe(0) }) it('adds +2 for favorite notes', async () => { ;(prisma.note.findMany as jest.Mock).mockResolvedValue(mockNotes) const result = await noteQuery('docker') // note-1 is favorite, should have higher score const dockerNote = result.find((n: ScoredNote) => n.id === 'note-1') expect(dockerNote?.isFavorite).toBe(true) }) it('adds +1 for pinned notes', async () => { ;(prisma.note.findMany as jest.Mock).mockResolvedValue(mockNotes) const result = await noteQuery('hooks') // note-2 is pinned const hooksNote = result.find((n: ScoredNote) => n.id === 'note-2') expect(hooksNote?.isPinned).toBe(true) }) it('adds +1 for recently updated notes (within 7 days)', async () => { const recentNote = { ...mockNotes[0], updatedAt: new Date(Date.now() - 1000), // just now } ;(prisma.note.findMany as jest.Mock).mockResolvedValue([recentNote]) const result = await noteQuery('docker') expect(result[0].score).toBeGreaterThan(0) }) it('filters by type when specified', async () => { ;(prisma.note.findMany as jest.Mock).mockResolvedValue(mockNotes) const result = await noteQuery('', { type: 'command' }) expect(result.every((n: ScoredNote) => n.type === 'command')).toBe(true) }) it('filters by tag when specified', async () => { ;(prisma.note.findMany as jest.Mock).mockResolvedValue(mockNotes) const result = await noteQuery('', { tag: 'docker' }) expect(result.length).toBe(1) expect(result[0].id).toBe('note-1') }) it('returns notes sorted by score descending', async () => { ;(prisma.note.findMany as jest.Mock).mockResolvedValue(mockNotes) const result = await noteQuery('') for (let i = 0; i < result.length - 1; i++) { expect(result[i].score).toBeGreaterThanOrEqual(result[i + 1].score) } }) it('returns highlight excerpt for matched notes', async () => { ;(prisma.note.findMany as jest.Mock).mockResolvedValue(mockNotes) const result = await noteQuery('docker') expect(result[0].highlight).toBeDefined() expect(typeof result[0].highlight).toBe('string') }) }) describe('searchNotes', () => { it('should be an alias for noteQuery', async () => { ;(prisma.note.findMany as jest.Mock).mockResolvedValue(mockNotes) const result = await searchNotes('docker') expect(result).toEqual(await noteQuery('docker')) }) it('passes filters to noteQuery', async () => { ;(prisma.note.findMany as jest.Mock).mockResolvedValue(mockNotes) const result = await searchNotes('', { type: 'snippet' }) expect(result.every((n: ScoredNote) => n.type === 'snippet')).toBe(true) }) }) describe('usage-based scoring boost', () => { it('calls getUsageStats for each note', async () => { ;(prisma.note.findMany as jest.Mock).mockResolvedValue([mockNotes[0]]) ;(getUsageStats as jest.Mock).mockResolvedValue({ views: 0, clicks: 0, relatedClicks: 0 }) await noteQuery('docker') expect(getUsageStats).toHaveBeenCalledWith('note-1', 7) }) it('handles getUsageStats returning zero values', async () => { ;(prisma.note.findMany as jest.Mock).mockResolvedValue([mockNotes[0]]) ;(getUsageStats as jest.Mock).mockResolvedValue({ views: 0, clicks: 0, relatedClicks: 0 }) const result = await noteQuery('docker') // Should return results without error expect(result).toBeDefined() expect(result.length).toBeGreaterThan(0) }) }) describe('ScoredNote interface', () => { it('returns correct structure for scored notes', async () => { ;(prisma.note.findMany as jest.Mock).mockResolvedValue([mockNotes[0]]) const result = await noteQuery('docker') expect(result[0]).toHaveProperty('id') expect(result[0]).toHaveProperty('title') expect(result[0]).toHaveProperty('content') expect(result[0]).toHaveProperty('type') expect(result[0]).toHaveProperty('isFavorite') expect(result[0]).toHaveProperty('isPinned') expect(result[0]).toHaveProperty('createdAt') expect(result[0]).toHaveProperty('updatedAt') expect(result[0]).toHaveProperty('tags') expect(result[0]).toHaveProperty('score') expect(result[0]).toHaveProperty('highlight') expect(result[0]).toHaveProperty('matchType') expect(['exact', 'fuzzy']).toContain(result[0].matchType) }) it('converts Date objects to ISO strings', async () => { ;(prisma.note.findMany as jest.Mock).mockResolvedValue([mockNotes[0]]) const result = await noteQuery('docker') expect(result[0].createdAt).toBe('2024-01-01T00:00:00.000Z') expect(result[0].updatedAt).toBe('2024-01-15T00:00:00.000Z') }) }) })