- Add query-parser.ts with AST for type:, tag:, is:favorite, is:pinned filters - Integrate parser into search API - Add real-time search with 300ms debounce in search-bar - Add keyboard navigation (↑↓ Enter ESC) for Spotlight-like UX - Add dropdown results display with loading state - Add 22 tests for query parser Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
146 lines
4.6 KiB
TypeScript
146 lines
4.6 KiB
TypeScript
import { parseQuery, QueryAST } from '@/lib/query-parser'
|
|
|
|
describe('query-parser', () => {
|
|
describe('basic text queries', () => {
|
|
it('returns text with no filters for simple text', () => {
|
|
const result = parseQuery('docker')
|
|
expect(result.text).toBe('docker')
|
|
expect(result.filters).toEqual({})
|
|
})
|
|
|
|
it('preserves multi-word text', () => {
|
|
const result = parseQuery('hello world')
|
|
expect(result.text).toBe('hello world')
|
|
expect(result.filters).toEqual({})
|
|
})
|
|
})
|
|
|
|
describe('type filter', () => {
|
|
it('extracts type filter from beginning of query', () => {
|
|
const result = parseQuery('type:command docker')
|
|
expect(result.text).toBe('docker')
|
|
expect(result.filters).toEqual({ type: 'command' })
|
|
})
|
|
|
|
it('handles query with only type filter', () => {
|
|
const result = parseQuery('type:snippet')
|
|
expect(result.text).toBe('')
|
|
expect(result.filters).toEqual({ type: 'snippet' })
|
|
})
|
|
|
|
it('extracts type filter from end of query', () => {
|
|
const result = parseQuery('docker type:command')
|
|
expect(result.text).toBe('docker')
|
|
expect(result.filters).toEqual({ type: 'command' })
|
|
})
|
|
})
|
|
|
|
describe('tag filter', () => {
|
|
it('extracts tag filter with text', () => {
|
|
const result = parseQuery('tag:api error')
|
|
expect(result.text).toBe('error')
|
|
expect(result.filters).toEqual({ tag: 'api' })
|
|
})
|
|
|
|
it('handles query with only tag filter', () => {
|
|
const result = parseQuery('tag:backend')
|
|
expect(result.text).toBe('')
|
|
expect(result.filters).toEqual({ tag: 'backend' })
|
|
})
|
|
})
|
|
|
|
describe('combined filters', () => {
|
|
it('parses multiple filters together', () => {
|
|
const result = parseQuery('docker tag:backend type:command')
|
|
expect(result.text).toBe('docker')
|
|
expect(result.filters).toEqual({ type: 'command', tag: 'backend' })
|
|
})
|
|
|
|
it('handles type, tag, and isFavorite combined', () => {
|
|
const result = parseQuery('type:snippet tag:python is:favorite')
|
|
expect(result.text).toBe('')
|
|
expect(result.filters).toEqual({ type: 'snippet', tag: 'python', isFavorite: true })
|
|
})
|
|
})
|
|
|
|
describe('boolean filters', () => {
|
|
it('extracts is:favorite filter', () => {
|
|
const result = parseQuery('is:favorite docker')
|
|
expect(result.text).toBe('docker')
|
|
expect(result.filters).toEqual({ isFavorite: true })
|
|
})
|
|
|
|
it('handles is:pinned filter alone', () => {
|
|
const result = parseQuery('is:pinned')
|
|
expect(result.text).toBe('')
|
|
expect(result.filters).toEqual({ isPinned: true })
|
|
})
|
|
|
|
it('handles both boolean filters with text', () => {
|
|
const result = parseQuery('is:favorite is:pinned docker')
|
|
expect(result.text).toBe('docker')
|
|
expect(result.filters).toEqual({ isFavorite: true, isPinned: true })
|
|
})
|
|
})
|
|
|
|
describe('edge cases', () => {
|
|
it('handles empty string', () => {
|
|
const result = parseQuery('')
|
|
expect(result.text).toBe('')
|
|
expect(result.filters).toEqual({})
|
|
})
|
|
|
|
it('handles whitespace only', () => {
|
|
const result = parseQuery(' ')
|
|
expect(result.text).toBe('')
|
|
expect(result.filters).toEqual({})
|
|
})
|
|
|
|
it('ignores empty type value', () => {
|
|
const result = parseQuery('type:')
|
|
expect(result.text).toBe('')
|
|
expect(result.filters).toEqual({})
|
|
})
|
|
|
|
it('ignores empty tag value', () => {
|
|
const result = parseQuery('tag:')
|
|
expect(result.text).toBe('')
|
|
expect(result.filters).toEqual({})
|
|
})
|
|
|
|
it('last duplicate filter wins for type', () => {
|
|
const result = parseQuery('type:command type:snippet docker')
|
|
expect(result.text).toBe('docker')
|
|
expect(result.filters).toEqual({ type: 'snippet' })
|
|
})
|
|
|
|
it('last duplicate filter wins for tag', () => {
|
|
const result = parseQuery('tag:python tag:javascript code')
|
|
expect(result.text).toBe('code')
|
|
expect(result.filters).toEqual({ tag: 'javascript' })
|
|
})
|
|
})
|
|
|
|
describe('case sensitivity', () => {
|
|
it('filter name is case insensitive', () => {
|
|
const result = parseQuery('TYPE:command')
|
|
expect(result.filters).toEqual({ type: 'command' })
|
|
})
|
|
|
|
it('filter value is case sensitive', () => {
|
|
const result = parseQuery('type:Command')
|
|
expect(result.filters).toEqual({ type: 'Command' })
|
|
})
|
|
|
|
it('is:favorite is case insensitive', () => {
|
|
const result = parseQuery('IS:FAVORITE docker')
|
|
expect(result.filters).toEqual({ isFavorite: true })
|
|
})
|
|
|
|
it('is:pinned is case insensitive', () => {
|
|
const result = parseQuery('IS:PINNED')
|
|
expect(result.filters).toEqual({ isPinned: true })
|
|
})
|
|
})
|
|
})
|