Files
Ken Yasue 41d65f58bb feat(calendar): replace list view with month/week/day grid + date detail dialog
カレンダー画面を一覧表示から実際のカレンダーグリッドへ変更。

- lib/calendar/grid.ts: 月グリッド/週/時間グリッド/取得範囲(rangeForView)の純粋な日付ヘルパー (+22 unit tests)
- components/calendar/: CalendarView(月/週/日 切替・prev/next/今日)・MonthView(truncateチップ)・WeekView・DayView(時間グリッド+終日)・EventDetailDialog(詳細・フォーカス管理)・sourceColors
- page.tsx: view/date searchParamsから範囲を計算しServer Componentで取得、CalendarViewへ渡す
- e2e: 表示切替・詳細ダイアログ・ナビステップ(週=7日/日=1日)を追加
2026-06-25 08:46:58 +02:00

176 lines
5.6 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
toISODate,
parseISODate,
addDays,
addMonths,
startOfWeek,
getMonthGrid,
getWeekDays,
getDayHours,
eventDateKey,
eventHour,
formatTime,
eventsOnDay,
rangeForView,
stepAnchor,
} from '@/lib/calendar/grid';
describe('calendar/grid', () => {
describe('toISODate / parseISODate', () => {
it('toISODate formats a local date as YYYY-MM-DD', () => {
expect(toISODate(new Date(2026, 5, 15))).toBe('2026-06-15');
expect(toISODate(new Date(2026, 0, 9))).toBe('2026-01-09');
});
it('parseISODate round-trips through toISODate', () => {
const d = parseISODate('2026-06-15');
expect(toISODate(d)).toBe('2026-06-15');
});
});
describe('startOfWeek', () => {
it('returns the Sunday of the given date week', () => {
// 2026-06-17 is a Wednesday
expect(toISODate(startOfWeek(new Date(2026, 5, 17)))).toBe('2026-06-14');
});
it('returns the same date when already on Sunday', () => {
expect(toISODate(startOfWeek(new Date(2026, 5, 14)))).toBe('2026-06-14');
});
});
describe('getMonthGrid', () => {
it('produces full Sun-Sat weeks covering the month', () => {
// June 2026: 1st is Monday, 30th is Tuesday
const weeks = getMonthGrid(2026, 5);
for (const week of weeks) {
expect(week).toHaveLength(7);
expect(week[0].getDay()).toBe(0); // Sunday
expect(week[6].getDay()).toBe(6); // Saturday
}
// First cell is the Sunday on/before June 1 (May 31)
expect(toISODate(weeks[0][0])).toBe('2026-05-31');
// Last cell is the Saturday on/after June 30 (July 4)
expect(toISODate(weeks[weeks.length - 1][6])).toBe('2026-07-04');
});
it('starts on the 1st when the month begins on Sunday', () => {
// February 2026: 1st is a Sunday
const weeks = getMonthGrid(2026, 1);
expect(toISODate(weeks[0][0])).toBe('2026-02-01');
});
it('produces 4 to 6 weeks', () => {
const weeks = getMonthGrid(2026, 5);
expect(weeks.length).toBeGreaterThanOrEqual(4);
expect(weeks.length).toBeLessThanOrEqual(6);
});
});
describe('getWeekDays / getDayHours', () => {
it('returns 7 consecutive days Sunday-Saturday', () => {
const days = getWeekDays(new Date(2026, 5, 17));
expect(days).toHaveLength(7);
expect(toISODate(days[0])).toBe('2026-06-14');
expect(toISODate(days[6])).toBe('2026-06-20');
});
it('returns hours 0..23', () => {
expect(getDayHours()).toEqual(Array.from({ length: 24 }, (_, i) => i));
});
});
describe('addDays / addMonths', () => {
it('addDays moves the date', () => {
expect(toISODate(addDays(new Date(2026, 5, 15), 10))).toBe('2026-06-25');
expect(toISODate(addDays(new Date(2026, 5, 15), -15))).toBe('2026-05-31');
});
it('addMonths rolls over the year', () => {
expect(toISODate(addMonths(new Date(2026, 11, 15), 1))).toBe(
'2027-01-15'
);
});
});
describe('eventDateKey / eventHour / formatTime', () => {
it('eventDateKey takes the first 10 chars', () => {
expect(eventDateKey('2026-06-15T10:30:00')).toBe('2026-06-15');
expect(eventDateKey('2026-06-15')).toBe('2026-06-15');
});
it('eventHour returns the hour for timed events and null otherwise', () => {
expect(eventHour('2026-06-15T10:30:00')).toBe(10);
expect(eventHour('2026-06-15')).toBeNull();
expect(eventHour('2026-06-15T09:00:00')).toBe(9);
});
it('formatTime renders HH:mm or 終日', () => {
expect(formatTime('2026-06-15T10:30:00')).toBe('10:30');
expect(formatTime('2026-06-15')).toBe('終日');
});
});
describe('eventsOnDay', () => {
const events = [
{ startAt: '2026-06-15T10:00:00', title: 'A' },
{ startAt: '2026-06-15', title: 'B' },
{ startAt: '2026-06-16T09:00:00', title: 'C' },
];
it('filters events whose start date matches the day key', () => {
const result = eventsOnDay(events, '2026-06-15');
expect(result.map((e) => e.title)).toEqual(['A', 'B']);
});
it('returns empty when nothing matches', () => {
expect(eventsOnDay(events, '2026-07-01')).toHaveLength(0);
});
});
describe('rangeForView', () => {
it('day range covers a single day ending at T23:59:59', () => {
const r = rangeForView('day', new Date(2026, 5, 15));
expect(r).toEqual({ from: '2026-06-15', to: '2026-06-15T23:59:59' });
});
it('week range covers Sun-Sat ending at T23:59:59', () => {
const r = rangeForView('week', new Date(2026, 5, 17));
expect(r).toEqual({ from: '2026-06-14', to: '2026-06-20T23:59:59' });
});
it('month range covers the whole grid including overflow days', () => {
const r = rangeForView('month', new Date(2026, 5, 15));
expect(r.from).toBe('2026-05-31');
expect(r.to).toBe('2026-07-04T23:59:59');
});
});
describe('stepAnchor', () => {
it('steps months for month view', () => {
expect(toISODate(stepAnchor('month', new Date(2026, 5, 15), 1))).toBe(
'2026-07-15'
);
expect(toISODate(stepAnchor('month', new Date(2026, 5, 15), -1))).toBe(
'2026-05-15'
);
});
it('steps weeks for week view', () => {
expect(toISODate(stepAnchor('week', new Date(2026, 5, 17), 1))).toBe(
'2026-06-24'
);
});
it('steps days for day view', () => {
expect(toISODate(stepAnchor('day', new Date(2026, 5, 15), 1))).toBe(
'2026-06-16'
);
expect(toISODate(stepAnchor('day', new Date(2026, 5, 15), -1))).toBe(
'2026-06-14'
);
});
});
});