Files
MailUI4Agents/client/electron/test/components/calendar.test.tsx

282 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, it, expect } from 'vitest';
import {
startOfDay, endOfDay, startOfMonth, endOfMonth, startOfWeek, endOfWeek,
isSameDay, addDays, addMonths, monthGrid, weekDays, remindAt,
bucketByDay, dayKey, renderReminder, toLocalInput, fromLocalInput,
describeRecurrence, describeRemindBefore
} from '../../src/lib/calendar';
import type { CalendarEvent } from '../../src/types';
let seq = 0;
function ev(over: Partial<CalendarEvent> = {}): CalendarEvent {
seq += 1;
return {
event_id: `e${String(seq).padStart(3, '0')}`,
title: `事件 ${seq}`,
description: '',
reminder_text: '',
agent_name: 'pi',
to_address: '',
recipients: [],
delivery_mode: 'separate',
event_time: '2026-09-03T14:30:00+08:00',
remind_before: 0,
recurrence: 'none',
status: 'active',
created_at: '2026-09-01T00:00:00Z',
updated_at: '2026-09-01T00:00:00Z',
created_by: 'jianf',
...over
};
}
describe('日期边界', () => {
it('startOfDay / endOfDay 用本地时区', () => {
const d = new Date(2026, 8, 3, 14, 30, 45, 123);
expect(startOfDay(d).getHours()).toBe(0);
expect(startOfDay(d).getMilliseconds()).toBe(0);
expect(endOfDay(d).getHours()).toBe(23);
expect(endOfDay(d).getMilliseconds()).toBe(999);
// 不修改入参
expect(d.getHours()).toBe(14);
});
it('startOfMonth / endOfMonth', () => {
const d = new Date(2026, 8, 15);
expect(startOfMonth(d).getDate()).toBe(1);
expect(endOfMonth(d).getDate()).toBe(30); // 9 月 30 天
expect(endOfMonth(new Date(2026, 1, 5)).getDate()).toBe(28); // 2026 年 2 月
});
it('周从周一开始,周日归到上一周末尾', () => {
// 2026-09-06 是周日
const sunday = new Date(2026, 8, 6);
expect(sunday.getDay()).toBe(0);
// 它所在周的周一应是 08-31不是 09-07
expect(dayKey(startOfWeek(sunday))).toBe('2026-08-31');
expect(dayKey(endOfWeek(sunday))).toBe('2026-09-06');
});
it('周一自己就是周首', () => {
const monday = new Date(2026, 8, 7);
expect(monday.getDay()).toBe(1);
expect(dayKey(startOfWeek(monday))).toBe('2026-09-07');
});
});
describe('日期运算', () => {
it('addDays 不修改入参', () => {
const d = new Date(2026, 8, 3);
const r = addDays(d, 5);
expect(dayKey(r)).toBe('2026-09-08');
expect(dayKey(d)).toBe('2026-09-03');
});
it('addDays 跨月', () => {
expect(dayKey(addDays(new Date(2026, 8, 29), 5))).toBe('2026-10-04');
});
it('addMonths 从月末加一个月不会溢出', () => {
// setMonth 在 1 月 31 日上加一个月会给 3 月 2/3 日
const jan31 = new Date(2026, 0, 31);
expect(dayKey(addMonths(jan31, 1))).toBe('2026-02-01');
});
it('isSameDay 只比年月日', () => {
expect(isSameDay(new Date(2026, 8, 3, 0, 0), new Date(2026, 8, 3, 23, 59))).toBe(true);
expect(isSameDay(new Date(2026, 8, 3), new Date(2026, 8, 4))).toBe(false);
expect(isSameDay(new Date(2026, 8, 3), new Date(2025, 8, 3))).toBe(false);
});
});
describe('monthGrid', () => {
it('固定 42 格6 行 × 7 列)', () => {
// 行数变化会让网格高度跳动,翻月时页面内容上下弹
expect(monthGrid(new Date(2026, 8, 15))).toHaveLength(42);
expect(monthGrid(new Date(2026, 1, 15))).toHaveLength(42);
});
it('首格是月首所在周的周一', () => {
// 2026-09-01 是周二 → 首格应是 08-31周一
const cells = monthGrid(new Date(2026, 8, 15));
expect(dayKey(cells[0])).toBe('2026-08-31');
});
it('格子连续无空洞', () => {
const cells = monthGrid(new Date(2026, 8, 15));
for (let i = 1; i < cells.length; i++) {
const diff = cells[i].getTime() - cells[i - 1].getTime();
// 允许夏令时造成的 ±1 小时偏差
expect(diff).toBeGreaterThanOrEqual(23 * 3600_000);
expect(diff).toBeLessThanOrEqual(25 * 3600_000);
}
});
it('包含整个当月', () => {
const anchor = new Date(2026, 8, 15);
const keys = monthGrid(anchor).map(dayKey);
expect(keys).toContain('2026-09-01');
expect(keys).toContain('2026-09-30');
});
});
describe('weekDays', () => {
it('给 7 天,周一起头', () => {
const days = weekDays(new Date(2026, 8, 3)); // 周四
expect(days).toHaveLength(7);
expect(dayKey(days[0])).toBe('2026-08-31');
expect(dayKey(days[6])).toBe('2026-09-06');
});
});
describe('remindAt', () => {
it('提前 30 分钟', () => {
const e = ev({ event_time: '2026-09-03T14:30:00+08:00', remind_before: 30 });
expect(remindAt(e).toISOString()).toBe('2026-09-03T06:00:00.000Z');
});
it('remind_before 为 0 时就是事件时间', () => {
const e = ev({ event_time: '2026-09-03T14:30:00+08:00', remind_before: 0 });
expect(remindAt(e).toISOString()).toBe('2026-09-03T06:30:00.000Z');
});
it('提前一整天', () => {
const e = ev({ event_time: '2026-09-03T14:30:00+08:00', remind_before: 1440 });
expect(remindAt(e).toISOString()).toBe('2026-09-02T06:30:00.000Z');
});
});
describe('bucketByDay', () => {
it('按本地日期分桶,不用 UTC 前缀', () => {
// 东八区 22:00 → UTC 是前一天 14:00。用 toISOString().slice(0,10) 会归错天
const e = ev({ event_time: '2026-09-03T22:00:00+08:00' });
const m = bucketByDay([e]);
expect([...m.keys()]).toEqual(['2026-09-03']);
});
it('同一天内按时间正序', () => {
const late = ev({ event_time: '2026-09-03T18:00:00+08:00', title: '晚' });
const early = ev({ event_time: '2026-09-03T09:00:00+08:00', title: '早' });
const m = bucketByDay([late, early]);
expect(m.get('2026-09-03')!.map(x => x.title)).toEqual(['早', '晚']);
});
it('同刻事件用 event_id 兜底定序', () => {
const ts = '2026-09-03T09:00:00+08:00';
const a = ev({ event_id: 'aaa', event_time: ts });
const z = ev({ event_id: 'zzz', event_time: ts });
const m1 = bucketByDay([a, z]).get('2026-09-03')!.map(x => x.event_id);
const m2 = bucketByDay([z, a]).get('2026-09-03')!.map(x => x.event_id);
expect(m1).toEqual(m2);
expect(m1).toEqual(['aaa', 'zzz']);
});
it('时间解析失败的脏数据被跳过而不是让整个视图空白', () => {
const bad = ev({ event_time: '不是时间' });
const good = ev({ event_time: '2026-09-03T09:00:00+08:00' });
const m = bucketByDay([bad, good]);
expect([...m.keys()]).toEqual(['2026-09-03']);
});
it('空输入给空 Map', () => {
expect(bucketByDay([]).size).toBe(0);
});
});
describe('renderReminder', () => {
it('替换三个变量', () => {
const out = renderReminder('【{title}】{time} — {description}', {
title: '发布评审',
description: '看 llmsproxy 的部署脚本',
event_time: '2026-09-03T14:30:00+08:00'
});
expect(out).toBe('【发布评审】2026-09-03 14:30 — 看 llmsproxy 的部署脚本');
});
it('同一变量出现多次全部替换', () => {
// replaceAll 而非 replace后者只换第一个
const out = renderReminder('{title} / {title}', {
title: 'X',
event_time: '2026-09-03T14:30:00+08:00'
});
expect(out).toBe('X / X');
});
it('description 缺失时替换成空串而不是 undefined', () => {
const out = renderReminder('{description}|', {
title: 'T',
event_time: '2026-09-03T14:30:00+08:00'
});
expect(out).toBe('|');
});
it('没有变量的模板原样返回', () => {
const out = renderReminder('纯文本提醒', {
title: 'T',
event_time: '2026-09-03T14:30:00+08:00'
});
expect(out).toBe('纯文本提醒');
});
it('时间解析失败时回退到原始串', () => {
const out = renderReminder('{time}', { title: 'T', event_time: '坏时间' });
expect(out).toBe('坏时间');
});
});
describe('datetime-local 往返', () => {
it('toLocalInput 给本地时区的 YYYY-MM-DDTHH:mm', () => {
const d = new Date(2026, 8, 3, 14, 30);
expect(toLocalInput(d)).toBe('2026-09-03T14:30');
});
it('补零', () => {
const d = new Date(2026, 0, 5, 9, 5);
expect(toLocalInput(d)).toBe('2026-01-05T09:05');
});
it('fromLocalInput 按本地时区解析(人写的就是本地时间)', () => {
const iso = fromLocalInput('2026-09-03T14:30');
// 结果应与本地构造的 Date 一致
expect(iso).toBe(new Date(2026, 8, 3, 14, 30).toISOString());
});
it('往返不丢分钟', () => {
const original = new Date(2026, 8, 3, 14, 30);
expect(toLocalInput(new Date(fromLocalInput(toLocalInput(original))))).toBe('2026-09-03T14:30');
});
it('非法输入给空串', () => {
expect(fromLocalInput('')).toBe('');
expect(fromLocalInput('坏值')).toBe('');
});
});
describe('人类可读描述', () => {
it('重复规则', () => {
expect(describeRecurrence(ev({ recurrence: 'none' }))).toBe('不重复');
expect(describeRecurrence(ev({ recurrence: 'daily' }))).toBe('每天');
expect(describeRecurrence(ev({ recurrence: 'weekly' }))).toBe('每周');
expect(describeRecurrence(ev({ recurrence: 'monthly' }))).toBe('每月');
});
it('带终止时间', () => {
const e = ev({ recurrence: 'daily', recurrence_end: '2026-12-31T00:00:00+08:00' });
expect(describeRecurrence(e)).toBe('每天,至 2026-12-31');
});
it('none 时忽略 recurrence_end', () => {
const e = ev({ recurrence: 'none', recurrence_end: '2026-12-31T00:00:00+08:00' });
expect(describeRecurrence(e)).toBe('不重复');
});
it('提前提醒', () => {
expect(describeRemindBefore(0)).toBe('到点提醒');
expect(describeRemindBefore(15)).toBe('提前 15 分钟');
expect(describeRemindBefore(60)).toBe('提前 1 小时');
expect(describeRemindBefore(120)).toBe('提前 2 小时');
expect(describeRemindBefore(90)).toBe('提前 1 小时 30 分钟');
expect(describeRemindBefore(1440)).toBe('提前 24 小时');
});
});