Files
website/src/lib/groceries.test.ts
Stan Runge 60370d4572
All checks were successful
Deploy production / deploy (push) Successful in 1m13s
groceries: relevance-ranked search (plain/basic products first)
- search.ts: score on the core name (brand/size stripped): exact or query+base suffix
  (kipdij+filet), head-final compounds, prefix, inside; penalties per extra word and per
  flavour/processing word (search-words.ts, editable); bonuses for store brands, the
  query's dominant category, store count, favourites; trigram tiebreak
- groceries.synonyms table (seeded: kipdij/kippendij, pasta → spaghetti, …) expands queries
- list rows show all pack sizes of a group; group picker uses the same ranking
- scripts/groceries-search-eval.ts prints top results + latency; preflight now 8 tables
2026-10-25 20:20:34 +00:00

140 lines
3.5 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
bestPerStore,
cheapest,
formatAmount,
formatUnitPrice,
packLabel,
packSizes,
parseEuros,
sortByPrice,
toOffers,
type PricedItem
} from './groceries';
const it_ = (
id: number,
storeId: number,
price: number,
unit: number,
promo: number | null = null
): PricedItem => ({
id,
storeId,
priceCents: price,
promoPriceCents: promo,
unitPriceCents: unit,
promoUnitPriceCents: promo == null ? null : Math.round((unit * promo) / price),
available: true
});
describe('grocery price helpers', () => {
const items = [
it_(1, 1, 119, 119),
it_(2, 1, 165, 110),
it_(3, 2, 115, 115),
it_(4, 2, 200, 200, 100)
];
it('finds the cheapest by unit price, promos included', () => {
const c = cheapest(items, 'unit');
expect(c.overall).toBe(4);
expect([...c.perStore].sort()).toEqual([2, 4]);
});
it('finds the cheapest by pack price', () => {
expect(cheapest(items, 'pack').overall).toBe(4);
expect(cheapest(items.slice(0, 3), 'pack').overall).toBe(3);
});
it('sorts by unit price', () => {
expect(sortByPrice(items, 'unit').map((i) => i.id)).toEqual([4, 2, 3, 1]);
});
it('formats', () => {
expect(formatUnitPrice(250, 'kg')).toMatch(/2,50\/kg$/);
expect(formatAmount(1.98, 'l')).toBe('1,98 L');
expect(formatUnitPrice(null, 'kg')).toBeNull();
});
});
describe('parseEuros', () => {
it('reads Dutch and English prices', () => {
expect(parseEuros('1,29')).toBe(129);
expect(parseEuros('€ 2.5')).toBe(250);
expect(parseEuros('3')).toBe(300);
});
it('rejects nonsense', () => {
expect(parseEuros('')).toBeNull();
expect(parseEuros('0')).toBeNull();
expect(parseEuros('1,299')).toBeNull();
expect(parseEuros('abc')).toBeNull();
});
});
describe('bestPerStore', () => {
const item = (
id: number,
storeId: number,
price: number,
unit: number | null,
promo?: number
) => ({
id,
storeId,
name: `p${id}`,
sizeText: null,
unit: 'kg',
unitAmount: 1,
promoLabel: promo ? '1+1' : null,
url: null,
available: true,
priceCents: price,
promoPriceCents: promo ?? null,
unitPriceCents: unit,
promoUnitPriceCents: promo ?? null
});
it('keeps the cheapest offer per store, cheapest store first, with the spread', () => {
const offers = toOffers(
[item(1, 1, 300, 300), item(2, 1, 250, 250), item(3, 2, 400, 400, 200), item(4, 3, 500, 500)],
[
{
id: 9,
itemId: null,
storeId: 4,
priceCents: 180,
promo: false,
sizeText: '1 kg',
unit: 'kg',
unitPriceCents: 180,
observedOn: '2026-10-20',
photoUrl: null,
note: null
}
]
);
const r = bestPerStore(offers, 'unit');
expect(r.stores.map((o) => o.key)).toEqual(['m9', 'i3', 'i2', 'i4']);
expect(r.best?.manual?.id).toBe(9);
expect(r.stores[1].regularCents).toBe(400);
expect(r.spread).toEqual({ min: 180, max: 500, pct: 178 });
});
it('has no spread for one store', () => {
expect(bestPerStore(toOffers([item(1, 1, 100, 100)], []), 'pack').spread).toBeNull();
});
});
describe('packSizes', () => {
it('lists distinct pack sizes smallest first with friendly units', () => {
expect(
packSizes([
{ unitAmount: 0.8, unit: 'kg' },
{ unitAmount: 0.22, unit: 'kg' },
{ unitAmount: 0.45, unit: 'kg' },
{ unitAmount: 0.22, unit: 'kg' },
{ unitAmount: 1.5, unit: 'kg', available: false },
{ unitAmount: null, unit: null }
])
).toEqual(['220 g', '450 g', '800 g']);
expect(packLabel(1.5, 'l')).toBe('1,5 L');
expect(packLabel(0.33, 'l')).toBe('330 ml');
expect(packLabel(6, 'piece')).toBe('6 st');
});
});