You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sims-hq/packages/search-core/test/search-core.test.ts

92 lines
3.8 KiB
TypeScript

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> = {}): 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')
})
})