97 lines
3.7 KiB
TypeScript
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);
|
|
});
|
|
});
|