splitwise / tests /unit /forms /expense-form.test.ts
assafvayner's picture
assafvayner HF Staff
fix(photos,forms): own-property content types, dimension bounds, VP8L signature, trimmed inputs
fa363c5
Raw History Blame Contribute Delete
4.98 kB
import { describe, expect, it } from 'vitest';
import { buildExpenseBody, emptyExpenseForm, expenseToFormValues, readExpenseForm } from '../../../src/lib/server/forms/expense-form';
import type { Expense } from '../../../src/lib/domain/types';
const members = ['u_a', 'u_b', 'u_c'];
const fd = (entries: [string, string][]) => {
const f = new FormData();
for (const [k, v] of entries) f.append(k, v);
return f;
};
describe('readExpenseForm', () => {
it('reads all fields, keeping only known participants', () => {
const values = readExpenseForm(
fd([['description', ' Pizza '], ['amount', '30'], ['date', '2026-08-01'], ['paidBy', 'u_a'], ['mode', 'percent'],
['participants', 'u_a'], ['participants', 'u_b'], ['participants', 'u_zzz'], ['pct_u_a', '25'], ['pct_u_b', '75'], ['exact_u_a', '1'], ['notes', 'n'], ['removePhoto', 'on']]),
members
);
expect(values).toEqual({
description: 'Pizza', amount: '30', date: '2026-08-01', paidBy: 'u_a', mode: 'percent', participants: ['u_a', 'u_b'],
pct: { u_a: '25', u_b: '75' }, exact: { u_a: '1' }, notes: 'n', removePhoto: true
});
});
it('defaults unknown mode to equal', () => {
expect(readExpenseForm(fd([['mode', 'weird']]), members).mode).toBe('equal');
});
});
describe('buildExpenseBody', () => {
const base = { ...emptyExpenseForm('u_a', members, '2026-08-01'), description: 'Pizza', amount: '30' };
const opts = { activeMemberIds: members, currency: 'USD' };
it('builds an equal split', () => {
const r = buildExpenseBody({ ...base, participants: ['u_a', 'u_b'] }, opts);
expect(r).toEqual({
ok: true,
body: { description: 'Pizza', amountCents: 3000, currency: 'USD', date: '2026-08-01', paidBy: 'u_a', split: { mode: 'equal', participants: ['u_a', 'u_b'] }, shares: { u_a: 1500, u_b: 1500 } }
});
});
it('builds percent and exact splits from the participant inputs', () => {
const pct = buildExpenseBody({ ...base, mode: 'percent', participants: ['u_a', 'u_b'], pct: { u_a: '25', u_b: '75', u_c: '100' } }, opts);
expect(pct.ok && pct.body.split).toEqual({ mode: 'percent', bp: { u_a: 2500, u_b: 7500 } });
expect(pct.ok && pct.body.shares).toEqual({ u_a: 750, u_b: 2250 });
const exact = buildExpenseBody({ ...base, mode: 'exact', participants: ['u_a', 'u_b'], exact: { u_a: '10', u_b: '20' } }, opts);
expect(exact.ok && exact.body.shares).toEqual({ u_a: 1000, u_b: 2000 });
});
it('includes notes only when non-empty', () => {
const r = buildExpenseBody({ ...base, notes: ' ' }, opts);
expect(r.ok && 'notes' in r.body).toBe(false);
const r2 = buildExpenseBody({ ...base, notes: 'hi' }, opts);
expect(r2.ok && r2.body.notes).toBe('hi');
});
it('trims the description before storing it', () => {
const r = buildExpenseBody({ ...base, description: ' Pizza ' }, opts);
expect(r.ok && r.body.description).toBe('Pizza');
});
it('reports field errors', () => {
const errs = (v: Partial<typeof base>) => {
const r = buildExpenseBody({ ...base, ...v }, opts);
return r.ok ? {} : r.errors;
};
expect(errs({ description: '' })).toHaveProperty('description');
expect(errs({ amount: 'abc' })).toHaveProperty('amount');
expect(errs({ amount: '0' })).toHaveProperty('amount');
expect(errs({ date: '2026-02-30' })).toHaveProperty('date');
expect(errs({ paidBy: 'u_admin' })).toHaveProperty('paidBy');
expect(errs({ participants: [] })).toHaveProperty('participants');
expect(errs({ participants: ['u_a', 'u_ghost'] })).toHaveProperty('participants');
expect(errs({ mode: 'percent', pct: { u_a: '50' } })).toHaveProperty('shares');
expect(errs({ mode: 'percent', pct: { u_a: '50', u_b: '30', u_c: '30' } })).toHaveProperty('shares');
expect(errs({ mode: 'exact', exact: { u_a: '10', u_b: '10', u_c: '5' } })).toHaveProperty('shares');
expect(errs({ mode: 'exact', exact: { u_a: 'x', u_b: '10', u_c: '10' } })).toHaveProperty('shares');
});
it('rejects a payer who is not an active member (photo/notes untouched)', () => {
const r = buildExpenseBody({ ...base, paidBy: 'u_c' }, { ...opts, activeMemberIds: ['u_a', 'u_b'] });
expect(r.ok).toBe(false);
});
});
describe('expenseToFormValues', () => {
it('maps every split mode back to inputs', () => {
const audit = { createdBy: 'u_a', createdAt: 'x', updatedBy: 'u_a', updatedAt: 'x' };
const e: Expense = { id: 'e_1', version: 1, description: 'D', amountCents: 1234, currency: 'USD', date: '2026-08-01', paidBy: 'u_b', split: { mode: 'percent', bp: { u_a: 3333, u_b: 6667 } }, shares: { u_a: 411, u_b: 823 }, notes: 'n', ...audit };
expect(expenseToFormValues(e)).toEqual({ description: 'D', amount: '12.34', date: '2026-08-01', paidBy: 'u_b', mode: 'percent', participants: ['u_a', 'u_b'], pct: { u_a: '33.33', u_b: '66.67' }, exact: {}, notes: 'n', removePhoto: false });
const ex: Expense = { ...e, split: { mode: 'exact', cents: { u_a: 34, u_b: 1200 } } };
expect(expenseToFormValues(ex).exact).toEqual({ u_a: '0.34', u_b: '12.00' });
});
});