Files
HamHam/tests/unit/calculator.test.ts
T

97 lines
3.7 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { ingredientSchema } from '../../src/lib/recipe';
import {
positive,
portionFactor,
ingredientFactor,
scaledAmount,
formatAmount,
convert,
renderStep,
mergeShopping,
} from '../../src/lib/calculator';
import { pasta } from '../fixtures';
const chicken = ingredientSchema.parse({
id: 'chicken',
name: 'Hühnerbrust',
amount: 400,
unit: 'g',
});
describe('Precise scaling from immutable originals', () => {
it('scales 2 to 8 portions', () => {
const f = portionFactor(2, 8);
expect(f).toBe('4');
expect(scaledAmount(chicken, f)).toBe('1600');
});
it('scales all ingredients from 400 g to 800 g', () => {
const f = ingredientFactor(chicken, 800);
expect(f).toBe('2');
expect(scaledAmount({ ...chicken, amount: 200 }, f)).toBe('400');
expect(scaledAmount({ ...chicken, amount: 50 }, f)).toBe('100');
});
it('supports downscaling and comma decimals', () => {
expect(ingredientFactor(chicken, '0,2', 'kg')).toBe('0.5');
expect(portionFactor(2, '1,5')).toBe('0.75');
});
it('normalizes kg, liters and spoons without conflating mass with volume', () => {
expect(convert(1.5, 'kg', 'g').toString()).toBe('1500');
expect(convert(250, 'ml', 'l').toString()).toBe('0.25');
expect(convert(2, 'EL', 'TL').toString()).toBe('6');
expect(() => convert(2, 'g', 'ml')).toThrow();
});
it('scales optional ingredients and leaves unquantified ingredients unchanged', () => {
expect(scaledAmount({ ...chicken, optional: true }, 2)).toBe('800');
expect(scaledAmount({ ...chicken, amount: null }, 2)).toBeNull();
expect(() => ingredientFactor({ ...chicken, amount: null }, 3)).toThrow();
});
it.each([0, -1, 'NaN', 'Infinity', '', 'abc', 1000000001])('rejects invalid amounts: %s', (v) => {
expect(() => positive(v)).toThrow();
});
it('prevents division by zero', () => {
expect(() => ingredientFactor({ ...chicken, amount: 0 }, 1)).toThrow();
expect(() => portionFactor(0, 4)).toThrow();
});
it('rejects extreme scale factors before they reach the interface', () => {
expect(() => ingredientFactor({ ...chicken, amount: 0.000001 }, 1000000)).toThrow();
expect(() => portionFactor(0.000001, 1000000)).toThrow();
});
it('keeps exact quantities despite repeated rescaling', () => {
for (let n = 1; n <= 100; n++) {
expect(scaledAmount(chicken, ingredientFactor(chicken, n))).toBe(String(n));
}
expect(scaledAmount(chicken, '1')).toBe('400');
expect(scaledAmount({ ...chicken, amount: 0.1 }, 3)).toBe('0.3');
});
it('uses readable fractions and final display rounding', () => {
expect(formatAmount(0.5)).toBe('½');
expect(formatAmount(1.25)).toBe('1¼');
expect(formatAmount(2.75)).toBe('2¾');
expect(formatAmount(10.6666)).toBe('10,67');
expect(formatAmount(null)).toBe('');
});
it('updates only structured references in instructions', () => {
expect(renderStep('{{pasta}} für 10 Minuten kochen.', pasta, '2')).toBe(
'400 g Pasta für 10 Minuten kochen.',
);
});
it('merges compatible shopping quantities across recipes', () => {
const list = mergeShopping([], [chicken], '2');
const result = mergeShopping(list, [{ ...chicken, amount: 0.5, unit: 'kg' }], '1');
expect(result).toHaveLength(1);
expect(result[0].amount).toBe('1300');
expect(list[0].amount).toBe('800');
});
it('keeps incompatible shopping units and notes separate', () => {
const a = mergeShopping([], [chicken], '1');
const b = mergeShopping(
a,
[
{ ...chicken, unit: 'Stück' },
{ ...chicken, note: 'gekocht' },
],
'1',
);
expect(b).toHaveLength(3);
});
});