v2.2.15: Astra Office Refactor & Multi-Service Integration
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import { parseIcs, selectUpcoming } from '../src/features/calendar/icsParser';
|
||||
|
||||
describe('parseIcs', () => {
|
||||
test('returns empty array on invalid / empty input', () => {
|
||||
expect(parseIcs('')).toEqual([]);
|
||||
expect(parseIcs(null as any)).toEqual([]);
|
||||
expect(parseIcs(undefined as any)).toEqual([]);
|
||||
expect(parseIcs('random text no VEVENT')).toEqual([]);
|
||||
});
|
||||
|
||||
test('parses single timed event', () => {
|
||||
const ics = [
|
||||
'BEGIN:VCALENDAR',
|
||||
'BEGIN:VEVENT',
|
||||
'SUMMARY:팀 미팅',
|
||||
'DTSTART:20260520T130000Z',
|
||||
'DTEND:20260520T140000Z',
|
||||
'LOCATION:회의실 A',
|
||||
'END:VEVENT',
|
||||
'END:VCALENDAR',
|
||||
].join('\r\n');
|
||||
const events = parseIcs(ics);
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0].summary).toBe('팀 미팅');
|
||||
expect(events[0].location).toBe('회의실 A');
|
||||
expect(events[0].allDay).toBe(false);
|
||||
// 2026-05-20 13:00 UTC
|
||||
expect(events[0].start.getUTCFullYear()).toBe(2026);
|
||||
expect(events[0].start.getUTCMonth()).toBe(4);
|
||||
expect(events[0].start.getUTCDate()).toBe(20);
|
||||
expect(events[0].start.getUTCHours()).toBe(13);
|
||||
expect(events[0].end?.getUTCHours()).toBe(14);
|
||||
});
|
||||
|
||||
test('parses all-day event via VALUE=DATE', () => {
|
||||
const ics = [
|
||||
'BEGIN:VEVENT',
|
||||
'SUMMARY:생일',
|
||||
'DTSTART;VALUE=DATE:20260615',
|
||||
'DTEND;VALUE=DATE:20260616',
|
||||
'END:VEVENT',
|
||||
].join('\n');
|
||||
const events = parseIcs(ics);
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0].allDay).toBe(true);
|
||||
expect(events[0].start.getFullYear()).toBe(2026);
|
||||
expect(events[0].start.getMonth()).toBe(5);
|
||||
expect(events[0].start.getDate()).toBe(15);
|
||||
});
|
||||
|
||||
test('unfolds line continuations (RFC 5545)', () => {
|
||||
// ICS 75자 wrap — 다음 줄이 공백/탭으로 시작하면 같은 필드의 연속.
|
||||
const ics = [
|
||||
'BEGIN:VEVENT',
|
||||
'SUMMARY:긴 제목 첫 부분',
|
||||
' 두번째 줄 이어짐',
|
||||
'DTSTART:20260520T130000Z',
|
||||
'END:VEVENT',
|
||||
].join('\r\n');
|
||||
const events = parseIcs(ics);
|
||||
expect(events[0].summary).toBe('긴 제목 첫 부분두번째 줄 이어짐');
|
||||
});
|
||||
|
||||
test('unescapes ICS escape sequences in summary / location', () => {
|
||||
const ics = [
|
||||
'BEGIN:VEVENT',
|
||||
'SUMMARY:1\\, 2\\, 3 — 회의\\nFollow-up',
|
||||
'LOCATION:Zoom\\, 본관 3층',
|
||||
'DTSTART:20260520T130000Z',
|
||||
'END:VEVENT',
|
||||
].join('\n');
|
||||
const events = parseIcs(ics);
|
||||
expect(events[0].summary).toBe('1, 2, 3 — 회의 Follow-up');
|
||||
expect(events[0].location).toBe('Zoom, 본관 3층');
|
||||
});
|
||||
|
||||
test('parses multiple VEVENTs and gives default summary when missing', () => {
|
||||
const ics = [
|
||||
'BEGIN:VEVENT',
|
||||
'DTSTART:20260520T130000Z',
|
||||
'END:VEVENT',
|
||||
'BEGIN:VEVENT',
|
||||
'SUMMARY:두번째',
|
||||
'DTSTART:20260521T130000Z',
|
||||
'END:VEVENT',
|
||||
].join('\n');
|
||||
const events = parseIcs(ics);
|
||||
expect(events).toHaveLength(2);
|
||||
expect(events[0].summary).toBe('(제목 없음)');
|
||||
expect(events[1].summary).toBe('두번째');
|
||||
});
|
||||
|
||||
test('drops VEVENT without DTSTART', () => {
|
||||
const ics = [
|
||||
'BEGIN:VEVENT',
|
||||
'SUMMARY:DTSTART 누락',
|
||||
'END:VEVENT',
|
||||
].join('\n');
|
||||
expect(parseIcs(ics)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectUpcoming', () => {
|
||||
const now = new Date('2026-05-20T12:00:00Z');
|
||||
const mkEv = (offsetMin: number, label = 'evt'): any => ({
|
||||
start: new Date(now.getTime() + offsetMin * 60 * 1000),
|
||||
end: undefined, summary: label, location: '', description: '', allDay: false,
|
||||
});
|
||||
|
||||
test('filters past events older than 1 hour', () => {
|
||||
const events = [
|
||||
mkEv(-120, 'old'), // 2 hours ago — dropped
|
||||
mkEv(-30, 'recent'), // 30 min ago — kept (within 1h window)
|
||||
mkEv(60, 'soon'), // 1 hour future — kept
|
||||
];
|
||||
const upcoming = selectUpcoming(events, 14, now);
|
||||
expect(upcoming.map((e) => e.summary)).toEqual(['recent', 'soon']);
|
||||
});
|
||||
|
||||
test('filters events beyond cutoff days', () => {
|
||||
const events = [
|
||||
mkEv(60, 'soon'),
|
||||
mkEv(60 * 24 * 15, 'too-far'), // 15 days — beyond 14-day cutoff
|
||||
];
|
||||
const upcoming = selectUpcoming(events, 14, now);
|
||||
expect(upcoming.map((e) => e.summary)).toEqual(['soon']);
|
||||
});
|
||||
|
||||
test('sorts upcoming events by start time', () => {
|
||||
const events = [mkEv(180, 'c'), mkEv(60, 'a'), mkEv(120, 'b')];
|
||||
const upcoming = selectUpcoming(events, 14, now);
|
||||
expect(upcoming.map((e) => e.summary)).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user