feat: MVP-4 Sprint 1 - Query parser, real-time search, keyboard navigation
- 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>
This commit is contained in:
145
__tests__/query-parser.test.ts
Normal file
145
__tests__/query-parser.test.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
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 })
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user