/ tests / unit / search / query-parser.test.ts
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  });