import { describe, expect, it } from 'vitest' import type { Item } from '@sims/domain' import { buildIndex, normalize, phoneticKey, querySearch } from '@sims/search-core' function item(name: string, extra: Partial = {}): Item { return { id: name, tenantId: 't1', code: name.replace(/\s+/g, '').toLowerCase(), name, hsn: '', taxClassCode: 'GST18', unit: { code: 'PCS', decimals: 0 }, salePricePaise: 1000, priceIncludesTax: true, barcodes: [], batchTracked: false, status: 'active', ...extra, } } const names = (items: Item[]): string[] => items.map((i) => i.name) describe('normalize', () => { it('lowercases, strips punctuation, collapses spaces, keeps digits', () => { expect(normalize(' Aashirvaad Atta, 5kg! ')).toBe('aashirvaad atta 5kg') expect(normalize('Coca-Cola')).toBe('coca cola') }) }) describe('phoneticKey — Indic folding', () => { it('folds the mandated collisions to one key', () => { expect(phoneticKey('chawal')).toBe(phoneticKey('chaval')) // w→v (the mandatory case) expect(phoneticKey('paneer')).toBe(phoneticKey('panir')) // ee→i expect(phoneticKey('jeera')).toBe(phoneticKey('zeera')) // z→j + ee→i expect(phoneticKey('phal')).toBe(phoneticKey('fal')) // ph→f expect(phoneticKey('sharbat')).toBe(phoneticKey('sarbat')) // sh→s expect(phoneticKey('dhaniya')).toBe(phoneticKey('daniya')) // dh→d expect(phoneticKey('bhindi')).toBe(phoneticKey('bindi')) // bh→b expect(phoneticKey('thums')).toBe(phoneticKey('tums')) // th→t }) it('folds long vowels to short', () => { expect(phoneticKey('aata')).toBe('ata') // aa→a expect(phoneticKey('cheese')).toBe('chise') // ee→i (trailing vowel kept) expect(phoneticKey('noodle')).toBe(phoneticKey('nudle')) // oo→u }) it('drops doubled letters', () => { expect(phoneticKey('kotta')).toBe(phoneticKey('kota')) expect(phoneticKey('mummy')).toBe('mumy') }) it('is stable for already-folded input', () => { expect(phoneticKey('chaval')).toBe('chaval') }) }) describe('querySearch — tiered ranking', () => { const cat = [ item('Tomato'), item('Tomato Ketchup'), item('Green Tomato'), item('Bottom Shelf Oil'), ] const index = buildIndex(cat) it('orders whole-prefix before token-prefix before substring', () => { const out = names(querySearch(index, 'tom')) // tier 1 (name prefix, shorter first), tier 2 (token prefix), tier 4 (substring) expect(out).toEqual(['Tomato', 'Tomato Ketchup', 'Green Tomato', 'Bottom Shelf Oil']) }) it('breaks ties on the shorter name', () => { const out = names(querySearch(buildIndex([item('Tomato Ketchup'), item('Tomato')]), 'tomato')) expect(out).toEqual(['Tomato', 'Tomato Ketchup']) }) it('returns nothing for an empty query', () => { expect(querySearch(index, ' ')).toEqual([]) }) }) describe('querySearch — phonetic tier finds transliteration variants', () => { it('finds a Chawal-spelled item when the cashier types "chaval" and vice versa', () => { const index = buildIndex([item('Chawal Basmati'), item('Sugar')]) expect(names(querySearch(index, 'chaval'))).toContain('Chawal Basmati') const index2 = buildIndex([item('Chaval Premium'), item('Sugar')]) expect(names(querySearch(index2, 'chawal'))).toContain('Chaval Premium') }) it('ranks an exact prefix above a phonetic-only match', () => { const index = buildIndex([item('Chaval Rice'), item('Chawal Gold')]) // typing "chaw": "Chawal Gold" is a whole-prefix (tier 1), "Chaval Rice" only phonetic (tier 3) expect(names(querySearch(index, 'chaw'))).toEqual(['Chawal Gold', 'Chaval Rice']) }) it('matches against the regional secondary name', () => { const index = buildIndex([item('Coriander', { nameSecondary: 'Dhaniya' })]) expect(names(querySearch(index, 'daniya'))).toContain('Coriander') }) })