query-parser.test.ts
1 /** 2 * Tests for query parser 3 */ 4 import { describe, it, expect } from 'vitest'; 5 import { 6 parseQuery, 7 getRequiredTerms, 8 getOptionalTerms, 9 getExcludedTerms, 10 highlightMatches, 11 } from '../../../src/lib/search/query-parser'; 12 13 describe('parseQuery', () => { 14 it('parses simple terms', () => { 15 const result = parseQuery('react javascript'); 16 17 expect(result.isEmpty).toBe(false); 18 expect(result.terms.length).toBeGreaterThan(0); 19 expect(result.terms.some((t) => t.term === 'react')).toBe(true); 20 }); 21 22 it('handles empty query', () => { 23 const result = parseQuery(''); 24 25 expect(result.isEmpty).toBe(true); 26 expect(result.terms).toHaveLength(0); 27 }); 28 29 it('handles whitespace-only query', () => { 30 const result = parseQuery(' '); 31 32 expect(result.isEmpty).toBe(true); 33 }); 34 35 it('parses negated terms', () => { 36 const result = parseQuery('-exclude this'); 37 38 const excludedTerms = result.terms.filter((t) => t.negated); 39 expect(excludedTerms.length).toBeGreaterThan(0); 40 expect(excludedTerms[0].term).toBe('exclude'); // query parser uses stem: false 41 }); 42 43 it('parses OR operator', () => { 44 const result = parseQuery('react | vue'); 45 46 const orTerms = result.terms.filter((t) => t.isOr); 47 expect(orTerms.length).toBeGreaterThan(0); 48 }); 49 50 it('parses field filters', () => { 51 const result = parseQuery('domain:github.com react'); 52 53 expect(result.filters.domain).toBe('github.com'); 54 }); 55 56 it('parses workspace filter', () => { 57 const result = parseQuery('workspace:MyProject test'); 58 59 expect(result.filters.workspace).toBe('MyProject'); 60 }); 61 62 it('parses type filter', () => { 63 const result = parseQuery('type:workspace test'); 64 65 expect(result.filters.type).toBe('workspace'); 66 }); 67 68 it('parses quoted phrases', () => { 69 const result = parseQuery('"hello world" test'); 70 71 // Should find terms from the quoted phrase 72 expect(result.terms.some((t) => t.original === 'hello world')).toBe(true); 73 }); 74 75 it('stores original query', () => { 76 const query = 'react hooks tutorial'; 77 const result = parseQuery(query); 78 79 expect(result.original).toBe(query); 80 }); 81 }); 82 83 describe('getRequiredTerms', () => { 84 it('returns non-negated, non-OR terms', () => { 85 const query = parseQuery('required -excluded | optional'); 86 const required = getRequiredTerms(query); 87 88 expect(required.every((t) => !t.negated && !t.isOr)).toBe(true); 89 expect(required.some((t) => t.term === 'required')).toBe(true); // not stemmed in query parser 90 }); 91 }); 92 93 describe('getOptionalTerms', () => { 94 it('returns OR terms', () => { 95 const query = parseQuery('react | vue'); 96 const optional = getOptionalTerms(query); 97 98 expect(optional.every((t) => t.isOr)).toBe(true); 99 }); 100 }); 101 102 describe('getExcludedTerms', () => { 103 it('returns negated terms', () => { 104 const query = parseQuery('include -exclude'); 105 const excluded = getExcludedTerms(query); 106 107 expect(excluded.every((t) => t.negated)).toBe(true); 108 expect(excluded.length).toBeGreaterThan(0); 109 }); 110 }); 111 112 describe('highlightMatches', () => { 113 it('wraps matched terms with highlight tags', () => { 114 const text = 'Hello World'; 115 const result = highlightMatches(text, ['world']); 116 117 expect(result).toContain('<mark>World</mark>'); 118 }); 119 120 it('handles multiple matches', () => { 121 const text = 'Hello World, Hello Again'; 122 const result = highlightMatches(text, ['hello']); 123 124 expect(result.match(/<mark>/g)?.length).toBe(2); 125 }); 126 127 it('returns original text when no matches', () => { 128 const text = 'Hello World'; 129 const result = highlightMatches(text, ['foo']); 130 131 expect(result).toBe(text); 132 }); 133 134 it('allows custom highlight tags', () => { 135 const text = 'Hello World'; 136 const result = highlightMatches(text, ['hello'], '<b>', '</b>'); 137 138 expect(result).toContain('<b>Hello</b>'); 139 }); 140 141 it('handles empty matches array', () => { 142 const text = 'Hello World'; 143 const result = highlightMatches(text, []); 144 145 expect(result).toBe(text); 146 }); 147 });