From 6b6b9e7d0167f45fe610a19b5e5e27ecf69526cd Mon Sep 17 00:00:00 2001 From: Barcode Betty Date: Mon, 30 Mar 2026 18:53:40 +0000 Subject: [PATCH] feat: add utility functions with unit tests Add formatCurrency, formatDate, and storeSlugs utilities in src/utils/ with 21 vitest unit tests covering standard and edge cases. Co-Authored-By: Paperclip --- src/utils/__tests__/formatCurrency.test.ts | 33 ++++++++++++ src/utils/__tests__/formatDate.test.ts | 62 ++++++++++++++++++++++ src/utils/__tests__/storeSlugs.test.ts | 46 ++++++++++++++++ src/utils/formatCurrency.ts | 10 ++++ src/utils/formatDate.ts | 34 ++++++++++++ src/utils/storeSlugs.ts | 13 +++++ 6 files changed, 198 insertions(+) create mode 100644 src/utils/__tests__/formatCurrency.test.ts create mode 100644 src/utils/__tests__/formatDate.test.ts create mode 100644 src/utils/__tests__/storeSlugs.test.ts create mode 100644 src/utils/formatCurrency.ts create mode 100644 src/utils/formatDate.ts create mode 100644 src/utils/storeSlugs.ts diff --git a/src/utils/__tests__/formatCurrency.test.ts b/src/utils/__tests__/formatCurrency.test.ts new file mode 100644 index 0000000..cb7cbfa --- /dev/null +++ b/src/utils/__tests__/formatCurrency.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from 'vitest'; +import { formatCurrency } from '../formatCurrency'; + +describe('formatCurrency', () => { + it('formats 0 cents as $0.00', () => { + expect(formatCurrency(0)).toBe('$0.00'); + }); + + it('formats 199 cents as $1.99', () => { + expect(formatCurrency(199)).toBe('$1.99'); + }); + + it('formats 10000 cents as $100.00', () => { + expect(formatCurrency(10000)).toBe('$100.00'); + }); + + it('handles negative values', () => { + expect(formatCurrency(-500)).toBe('-$5.00'); + }); + + it('handles large numbers', () => { + expect(formatCurrency(99999999)).toBe('$999,999.99'); + }); + + it('supports custom locale', () => { + expect(formatCurrency(1999, 'de-DE', 'EUR')).toContain('19,99'); + }); + + it('supports custom currency', () => { + const result = formatCurrency(1000, 'en-US', 'EUR'); + expect(result).toContain('10.00'); + }); +}); diff --git a/src/utils/__tests__/formatDate.test.ts b/src/utils/__tests__/formatDate.test.ts new file mode 100644 index 0000000..8e291d4 --- /dev/null +++ b/src/utils/__tests__/formatDate.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { formatDate } from '../formatDate'; + +describe('formatDate', () => { + describe('short style', () => { + it('formats an ISO date string', () => { + const result = formatDate('2024-03-15', 'short'); + expect(result).toMatch(/Mar 15, 2024/); + }); + + it('formats a Date object', () => { + const result = formatDate(new Date('2024-03-15'), 'short'); + expect(result).toMatch(/Mar 15, 2024/); + }); + }); + + describe('long style', () => { + it('formats with weekday and full month name', () => { + const result = formatDate('2024-03-15', 'long'); + expect(result).toMatch(/Friday/); + expect(result).toMatch(/March/); + }); + }); + + describe('relative style', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('returns "just now" for very recent dates', () => { + const now = new Date('2024-01-01T12:00:00Z'); + vi.setSystemTime(now); + const result = formatDate(new Date('2024-01-01T11:59:59Z'), 'relative'); + expect(result).toBe('just now'); + }); + + it('returns minutes ago', () => { + const now = new Date('2024-01-01T12:00:00Z'); + vi.setSystemTime(now); + const result = formatDate(new Date('2024-01-01T11:45:00Z'), 'relative'); + expect(result).toBe('15m ago'); + }); + + it('returns hours ago', () => { + const now = new Date('2024-01-01T12:00:00Z'); + vi.setSystemTime(now); + const result = formatDate(new Date('2024-01-01T09:00:00Z'), 'relative'); + expect(result).toBe('3h ago'); + }); + + it('returns days ago', () => { + const now = new Date('2024-01-05T12:00:00Z'); + vi.setSystemTime(now); + const result = formatDate(new Date('2024-01-01T12:00:00Z'), 'relative'); + expect(result).toBe('4d ago'); + }); + }); +}); diff --git a/src/utils/__tests__/storeSlugs.test.ts b/src/utils/__tests__/storeSlugs.test.ts new file mode 100644 index 0000000..9a06429 --- /dev/null +++ b/src/utils/__tests__/storeSlugs.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect } from 'vitest'; +import { getStore, getStoreName, STORE_SLUGS } from '../storeSlugs'; + +describe('storeSlugs', () => { + describe('STORE_SLUGS constant', () => { + it('contains meijer, kroger, and target', () => { + expect(STORE_SLUGS).toHaveProperty('meijer'); + expect(STORE_SLUGS).toHaveProperty('kroger'); + expect(STORE_SLUGS).toHaveProperty('target'); + }); + }); + + describe('getStore', () => { + it('returns store data for known slug', () => { + const store = getStore('meijer'); + expect(store).toEqual({ + name: 'Meijer', + color: '#e31837', + icon: '/icons/stores/meijer.svg', + }); + }); + + it('returns null for unknown slug', () => { + expect(getStore('unknown-store')).toBeNull(); + }); + + it('is case insensitive', () => { + expect(getStore('KROGER')).toBeTruthy(); + expect(getStore('Target')).toBeTruthy(); + }); + }); + + describe('getStoreName', () => { + it('returns store name for known slug', () => { + expect(getStoreName('kroger')).toBe('Kroger'); + }); + + it('returns raw slug for unknown store', () => { + expect(getStoreName('unknown-store')).toBe('unknown-store'); + }); + + it('is case insensitive', () => { + expect(getStoreName('TARGET')).toBe('Target'); + }); + }); +}); diff --git a/src/utils/formatCurrency.ts b/src/utils/formatCurrency.ts new file mode 100644 index 0000000..22d63db --- /dev/null +++ b/src/utils/formatCurrency.ts @@ -0,0 +1,10 @@ +export function formatCurrency( + cents: number, + locale = 'en-US', + currency = 'USD' +): string { + return new Intl.NumberFormat(locale, { + style: 'currency', + currency, + }).format(cents / 100); +} diff --git a/src/utils/formatDate.ts b/src/utils/formatDate.ts new file mode 100644 index 0000000..f726cee --- /dev/null +++ b/src/utils/formatDate.ts @@ -0,0 +1,34 @@ +export function formatDate( + date: string | Date, + style: 'short' | 'long' | 'relative' = 'short' +): string { + const d = typeof date === 'string' ? new Date(date) : date; + + if (style === 'short') { + return d.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + } + + if (style === 'long') { + return d.toLocaleDateString('en-US', { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }); + } + + // relative + const diff = Date.now() - d.getTime(); + const seconds = Math.floor(diff / 1000); + if (seconds < 60) return 'just now'; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +} diff --git a/src/utils/storeSlugs.ts b/src/utils/storeSlugs.ts new file mode 100644 index 0000000..4fef82f --- /dev/null +++ b/src/utils/storeSlugs.ts @@ -0,0 +1,13 @@ +export const STORE_SLUGS: Record = { + meijer: { name: 'Meijer', color: '#e31837', icon: '/icons/stores/meijer.svg' }, + kroger: { name: 'Kroger', color: '#0033a0', icon: '/icons/stores/kroger.svg' }, + target: { name: 'Target', color: '#cc0000', icon: '/icons/stores/target.svg' }, +}; + +export function getStore(slug: string) { + return STORE_SLUGS[slug.toLowerCase()] ?? null; +} + +export function getStoreName(slug: string): string { + return getStore(slug)?.name ?? slug; +}