groceries: mobile-first tracker redesign — compact rows per product group (cheapest store badge, unit price, spread, promo), tap to compare stores; category tree (23 tops, subcategories, icons/colours) with chips, store tiles, sort and non-food toggle; owner shelf prices (manual source with date, photo, note)
Some checks failed
Deploy production / deploy (push) Failing after 1m1s
Some checks failed
Deploy production / deploy (push) Failing after 1m1s
This commit is contained in:
@@ -1,5 +1,14 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { cheapest, formatAmount, formatUnitPrice, sortByPrice, type PricedItem } from './groceries';
|
||||
import {
|
||||
bestPerStore,
|
||||
cheapest,
|
||||
formatAmount,
|
||||
formatUnitPrice,
|
||||
parseEuros,
|
||||
sortByPrice,
|
||||
toOffers,
|
||||
type PricedItem
|
||||
} from './groceries';
|
||||
|
||||
const it_ = (
|
||||
id: number,
|
||||
@@ -42,3 +51,69 @@ describe('grocery price helpers', () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user