import { describe, expect, it } from 'vitest'; import { parse } from './parse'; import { compile } from './compile'; import type { FieldType } from './types'; const FIELDS: Record = { genre: 'text', title: 'text', plot: 'fulltext', type: 'enum', minutes: 'number', release_date: 'date' }; describe('parse', () => { it('parses a single quoted text atom as is', () => { expect(parse('genre:"Horror"', FIELDS)).toEqual({ match: 'all', children: [{ field: 'genre', operator: 'is', value: 'Horror' }] }); }); it('parses NOT as isNot / notMatches by field type', () => { expect(parse('NOT genre:"Horror"', FIELDS)).toEqual({ match: 'all', children: [{ field: 'genre', operator: 'isNot', value: 'Horror' }] }); expect(parse('NOT plot:"car"', FIELDS)).toEqual({ match: 'all', children: [{ field: 'plot', operator: 'notMatches', value: 'car' }] }); }); it('parses a quoted fulltext atom as matches', () => { expect(parse('plot:"car"', FIELDS)).toEqual({ match: 'all', children: [{ field: 'plot', operator: 'matches', value: 'car' }] }); }); it('parses wildcard forms', () => { expect(parse('title:*night*', FIELDS)).toEqual({ match: 'all', children: [{ field: 'title', operator: 'contains', value: 'night' }] }); expect(parse('title:The*', FIELDS)).toEqual({ match: 'all', children: [{ field: 'title', operator: 'startsWith', value: 'The' }] }); }); it('parses number/date ranges disambiguated by field type', () => { expect(parse('minutes:{30 TO *}', FIELDS)).toEqual({ match: 'all', children: [{ field: 'minutes', operator: 'gt', value: '30' }] }); expect(parse('release_date:{* TO 2010-01-01}', FIELDS)).toEqual({ match: 'all', children: [{ field: 'release_date', operator: 'before', value: '2010-01-01' }] }); expect(parse('minutes:[30 TO 90]', FIELDS)).toEqual({ match: 'all', children: [{ field: 'minutes', operator: 'between', value: '30', value2: '90' }] }); expect(parse('minutes:42', FIELDS)).toEqual({ match: 'all', children: [{ field: 'minutes', operator: 'eq', value: '42' }] }); }); it('parses a nested group', () => { expect(parse('type:"movie" AND (genre:"Horror" OR genre:"Thriller")', FIELDS)).toEqual({ match: 'all', children: [ { field: 'type', operator: 'is', value: 'movie' }, { match: 'any', children: [ { field: 'genre', operator: 'is', value: 'Horror' }, { field: 'genre', operator: 'is', value: 'Thriller' } ] } ] }); }); it('returns null on out-of-subset input', () => { expect(parse('genre:"a" AND genre:"b" OR genre:"c"', FIELDS)).toBeNull(); // mixed AND/OR at one level expect(parse('unknown_field:"x"', FIELDS)).toBeNull(); // unknown field expect(parse('genre:"a" AND (type:"movie" AND (genre:"b"))', FIELDS)).toBeNull(); // two levels of nesting expect(parse('title:jo~2', FIELDS)).toBeNull(); // fuzzy — not in subset }); }); describe('parse: special-char round-trips', () => { it('round-trips quoted values with lucene specials (compile∘parse)', () => { for (const v of ['a*b', 'x y', 'a\\', 'has"quote', '(paren)', 'a AND b', 'a:b']) { const g = { match: 'all' as const, children: [{ field: 'genre', operator: 'is' as const, value: v }] }; expect(parse(compile(g), FIELDS)).toEqual(g); } }); it('round-trips text contains/startsWith values with specials', () => { for (const v of ['a*b', 'x y', 'plain']) { const c = { match: 'all' as const, children: [{ field: 'title', operator: 'contains' as const, value: v }] }; expect(parse(compile(c), FIELDS)).toEqual(c); } for (const v of ['star*', 'x y', 'ab']) { const s = { match: 'all' as const, children: [{ field: 'title', operator: 'startsWith' as const, value: v }] }; expect(parse(compile(s), FIELDS)).toEqual(s); } }); });