mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18:41:08 +02:00
fix(tests): update calendar event tests for pure function API, delete orphaned todo tests
Calendar: Rewrite events.test.ts and events-recurrence.test.ts to test pure functions (getEventsForDay, getEventsInRange, expandRecurringEvents) from queries.ts instead of removed store methods. Todo: Delete projects.test.ts and reminders.test.ts — the API files they tested were removed in the local-first migration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f628026b9e
commit
4d26196590
4 changed files with 193 additions and 695 deletions
|
|
@ -1,27 +1,27 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import type { CalendarEvent } from '@calendar/shared';
|
||||
import type { PaginationMeta } from '$lib/api/events';
|
||||
|
||||
const defaultPagination: PaginationMeta = { offset: 0, count: 0 };
|
||||
// Mock local-store to avoid import.meta.env issues in tests
|
||||
vi.mock('$lib/data/local-store', () => ({
|
||||
eventCollection: {},
|
||||
calendarCollection: {},
|
||||
}));
|
||||
|
||||
vi.mock('$lib/api/events', () => ({
|
||||
getEvents: vi.fn(),
|
||||
createEvent: vi.fn(),
|
||||
updateEvent: vi.fn(),
|
||||
deleteEvent: vi.fn(),
|
||||
vi.mock('@manacore/local-store/svelte', () => ({
|
||||
useLiveQueryWithDefault: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('$lib/api/birthdays', () => ({
|
||||
BIRTHDAY_CALENDAR: { id: 'birthday-cal', name: 'Birthdays', color: '#ec4899' },
|
||||
}));
|
||||
|
||||
vi.mock('@manacore/shared-ui', () => ({
|
||||
toastStore: { error: vi.fn(), success: vi.fn() },
|
||||
}));
|
||||
|
||||
import * as api from '$lib/api/events';
|
||||
import { expandRecurringEvents } from '$lib/data/queries';
|
||||
import { eventsStore } from './events.svelte';
|
||||
|
||||
const mockGetEvents = vi.mocked(api.getEvents);
|
||||
const mockUpdateEvent = vi.mocked(api.updateEvent);
|
||||
const mockDeleteEvent = vi.mocked(api.deleteEvent);
|
||||
|
||||
function makeEvent(overrides: Partial<CalendarEvent> = {}): CalendarEvent {
|
||||
return {
|
||||
id: 'evt-1',
|
||||
|
|
@ -48,141 +48,73 @@ function makeEvent(overrides: Partial<CalendarEvent> = {}): CalendarEvent {
|
|||
};
|
||||
}
|
||||
|
||||
describe('eventsStore - recurrence', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
eventsStore.clear();
|
||||
eventsStore.clearDraftEvent();
|
||||
describe('expandRecurringEvents', () => {
|
||||
it('should expand daily recurring event', () => {
|
||||
const events = [
|
||||
makeEvent({
|
||||
id: 'r1',
|
||||
startTime: '2026-03-01T09:00:00',
|
||||
endTime: '2026-03-01T09:30:00',
|
||||
recurrenceRule: 'FREQ=DAILY',
|
||||
}),
|
||||
];
|
||||
const result = expandRecurringEvents(events, new Date('2026-03-01'), new Date('2026-03-07'));
|
||||
expect(result.length).toBeGreaterThanOrEqual(6);
|
||||
for (const e of result) {
|
||||
expect(e.id).toContain('r1__recurrence__');
|
||||
expect(e.parentEventId).toBe('r1');
|
||||
}
|
||||
});
|
||||
|
||||
describe('expansion', () => {
|
||||
it('should expand daily recurring event', async () => {
|
||||
mockGetEvents.mockResolvedValue({
|
||||
data: [
|
||||
makeEvent({
|
||||
id: 'r1',
|
||||
startTime: '2026-03-01T09:00:00',
|
||||
endTime: '2026-03-01T09:30:00',
|
||||
recurrenceRule: 'FREQ=DAILY',
|
||||
}),
|
||||
],
|
||||
pagination: defaultPagination,
|
||||
error: null,
|
||||
});
|
||||
await eventsStore.fetchEvents(new Date('2026-03-01'), new Date('2026-03-07'));
|
||||
const events = eventsStore.events;
|
||||
expect(events.length).toBeGreaterThanOrEqual(6);
|
||||
for (const e of events) {
|
||||
expect(e.id).toContain('r1__recurrence__');
|
||||
expect(e.parentEventId).toBe('r1');
|
||||
}
|
||||
});
|
||||
|
||||
it('should preserve event duration in occurrences', async () => {
|
||||
mockGetEvents.mockResolvedValue({
|
||||
data: [
|
||||
makeEvent({
|
||||
id: 'w1',
|
||||
startTime: '2026-03-02T14:00:00',
|
||||
endTime: '2026-03-02T15:00:00',
|
||||
recurrenceRule: 'FREQ=WEEKLY',
|
||||
}),
|
||||
],
|
||||
pagination: defaultPagination,
|
||||
error: null,
|
||||
});
|
||||
await eventsStore.fetchEvents(new Date('2026-03-01'), new Date('2026-03-31'));
|
||||
for (const e of eventsStore.events) {
|
||||
const dur = new Date(e.endTime).getTime() - new Date(e.startTime).getTime();
|
||||
expect(dur).toBe(3600000);
|
||||
}
|
||||
});
|
||||
|
||||
it('should respect exceptions', async () => {
|
||||
mockGetEvents.mockResolvedValue({
|
||||
data: [
|
||||
makeEvent({
|
||||
id: 'exc',
|
||||
startTime: '2026-03-01T09:00:00',
|
||||
endTime: '2026-03-01T09:30:00',
|
||||
recurrenceRule: 'FREQ=DAILY',
|
||||
recurrenceExceptions: ['2026-03-03', '2026-03-05'],
|
||||
}),
|
||||
],
|
||||
pagination: defaultPagination,
|
||||
error: null,
|
||||
});
|
||||
await eventsStore.fetchEvents(new Date('2026-03-01'), new Date('2026-03-07'));
|
||||
const dates = eventsStore.events.map((e) => e.id.split('__recurrence__')[1]);
|
||||
expect(dates).not.toContain('2026-03-03');
|
||||
expect(dates).not.toContain('2026-03-05');
|
||||
expect(dates).toContain('2026-03-01');
|
||||
});
|
||||
|
||||
it('should not expand non-recurring events', async () => {
|
||||
mockGetEvents.mockResolvedValue({
|
||||
data: [makeEvent({ id: 'normal' })],
|
||||
pagination: defaultPagination,
|
||||
error: null,
|
||||
});
|
||||
await eventsStore.fetchEvents(new Date('2026-03-01'), new Date('2026-03-31'));
|
||||
expect(eventsStore.events).toHaveLength(1);
|
||||
expect(eventsStore.events[0].id).toBe('normal');
|
||||
});
|
||||
it('should preserve event duration in occurrences', () => {
|
||||
const events = [
|
||||
makeEvent({
|
||||
id: 'w1',
|
||||
startTime: '2026-03-02T14:00:00',
|
||||
endTime: '2026-03-02T15:00:00',
|
||||
recurrenceRule: 'FREQ=WEEKLY',
|
||||
}),
|
||||
];
|
||||
const result = expandRecurringEvents(events, new Date('2026-03-01'), new Date('2026-03-31'));
|
||||
for (const e of result) {
|
||||
const dur = new Date(e.endTime).getTime() - new Date(e.startTime).getTime();
|
||||
expect(dur).toBe(3600000);
|
||||
}
|
||||
});
|
||||
|
||||
describe('helpers', () => {
|
||||
it('isRecurrenceOccurrence', () => {
|
||||
expect(eventsStore.isRecurrenceOccurrence('evt__recurrence__2026-03-15')).toBe(true);
|
||||
expect(eventsStore.isRecurrenceOccurrence('evt-1')).toBe(false);
|
||||
});
|
||||
|
||||
it('getParentEventId', () => {
|
||||
expect(eventsStore.getParentEventId('evt-1__recurrence__2026-03-15')).toBe('evt-1');
|
||||
expect(eventsStore.getParentEventId('evt-1')).toBe('evt-1');
|
||||
});
|
||||
it('should respect exceptions', () => {
|
||||
const events = [
|
||||
makeEvent({
|
||||
id: 'exc',
|
||||
startTime: '2026-03-01T09:00:00',
|
||||
endTime: '2026-03-01T09:30:00',
|
||||
recurrenceRule: 'FREQ=DAILY',
|
||||
recurrenceExceptions: ['2026-03-03', '2026-03-05'],
|
||||
}),
|
||||
];
|
||||
const result = expandRecurringEvents(events, new Date('2026-03-01'), new Date('2026-03-07'));
|
||||
const dates = result.map((e) => e.id.split('__recurrence__')[1]);
|
||||
expect(dates).not.toContain('2026-03-03');
|
||||
expect(dates).not.toContain('2026-03-05');
|
||||
expect(dates).toContain('2026-03-01');
|
||||
});
|
||||
|
||||
describe('deleteRecurrenceOccurrence', () => {
|
||||
it('should add exception and remove from local state', async () => {
|
||||
mockGetEvents.mockResolvedValue({
|
||||
data: [
|
||||
makeEvent({
|
||||
id: 'r1',
|
||||
startTime: '2026-03-01T09:00:00',
|
||||
endTime: '2026-03-01T09:30:00',
|
||||
recurrenceRule: 'FREQ=DAILY',
|
||||
}),
|
||||
],
|
||||
pagination: defaultPagination,
|
||||
error: null,
|
||||
});
|
||||
await eventsStore.fetchEvents(new Date('2026-03-01'), new Date('2026-03-07'));
|
||||
mockUpdateEvent.mockResolvedValue({ data: makeEvent({ id: 'r1' }), error: null });
|
||||
await eventsStore.deleteRecurrenceOccurrence('r1__recurrence__2026-03-03');
|
||||
expect(mockUpdateEvent).toHaveBeenCalledWith('r1', { recurrenceExceptions: ['2026-03-03'] });
|
||||
expect(eventsStore.events.map((e) => e.id)).not.toContain('r1__recurrence__2026-03-03');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteRecurrenceSeries', () => {
|
||||
it('should delete parent event', async () => {
|
||||
mockGetEvents.mockResolvedValue({
|
||||
data: [
|
||||
makeEvent({
|
||||
id: 'r1',
|
||||
startTime: '2026-03-01T09:00:00',
|
||||
endTime: '2026-03-01T09:30:00',
|
||||
recurrenceRule: 'FREQ=DAILY',
|
||||
}),
|
||||
],
|
||||
pagination: defaultPagination,
|
||||
error: null,
|
||||
});
|
||||
await eventsStore.fetchEvents(new Date('2026-03-01'), new Date('2026-03-07'));
|
||||
mockDeleteEvent.mockResolvedValue({ data: null, error: null });
|
||||
await eventsStore.deleteRecurrenceSeries('r1__recurrence__2026-03-03');
|
||||
expect(mockDeleteEvent).toHaveBeenCalledWith('r1');
|
||||
});
|
||||
it('should not expand non-recurring events', () => {
|
||||
const events = [makeEvent({ id: 'normal' })];
|
||||
const result = expandRecurringEvents(events, new Date('2026-03-01'), new Date('2026-03-31'));
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('normal');
|
||||
});
|
||||
});
|
||||
|
||||
describe('recurrence helpers', () => {
|
||||
it('isRecurrenceOccurrence', () => {
|
||||
expect(eventsStore.isRecurrenceOccurrence('evt__recurrence__2026-03-15')).toBe(true);
|
||||
expect(eventsStore.isRecurrenceOccurrence('evt-1')).toBe(false);
|
||||
});
|
||||
|
||||
it('getParentEventId', () => {
|
||||
expect(eventsStore.getParentEventId('evt-1__recurrence__2026-03-15')).toBe('evt-1');
|
||||
expect(eventsStore.getParentEventId('evt-1')).toBe('evt-1');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,32 +1,27 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import type { CalendarEvent } from '@calendar/shared';
|
||||
import type { PaginationMeta } from '$lib/api/events';
|
||||
|
||||
const defaultPagination: PaginationMeta = { offset: 0, count: 0 };
|
||||
// Mock local-store to avoid import.meta.env issues in tests
|
||||
vi.mock('$lib/data/local-store', () => ({
|
||||
eventCollection: {},
|
||||
calendarCollection: {},
|
||||
}));
|
||||
|
||||
// Mock dependencies before importing the store
|
||||
vi.mock('$lib/api/events', () => ({
|
||||
getEvents: vi.fn(),
|
||||
createEvent: vi.fn(),
|
||||
updateEvent: vi.fn(),
|
||||
deleteEvent: vi.fn(),
|
||||
vi.mock('@manacore/local-store/svelte', () => ({
|
||||
useLiveQueryWithDefault: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('$lib/api/birthdays', () => ({
|
||||
BIRTHDAY_CALENDAR: { id: 'birthday-cal', name: 'Birthdays', color: '#ec4899' },
|
||||
}));
|
||||
|
||||
vi.mock('@manacore/shared-ui', () => ({
|
||||
toastStore: {
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
},
|
||||
toastStore: { error: vi.fn(), success: vi.fn() },
|
||||
}));
|
||||
|
||||
import * as api from '$lib/api/events';
|
||||
import { getEventsForDay, getEventsInRange } from '$lib/data/queries';
|
||||
import { eventsStore } from './events.svelte';
|
||||
|
||||
const mockGetEvents = vi.mocked(api.getEvents);
|
||||
const mockCreateEvent = vi.mocked(api.createEvent);
|
||||
const mockUpdateEvent = vi.mocked(api.updateEvent);
|
||||
const mockDeleteEvent = vi.mocked(api.deleteEvent);
|
||||
|
||||
function makeEvent(overrides: Partial<CalendarEvent> = {}): CalendarEvent {
|
||||
return {
|
||||
id: 'evt-1',
|
||||
|
|
@ -53,334 +48,116 @@ function makeEvent(overrides: Partial<CalendarEvent> = {}): CalendarEvent {
|
|||
};
|
||||
}
|
||||
|
||||
describe('eventsStore', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
eventsStore.clear();
|
||||
eventsStore.clearDraftEvent();
|
||||
describe('getEventsForDay', () => {
|
||||
it('should return events that start on the given day', () => {
|
||||
const events = [
|
||||
makeEvent({ id: 'evt-1', startTime: '2026-03-15T10:00:00', endTime: '2026-03-15T11:00:00' }),
|
||||
];
|
||||
const result = getEventsForDay(events, new Date('2026-03-15'));
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('evt-1');
|
||||
});
|
||||
|
||||
describe('getEventsForDay', () => {
|
||||
it('should return events that start on the given day', async () => {
|
||||
const event = makeEvent({
|
||||
id: 'evt-1',
|
||||
startTime: '2026-03-15T10:00:00',
|
||||
endTime: '2026-03-15T11:00:00',
|
||||
});
|
||||
mockGetEvents.mockResolvedValue({
|
||||
data: [event],
|
||||
pagination: defaultPagination,
|
||||
error: null,
|
||||
});
|
||||
it('should not return events from a different day', () => {
|
||||
const events = [
|
||||
makeEvent({ startTime: '2026-03-15T10:00:00', endTime: '2026-03-15T11:00:00' }),
|
||||
];
|
||||
const result = getEventsForDay(events, new Date('2026-03-16'));
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
await eventsStore.fetchEvents(new Date('2026-03-01'), new Date('2026-03-31'));
|
||||
const result = eventsStore.getEventsForDay(new Date('2026-03-15'), false);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('evt-1');
|
||||
});
|
||||
|
||||
it('should not return events from a different day', async () => {
|
||||
const event = makeEvent({
|
||||
startTime: '2026-03-15T10:00:00',
|
||||
endTime: '2026-03-15T11:00:00',
|
||||
});
|
||||
mockGetEvents.mockResolvedValue({
|
||||
data: [event],
|
||||
pagination: defaultPagination,
|
||||
error: null,
|
||||
});
|
||||
|
||||
await eventsStore.fetchEvents(new Date('2026-03-01'), new Date('2026-03-31'));
|
||||
const result = eventsStore.getEventsForDay(new Date('2026-03-16'), false);
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should include all-day events that span the given day', async () => {
|
||||
const event = makeEvent({
|
||||
it('should include all-day events that span the given day', () => {
|
||||
const events = [
|
||||
makeEvent({
|
||||
id: 'allday-1',
|
||||
startTime: '2026-03-14T00:00:00',
|
||||
endTime: '2026-03-16T23:59:59',
|
||||
isAllDay: true,
|
||||
});
|
||||
mockGetEvents.mockResolvedValue({
|
||||
data: [event],
|
||||
pagination: defaultPagination,
|
||||
error: null,
|
||||
});
|
||||
|
||||
await eventsStore.fetchEvents(new Date('2026-03-01'), new Date('2026-03-31'));
|
||||
const result = eventsStore.getEventsForDay(new Date('2026-03-15'), false);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('allday-1');
|
||||
});
|
||||
|
||||
it('should include draft event when includeDraft is true', async () => {
|
||||
mockGetEvents.mockResolvedValue({ data: [], pagination: defaultPagination, error: null });
|
||||
await eventsStore.fetchEvents(new Date('2026-03-01'), new Date('2026-03-31'));
|
||||
|
||||
eventsStore.createDraftEvent({
|
||||
startTime: '2026-03-15T09:00:00',
|
||||
endTime: '2026-03-15T10:00:00',
|
||||
});
|
||||
|
||||
const result = eventsStore.getEventsForDay(new Date('2026-03-15'), true);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('__draft__');
|
||||
});
|
||||
|
||||
it('should exclude draft event when includeDraft is false', async () => {
|
||||
mockGetEvents.mockResolvedValue({ data: [], pagination: defaultPagination, error: null });
|
||||
await eventsStore.fetchEvents(new Date('2026-03-01'), new Date('2026-03-31'));
|
||||
|
||||
eventsStore.createDraftEvent({
|
||||
startTime: '2026-03-15T09:00:00',
|
||||
endTime: '2026-03-15T10:00:00',
|
||||
});
|
||||
|
||||
const result = eventsStore.getEventsForDay(new Date('2026-03-15'), false);
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEventsInRange', () => {
|
||||
it('should return events that overlap with the given range', async () => {
|
||||
const events = [
|
||||
makeEvent({
|
||||
id: 'evt-1',
|
||||
startTime: '2026-03-15T10:00:00',
|
||||
endTime: '2026-03-15T11:00:00',
|
||||
}),
|
||||
makeEvent({
|
||||
id: 'evt-2',
|
||||
startTime: '2026-03-20T14:00:00',
|
||||
endTime: '2026-03-20T15:00:00',
|
||||
}),
|
||||
];
|
||||
mockGetEvents.mockResolvedValue({ data: events, pagination: defaultPagination, error: null });
|
||||
|
||||
await eventsStore.fetchEvents(new Date('2026-03-01'), new Date('2026-03-31'));
|
||||
const result = eventsStore.getEventsInRange(new Date('2026-03-14'), new Date('2026-03-16'));
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('evt-1');
|
||||
});
|
||||
|
||||
it('should return events that partially overlap the range', async () => {
|
||||
const event = makeEvent({
|
||||
startTime: '2026-03-14T22:00:00',
|
||||
endTime: '2026-03-15T02:00:00',
|
||||
});
|
||||
mockGetEvents.mockResolvedValue({
|
||||
data: [event],
|
||||
pagination: defaultPagination,
|
||||
error: null,
|
||||
});
|
||||
|
||||
await eventsStore.fetchEvents(new Date('2026-03-01'), new Date('2026-03-31'));
|
||||
const result = eventsStore.getEventsInRange(
|
||||
new Date('2026-03-15T00:00:00'),
|
||||
new Date('2026-03-15T23:59:59')
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should return empty array when no events in range', async () => {
|
||||
const event = makeEvent({
|
||||
startTime: '2026-03-20T10:00:00',
|
||||
endTime: '2026-03-20T11:00:00',
|
||||
});
|
||||
mockGetEvents.mockResolvedValue({
|
||||
data: [event],
|
||||
pagination: defaultPagination,
|
||||
error: null,
|
||||
});
|
||||
|
||||
await eventsStore.fetchEvents(new Date('2026-03-01'), new Date('2026-03-31'));
|
||||
const result = eventsStore.getEventsInRange(new Date('2026-03-14'), new Date('2026-03-16'));
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createDraftEvent / clearDraftEvent', () => {
|
||||
it('should create a draft event with __draft__ id', () => {
|
||||
const draft = eventsStore.createDraftEvent({
|
||||
title: 'Draft Meeting',
|
||||
startTime: '2026-03-15T10:00:00',
|
||||
endTime: '2026-03-15T11:00:00',
|
||||
});
|
||||
|
||||
expect(draft.id).toBe('__draft__');
|
||||
expect(draft.title).toBe('Draft Meeting');
|
||||
expect(eventsStore.draftEvent).not.toBeNull();
|
||||
expect(eventsStore.draftEvent?.id).toBe('__draft__');
|
||||
});
|
||||
|
||||
it('should clear the draft event', () => {
|
||||
eventsStore.createDraftEvent({
|
||||
title: 'Draft',
|
||||
startTime: '2026-03-15T10:00:00',
|
||||
endTime: '2026-03-15T11:00:00',
|
||||
});
|
||||
|
||||
expect(eventsStore.draftEvent).not.toBeNull();
|
||||
|
||||
eventsStore.clearDraftEvent();
|
||||
expect(eventsStore.draftEvent).toBeNull();
|
||||
});
|
||||
|
||||
it('should set default values for missing fields', () => {
|
||||
const draft = eventsStore.createDraftEvent({});
|
||||
|
||||
expect(draft.calendarId).toBe('');
|
||||
expect(draft.title).toBe('');
|
||||
expect(draft.isAllDay).toBe(false);
|
||||
expect(draft.status).toBe('confirmed');
|
||||
expect(draft.description).toBeNull();
|
||||
expect(draft.location).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDraftEvent', () => {
|
||||
it('should return true for __draft__ id', () => {
|
||||
expect(eventsStore.isDraftEvent('__draft__')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for regular event ids', () => {
|
||||
expect(eventsStore.isDraftEvent('evt-1')).toBe(false);
|
||||
expect(eventsStore.isDraftEvent('')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteEvent (optimistic update)', () => {
|
||||
it('should remove event optimistically and restore on error', async () => {
|
||||
const events = [makeEvent({ id: 'evt-1' }), makeEvent({ id: 'evt-2', title: 'Second' })];
|
||||
mockGetEvents.mockResolvedValue({ data: events, pagination: defaultPagination, error: null });
|
||||
await eventsStore.fetchEvents(new Date('2026-03-01'), new Date('2026-03-31'));
|
||||
|
||||
// Verify both events exist
|
||||
expect(eventsStore.events).toHaveLength(2);
|
||||
|
||||
// Simulate API error on delete
|
||||
mockDeleteEvent.mockResolvedValue({
|
||||
data: null,
|
||||
error: { message: 'Server error', code: 'SERVER_ERROR', status: 500 },
|
||||
});
|
||||
|
||||
await eventsStore.deleteEvent('evt-1');
|
||||
|
||||
// Event should be restored after error
|
||||
expect(eventsStore.events).toHaveLength(2);
|
||||
const ids = eventsStore.events.map((e) => e.id);
|
||||
expect(ids).toContain('evt-1');
|
||||
});
|
||||
|
||||
it('should permanently remove event on successful delete', async () => {
|
||||
const events = [makeEvent({ id: 'evt-1' })];
|
||||
mockGetEvents.mockResolvedValue({ data: events, pagination: defaultPagination, error: null });
|
||||
await eventsStore.fetchEvents(new Date('2026-03-01'), new Date('2026-03-31'));
|
||||
|
||||
mockDeleteEvent.mockResolvedValue({ data: null, error: null });
|
||||
|
||||
await eventsStore.deleteEvent('evt-1');
|
||||
|
||||
expect(eventsStore.events).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createEvent', () => {
|
||||
it('should add the created event to the store', async () => {
|
||||
mockGetEvents.mockResolvedValue({ data: [], pagination: defaultPagination, error: null });
|
||||
await eventsStore.fetchEvents(new Date('2026-03-01'), new Date('2026-03-31'));
|
||||
|
||||
const newEvent = makeEvent({ id: 'new-1', title: 'New Event' });
|
||||
mockCreateEvent.mockResolvedValue({ data: newEvent, error: null });
|
||||
|
||||
await eventsStore.createEvent({
|
||||
calendarId: 'cal-1',
|
||||
title: 'New Event',
|
||||
startTime: '2026-03-15T10:00:00',
|
||||
endTime: '2026-03-15T11:00:00',
|
||||
});
|
||||
|
||||
expect(eventsStore.events).toHaveLength(1);
|
||||
expect(eventsStore.events[0].id).toBe('new-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateEvent', () => {
|
||||
it('should replace the updated event in the store', async () => {
|
||||
const event = makeEvent({ id: 'evt-1', title: 'Original' });
|
||||
mockGetEvents.mockResolvedValue({
|
||||
data: [event],
|
||||
pagination: defaultPagination,
|
||||
error: null,
|
||||
});
|
||||
await eventsStore.fetchEvents(new Date('2026-03-01'), new Date('2026-03-31'));
|
||||
|
||||
const updated = makeEvent({ id: 'evt-1', title: 'Updated' });
|
||||
mockUpdateEvent.mockResolvedValue({ data: updated, error: null });
|
||||
|
||||
await eventsStore.updateEvent('evt-1', { title: 'Updated' });
|
||||
|
||||
expect(eventsStore.events).toHaveLength(1);
|
||||
expect(eventsStore.events[0].title).toBe('Updated');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchEvents', () => {
|
||||
it('should set loading state during fetch', async () => {
|
||||
let resolvePromise: (value: unknown) => void;
|
||||
const promise = new Promise((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
mockGetEvents.mockReturnValue(promise as ReturnType<typeof api.getEvents>);
|
||||
|
||||
const fetchPromise = eventsStore.fetchEvents(new Date('2026-03-01'), new Date('2026-03-31'));
|
||||
|
||||
expect(eventsStore.loading).toBe(true);
|
||||
|
||||
resolvePromise!({ data: [], pagination: defaultPagination, error: null });
|
||||
await fetchPromise;
|
||||
|
||||
expect(eventsStore.loading).toBe(false);
|
||||
});
|
||||
|
||||
it('should set error on API failure', async () => {
|
||||
mockGetEvents.mockResolvedValue({
|
||||
data: null,
|
||||
pagination: null,
|
||||
error: { message: 'Network error', code: 'NETWORK_ERROR' },
|
||||
});
|
||||
|
||||
await eventsStore.fetchEvents(new Date('2026-03-01'), new Date('2026-03-31'));
|
||||
|
||||
expect(eventsStore.error).toBe('Network error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getById', () => {
|
||||
it('should return event by ID', async () => {
|
||||
const event = makeEvent({ id: 'evt-1', title: 'Found' });
|
||||
mockGetEvents.mockResolvedValue({
|
||||
data: [event],
|
||||
pagination: defaultPagination,
|
||||
error: null,
|
||||
});
|
||||
await eventsStore.fetchEvents(new Date('2026-03-01'), new Date('2026-03-31'));
|
||||
|
||||
expect(eventsStore.getById('evt-1')?.title).toBe('Found');
|
||||
});
|
||||
|
||||
it('should return undefined for unknown ID', async () => {
|
||||
mockGetEvents.mockResolvedValue({ data: [], pagination: defaultPagination, error: null });
|
||||
await eventsStore.fetchEvents(new Date('2026-03-01'), new Date('2026-03-31'));
|
||||
|
||||
expect(eventsStore.getById('nonexistent')).toBeUndefined();
|
||||
});
|
||||
}),
|
||||
];
|
||||
const result = getEventsForDay(events, new Date('2026-03-15'));
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('allday-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEventsInRange', () => {
|
||||
it('should return events that overlap with the given range', () => {
|
||||
const events = [
|
||||
makeEvent({ id: 'evt-1', startTime: '2026-03-15T10:00:00', endTime: '2026-03-15T11:00:00' }),
|
||||
makeEvent({ id: 'evt-2', startTime: '2026-03-20T14:00:00', endTime: '2026-03-20T15:00:00' }),
|
||||
];
|
||||
const result = getEventsInRange(events, new Date('2026-03-14'), new Date('2026-03-16'));
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('evt-1');
|
||||
});
|
||||
|
||||
it('should return events that partially overlap the range', () => {
|
||||
const events = [
|
||||
makeEvent({ startTime: '2026-03-14T22:00:00', endTime: '2026-03-15T02:00:00' }),
|
||||
];
|
||||
const result = getEventsInRange(
|
||||
events,
|
||||
new Date('2026-03-15T00:00:00'),
|
||||
new Date('2026-03-15T23:59:59')
|
||||
);
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should return empty array when no events in range', () => {
|
||||
const events = [
|
||||
makeEvent({ startTime: '2026-03-20T10:00:00', endTime: '2026-03-20T11:00:00' }),
|
||||
];
|
||||
const result = getEventsInRange(events, new Date('2026-03-14'), new Date('2026-03-16'));
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createDraftEvent / clearDraftEvent', () => {
|
||||
it('should create a draft event with __draft__ id', () => {
|
||||
const draft = eventsStore.createDraftEvent({
|
||||
title: 'Draft Meeting',
|
||||
startTime: '2026-03-15T10:00:00',
|
||||
endTime: '2026-03-15T11:00:00',
|
||||
});
|
||||
|
||||
expect(draft.id).toBe('__draft__');
|
||||
expect(draft.title).toBe('Draft Meeting');
|
||||
expect(eventsStore.draftEvent).not.toBeNull();
|
||||
expect(eventsStore.draftEvent?.id).toBe('__draft__');
|
||||
});
|
||||
|
||||
it('should clear the draft event', () => {
|
||||
eventsStore.createDraftEvent({
|
||||
title: 'Draft',
|
||||
startTime: '2026-03-15T10:00:00',
|
||||
endTime: '2026-03-15T11:00:00',
|
||||
});
|
||||
|
||||
expect(eventsStore.draftEvent).not.toBeNull();
|
||||
eventsStore.clearDraftEvent();
|
||||
expect(eventsStore.draftEvent).toBeNull();
|
||||
});
|
||||
|
||||
it('should set default values for missing fields', () => {
|
||||
const draft = eventsStore.createDraftEvent({});
|
||||
|
||||
expect(draft.calendarId).toBe('');
|
||||
expect(draft.title).toBe('');
|
||||
expect(draft.isAllDay).toBe(false);
|
||||
expect(draft.status).toBe('confirmed');
|
||||
expect(draft.description).toBeNull();
|
||||
expect(draft.location).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDraftEvent', () => {
|
||||
it('should return true for __draft__ id', () => {
|
||||
expect(eventsStore.isDraftEvent('__draft__')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for regular event ids', () => {
|
||||
expect(eventsStore.isDraftEvent('evt-1')).toBe(false);
|
||||
expect(eventsStore.isDraftEvent('')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,134 +0,0 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock the client module
|
||||
vi.mock('./client', () => ({
|
||||
apiClient: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import {
|
||||
getProjects,
|
||||
getProject,
|
||||
createProject,
|
||||
updateProject,
|
||||
deleteProject,
|
||||
archiveProject,
|
||||
reorderProjects,
|
||||
} from './projects';
|
||||
import { apiClient } from './client';
|
||||
|
||||
const mockClient = vi.mocked(apiClient);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getProjects', () => {
|
||||
it('should fetch all projects', async () => {
|
||||
const projects = [
|
||||
{ id: 'p1', name: 'Work' },
|
||||
{ id: 'p2', name: 'Personal' },
|
||||
];
|
||||
mockClient.get.mockResolvedValue({ projects });
|
||||
|
||||
const result = await getProjects();
|
||||
|
||||
expect(mockClient.get).toHaveBeenCalledWith('/api/v1/projects');
|
||||
expect(result).toEqual(projects);
|
||||
});
|
||||
|
||||
it('should return empty array when no projects', async () => {
|
||||
mockClient.get.mockResolvedValue({ projects: [] });
|
||||
|
||||
const result = await getProjects();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProject', () => {
|
||||
it('should fetch a single project by id', async () => {
|
||||
const project = { id: 'p1', name: 'Work' };
|
||||
mockClient.get.mockResolvedValue({ project });
|
||||
|
||||
const result = await getProject('p1');
|
||||
|
||||
expect(mockClient.get).toHaveBeenCalledWith('/api/v1/projects/p1');
|
||||
expect(result).toEqual(project);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createProject', () => {
|
||||
it('should POST a new project', async () => {
|
||||
const data = { name: 'New Project', color: '#ff0000', icon: 'star' };
|
||||
const project = { id: 'p-new', ...data };
|
||||
mockClient.post.mockResolvedValue({ project });
|
||||
|
||||
const result = await createProject(data);
|
||||
|
||||
expect(mockClient.post).toHaveBeenCalledWith('/api/v1/projects', data);
|
||||
expect(result).toEqual(project);
|
||||
});
|
||||
|
||||
it('should create project with only name', async () => {
|
||||
const data = { name: 'Minimal Project' };
|
||||
const project = { id: 'p-min', name: 'Minimal Project' };
|
||||
mockClient.post.mockResolvedValue({ project });
|
||||
|
||||
const result = await createProject(data);
|
||||
|
||||
expect(mockClient.post).toHaveBeenCalledWith('/api/v1/projects', data);
|
||||
expect(result).toEqual(project);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateProject', () => {
|
||||
it('should PUT updated project', async () => {
|
||||
const data = { name: 'Updated Name', color: '#00ff00' };
|
||||
const project = { id: 'p1', ...data };
|
||||
mockClient.put.mockResolvedValue({ project });
|
||||
|
||||
const result = await updateProject('p1', data);
|
||||
|
||||
expect(mockClient.put).toHaveBeenCalledWith('/api/v1/projects/p1', data);
|
||||
expect(result).toEqual(project);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteProject', () => {
|
||||
it('should DELETE project', async () => {
|
||||
mockClient.delete.mockResolvedValue(undefined);
|
||||
|
||||
await deleteProject('p1');
|
||||
|
||||
expect(mockClient.delete).toHaveBeenCalledWith('/api/v1/projects/p1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('archiveProject', () => {
|
||||
it('should POST to archive endpoint', async () => {
|
||||
const project = { id: 'p1', name: 'Work', isArchived: true };
|
||||
mockClient.post.mockResolvedValue({ project });
|
||||
|
||||
const result = await archiveProject('p1');
|
||||
|
||||
expect(mockClient.post).toHaveBeenCalledWith('/api/v1/projects/p1/archive');
|
||||
expect(result).toEqual(project);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reorderProjects', () => {
|
||||
it('should PUT reorder with project IDs', async () => {
|
||||
mockClient.put.mockResolvedValue(undefined);
|
||||
|
||||
await reorderProjects(['p1', 'p2', 'p3']);
|
||||
|
||||
expect(mockClient.put).toHaveBeenCalledWith('/api/v1/projects/reorder', {
|
||||
projectIds: ['p1', 'p2', 'p3'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock the client module
|
||||
vi.mock('./client', () => ({
|
||||
apiClient: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
put: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { getReminders, createReminder, deleteReminder } from './reminders';
|
||||
import { apiClient } from './client';
|
||||
|
||||
const mockClient = vi.mocked(apiClient);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getReminders', () => {
|
||||
it('should fetch reminders for a task', async () => {
|
||||
const reminders = [
|
||||
{ id: 'r1', taskId: 't1', minutesBefore: 15, type: 'push' },
|
||||
{ id: 'r2', taskId: 't1', minutesBefore: 60, type: 'email' },
|
||||
];
|
||||
mockClient.get.mockResolvedValue({ reminders });
|
||||
|
||||
const result = await getReminders('t1');
|
||||
|
||||
expect(mockClient.get).toHaveBeenCalledWith('/api/v1/tasks/t1/reminders');
|
||||
expect(result).toEqual(reminders);
|
||||
});
|
||||
|
||||
it('should return empty array when no reminders', async () => {
|
||||
mockClient.get.mockResolvedValue({ reminders: [] });
|
||||
|
||||
const result = await getReminders('t1');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createReminder', () => {
|
||||
it('should POST a new reminder', async () => {
|
||||
const data = { minutesBefore: 30, type: 'push' as const };
|
||||
const reminder = { id: 'r-new', taskId: 't1', ...data };
|
||||
mockClient.post.mockResolvedValue({ reminder });
|
||||
|
||||
const result = await createReminder('t1', data);
|
||||
|
||||
expect(mockClient.post).toHaveBeenCalledWith('/api/v1/tasks/t1/reminders', data);
|
||||
expect(result).toEqual(reminder);
|
||||
});
|
||||
|
||||
it('should create reminder with only minutesBefore', async () => {
|
||||
const data = { minutesBefore: 10 };
|
||||
const reminder = { id: 'r-new', taskId: 't1', minutesBefore: 10 };
|
||||
mockClient.post.mockResolvedValue({ reminder });
|
||||
|
||||
const result = await createReminder('t1', data);
|
||||
|
||||
expect(mockClient.post).toHaveBeenCalledWith('/api/v1/tasks/t1/reminders', data);
|
||||
expect(result).toEqual(reminder);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteReminder', () => {
|
||||
it('should DELETE reminder by id', async () => {
|
||||
mockClient.delete.mockResolvedValue(undefined);
|
||||
|
||||
await deleteReminder('r1');
|
||||
|
||||
expect(mockClient.delete).toHaveBeenCalledWith('/api/v1/reminders/r1');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue