diff --git a/apps/calendar/apps/web/src/lib/stores/events-recurrence.test.ts b/apps/calendar/apps/web/src/lib/stores/events-recurrence.test.ts index a147dcb47..65ea7b69c 100644 --- a/apps/calendar/apps/web/src/lib/stores/events-recurrence.test.ts +++ b/apps/calendar/apps/web/src/lib/stores/events-recurrence.test.ts @@ -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 { return { id: 'evt-1', @@ -48,141 +48,73 @@ function makeEvent(overrides: Partial = {}): 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'); }); }); diff --git a/apps/calendar/apps/web/src/lib/stores/events.test.ts b/apps/calendar/apps/web/src/lib/stores/events.test.ts index 3c029cdd4..1983b14b4 100644 --- a/apps/calendar/apps/web/src/lib/stores/events.test.ts +++ b/apps/calendar/apps/web/src/lib/stores/events.test.ts @@ -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 { return { id: 'evt-1', @@ -53,334 +48,116 @@ function makeEvent(overrides: Partial = {}): 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); - - 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); }); }); diff --git a/apps/todo/apps/web/src/lib/api/projects.test.ts b/apps/todo/apps/web/src/lib/api/projects.test.ts deleted file mode 100644 index 520012fd4..000000000 --- a/apps/todo/apps/web/src/lib/api/projects.test.ts +++ /dev/null @@ -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'], - }); - }); -}); diff --git a/apps/todo/apps/web/src/lib/api/reminders.test.ts b/apps/todo/apps/web/src/lib/api/reminders.test.ts deleted file mode 100644 index ec8b91a7c..000000000 --- a/apps/todo/apps/web/src/lib/api/reminders.test.ts +++ /dev/null @@ -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'); - }); -});