diff --git a/frontend/src/lib/palette.test.ts b/frontend/src/lib/palette.test.ts new file mode 100644 index 0000000..995c356 --- /dev/null +++ b/frontend/src/lib/palette.test.ts @@ -0,0 +1,137 @@ +import { describe, it, expect } from 'vitest' +import { + palette, + CategoryType, + headerGradient, + overviewHeaderGradient, + amountColorClass, +} from './palette' + +describe('palette exports', () => { + it('exports all 7 CategoryType values', () => { + const expectedTypes: CategoryType[] = [ + 'income', + 'bill', + 'variable_expense', + 'debt', + 'saving', + 'investment', + 'carryover', + ] + expectedTypes.forEach((type) => { + expect(palette[type]).toBeDefined() + }) + }) + + it('has exactly 7 entries', () => { + expect(Object.keys(palette)).toHaveLength(7) + }) + + it('each category has light, medium, and base shades', () => { + Object.entries(palette).forEach(([, shades]) => { + expect(shades.light).toBeTruthy() + expect(shades.medium).toBeTruthy() + expect(shades.base).toBeTruthy() + }) + }) + + it('all shade values are non-empty oklch strings', () => { + Object.entries(palette).forEach(([, shades]) => { + expect(shades.light).toMatch(/oklch/) + expect(shades.medium).toMatch(/oklch/) + expect(shades.base).toMatch(/oklch/) + }) + }) +}) + +describe('headerGradient', () => { + it('returns a CSSProperties object for a valid category type', () => { + const result = headerGradient('bill') + expect(result).toBeDefined() + expect(typeof result).toBe('object') + }) + + it('returns an object with a background property', () => { + const result = headerGradient('income') + expect(result).toHaveProperty('background') + }) + + it('background value is a linear-gradient', () => { + const result = headerGradient('bill') + expect(String(result.background)).toMatch(/linear-gradient/) + }) + + it('gradient uses the light and medium shades of the category', () => { + const result = headerGradient('saving') + expect(String(result.background)).toContain(palette.saving.light) + expect(String(result.background)).toContain(palette.saving.medium) + }) +}) + +describe('overviewHeaderGradient', () => { + it('returns a CSSProperties object', () => { + const result = overviewHeaderGradient() + expect(result).toBeDefined() + expect(typeof result).toBe('object') + }) + + it('returns an object with a background property', () => { + const result = overviewHeaderGradient() + expect(result).toHaveProperty('background') + }) + + it('background value is a multi-stop linear-gradient', () => { + const result = overviewHeaderGradient() + const bg = String(result.background) + expect(bg).toMatch(/linear-gradient/) + // Should have multiple color stops (at least 3 commas after the direction) + const stops = bg.split('oklch').length - 1 + expect(stops).toBeGreaterThanOrEqual(3) + }) +}) + +describe('amountColorClass', () => { + it('returns text-success for positive income (isIncome=true, actual > 0)', () => { + expect(amountColorClass({ type: 'income', actual: 100, budgeted: 0, isIncome: true })).toBe( + 'text-success' + ) + }) + + it('returns empty string for zero income (isIncome=true, actual = 0)', () => { + expect(amountColorClass({ type: 'income', actual: 0, budgeted: 0, isIncome: true })).toBe('') + }) + + it('returns text-warning when actual > budgeted (over-budget expense)', () => { + expect(amountColorClass({ type: 'bill', actual: 200, budgeted: 100 })).toBe('text-warning') + }) + + it('returns empty string when actual equals budgeted (exactly on budget)', () => { + expect(amountColorClass({ type: 'bill', actual: 100, budgeted: 100 })).toBe('') + }) + + it('returns empty string when actual < budgeted (under budget)', () => { + expect(amountColorClass({ type: 'bill', actual: 50, budgeted: 100 })).toBe('') + }) + + it('returns empty string when isAvailable=true and actual is zero', () => { + expect(amountColorClass({ type: 'bill', actual: 0, budgeted: 0, isAvailable: true })).toBe('') + }) + + it('returns text-success when isAvailable=true and actual > 0', () => { + expect( + amountColorClass({ type: 'bill', actual: 500, budgeted: 0, isAvailable: true }) + ).toBe('text-success') + }) + + it('returns text-destructive when isAvailable=true and actual < 0', () => { + expect( + amountColorClass({ type: 'bill', actual: -100, budgeted: 0, isAvailable: true }) + ).toBe('text-destructive') + }) + + it('returns text-destructive for negative income (isIncome=true, actual < 0)', () => { + expect( + amountColorClass({ type: 'income', actual: -50, budgeted: 0, isIncome: true }) + ).toBe('text-destructive') + }) +})