import { describe, it, expect, vi, afterEach } from 'vitest'; import { getWeekStart, prevWeek, nextWeek, weekDays, isToday, formatWeekRange, formatDayLabel, sortEasiestFirst } from './week'; describe('getWeekStart', () => { it('returns Monday for a Monday input', () => { // 2026-03-30 is a Monday const d = new Date('2026-03-30T12:00:00Z'); expect(getWeekStart(d)).toBe('2026-03-30'); }); it('returns Monday for a Wednesday input', () => { // 2026-04-01 is a Wednesday → week starts 2026-03-30 const d = new Date('2026-04-01T12:00:00Z'); expect(getWeekStart(d)).toBe('2026-03-30'); }); it('returns Monday for a Sunday input (edge case — goes back 6 days)', () => { // 2026-04-05 is a Sunday → week starts 2026-03-30 const d = new Date('2026-04-05T12:00:00Z'); expect(getWeekStart(d)).toBe('2026-03-30'); }); it('returns Monday for a Saturday input', () => { // 2026-04-04 is a Saturday → week starts 2026-03-30 const d = new Date('2026-04-04T12:00:00Z'); expect(getWeekStart(d)).toBe('2026-03-30'); }); it('handles year boundary correctly (Dec 28 2025 → week starts Dec 22 2025)', () => { const d = new Date('2025-12-28T12:00:00Z'); expect(getWeekStart(d)).toBe('2025-12-22'); }); }); describe('prevWeek', () => { it('returns the Monday 7 days before', () => { expect(prevWeek('2026-03-30')).toBe('2026-03-23'); }); it('handles month boundary', () => { expect(prevWeek('2026-04-06')).toBe('2026-03-30'); }); it('handles year boundary', () => { expect(prevWeek('2026-01-05')).toBe('2025-12-29'); }); }); describe('nextWeek', () => { it('returns the Monday 7 days after', () => { expect(nextWeek('2026-03-30')).toBe('2026-04-06'); }); it('handles month boundary', () => { expect(nextWeek('2026-03-30')).toBe('2026-04-06'); }); it('handles year boundary', () => { expect(nextWeek('2025-12-29')).toBe('2026-01-05'); }); }); describe('weekDays', () => { it('returns exactly 7 dates', () => { expect(weekDays('2026-03-30')).toHaveLength(7); }); it('starts on the given weekStart', () => { const days = weekDays('2026-03-30'); expect(days[0]).toBe('2026-03-30'); }); it('ends 6 days after weekStart', () => { const days = weekDays('2026-03-30'); expect(days[6]).toBe('2026-04-05'); }); it('has consecutive daily dates', () => { const days = weekDays('2026-03-30'); for (let i = 1; i < 7; i++) { const prev = new Date(days[i - 1] + 'T00:00:00Z'); const curr = new Date(days[i] + 'T00:00:00Z'); expect(curr.getTime() - prev.getTime()).toBe(86400000); } }); it('handles month boundary correctly', () => { const days = weekDays('2026-03-30'); expect(days[1]).toBe('2026-03-31'); expect(days[2]).toBe('2026-04-01'); }); }); describe('isToday', () => { afterEach(() => { vi.useRealTimers(); }); it('returns true for today (UTC)', () => { vi.useFakeTimers(); vi.setSystemTime(new Date('2026-04-03T10:00:00Z')); expect(isToday('2026-04-03')).toBe(true); }); it('returns false for yesterday', () => { vi.useFakeTimers(); vi.setSystemTime(new Date('2026-04-03T10:00:00Z')); expect(isToday('2026-04-02')).toBe(false); }); it('returns false for tomorrow', () => { vi.useFakeTimers(); vi.setSystemTime(new Date('2026-04-03T10:00:00Z')); expect(isToday('2026-04-04')).toBe(false); }); }); describe('formatWeekRange', () => { it('returns a non-empty string', () => { expect(formatWeekRange('2026-03-30')).toBeTruthy(); }); it('contains both start and end month info', () => { const range = formatWeekRange('2026-03-30'); // Start is March 30, end is April 5 — range should span both months expect(range).toContain('–'); }); }); describe('formatDayLabel', () => { it('returns a non-empty string', () => { expect(formatDayLabel('2026-03-30')).toBeTruthy(); }); it('contains a comma separator', () => { expect(formatDayLabel('2026-03-30')).toContain(','); }); }); describe('sortEasiestFirst', () => { it('sorts easy before medium before hard', () => { const recipes = [ { id: '1', name: 'Hard', effort: 'hard', cookTimeMin: 10 }, { id: '2', name: 'Easy', effort: 'easy', cookTimeMin: 10 }, { id: '3', name: 'Medium', effort: 'medium', cookTimeMin: 10 } ]; const sorted = sortEasiestFirst(recipes); expect(sorted.map((r) => r.effort)).toEqual(['easy', 'medium', 'hard']); }); it('sorts by cookTimeMin ascending within same effort', () => { const recipes = [ { id: '1', name: 'Slow Easy', effort: 'easy', cookTimeMin: 60 }, { id: '2', name: 'Fast Easy', effort: 'easy', cookTimeMin: 15 } ]; const sorted = sortEasiestFirst(recipes); expect(sorted[0].name).toBe('Fast Easy'); }); it('treats missing effort as after hard', () => { const recipes = [ { id: '1', name: 'No effort', effort: undefined, cookTimeMin: 5 }, { id: '2', name: 'Hard', effort: 'hard', cookTimeMin: 5 } ]; const sorted = sortEasiestFirst(recipes); expect(sorted[0].effort).toBe('hard'); }); it('treats missing cookTimeMin as after defined values', () => { const recipes = [ { id: '1', name: 'No time', effort: 'easy', cookTimeMin: undefined }, { id: '2', name: 'Has time', effort: 'easy', cookTimeMin: 30 } ]; const sorted = sortEasiestFirst(recipes); expect(sorted[0].name).toBe('Has time'); }); it('does not mutate the original array', () => { const recipes = [ { id: '1', name: 'Hard', effort: 'hard', cookTimeMin: 10 }, { id: '2', name: 'Easy', effort: 'easy', cookTimeMin: 10 } ]; const original = [...recipes]; sortEasiestFirst(recipes); expect(recipes[0].effort).toBe(original[0].effort); }); });