- Fix logic bug `{#if !isPlanner === false}` - view/cook buttons now visible for all roles, swap only for planner
- Convert Tauschen from dead button to link with suggestions href
- Add week.ts unit tests (23 tests covering getWeekStart Sunday edge case, prevWeek/nextWeek, weekDays, isToday, formatWeekRange)
- Fix isToday to use UTC consistently (.toISOString().slice(0,10)) instead of local date
- Add server-side role guard to createPlan action (403 for members)
- Add weekStart format validation in createPlan action
- Add isSelected prop to DayMealCard with green treatment
- Make variety banner sticky on mobile (always visible per spec)
- Add day name abbreviation above date badge in desktop column headers
- Remove placeholder Navigation text from desktop sidebar
- Add aria-label to desktop empty tile buttons
- Add variety score partial failure test, multiple overlaps test, WeekStrip today+selected test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
147 lines
3.8 KiB
TypeScript
147 lines
3.8 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||
import {
|
||
getWeekStart,
|
||
prevWeek,
|
||
nextWeek,
|
||
weekDays,
|
||
isToday,
|
||
formatWeekRange,
|
||
formatDayLabel
|
||
} 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(',');
|
||
});
|
||
});
|